| package org.unicode.cldr.util; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| |
| import com.ibm.icu.impl.Utility; |
| |
| /** |
| * A comparator that can be built from a series of 'less than' relations. |
| * |
| * <pre> |
| * DiscreteComparator<String> comp = new Builder<String>(order).add("c", "d", "b", "a").add("m", "n", "d").get(); |
| * if (comp.compare("a", "m")) ... |
| * </pre> |
| * |
| * @author markdavis |
| * @param <T> |
| * any class |
| */ |
| public class DiscreteComparator<T> implements Comparator<T> { |
| /** |
| * The builder can take three different orders: input order, natural order |
| * (assumes T is Comparable<T>), and arbitrary. |
| */ |
| public enum Ordering { |
| CHRONOLOGICAL, NATURAL, ARBITRARY |
| } |
| |
| private static final boolean DEBUG = false; |
| private static int debugIndent; |
| |
| private Map<T, Integer> ordering; |
| private Comparator<T> backupOrdering; |
| |
| private DiscreteComparator(Map<T, Integer> ordering, Comparator<T> backupOrdering) { |
| this.backupOrdering = backupOrdering; |
| this.ordering = ordering; |
| } |
| |
| /** |
| * Compare the items. If there was a backup comparator, it will be used for |
| * items not specified explicitly. However, all explicit items will always |
| * come before all others, to preserve transitivity. |
| * |
| * @param o1 |
| * first item |
| * @param o2 |
| * second item |
| * @return integer representing the order |
| * @exception MissingItemException |
| * thrown if there is no backup comparator, and at least one of |
| * the items is not explicit in the collator. |
| */ |
| public int compare(T o1, T o2) { |
| // TODO add option for back ordering |
| Integer a = ordering.get(o1); |
| Integer b = ordering.get(o2); |
| if (a != null && b != null) { |
| } |
| if (a == null) { |
| if (backupOrdering != null) { |
| if (b == null) { |
| return backupOrdering.compare(o1, o2); |
| } else { |
| return 1; // b is in, so less |
| } |
| } |
| throw new MissingItemException("Item not in ordering:\t" + o1); |
| } else if (b == null) { |
| if (backupOrdering != null) { |
| return -1; // a is in, so less |
| } |
| throw new MissingItemException("Item not in ordering:\t" + o2); |
| } |
| return a.compareTo(b); |
| } |
| |
| /** |
| * Get a list of the explicit items. |
| * |
| * @return a list |
| */ |
| public List<T> getOrdering() { |
| return new ArrayList<T>(ordering.keySet()); |
| } |
| |
| @Override |
| public String toString() { |
| return ordering.keySet().toString(); |
| } |
| |
| /** |
| * Builder for DiscreteComparator |
| * |
| * @param <T> |
| * any class |
| */ |
| public static class Builder<T> { |
| |
| // builds a topological sort from an input set of relations |
| // with O(n) algorithm |
| private Map<T, Node<T>> all; |
| private Comparator<T> backupOrdering; |
| private Ordering order; |
| |
| /** |
| * Pass the order you want for the results. |
| * |
| * @param order |
| */ |
| public Builder(Ordering order) { |
| this.order = order; |
| all = order == Ordering.CHRONOLOGICAL ? new LinkedHashMap<T, Node<T>>() |
| : order == Ordering.NATURAL ? new TreeMap<T, Node<T>>() |
| : new HashMap<T, Node<T>>(); |
| } |
| |
| /** |
| * If there is a backup comparator, specify it here. |
| * |
| * @param backupOrdering |
| * @return this, for chaining |
| */ |
| public Builder<T> setFallbackComparator(Comparator<T> backupOrdering) { |
| this.backupOrdering = backupOrdering; |
| return this; |
| } |
| |
| /** |
| * Add explicitly ordered items, from least to greatest |
| * |
| * @param items |
| * @return this, for chaining |
| */ |
| public Builder<T> add(Collection<T> items) { |
| if (items.size() < 2) { |
| if (items.size() == 1) { |
| T item = items.iterator().next(); |
| if (!all.containsKey(item)) { |
| addNew(item); |
| } |
| } |
| return this; |
| } |
| T last = null; |
| boolean first = true; |
| for (T item : items) { |
| if (first) { |
| first = false; |
| } else { |
| add(last, item); |
| } |
| last = item; |
| } |
| return this; |
| } |
| |
| /** |
| * Add explicitly ordered items, from least to greatest |
| * |
| * @param items |
| * @return this, for chaining |
| */ |
| @SuppressWarnings("unchecked") |
| public Builder<T> add(T... items) { |
| if (items.length < 2) { |
| if (items.length == 1) { |
| T item = items[0]; |
| if (!all.containsKey(item)) { |
| addNew(item); |
| } |
| } |
| } |
| T last = null; |
| boolean first = true; |
| for (T item : items) { |
| if (first) { |
| first = false; |
| } else { |
| add(last, item); |
| } |
| last = item; |
| } |
| return this; |
| } |
| |
| /** |
| * Add explicitly ordered items |
| * |
| * @param a |
| * lesser |
| * @param b |
| * greater |
| * @return this, for chaining |
| */ |
| public Builder<T> add(T a, T b) { |
| // if (a.equals(b)) { |
| // throw new CycleException("Can't add equal items", a, a); |
| // } |
| Node<T> aNode = all.get(a); |
| if (aNode == null) { |
| aNode = addNew(a); |
| } |
| Node<T> bNode = all.get(b); |
| if (bNode == null) { |
| bNode = addNew(b); |
| } |
| addLink(aNode, bNode); |
| if (DEBUG) { |
| System.out.println(a + " + " + b + " => " + this); |
| } |
| return this; |
| } |
| |
| /** |
| * Get the comparator you've been building. After this call, the builder is |
| * reset (if there is no error). |
| * |
| * @return comparator |
| * @exception CycleException |
| * throwing if there is (at least one) cycle, like a > b > c > |
| * a. If so, call getCycle to see the cycle that triggered the |
| * exception. |
| */ |
| public DiscreteComparator<T> get() { |
| if (DEBUG) { |
| debugIndent = new Exception().getStackTrace().length; |
| } |
| Map<T, Integer> ordering = new LinkedHashMap<T, Integer>(); |
| for (Node<T> subNode : all.values()) { |
| if (!subNode.visited) { |
| subNode.visit(ordering); |
| } |
| } |
| // clean up, so another call doesn't mess things up |
| all.clear(); |
| return new DiscreteComparator<T>(ordering, backupOrdering); |
| } |
| |
| /** |
| * Call only after getting a CycleException |
| * |
| * @return list of items that form a cycle, in order from least to greatest |
| */ |
| public List<T> getCycle() { |
| List<T> result = new LinkedList<T>(); |
| Collection<Node<T>> lesser = all.values(); |
| main: while (true) { |
| for (Node<T> item : lesser) { |
| if (item.chained) { |
| if (result.contains(item.me)) { |
| return result; |
| } |
| result.add(0, item.me); |
| lesser = item.less; |
| continue main; |
| } |
| } |
| throw new IllegalArgumentException("Must only be called after a CycleException"); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return order + ":\t" + all.values().toString(); |
| } |
| |
| private void addLink(Node<T> aNode, Node<T> bNode) { |
| bNode.less.add(aNode); |
| } |
| |
| private Node<T> addNew(T a) { |
| Node<T> aNode = new Node<T>(a, order); |
| all.put(a, aNode); |
| return aNode; |
| } |
| } |
| |
| private static class Node<T> implements Comparable<Node<T>> { |
| private Set<Node<T>> less; |
| private T me; |
| private boolean visited = false; |
| private boolean chained = false; |
| |
| public Node(T a, Ordering order) { |
| less = new LinkedHashSet<Node<T>>(); |
| less = order == Ordering.CHRONOLOGICAL ? new LinkedHashSet<Node<T>>() |
| : order == Ordering.NATURAL ? new TreeSet<Node<T>>() |
| : new HashSet<Node<T>>(); |
| |
| me = a; |
| } |
| |
| private void visit(Map<T, Integer> resultOrdering) { |
| Node<T> currentNode = this; |
| if (DEBUG) { |
| String gap = Utility.repeat(" ", new Exception().getStackTrace().length - debugIndent); |
| System.out.println("Visiting:\t" + gap + currentNode + " => " + resultOrdering.keySet()); |
| } |
| currentNode.visited = true; |
| currentNode.chained = true; |
| for (Node<T> subNode : currentNode.less) { |
| if (subNode.chained) { |
| throw new CycleException("Cycle in input data"); |
| } |
| if (subNode.visited) { |
| continue; |
| } |
| subNode.visit(resultOrdering); |
| } |
| currentNode.chained = false; |
| resultOrdering.put(currentNode.me, resultOrdering.size()); |
| } |
| |
| public String toString() { |
| StringBuilder result = new StringBuilder(); |
| result.append(me == null ? null : me.toString()).append(" >"); |
| for (Node<T> lesser : less) { |
| result.append(" ").append(lesser.me); |
| } |
| return result.toString(); |
| } |
| |
| @SuppressWarnings({ "unchecked", "rawtypes" }) |
| public int compareTo(Node<T> o) { |
| return ((Comparable) me).compareTo((Comparable) (o.me)); |
| } |
| } |
| |
| /** |
| * Exception for indicating that a cycle was found. |
| */ |
| public static class CycleException extends RuntimeException { |
| private static final long serialVersionUID = 1L; |
| |
| public <T> CycleException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * Exception indicating that there is no backup comparator, and at least one |
| * of the items compared is not explicitly set. |
| */ |
| public static class MissingItemException extends RuntimeException { |
| private static final long serialVersionUID = 1L; |
| |
| public MissingItemException(String message) { |
| super(message); |
| } |
| } |
| } |