All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almworks.jira.structure.api.util.TotalOrder Maven / Gradle / Ivy

There is a newer version: 17.25.3
Show newest version
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()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy