blob: b656092809c6bfd1f24c0f980f6e17230d1295db [file] [log] [blame]
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);
}
}
}