/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.util; import java.io.Serializable; import javax.measure.unit.Unit; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * A range between a minimum and maximum comparable. The minimum/maximum may be included, excluded * or unbounded. The later case is indicated by {@code null} values on one or both ends. * <p> * This class is a method compatible replacement for the {@link javax.media.jai.util.Range} class * with the following differences: * * <ul> * <li><p>Unbounded ranges (i.e. {@code null} minimal or maximal values) are considered * <em>exclusive</em> rather than inclusive, since an iteration over the values will * never reach the infinite bound. This interpretation brings some simplification in * implementation and usage (e.g. a loop over the values should not attempt to process * the {@code null} value).</p></li> * * <li><p>{@link #subtract} returns an empty array if the whole range is subtracted.</p></li> * </ul> * * The exact {@linkplain #getElementClass element class} doesn't need to be known at compile time. * Widening conversions are allowed as needed (subclasses like {@link NumberRange} do that). This * class is weakly parameterized in order to allow this flexibility. If any constructor or method * is invoked with an argument value of illegal class, then an {@link IllegalArgumentException} is * thrown. The {@link ClassCastException} is thrown only in case of bug in the {@code Range} class * or subclasses implementation. * * @param <T> The type of range elements, typically {@link java.util.Date} or some subclass * of {@link Number}. * * @since 2.5 * * @source $URL$ * @version $Id$ * @author Jody Garnett * @author Martin Desruisseaux * * @see javax.media.jai.util.Range */ public class Range<T extends Comparable<? super T>> implements Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = -5393896130562660517L; /** * The class of elements. */ final Class<T> elementClass; /** * The minimal and maximal value. */ final T minValue, maxValue; /** * Whatever the minimal or maximum value is included. */ private final boolean isMinIncluded, isMaxIncluded; /** * Creates a new range bounded by a single inclusive value. The {@linkplain #getMinimum minimum} * and {@linkplain #getMaximum maximum} values are set to the given one. * * @param elementClass * The class of the range elements. * @param value * The minimal and maximum value (inclusive), or {@code null} for an unbounded range. */ public Range(final Class<T> elementClass, final T value) { this(elementClass, value, true, value, true); } /** * Creates a new range bounded by the given inclusive values. * * @param elementClass The class of the range elements. * @param minValue The minimal value (inclusive), or {@code null} if none. * @param maxValue The maximal value (inclusive), or {@code null} if none. */ public Range(final Class<T> elementClass, final T minValue, final T maxValue) { this(elementClass, minValue, true, maxValue, true); } /** * Creates a new range bounded by the given values. * * @param elementClass * The class of the range elements. * @param minValue * The minimal value, or {@code null} if none. * @param isMinIncluded * {@code true} if the minimal value is inclusive, or {@code false} if exclusive. * @param maxValue * The maximal value, or {@code null} if none. * @param isMaxIncluded * {@code true} if the maximal value is inclusive, or {@code false} if exclusive. */ public Range(final Class<T> elementClass, final T minValue, final boolean isMinIncluded, final T maxValue, final boolean isMaxIncluded) { ensureNonNull("elementClass", elementClass); this.elementClass = elementClass; /* * The "included" flags must be forced to 'false' if 'minValue' or 'maxValue' are null. * This is required for proper working of algorithms implemented in this class. */ this.minValue = minValue; this.maxValue = maxValue; this.isMinIncluded = isMinIncluded && minValue != null; this.isMaxIncluded = isMaxIncluded && maxValue != null; checkElementClass(); if (minValue != null) ensureCompatible(minValue.getClass()); if (maxValue != null) ensureCompatible(maxValue.getClass()); } /** * Creates a new range using the same element class than this range. This method will * be overriden by subclasses in order to create a range of a more specific type. */ Range<T> create(final T minValue, final boolean isMinIncluded, final T maxValue, final boolean isMaxIncluded) { return new Range<T>(elementClass, minValue, isMinIncluded, maxValue, isMaxIncluded); } /** * Ensures that the given argument is non-null. */ static void ensureNonNull(final String name, final Object value) throws IllegalArgumentException { if (value == null) { throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name)); } } /** * Ensures that the given range use the same element class than this range. */ @SuppressWarnings("unchecked") private Range<? extends T> ensureCompatible(final Range<?> range) throws IllegalArgumentException { ensureNonNull("range", range); ensureCompatible(range.elementClass); return (Range<? extends T>) range; } /** * Ensures that the given type is compatible with the type expected by this range. */ private void ensureCompatible(final Class<?> type) throws IllegalArgumentException { if (!elementClass.isAssignableFrom(type)) { throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2, type, elementClass)); } } /** * Ensures that {@link #elementClass} is compatible with the type expected by this range class. * Used at construction time for argument check. To be overriden by {@link NumberRange} only. */ void checkElementClass() throws IllegalArgumentException { } /** * Returns an initially empty array of the given length. To be overriden by {@link NumberRange} * and subclasses in order to create arrays of more specific type. */ @SuppressWarnings("unchecked") // Generic array creation. Range<T>[] newArray(final int length) { return new Range[length]; } /** * Returns the class of elements in this range. The element class extends {@link Comparable}. * * @return The class of elements in this range. * * @see javax.media.jai.util.Range#getElementClass */ public Class<T> getElementClass() { return elementClass; } /** * Returns the minimal value, or {@code null} if unbounded. If {@link #isMinIncluded} * is {@code true}, then the value is considered included in the set. Otherwise it is * considered excluded. * * @return The minimal value. * * @see javax.media.jai.util.Range#getMinValue */ public T getMinValue() { return minValue; } /** * Indicates if {@link #getMinValue} is included in the range. * * @return {@code true} if the minimal value is inclusive. * * @see javax.media.jai.util.Range#isMinIncluded */ public boolean isMinIncluded() { return isMinIncluded; } /** * Returns the maximal value, or {@code null} if unbounded. If {@link #isMaxIncluded} * is {@code true}, then the value is considered included in the set. Otherwise it is * considered excluded. * * @return The maximal value. * * @see javax.media.jai.util.Range#getMaxValue */ public T getMaxValue() { return maxValue; } /** * Indicates if {@link #getMaxValue} is included in the range. * * @return {@code true} if the maximal value is inclusive. * * @see javax.media.jai.util.Range#isMaxIncluded */ public boolean isMaxIncluded() { return isMaxIncluded; } /** * Returns {@code true} if this range is empty. A range is empty if the * {@linkplain #getMinValue minimum value} is smaller than the * {@linkplain #getMaxValue maximum value}, or if they are equals while * at least one of them is exclusive. * * @return {@code true} if this range is empty. * * @see javax.media.jai.util.Range#isEmpty */ public boolean isEmpty() { if (minValue == null || maxValue == null) { return false; // Unbounded: can't be empty. } final int c = minValue.compareTo(maxValue); if (c < 0) { return false; // Minimum is smaller than maximum. } // If min and max are equals, empty if at least one of them is exclusive. return c != 0 || !isMinIncluded || !isMaxIncluded; } /** * Returns {@code true} if this range contains the given value. A range never contains the * {@code null} value. This is consistent with the {@linkplain Range class javadoc} stating * that null {@linkplain #getMinValue minimum} or {@linkplain #getMaxValue maximum} values * are exclusive. * * @param value The value to check for inclusion in this range. * @return {@code true} if the given value is included in this range. * @throws IllegalArgumentException is the given value can not be converted to a valid type * through widening conversion. */ public boolean contains(final Comparable<?> value) throws IllegalArgumentException { if (value == null) { return false; } ensureCompatible(value.getClass()); @SuppressWarnings("unchecked") final T c = (T) value; return containsNC(c); } /** * Implementation of {@link #contains(T)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final boolean containsNC(final T value) { if (minValue != null) { final int c = minValue.compareTo(value); if (c >= 0) { if (c != 0 || !isMinIncluded) { return false; } } } if (maxValue != null) { final int c = maxValue.compareTo(value); if (c <= 0) { if (c != 0 || !isMaxIncluded) { return false; } } } return true; } /** * Returns {@code true} if this range contains fully the given range. * * @param range The range to check for inclusion in this range. * @return {@code true} if the given range is included in this range. * @throws IllegalArgumentException is the given range can not be converted to a valid type * through widening conversion. */ public boolean contains(final Range<?> range) throws IllegalArgumentException { return containsNC(ensureCompatible(range)); } /** * Implementation of {@link #contains(Range)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final boolean containsNC(final Range<? extends T> range) { return (minValue == null || compareMinTo(range.minValue, range.isMinIncluded ? 0 : +1) <= 0) && (maxValue == null || compareMaxTo(range.maxValue, range.isMaxIncluded ? 0 : -1) >= 0); } /** * Returns {@code true} if this range intersects the given range. * * @param range The range to check for intersection with this range. * @return {@code true} if the given range intersects this range. * @throws IllegalArgumentException is the given range can not be converted to a valid type * through widening conversion. * * @see javax.media.jai.util.Range#intersects */ public boolean intersects(final Range<?> range) throws IllegalArgumentException { return intersectsNC(ensureCompatible(range)); } /** * Implementation of {@link #intersects(Range)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final boolean intersectsNC(final Range<? extends T> range) { return compareMinTo(range.maxValue, range.isMaxIncluded ? 0 : -1) <= 0 && compareMaxTo(range.minValue, range.isMinIncluded ? 0 : +1) >= 0; } /** * Returns the intersection between this range and the provided range. * * @param range The range to intersect. * @return The intersection of this range with the provided range. * @throws IllegalArgumentException is the given range can not be converted to a valid type * through widening conversion. * * @see javax.media.jai.util.Range#intersect */ public Range<?> intersect(final Range<?> range) throws IllegalArgumentException { return intersectNC(ensureCompatible(range)); } /** * Implementation of {@link #intersect(Range)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final Range<? extends T> intersectNC(final Range<? extends T> range) throws IllegalArgumentException { final Range<? extends T> intersect, min, max; min = compareMinTo(range.minValue, range.isMinIncluded ? 0 : +1) < 0 ? range : this; max = compareMaxTo(range.maxValue, range.isMaxIncluded ? 0 : -1) > 0 ? range : this; if (min == max) { intersect = min; } else { intersect = create(min.minValue, min.isMinIncluded, max.maxValue, max.isMaxIncluded); } assert intersect.isEmpty() == !intersects(range) : intersect; return intersect; } /** * Returns the range of values that are in this range but not in the given range. * This method returns an array of length 0, 1 or 2: * <p> * <ul> * <li>If the given range contains fully this range, returns an array of length 0.</li> * <li>If the given range is in the middle of this range, then the subtraction results in * two disjoint ranges which will be returned as two elements in the array.</li> * <li>Otherwise returns an array of length 1.</li> * </ul> * * @param range The range to substract. * @return This range without the given range. * @throws IllegalArgumentException is the given range can not be converted to a valid type * through widening conversion. * * @see javax.media.jai.util.Range#subtract */ public Range<?>[] subtract(final Range<?> range) throws IllegalArgumentException { return subtractNC(ensureCompatible(range)); } /** * Implementation of {@link #subtract(Range)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final Range<T>[] subtractNC(final Range<? extends T> range) throws IllegalArgumentException { final Range<T> subtract; if (!intersects(range)) { subtract = this; } else { final boolean clipMin = compareMinTo(range.minValue, range.isMinIncluded ? 0 : +1) >= 0; final boolean clipMax = compareMaxTo(range.maxValue, range.isMaxIncluded ? 0 : -1) <= 0; if (clipMin) { if (clipMax) { // The given range contains fully this range. assert range.contains(this) : range; return newArray(0); } subtract = create(range.maxValue, !range.isMaxIncluded, maxValue, isMaxIncluded); } else { if (!clipMax) { final Range<T>[] array = newArray(2); array[0] = create(minValue, isMinIncluded, range.minValue, !range.isMinIncluded); array[1] = create(range.maxValue, !range.isMaxIncluded, maxValue, isMaxIncluded); return array; } subtract = create(minValue, isMinIncluded, range.minValue, !range.isMinIncluded); } } assert contains(subtract) : subtract; assert !subtract.intersects(range) : subtract; final Range<T>[] array = newArray(1); array[0] = subtract; return array; } /** * Returns the union of this range with the given range. * * @param range The range to add to this range. * @return The union of this range with the given range. * @throws IllegalArgumentException is the given range can not be converted to a valid type * through widening conversion. * * @see javax.media.jai.util.Range#union */ public Range<?> union(final Range<?> range) throws IllegalArgumentException { return unionNC(ensureCompatible(range)); } /** * Implementation of {@link #union(Range)} to be invoked directly by subclasses. * "NC" stands for "No Cast" - this method do not try to cast the value to a compatible type. */ final Range<?> unionNC(final Range<? extends T> range) throws IllegalArgumentException { final Range<? extends T> union, min, max; min = compareMinTo(range.minValue, range.isMinIncluded ? 0 : +1) > 0 ? range : this; max = compareMaxTo(range.maxValue, range.isMaxIncluded ? 0 : -1) < 0 ? range : this; if (min == max) { union = min; } else { union = create(min.minValue, min.isMinIncluded, max.maxValue, max.isMaxIncluded); } assert union.contains(min) : min; assert union.contains(max) : max; return union; } /** * Compares the provided value with the {@linkplain #getMinValue minimum value}, * taking in account the included or excluded state. * * @param value The value to compare to this range {@linkplain #getMinValue minimum}. * @param delta 0 if the value is inclusive, -1 if exclusive and the first inclusive value is * lower, or +1 if exclusive and the first inclusive value is higher than the given one. * @return 0 if the given value is equal to the {@link #minValue} and both are included, * negative if {@link #minValue} is lower, positive if {@link #minValue} is higher. */ final int compareMinTo(final T value, final int delta) { if (value == null) { /* * The given value is infinity. It could be positive or negative infinity (i.e. greater * or lower than this range value), which we can infer from the 'delta' value in this * Range implementation. 'delta' should never be 0 since infinities are exclusive. */ return delta; } if (minValue == null) { /* * This range bound is negative infinity while the given value is not. Note that we * perform this test after the 'value' test because if both values are infinity, we * want to retain the interpretation given by the argument (conceptually, the given * infinity still included in this range if 'delta' is -1). */ return -1; } final int c = minValue.compareTo(value); if (c == 0) { if (isMinIncluded) { /* * Returns 0 if 'value' is inclusive as well, or -1 if 'value' is exclusive * and smaller than first inclusive value (indicated by 'delta' = +1). */ return -delta; } if (delta <= 0) { /* * The range 'minValue' is exclusive, so the first inclusive value is greater. * The only case where we don't return +1 is if the given argument is in the * same case (first inclusive value is greater the given value). */ return +1; } } return c; } /** * Compares the provided value with the {@linkplain #getMaxValue maximum value}, * taking in account the included or excluded state. * * @param value The value to compare to this range {@linkplain #getMaxValue maximum}. * @param delta 0 if the value is inclusive, -1 if exclusive and the first inclusive value is * lower, or +1 if exclusive and the first inclusive value is higher than the given one. * @return 0 if the given value is equal to the {@link #maxValue} and both are included, * negative if {@link #maxValue} is lower, positive if {@link #maxValue} is higher. */ final int compareMaxTo(final T value, final int delta) { // Same comments than 'compareMinTo' with logic reversed. if (value == null) { return delta; } if (maxValue == null) { return +1; } final int c = maxValue.compareTo(value); if (c == 0) { if (isMaxIncluded) { return -delta; } if (delta >= 0) { return -1; } } return c; } /** * Compares this range with the given object for equality. * * @param object The object to compare with this range for equality. * @return {@code true} if the given object is equals to this range. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (object != null && object.getClass().equals(getClass())) { final Range<?> other = (Range) object; if (Utilities.equals(elementClass, other.elementClass)) { if (isEmpty()) { return other.isEmpty(); } return Utilities.equals(minValue, other.minValue) && Utilities.equals(maxValue, other.maxValue) && isMinIncluded == other.isMinIncluded && isMaxIncluded == other.isMaxIncluded; } } return false; } /** * Returns a hash code value for this range. */ @Override public int hashCode() { int result = (int) serialVersionUID; if (!isEmpty()) { result += elementClass.hashCode(); result = Utilities.hash(isMaxIncluded, result); result = Utilities.hash(isMinIncluded, result); result = Utilities.hash(maxValue, result); result = Utilities.hash(minValue, result); } return result; } /** * To be overriden by {@link MeasurementRange} only. */ Unit<?> getUnits() { return null; } /** * Returns a string representation of this range. */ @Override public String toString() { if (isEmpty()) { return "[]"; } final StringBuilder buffer = new StringBuilder(); buffer.append(isMinIncluded ? '[' : '('); if (minValue == null) { buffer.append("-\u221E"); // Infinity } else { buffer.append(minValue); } buffer.append(", "); if (maxValue == null) { buffer.append('\u221E'); // Infinity } else { buffer.append(maxValue); } buffer.append(isMaxIncluded ? ']' : ')'); final Unit<?> units = getUnits(); if (units != null) { buffer.append(' ').append(units); } return buffer.toString(); } }