blob: 1e2803df72b03cba7e8fcebb29a8648d0b2632f0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.help.internal.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
/*
* Provides an algorithm for determining a recommended sequence of items that
* satisfies a primary sequence and as many secondary sequences as possible.
*
* For example, this is used to determine the display order of books on the tocs
* based on the active product's preferred order as well as all other products'
* preferred orders.
*/
public class SequenceResolver<T> {
private List<T> primaryList;
private List<T>[] secondaryLists;
private ListIterator<T> primaryIter;
private ListIterator<T>[] secondaryIters;
private Set<T> processedItems;
/*
* Merges the given primary and secondary orderings such that all ordering
* conditions from the primary are satisfied, and as many secondary ordering
* conditions as reasonably possible are also satisfied. An ordering condition
* is a pair of adjacent items in any ordering.
*
* For example:
*
* primary = {b, d, f}
* secondary[0] = {c, d, e}
* secondary[1] = {a, b, c}
* -------------------------------
* result = {a, b, c, d, e, f}
*
* The algorithm works in iterations, where at each iteration we determine
* the next element in the recommended sequence. We maintain a pointer for
* each ordering to keep track of what we've already ordered.
*
* To determine the next item, we locate the current element in each ordering.
* These are the candidates for the next item as the next item can only be
* one of these.
*
* The top candidates are selected from the list of candidates, where a top
* candidate is one that has the lowest rank (there can be many). Rank is
* determined by how many other candidates appear before that candidate
* item in all the orderings. That is, we find out how many items
* the orderings list before each candidate.
*
* If a candidate has no other candidates listed before it, it will be
* the next item. If there are many, the first one is selected. If one of
* the top candidates is from the primary ordering (can only be one), it is
* automatically selected.
*
* Using the top rank ensures that if there are conflicts, and that
* as many orderings as possible are satisfied. For example, if most orderings
* want x before y, but a few want the opposite, x will be placed before y.
*/
public List<T> getSequence(List<T> primary, List<T>[] secondary) {
primaryList = primary;
secondaryLists = secondary;
prepareDataStructures();
List<T> order = new ArrayList<>();
T item;
while ((item = getNextItem()) != null) {
processedItems.add(item);
advanceIterator(primaryIter);
for (int i=0;i<secondaryIters.length;++i) {
advanceIterator(secondaryIters[i]);
}
order.add(item);
}
return order;
}
/*
* Create the data structures necessary for later operations.
*/
@SuppressWarnings("unchecked")
private void prepareDataStructures() {
primaryIter = primaryList.listIterator();
secondaryIters = new ListIterator[secondaryLists.length];
for (int i=0;i<secondaryLists.length;++i) {
secondaryIters[i] = secondaryLists[i].listIterator();
}
processedItems = new HashSet<>();
}
/*
* Determine the next item in the sequence based on the top
* candidate items.
*/
private T getNextItem() {
Candidate<T>[] candidates = getTopCandidates();
switch(candidates.length) {
case 0:
return null;
case 1:
return candidates[0].item;
default:
for (int i=0;i<candidates.length;++i) {
if (candidates[i].isPrimary) {
return candidates[i].item;
}
}
return candidates[0].item;
}
}
/*
* Retrieves the top candidates from all the available next item candidates.
* These are the candidates that have the lowest rank
*/
private Candidate<T>[] getTopCandidates() {
Candidate<T>[] candidates = getEligibleCandidates();
rankCandidates(candidates);
if (candidates.length > 0) {
int topRank = candidates[0].rank;
for (int i=1;i<candidates.length;++i) {
if (candidates[i].rank < topRank) {
topRank = candidates[i].rank;
}
}
List<Candidate<T>> topCandidates = new ArrayList<>();
for (int i=0;i<candidates.length;++i) {
if (candidates[i].rank == topRank) {
topCandidates.add(candidates[i]);
}
}
return toCandidatesArray(topCandidates);
}
return candidates;
}
/*
* Returns all eligible candidates. A candidate is eligible if it does not
* conflict with the primary candidate. That is, if the primary candidate's list
* has that candidate after the primary candidate, it is contradicting the primary
* sequence and is not eligible.
*/
private Candidate<T>[] getEligibleCandidates() {
Candidate<T>[] allCandidates = getAllCandidates();
Candidate<T> primary = null;
for (int i=0;i<allCandidates.length;++i) {
if (allCandidates[i].isPrimary) {
primary = allCandidates[i];
break;
}
}
// if we have no primary candidate then they're all eligible
if (primary != null) {
List<Candidate<T>> eligibleCandidates = new ArrayList<>(allCandidates.length);
// primary candidate is always eligible
eligibleCandidates.add(primary);
Set<T> primarySet = Collections.singleton(primary.item);
for (int i=0;i<allCandidates.length;++i) {
Candidate<T> c = allCandidates[i];
if (c != primary) {
// does it contradict the primary sequence? if not, it is eligible
if (countPrecedingItems(c.item, primary.src, primarySet) == 0) {
eligibleCandidates.add(c);
}
}
}
return toCandidatesArray(eligibleCandidates);
}
return allCandidates;
}
/*
* Retrieve all the candidates for the next item in sequence, with
* no duplicates.
*/
private Candidate<T>[] getAllCandidates() {
List<Candidate<T>> candidates = new ArrayList<>();
T item = getNextItem(primaryIter);
if (item != null) {
Candidate<T> c = new Candidate<>();
c.item = item;
c.isPrimary = true;
c.src = primaryList;
candidates.add(c);
}
for (int i=0;i<secondaryIters.length;++i) {
item = getNextItem(secondaryIters[i]);
if (item != null) {
Candidate<T> c = new Candidate<>();
c.item = item;
c.isPrimary = false;
c.src = secondaryLists[i];
if (!candidates.contains(c)) {
candidates.add(c);
}
}
}
return toCandidatesArray(candidates);
}
/** Helper function as we cannot create arrays of parameterized types */
@SuppressWarnings("unchecked")
private Candidate<T>[] toCandidatesArray(List<Candidate<T>> candidates) {
return candidates.toArray(new Candidate[candidates.size()]);
}
/*
* Assign a rank to each of the given candidates. Rank is determined by
* how many preceding candidates appear before that candidate in the orderings.
* This essentially means how far back this item should be in the final
* sequence.
*/
private void rankCandidates(Candidate<T>[] candidates) {
// for quick lookup
Set<T> candidateItems = new HashSet<>();
for (int i=0;i<candidates.length;++i) {
candidateItems.add(candidates[i].item);
}
for (int i=0;i<candidates.length;++i) {
Candidate<T> c = candidates[i];
for (int j=0;j<candidates.length;++j) {
c.rank += countPrecedingItems(c.item, candidates[j].src, candidateItems);
}
}
}
/*
* Counts the number of elements from the given set that come before
* the given item in the given list.
*/
private int countPrecedingItems(Object item, List<?> list, Set<?> set) {
int count = 0;
Iterator<?> iter = list.iterator();
while (iter.hasNext()) {
Object next = iter.next();
if (next.equals(item)) {
return count;
}
if (set.contains(next)) {
++count;
}
}
return 0;
}
/*
* Returns the next item available to this iterator, without moving it
* forward.
*/
private T getNextItem(ListIterator<T> iter) {
if (iter.hasNext()) {
T next = iter.next();
iter.previous();
return next;
}
return null;
}
/*
* Advances the given iterator to the next item in its
* sequence that we haven't yet processed.
*/
private void advanceIterator(ListIterator<T> iter) {
while (iter.hasNext()) {
T item = iter.next();
if (!processedItems.contains(item)) {
iter.previous();
break;
}
}
}
/*
* A candidate item; one that could potentially be the next one in the final
* sequence.
*/
private static class Candidate<T> {
public T item;
public boolean isPrimary;
public int rank;
public List<?> src;
@Override
public boolean equals(Object obj) {
return item.equals(obj);
}
@Override
public int hashCode() {
return item.hashCode();
}
}
}