com.almworks.jira.structure.api.util.TotalOrder Maven / Gradle / Ivy
Show all versions of structure-api Show documentation
package com.almworks.jira.structure.api.util;
import org.jetbrains.annotations.Nullable;
import java.text.CollationKey;
import java.text.Collator;
import java.util.Comparator;
import java.util.Locale;
/**
* This class provides total ordering for objects of any type.
*
* When comparing two objects, it is first determined if the objects are mutually comparable. The rules below specify
* which objects are comparable and how are they compared.
*
* If two objects are not comparable, then their relative order is determined by
* comparing their classes. Among classes, numbers and {@code ComparableTuple} instances come first, then come
* {@code String} instances, then come all other classes, in order of their class names.
*
* Rules for comparable objects:
*
* - Numbers of primitive-equivalent types ({@code Long}, {@code Double}, etc, but not {@code BigDecimal}) are
* compared as numbers.
* - Strings are compared according to parameters used to create an instance of {@code TotalOrder}.
* Strict comparison, case-insensitive comparison and collator-based comparison are available. See the factory methods.
* - Instances of {@link ComparableTuple} are compared according to the rules of {@code ComparableTuple}. Also,
* {@code ComparableTuple} and numbers (as in the first rule) are mutually comparable, as if the number was a first element of a tuple.
* - If none of the above apply, instances of the same class that implements {@link Comparable} are compared using the
* class' {@code compareTo()} method.
* - Two objects of the same class that is not {@code Comparable} are compared as their {@code toString()} values.
*
*
* Notes:
*
* - String comparison rules (collator-based or case-insensitive comparison) apply only when comparing {@code String}
* objects. They do not apply when comparing {@code ComparableTuple} elements, when comparing {@code toString()} values,
* or when comparing anything inside some class' {@code compareTo()} method.
* - {@code String} and {@code ComparableTuple} are not mutually comparable (unlike numbers and {@code ComparableTuple}), because of the different
* rules for comparing strings.
*
*
* Usage
*
*
* To sort a set of values, one needs to create a image of that set using {@link #wrap} function, then sort
* that image using {@link TotalOrder#COMPARATOR}. To link back to the original values or some other keys,
* use wrappers with payload - see {@link #wrap(Object, Object)}.
*
*
* Although there is a convenience method {@link #compare(Object, Object)} for single comparison,
* this class does not implement {@code Comparator<Object>}, to avoid performance pitfall when sorting
* non-prepared values.
*
*/
public class TotalOrder {
public static final Comparator COMPARATOR = new ValueComparator();
@Nullable
private final Locale myCaseInsensitiveLocale;
@Nullable
private final Collator myCollator;
private TotalOrder(@Nullable Locale caseInsensitiveLocale, @Nullable Collator collator) {
assert caseInsensitiveLocale == null || collator == null;
myCaseInsensitiveLocale = caseInsensitiveLocale;
myCollator = collator;
}
public static TotalOrder withStrictStringComparison() {
return new TotalOrder(null, null);
}
public static TotalOrder withCaseInsensitiveStringComparison() {
return withCaseInsensitiveStringComparison(null);
}
public static TotalOrder withCaseInsensitiveStringComparison(Locale locale) {
return new TotalOrder(locale == null ? Locale.ROOT : locale, null);
}
public static TotalOrder withCollatorStringComparison(Locale locale) {
return new TotalOrder(null, getCollator(locale));
}
public static TotalOrder withCollatorStringComparison(Locale locale, int strength) {
Collator collator = getCollator(locale);
collator.setStrength(strength);
return new TotalOrder(null, collator);
}
public static TotalOrder withCollatorStringComparison(Locale locale, int strength, int decomposition) {
Collator collator = getCollator(locale);
collator.setStrength(strength);
collator.setDecomposition(decomposition);
return new TotalOrder(null, collator);
}
public static TotalOrder withCollator(Collator collator) {
return new TotalOrder(null, collator);
}
private static Collator getCollator(Locale locale) {
Collator collator = Collator.getInstance(locale);
if (collator == null) {
throw new IllegalArgumentException("cannot get collator for locale " + locale);
}
return collator;
}
public ValueWrapper wrap(Object value) {
return new ValueWrapper(prepareValue(value));
}
public PayloadWrapper wrap(Object value, T payload) {
return new PayloadWrapper<>(prepareValue(value), payload);
}
/**
* Creates the value that is going to be compared.
*/
private Object prepareValue(Object value) {
if (value == null) return null;
// numbers are converted to ComparableTuple for polymorphic comparison
if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) {
return ComparableTuple.of(((Number) value).longValue());
}
if (value instanceof Double || value instanceof Float) {
return ComparableTuple.of(((Number) value).doubleValue());
}
// strings can be converted to lowercase strings or collation keys
if (value instanceof String) {
if (myCaseInsensitiveLocale != null) {
return ((String) value).toLowerCase(myCaseInsensitiveLocale);
}
if (myCollator != null) {
return myCollator.getCollationKey((String) value);
}
}
// other values compared as is
return value;
}
public int compare(Object o1, Object o2) {
ValueWrapper v1 = wrap(o1);
ValueWrapper v2 = wrap(o2);
return COMPARATOR.compare(v1, v2);
}
public class ValueWrapper {
private final Object myValue;
private final Class myClass;
private final boolean myComparable;
public ValueWrapper(Object value) {
myValue = value;
myClass = value == null ? null : value.getClass();
myComparable = value instanceof Comparable;
}
public Object getValue() {
return myValue;
}
public Class getValueClass() {
return myClass;
}
public boolean isComparable() {
return myComparable;
}
public TotalOrder getOrder() {
return TotalOrder.this;
}
@Override
public String toString() {
// return myValue + "(" + myClass + "," + myComparable + ")";
return String.valueOf(myValue);
}
}
public class PayloadWrapper extends ValueWrapper {
private final T myPayload;
public PayloadWrapper(Object value, T payload) {
super(value);
myPayload = payload;
}
public T getPayload() {
return myPayload;
}
@Override
public String toString() {
return super.toString() + "(" + myPayload + ")";
}
}
private static final class ValueComparator implements Comparator {
private static final Class[] CLASS_PRECEDENCE = {
ComparableTuple.class, String.class, CollationKey.class
};
@Override
public int compare(ValueWrapper o1, ValueWrapper o2) {
assert o1 != null;
assert o2 != null;
if (o1 == o2) return 0;
assert o1.getOrder() == o2.getOrder() : "cannot compare values from two different TotalOrder instances " + o1 + " " + o2;
Class c1 = o1.getValueClass();
Class c2 = o2.getValueClass();
// null class means null value - nulls come last
if (c1 == null) return c2 == null ? 0 : 1;
if (c2 == null) return -1;
if (c1 == c2) {
// values of the same class
Object v1 = o1.getValue();
Object v2 = o2.getValue();
assert v1 != null : o1;
assert v2 != null : o2;
if (o1.isComparable()) {
assert o2.isComparable() : o1 + " " + o2;
//noinspection unchecked
return ((Comparable) v1).compareTo(v2);
}
return v1.toString().compareTo(v2.toString());
}
// class comparison
for (Class cls : CLASS_PRECEDENCE) {
if (c1 == cls) return -1;
if (c2 == cls) return 1;
}
return c1.getSimpleName().compareTo(c2.getSimpleName());
}
}
}