/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.measure;

import java.util.Objects;
import java.util.Formatter;
import java.util.Formattable;
import java.util.FormattableFlags;
import java.io.Serializable;
import javax.measure.Unit;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Emptiable;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.internal.Strings;
import org.apache.sis.util.internal.ArgumentCheckByAssertion;
import org.apache.sis.util.collection.CheckedContainer;


/**
 * A set of minimum and maximum values of a certain class, allowing
 * a user to determine if a value of the same class is contained inside the range.
 * The minimum and maximum values do not have to be included in the range, and
 * can be null.  If the minimum or maximum values are null, the range is said to
 * be unbounded on that endpoint. If both the minimum and maximum are null,
 * the range is completely unbounded and all values of that class are contained
 * within the range. Null values are always considered <em>exclusive</em>,
 * since iterations over the values will never reach the infinite endpoint.
 *
 * <p>The minimal and maximal values (the <cite>endpoints</cite>) may be inclusive or exclusive.
 * Numeric ranges where both endpoints are inclusive are called <cite>closed intervals</cite>
 * and are represented by square brackets, for example "{@code [0 … 255]}".
 * Numeric ranges where both endpoints are exclusive are called <cite>open intervals</cite>
 * and are represented by parenthesis, for example "{@code (0 … 256)}".</p>
 *
 * <h2>Type and value of range elements</h2>
 * To be a member of a {@code Range}, the {@code <E>} type defining the range must implement the
 * {@link Comparable} interface. All argument values given to the methods of this class shall be
 * or contain instances of that {@code <E>} type. The type is enforced by parameterized type,
 * but some subclasses may put additional constraints. For example, {@link MeasurementRange} will
 * additionally checks the units of measurement. Consequently, every methods defined in this class
 * may throw an {@link IllegalArgumentException} if a given argument does not met some constraint
 * beyond the type.
 *
 * <h2>Relationship with ISO 19123 definition of range</h2>
 * The ISO 19123 standard (<cite>Coverage geometry and functions</cite>) defines the range as the set
 * (either finite or transfinite) of feature attribute
 * values associated by a function (the coverage) with the
 * elements of the coverage domain. In other words, if we see a coverage as a function, then a range
 * is the set of possible return values.
 *
 * <p>The characteristics of the spatial domain are defined by the ISO 19123 standard whereas the
 * characteristics of the attribute range are not part of that standard. In Apache SIS, those
 * characteristics are described by the {@code SampleDimension} class,
 * which may contain one or many {@code Range} instances. Consequently, this {@code Range} class
 * is closely related, but not identical, to the ISO 19123 definition or range.</p>
 *
 * <p>Ranges are not necessarily numeric. Numeric and non-numeric ranges can be associated to
 * discrete coverages, while typically only
 * numeric ranges can be associated to continuous coverages.</p>
 *
 * <h2>Immutability and thread safety</h2>
 * This class and the {@link NumberRange} / {@link MeasurementRange} subclasses are immutable,
 * and thus inherently thread-safe. Other subclasses may or may not be immutable, at implementation choice.
 * But implementers are encouraged to make sure that all subclasses remain immutable for more predictable behavior.
 *
 * @author  Joe White
 * @author  Martin Desruisseaux (Geomatys)
 * @author  Jody Garnett (for parameterized type inspiration)
 * @version 1.4
 *
 * @param <E>  the type of range elements, typically a {@link Number} subclass or {@link java.util.Date}.
 *
 * @see RangeFormat
 * @see org.apache.sis.util.collection.RangeSet
 *
 * @since 0.3
 */
public class Range<E extends Comparable<? super E>> implements CheckedContainer<E>, Formattable, Emptiable, Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 603508245068333284L;

    /**
     * The base type of elements in this range.
     *
     * @see #getElementType()
     */
    final Class<E> elementType;

    /**
     * The minimal and maximal values.
     */
    @SuppressWarnings("serial")         // Not statically typed as Serializable.
    final E minValue, maxValue;

    /**
     * Whether the minimal or maximum value is included.
     */
    final boolean isMinIncluded, isMaxIncluded;

    /**
     * Constructs a range with the same type and the same values than the specified range.
     * This is a copy constructor.
     *
     * @param  range  the range to copy.
     */
    public Range(final Range<E> range) {
        elementType   = range.elementType;
        minValue      = range.minValue;
        isMinIncluded = range.isMinIncluded;
        maxValue      = range.maxValue;
        isMaxIncluded = range.isMaxIncluded;
        assert validate() : elementType;
    }

    /**
     * Creates a new range bounded by the given endpoint values.
     * If the given minimum value is greater than the maximum value, then the range {@linkplain #isEmpty() is empty}.
     *
     * <h4>Assertion</h4>
     * This constructor verifies the {@code minValue} and {@code maxValue} arguments type if Java assertions
     * are enabled. This verification is not performed in normal execution because theoretically unnecessary
     * unless Java generic types have been tricked.
     *
     * @param  elementType    the base type 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.
     * @throws AssertionError if assertions are enabled and the given values are not compatible with the given type.
     */
    @ArgumentCheckByAssertion
    public Range(final Class<E> elementType,
            final E minValue, final boolean isMinIncluded,
            final E maxValue, final boolean isMaxIncluded)
    {
        ArgumentChecks.ensureNonNull("elementType", elementType);
        /*
         * The `isMin/Maxincluded` 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.elementType   = elementType;
        this.minValue      = minValue;
        this.isMinIncluded = isMinIncluded && (minValue != null);
        this.maxValue      = maxValue;
        this.isMaxIncluded = isMaxIncluded && (maxValue != null);
        assert validate() : elementType;
    }

    /**
     * Creates a new range using the same element type than this range. This method will
     * be overridden by subclasses in order to create a range of a more specific type.
     *
     * <h4>API note</h4>
     * This method is invoked by all operations (union, intersection, <i>etc.</i>) that may create a new range.
     * But despite this fact, the return type of those methods are nailed down to {@code Range} (i.e. subclasses
     * shall not override the above-cited operations with covariant return type) because those operations may return
     * the given argument directly, and we have no guarantees on the type of those arguments.
     */
    Range<E> create(final E minValue, final boolean isMinIncluded,
                    final E maxValue, final boolean isMaxIncluded)
    {
        return new Range<>(elementType, minValue, isMinIncluded, maxValue, isMaxIncluded);
    }

    /**
     * Returns an initially empty array of {@code getClass()} type and of the given length.
     * This method is overridden by subclasses in order to create arrays of more specific type.
     * This method is invoked by the {@link #subtract(Range)} method. It is okay to use the new
     * array only if the ranges to store in that array are only {@code this} or new ranges created
     * by the {@link #create(Comparable, boolean, Comparable, boolean)} method - otherwise we may
     * get an {@link ArrayStoreException}.
     */
    @SuppressWarnings({"unchecked","rawtypes"})                     // Generic array creation.
    Range<E>[] newArray(final int length) {
        return new Range[length];
    }

    /**
     * To be overridden by {@link MeasurementRange} only.
     *
     * @return the unit of measurement, or {@code null}.
     */
    Unit<?> unit() {
        return null;
    }

    /**
     * Invoked by the constructors in order to ensure that the argument are of valid types.
     * This check is performed only when assertions are enabled. This test is not needed in
     * normal execution if the users do not bypass the checks performed by generic types.
     */
    private boolean validate() {
        ArgumentChecks.ensureCanCast("minValue", elementType, minValue);
        ArgumentChecks.ensureCanCast("maxValue", elementType, maxValue);
        return Comparable.class.isAssignableFrom(elementType);
    }

    /**
     * Returns the base type of elements in this range.
     * This is the type specified at construction time.
     */
    @Override
    public Class<E> getElementType() {
        return elementType;
    }

    /**
     * Returns the minimal value, or {@code null} if this range has no lower limit.
     * If non-null, the returned value is either inclusive or exclusive depending on
     * the boolean returned by {@link #isMinIncluded()}.
     *
     * @return the minimal value, or {@code null} if this range is unbounded on the lower side.
     */
    public E getMinValue() {
        return minValue;
    }

    /**
     * Returns {@code true} if the {@linkplain #getMinValue() minimal value} is inclusive,
     * or {@code false} if exclusive. Note that {@code null} values are always considered
     * exclusive.
     *
     * @return {@code true} if the minimal value is inclusive, or {@code false} if exclusive.
     */
    public boolean isMinIncluded() {
        return isMinIncluded;
    }

    /**
     * Returns the maximal value, or {@code null} if this range has no upper limit.
     * If non-null, the returned value is either inclusive or exclusive depending on
     * the boolean returned by {@link #isMaxIncluded()}.
     *
     * @return the maximal value, or {@code null} if this range is unbounded on the upper side.
     */
    public E getMaxValue() {
        return maxValue;
    }

    /**
     * Returns {@code true} if the {@linkplain #getMaxValue() maximal value} is inclusive,
     * or {@code false} if exclusive. Note that {@code null} values are always considered
     * exclusive.
     *
     * @return {@code true} if the maximal value is inclusive, or {@code false} if exclusive.
     */
    public boolean isMaxIncluded() {
        return isMaxIncluded;
    }

    /**
     * Returns {@code true} if this range is empty. A range is empty if the
     * {@linkplain #getMinValue() minimum value} is greater than the
     * {@linkplain #getMaxValue() maximum value}, or if they are equal while
     * at least one of them is exclusive, or if both bounds are NaN.
     *
     * <h4>API note</h4>
     * This method is final because often used by the internal implementation.
     * Making the method final ensures that the other methods behave consistently.
     *
     * @return {@code true} if this range is empty.
     */
    @Override
    public final boolean isEmpty() {
        if (minValue == null || maxValue == null) {
            return false;                               // Unbounded: cannot be empty.
        }
        final int c = minValue.compareTo(maxValue);
        if (c < 0) {
            return false;                               // Minimum is smaller than maximum.
        }
        if (c != 0) {                                   // Minimum is NaN or greater than maximum.
            return !isNaN(minValue);
        }
        // If min and max are equal, then the range is empty if at least one of them is exclusive.
        if (!isMinIncluded || !isMaxIncluded) {
            return true;
        }
        return isNaN(minValue);                         // At this point if min is NaN, max is also NaN.
    }

    /**
     * Returns {@code true} if the given value is NaN. This method tests only the primitive wrappers
     * because the behavior of their {@code compareTo(…)} method is clearly documented. Calls to this
     * method assume that NaNs are considered by {@code compareTo(…)} as greater than all other values.
     */
    private static boolean isNaN(final Object value) {
        if (value instanceof Double) return ((Double) value).isNaN();
        if (value instanceof Float)  return ((Float)  value).isNaN();
        return false;
    }

    /**
     * Returns {@code true} if this range is both left-bounded and right-bounded.
     * A {@code true} return value guarantees that:
     *
     * <ol>
     *   <li>both {@link #getMinValue()} and {@link #getMaxValue()} will return non-null values;</li>
     *   <li>if minimum and maximum values are numbers, then those numbers are finite.</li>
     * </ol>
     *
     * @return whether this range is left- and right-bounded.
     *
     * @since 1.0
     */
    public boolean isBounded() {
        return minValue != null && maxValue != null;
    }

    /**
     * Returns {@code true} if this range contains the given value. A range never contains the
     * {@code null} value. This is consistent with the <a href="#skip-navbar_top">class javadoc</a>
     * 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.
     */
    public boolean contains(final E value) {
        if (value == null) {
            return false;
        }
        /*
         * Implementation note: when testing for inclusion or intersection in a range
         * (or in a rectangle, cube, etc.), it is often easier to test when we do not
         * have inclusion than to test for inclusion. So we test when to return false
         * and if no such test pass, we can return true.
         *
         * We consistently use min/maxValue.compareTo(value) in this class rather than
         * the opposite argument order (namely value.compareTo(min/maxValue)) in the
         * hope to reduce the risk of inconsistent behavior if usera pass different
         * sub-classes for the `value` argument with different implementations of the
         * `compareTo` method. Intead than using those user implementations, we always
         * use the implementations provided by min/maxValue.
         */
        if (minValue != null) {
            final int c = minValue.compareTo(value);
            if (isMinIncluded ? (c > 0) : (c >= 0)) {
                return false;
            }
        }
        if (maxValue != null) {
            final int c = maxValue.compareTo(value);
            if (isMaxIncluded ? (c < 0) : (c <= 0)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if the supplied range is fully contained within this 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 if the given range is incompatible,
     *         for example because of incommensurable units of measurement.
     */
    public boolean contains(final Range<? extends E> range) {
        /*
         * We could implement this method as below:
         *
         *     return contains(range.minValue) && contains(range.maxValue);
         *
         * However, the above code performs more comparisons than necessary,
         * since it implicitly performs the following redundant checks:
         *
         *     (range.minValue < maxValue) redundant with (range.maxValue < maxValue)
         *     (range.maxValue > minValue) redundant with (range.minValue > minValue)
         *
         * We can implement this method with less comparisons as below:
         *
         *     return minValue.compareTo(range.minValue) <= 0 &&
         *            maxValue.compareTo(range.maxValue) >= 0;
         *
         * However, we still have a little bit of additional checks to perform for the
         * inclusion status of both ranges.  Since the same checks will be needed for
         * intersection methods,  we factor out the comparisons in `compareMinTo` and
         * `compareMaxTo` methods.
         */
        return (compareMinTo(range.minValue, range.isMinIncluded ? 0 : -1) <= 0) &&
               (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 if the given range is incompatible,
     *         for example because of incommensurable units of measurement.
     */
    public boolean intersects(final Range<? extends E> 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 given range.
     *
     * @param  range  the range to intersect.
     * @return the intersection of this range with the given range.
     * @throws IllegalArgumentException if the given range is incompatible,
     *         for example because of incommensurable units of measurement.
     */
    public Range<E> intersect(final Range<E> range) {
        if (range.isEmpty()) return range;
        if (this .isEmpty()) return this;
        /*
         * For two ranges [L₁ … H₁] and [L₂ … H₂], the intersection is given by
         * ([max(L₁, L₂) … min(H₁, H₂)]). Only two comparisons is needed.
         *
         * There is a small complication since we shall also handle the inclusive states.
         * so instead of extracting the minimal and maximal values directly, we will
         * find which range contains the highest minimal value, and which range contains
         * the smallest maximal value. If we find the same range in both case (which can
         * be either `this` or `range`), return that range. Otherwise we need to create a
         * new one.
         */
        final Range<E> 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 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 if the given range is incompatible,
     *         for example because of incommensurable units of measurement.
     */
    public Range<E> union(final Range<E> range) {
        if (range.isEmpty()) return this;
        if (this .isEmpty()) return range;

        final Range<E> 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;
    }

    /**
     * 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:
     *
     * <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 subtract.
     * @return this range without the given range, as an array of length 0, 1 or 2.
     * @throws IllegalArgumentException if the given range is incompatible,
     *         for example because of incommensurable units of measurement.
     */
    public Range<E>[] subtract(final Range<E> range) {
        /*
         * Implementation note: never store the `range` argument value in the array
         * returned by `newArray(int)`, otherwise we may get an ArrayStoreException.
         */
        final Range<E> 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<E>[] 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<E>[] array = newArray(1);
        array[0] = subtract;
        return array;
    }

    /**
     * Compares the {@linkplain #getMinValue() minimum value} of this range with the given endpoint of
     * another range. Since the given value is either the minimal or maximal value of another range,
     * it may be inclusive or exclusive. The latter is specified by {@code position} as below:
     *
     * <ul>
     *   <li> 0 if {@code value} is inclusive.</li>
     *   <li>-1 if {@code value} is exclusive and lower than the inclusive values of the other range.</li>
     *   <li>+1 if {@code value} is exclusive and higher than the inclusive values of the other range.</li>
     * </ul>
     *
     * Note that the non-zero position shall be exactly -1 or +1, not arbitrary negative or positive.
     *
     * @param  value     an endpoint value of the other range to be compared to the minimal value of this range.
     * @param  position  the position of {@code value} relative to the inclusive values of the other range.
     * @return position (-, + or 0) of the inclusive values of this range compared to the other range.
     *
     * @see #contains(Range)
     */
    private int compareMinTo(final E value, int position) {
        /*
         * Check for infinite values.  If the given value is infinite, it can be either positive or
         * negative infinity, which we can infer from the `position` argument. Note that `position`
         * cannot be 0 in such case, since infinities are always exclusive in this class.
         */
        if (minValue == null) {
            return (value == null) ? 0 : -1;
        }
        if (value == null) {
            return -position;
        }
        /*
         * Compare the two finite values. If they are not equal, we are done regardless the
         * inclusion states, because the difference between included and excluded values is
         * considered smaller than any quantity we can represent.
         */
        final int c = minValue.compareTo(value);
        if (c != 0) {
            return c;
        }
        /*
         * The two values are equal. If the `minValue` of this range is inclusive, then the given
         * `value` is directly at the "right" place (the beginning of the interior of this range),
         * so the `position` argument gives directly the position of the "true minValue" relative
         * to the interior of the other range.
         *
         * But if `minValue` is exclusive, then the "true minValue" of this range is one position
         * to the right  (where "position" is a counter for an infinitely small quantity, similar
         * to `dx` in calculus). The effect is to return 0 if the given `value` is also exclusive
         * and lower than the interior of the other range (position == -1),  and a positive value
         * in all other cases.
         */
        if (!isMinIncluded) {
            position++;
        }
        return position;
    }

    /**
     * Compares the {@linkplain #getMaxValue() maximum value} of this range with the given endpoint
     * of another range. See the comment in {@link #compareMinTo(Comparable, int)} for more details.
     */
    private int compareMaxTo(final E value, int position) {
        if (maxValue == null) {
            return (value == null) ? 0 : +1;
        }
        if (value == null) {
            return -position;
        }
        final int c = maxValue.compareTo(value);
        if (c != 0) {
            return c;
        }
        if (!isMaxIncluded) {
            position--;
        }
        return position;
    }

    /**
     * Compares this range with the given object for equality.
     * Two ranges are considered equal if they met the following conditions:
     *
     * <ul>
     *   <li>They are of the same {@linkplain #getClass() class}.</li>
     *   <li>They have the same {@linkplain #getElementType() element type}.</li>
     *   <li>Both ranges {@linkplain #isEmpty() are empty}, <strong>or</strong> (if at least one range is non-empty):
     *     <ul>
     *       <li>They have equal {@linkplain #getMinValue() minimum} value in the sense of {@link Object#equals(Object)}.</li>
     *       <li>They have equal {@linkplain #getMaxValue() maximum} value in the sense of {@link Object#equals(Object)}.</li>
     *       <li>They have equal {@linkplain #isMinIncluded() inclusive minimum} flag.</li>
     *       <li>They have equal {@linkplain #isMaxIncluded() inclusive maximum} flag.</li>
     *     </ul>
     *   <li>Any other requirement added by subclasses.
     *       In particular {@link MeasurementRange} compares also the units of measurement.</li>
     * </ul>
     *
     * Note that this method may return {@code true} even if the bounds are not strictly identical.
     * In particular this method returns {@code true} if the ranges are empty regardless their minimum and maximum values,
     * and also returns {@code true} if the bounds are wrappers for some {@link Float#NaN} or {@link Double#NaN} values
     * even if their {@linkplain Double#doubleToRawLongBits(double) raw bits pattern} are not the same.
     * The latter is because {@link Float#equals(Object)} and {@link Double#equals(Object)} consider all NaN values as equal.
     *
     * @param  object  the object to compare with this range for equality.
     * @return {@code true} if the given object is equal to this range.
     */
    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true;
        }
        if (object != null && object.getClass() == getClass()) {
            final Range<?> other = (Range<?>) object;
            if (Objects.equals(elementType, other.elementType)) {
                if (isEmpty()) {
                    return other.isEmpty();
                }
                return Objects.equals(minValue, other.minValue) &&
                       Objects.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 hash = elementType.hashCode();
        if (!isEmpty()) {
            hash = 13 * hash + Objects.hashCode(minValue);
            hash = 13 * hash + Objects.hashCode(maxValue);
            hash += isMinIncluded ?   17 :   37;
            hash += isMaxIncluded ? 1231 : 1237;
        }
        return hash ^ (int) serialVersionUID;
    }

    /**
     * Returns {@code true} if the given number is formatted with only one character.
     * We will use less space if the minimum and maximum values are formatted using
     * only one digit. This method assumes that we have verified that the element type
     * is an integer type before to invoke this method.
     */
    private static boolean isCompact(final Comparable<?> value, final boolean ifNull) {
        if (value == null) {
            return ifNull;
        }
        final long n = ((Number) value).longValue();
        return n >= 0 && n < 10;
    }

    /**
     * Returns a unlocalized string representation of this range. This method complies to the format
     * described in the <a href="https://en.wikipedia.org/wiki/ISO_31-11">ISO 31-11</a> standard,
     * except that the minimal and maximal values are separated by the "{@code …}" character
     * instead of coma. More specifically, the string representation is defined as below:
     *
     * <ul>
     *   <li>If the range {@linkplain #isEmpty() is empty}, then this method returns "{@code {}}".</li>
     *   <li>Otherwise if the minimal value is equal to the maximal value, then the string
     *       representation of that value is returned inside braces as in "{@code {value}}".</li>
     *   <li>Otherwise the string representation of the minimal and maximal values are formatted
     *       like "{@code [min … max]}" for inclusive endpoints or "{@code (min … max)}" for exclusive
     *       endpoints, or a mix of both styles. The "{@code ∞}" symbol is used in place of
     *       {@code min} or {@code max} for unbounded ranges.</li>
     * </ul>
     *
     * If this range is a {@link MeasurementRange}, then the {@linkplain Unit unit of measurement}
     * is appended to the above string representation except for empty ranges.
     *
     * @see RangeFormat
     * @see <a href="https://en.wikipedia.org/wiki/ISO_31-11">Wikipedia: ISO 31-11</a>
     */
    @Override
    public String toString() {
        if (isEmpty()) {
            return "{}";
        }
        final StringBuilder buffer = new StringBuilder(20);
        if (minValue != null && minValue.equals(maxValue)) {
            buffer.append('{').append(minValue).append('}');
        } else {
            buffer.append(isMinIncluded ? '[' : '(');
            if (minValue == null) {
                buffer.append("−∞");
            } else {
                buffer.append(minValue);
            }
            // Compact representation for integers, more space for real numbers.
            if (Numbers.isInteger(elementType) && isCompact(minValue, false) && isCompact(maxValue, true)) {
                buffer.append('…');
            } else {
                buffer.append(" … ");
            }
            if (maxValue == null) {
                buffer.append('∞');
            } else {
                buffer.append(maxValue);
            }
            buffer.append(isMaxIncluded ? ']' : ')');
        }
        final Unit<?> unit = unit();
        if (unit != null) {
            final String symbol = unit.toString();
            if (symbol != null && !symbol.isEmpty()) {
                if (Character.isLetterOrDigit(symbol.codePointAt(0))) {
                    buffer.append(' ');
                }
                buffer.append(symbol);
            }
        }
        return buffer.toString();
    }

    /**
     * Formats this range using the provided formatter. This method is invoked when a
     * {@code Range} object is formatted using the {@code "%s"} conversion specifier of
     * {@link Formatter}. Users don't need to invoke this method explicitly.
     *
     * <p>If the alternate flags is present (as in {@code "%#s"}), then the range will
     * be formatted using the {@linkplain RangeFormat#isAlternateForm() alternate form}
     * for exclusive bounds.</p>
     *
     * @param  formatter  the formatter in which to format this range.
     * @param  flags      {@link FormattableFlags#LEFT_JUSTIFY} for left alignment, or 0 for right alignment.
     * @param  width      minimal number of characters to write, padding with {@code ' '} if necessary.
     * @param  precision  maximal number of characters to write, or -1 if no limit.
     */
    @Override
    public void formatTo(final Formatter formatter, final int flags, final int width, int precision) {
        final String value;
        if (precision == 0) {
            value = "";
        } else {
            final RangeFormat format = new RangeFormat(formatter.locale(), elementType);
            format.setAlternateForm((flags & FormattableFlags.ALTERNATE) != 0);
            value = format.format(this, new StringBuffer(), null).toString();
        }
        Strings.formatTo(formatter, flags, width, precision, value);
    }
}
