Home Reference Source

packages/extra/src/LocalDateRange.js

/**
 * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper & Michał Sobkiewicz
 * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 * @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
 */

import { ArithmeticException, DateTimeException, DateTimeParseException, IllegalArgumentException, LocalDate, Period } from '@js-joda/core';

// TODO: hm... is this a good idea?? copied from joda currently, could we add a js-joda-utils module??
import { requireInstance, requireNonNull } from './assert';

/**
 * The day after the MIN date.
 */
const MINP1 = LocalDate.MIN.plusDays(1);
/**
 * The day before the MAX date.
 */
const MAXM1 = LocalDate.MAX.minusDays(1);

/**
 * A range of local dates.
 * 
 * A `LocalDateRange` represents a range of dates, from a start date to an end date.
 * Instances can be constructed from either a half-open or a closed range of dates.
 * Internally, the class stores the start and end dates, with the start inclusive and the end exclusive.
 * The end date is always greater than or equal to the start date.
 * 
 * The constants `LocalDate.MIN` and `LocalDate.MAX` can be used
 * to indicate an unbounded far-past or far-future. Note that there is no difference
 * between a half-open and a closed range when the end is `LocalDate.MAX`.
 * Empty ranges are allowed.
 * 
 * No range can end at `LocalDate.MIN` or `LocalDate.MIN.plusDays(1)`.
 * No range can start at `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
 * No empty range can exist at `LocalDate.MIN` or `LocalDate.MAX`.
 * 
 * Date ranges are not comparable. To compare the length of two ranges, it is
 * generally recommended to compare the number of days they contain.
 *
 */
export class LocalDateRange {
    /**
     * function overloading for {@link LocalDateRange.of}
     * - if called with `LocalDate` and `LocalDate`, {@link LocalDateRange._ofLocalDateLocalDate} is executed,
     * - if called with `LocalDate` and `Period`, {@link LocalDateRange._ofLocalDatePeriod} is executed,
     * - otherwise throws IllegalArgumentException.
     *
     * @param {LocalDate} startInclusive
     * @param {LocalDate|Period} endExclusiveOrPeriod
     * @return {LocalDateRange}
     */
    static of(startInclusive, endExclusiveOrPeriod) {
        if (startInclusive instanceof LocalDate && endExclusiveOrPeriod instanceof LocalDate) {
            return LocalDateRange._ofLocalDateLocalDate(startInclusive, endExclusiveOrPeriod);
        }
        if (startInclusive instanceof LocalDate && endExclusiveOrPeriod instanceof Period) {
            return LocalDateRange._ofLocalDatePeriod(startInclusive, endExclusiveOrPeriod);
        }
        const messageParts = [];
        if (!(startInclusive instanceof LocalDate)) {
            messageParts.push(`startInclusive must be an instance of LocalDate but is ${startInclusive.constructor.name}`);
        }
        if (!(endExclusiveOrPeriod instanceof LocalDate || endExclusiveOrPeriod instanceof Period)) {
            messageParts.push(`endExclusiveOrPeriod must be an instance of LocalDate or Period but is ${endExclusiveOrPeriod.constructor.name}`);
        }
        throw new IllegalArgumentException(messageParts.join(' and '));
    }

    //-----------------------------------------------------------------------
    /**
     * Obtains a half-open range of dates, including the start and excluding the end.
     * 
     * The range includes the start date and excludes the end date, unless the end is `LocalDate.MAX`.
     * The end date must be equal to or after the start date.
     * This definition permits an empty range located at a specific date.
     * 
     * The constants `LocalDate.MIN` and `LocalDate.MAX` can be used
     * to indicate an unbounded far-past or far-future.
     * 
     * The start inclusive date must not be `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
     * The end inclusive date must not be `LocalDate.MIN` or `LocalDate.MIN.plusDays(1)`.
     * No empty range can exist at `LocalDate.MIN` or `LocalDate.MAX`.
     *
     * @param {LocalDate} startInclusive - the inclusive start date, not null
     * @param {LocalDate} endExclusive - the exclusive end date, not null
     * @return {LocalDateRange} the half-open range, not null
     * @throws {DateTimeException} if the end is before the start,
     *   or the start date is `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`,
     *   or the end date is `LocalDate.MIN` or `LocalDate.MIN.plusDays(1)`
     * @protected
     */
    static _ofLocalDateLocalDate(startInclusive, endExclusive) {
        requireNonNull(startInclusive, 'startInclusive');
        requireNonNull(endExclusive, 'endExclusive');
        requireInstance(startInclusive, LocalDate, 'startInclusive');
        requireInstance(endExclusive, LocalDate, 'endExclusive');
        return new LocalDateRange(startInclusive, endExclusive);
    }

    /**
     * Obtains an instance of `LocalDateRange` from the start and a period.
     * 
     * The end date is calculated as the start plus the duration.
     * The period must not be negative.
     * 
     * The constant `LocalDate.MIN` can be used to indicate an unbounded far-past.
     * 
     * The period must not be zero or one day when the start date is `LocalDate.MIN`.
     *
     * @param {LocalDate} startInclusive - the inclusive start date, not null
     * @param {Period} period - the period from the start to the end, not null
     * @return {LocalDateRange} the range, not null
     * @throws {DateTimeException} if the end is before the start,
     *  or if the period addition cannot be made
     * @throws {ArithmeticException} if numeric overflow occurs when adding the period
     * @protected
     */
    static _ofLocalDatePeriod(startInclusive, period) {
        requireNonNull(startInclusive, 'startInclusive');
        requireNonNull(period, 'period');
        requireInstance(startInclusive, LocalDate, 'startInclusive');
        requireInstance(period, Period, 'period');
        if (period.isNegative()) {
            throw new DateTimeException('Period must not be zero or negative');
        }
        return new LocalDateRange(startInclusive, startInclusive.plus(period));
    }

    /**
     * Obtains a closed range of dates, including the start and end.
     * 
     * The range includes the start date and the end date.
     * The end date must be equal to or after the start date.
     * 
     * The constants `LocalDate.MIN` and `LocalDate.MAX` can be used
     * to indicate an unbounded far-past or far-future. In addition, an end date of
     * `LocalDate.MAX.minusDays(1)` will also create an unbounded far-future range.
     * 
     * The start inclusive date must not be `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
     * The end inclusive date must not be `LocalDate.MIN`.
     * 
     * @param {LocalDate} startInclusive - the inclusive start date, not null
     * @param {LocalDate} endInclusive - the inclusive end date, not null
     * @return {LocalDateRange} the closed range
     * @throws {DateTimeException} if the end is before the start,
     *   or the start date is `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`,
     *   or the end date is `LocalDate.MIN`
     */
    static ofClosed(startInclusive, endInclusive) {
        requireNonNull(startInclusive, 'startInclusive');
        requireNonNull(endInclusive, 'endInclusive');
        requireInstance(startInclusive, LocalDate, 'startInclusive');
        requireInstance(endInclusive, LocalDate, 'endInclusive');
        if (endInclusive.isBefore(startInclusive)) {
            throw new DateTimeException('Start date must be on or before end date');
        }
        const end = (endInclusive.equals(LocalDate.MAX) ? LocalDate.MAX : endInclusive.plusDays(1));
        return new LocalDateRange(startInclusive, end);
    }

    /**
     * Obtains an empty date range located at the specified date.
     * 
     * The empty range has zero length and contains no other dates or ranges.
     * An empty range cannot be located at `LocalDate.MIN`, `LocalDate.MIN.plusDays(1)`,
     * `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
     *
     * @param {LocalDate} date - the date where the empty range is located, not null
     * @return {LocalDateRange} the empty range, not null
     * @throws {DateTimeException} if the date is `LocalDate.MIN`, `LocalDate.MIN.plusDays(1)`,
     *   `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`
     */
    static ofEmpty(date) {
        requireNonNull(date, 'date');
        requireInstance(date, LocalDate, 'date');
        return new LocalDateRange(date, date);
    }

    /**
     * Obtains a range that is unbounded at the start and end.
     * 
     * @return {LocalDateRange} the range, with an unbounded start and unbounded end
     */
    static ofUnbounded() {
        return LocalDateRange.ALL;
    }

    /**
     * Obtains a range up to, but not including, the specified end date.
     * 
     * The range includes all dates from the unbounded start, denoted by `LocalDate.MIN`, to the end date.
     * The end date is exclusive and cannot be `LocalDate.MIN` or `LocalDate.MIN.plusDays(1)`.
     * 
     * @param {LocalDate} endExclusive - the exclusive end date, `LocalDate.MAX` treated as unbounded, not null
     * @return {LocalDateRange} the range, with an unbounded start
     * @throws {DateTimeException} if the end date is `LocalDate.MIN` or  `LocalDate.MIN.plusDays(1)`
     */
    static ofUnboundedStart(endExclusive) {
        requireNonNull(endExclusive, 'endExclusive');
        requireInstance(endExclusive, LocalDate, 'endExclusive');
        return LocalDateRange.of(LocalDate.MIN, endExclusive);
    }

    /**
     * Obtains a range from and including the specified start date.
     * 
     * The range includes all dates from the start date to the unbounded end, denoted by `LocalDate.MAX`.
     * The start date is inclusive and cannot be `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
     * 
     * @param {LocalDate} startInclusive - the inclusive start date, `LocalDate.MIN` treated as unbounded, not null
     * @return {LocalDateRange} the range, with an unbounded end
     * @throws {DateTimeException} if the start date is `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`
     */
    static ofUnboundedEnd(startInclusive) {
        return LocalDateRange.of(startInclusive, LocalDate.MAX);
    }

    //-----------------------------------------------------------------------
    /**
     * Obtains an instance of `LocalDateRange` from a text string such as
     * `2007-12-03/2007-12-04`, where the end date is exclusive.
     * 
     * The string must consist of one of the following three formats:
     * <ul>
     * <li>a representations of an {@link LocalDate}, followed by a forward slash,
     *  followed by a representation of a {@link LocalDate}
     * <li>a representation of an {@link LocalDate}, followed by a forward slash,
     *  followed by a representation of a {@link Period}
     * <li>a representation of a {@link Period}, followed by a forward slash,
     *  followed by a representation of an {@link LocalDate}
     * </ul>
     *
     * @param {string} text - the text to parse, not null
     * @return {LocalDateRange} the parsed range, not null
     * @throws {DateTimeParseException} if the text cannot be parsed
     */
    static parse(text) {
        requireNonNull(text, 'text');
        for (let i = 0; i < text.length; i++) {
            if (text[i] === '/') {
                const firstChar = text.charAt(0);
                if (firstChar === 'P' || firstChar === 'p') {
                    // period followed by date
                    const duration = Period.parse(text.slice(0, i));
                    const end = LocalDate.parse(text.slice(i + 1, text.length));
                    return LocalDateRange.of(end.minus(duration), end);
                } else {
                    // date followed by date or period
                    const start = LocalDate.parse(text.slice(0, i));
                    if (i + 1 < text.length) {
                        const c = text[i + 1];
                        if (c === 'P' || c === 'p') {
                            const duration = Period.parse(text.slice(i + 1, text.length));
                            return LocalDateRange.of(start, start.plus(duration));
                        }
                    }
                    const end = LocalDate.parse(text.slice(i + 1, text.length));
                    return LocalDateRange.of(start, end);
                }
            }
        }
        throw new DateTimeParseException('LocalDateRange cannot be parsed, no forward slash found', text, 0);
    }

    //-----------------------------------------------------------------------
    /**
     * Constructor.
     *
     * @param {LocalDate} startInclusive - the start date, inclusive, validated not null
     * @param {LocalDate} endExclusive - the end date, exclusive, validated not null
     * @private
     */
    constructor(startInclusive, endExclusive) {
        requireNonNull(startInclusive, 'startInclusive');
        requireNonNull(endExclusive, 'endExclusive');
        requireInstance(startInclusive, LocalDate, 'startInclusive');
        requireInstance(endExclusive, LocalDate, 'endExclusive');
        if (endExclusive.isBefore(startInclusive)) {
            throw new DateTimeException('End date must be on or after start date');
        }
        if (startInclusive.equals(MAXM1)) {
            throw new DateTimeException('Range must not start at LocalDate.MAX.minusDays(1)');
        }
        if (endExclusive.equals(MINP1)) {
            throw new DateTimeException('Range must not end at LocalDate.MIN.plusDays(1)');
        }
        if (endExclusive.equals(LocalDate.MIN) || startInclusive.equals(LocalDate.MAX)) {
            throw new DateTimeException('Empty range must not be at LocalDate.MIN or LocalDate.MAX');
        }
        this._start = startInclusive;
        this._end = endExclusive;
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the start date of this range, inclusive.
     * 
     * This will return `LocalDate#MIN` if the range is unbounded at the start.
     * In this case, the range includes all dates into the far-past.
     * 
     * This never returns `LocalDate.MAX` or `LocalDate.MAX.minusDays(1)`.
     *
     * @return {LocalDate} the start date
     */
    start() {
        return this._start;
    }

    /**
     * Gets the end date of this range, exclusive.
     * 
     * This will return `LocalDate.MAX` if the range is unbounded at the end.
     * In this case, the range includes all dates into the far-future.
     * 
     * This never returns `LocalDate.MIN` or `LocalDate.MIN.plusDays(1)`.
     *
     * @return {LocalDate} the end date, exclusive
     */
    end() {
        return this._end;
    }

    /**
     * Gets the end date of this range, inclusive.
     * 
     * This will return `LocalDate.MAX` if the range is unbounded at the end.
     * In this case, the range includes all dates into the far-future.
     * 
     * This returns the date before the end date.
     * 
     * This never returns `LocalDate.MIN`.
     * 
     * @return {LocalDate} the end date, inclusive
     */
    endInclusive() {
        if (this.isUnboundedEnd()) {
            return LocalDate.MAX;
        }
        return this._end.minusDays(1);
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if the range is empty.
     * 
     * An empty range occurs when the start date equals the end date.
     * 
     * An empty range is never unbounded.
     * 
     * @return {boolean} true if the range is empty
     */
    isEmpty() {
        return this._start.equals(this._end);
    }

    /**
     * Checks if the start of the range is unbounded.
     * 
     * An unbounded range is never empty.
     * 
     * @return {boolean} true if start is unbounded
     */
    isUnboundedStart() {
        return this._start.equals(LocalDate.MIN);
    }

    /**
     * Checks if the end of the range is unbounded.
     * 
     * An unbounded range is never empty.
     * 
     * @return {boolean} true if end is unbounded
     */
    isUnboundedEnd() {
        return this._end.equals(LocalDate.MAX);
    }

    //-----------------------------------------------------------------------
    /**
     * Returns a copy of this range with the start date adjusted.
     * 
     * This returns a new instance with the start date altered.
     * Since `LocalDate` implements `TemporalAdjuster` any
     * local date can simply be passed in.
     * 
     * For example, to adjust the start to one week earlier:
     * <pre>
     *  range = range.withStart(date -&gt; date.minus(1, ChronoUnit.WEEKS));
     * </pre>
     * 
     * @param {TemporalAdjuster} adjuster - the adjuster to use, not null
     * @return {LocalDateRange} a copy of this range with the start date adjusted
     * @throws {DateTimeException} if the new start date is after the current end date
     */
    withStart(adjuster) {
        return LocalDateRange.of(this._start.with(adjuster), this._end);
    }

    /**
     * Returns a copy of this range with the end date adjusted.
     * 
     * This returns a new instance with the exclusive end date altered.
     * Since `LocalDate` implements `TemporalAdjuster` any
     * local date can simply be passed in.
     * 
     * For example, to adjust the end to one week later:
     * <pre>
     *  range = range.withEnd(date -&gt; date.plus(1, ChronoUnit.WEEKS));
     * </pre>
     * 
     * @param {TemporalAdjuster} adjuster - the adjuster to use, not null
     * @return {LocalDateRange} a copy of this range with the end date adjusted
     * @throws {DateTimeException} if the new end date is before the current start date
     */
    withEnd(adjuster) {
        return LocalDateRange.of(this._start, this._end.with(adjuster));
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this range contains the specified date.
     * 
     * This checks if the specified date is within the bounds of this range.
     * If this range is empty then this method always returns false.
     * Else if this range has an unbounded start then `contains(LocalDate#MIN)` returns true.
     * Else if this range has an unbounded end then `contains(LocalDate#MAX)` returns true.
     * 
     * @param {LocalDate} date - the date to check for, not null
     * @return {boolean} true if this range contains the date
     */
    contains(date) {
        requireNonNull(date, 'date');
        return this._start.compareTo(date) <= 0 && (date.compareTo(this._end) < 0 || this.isUnboundedEnd());
    }

    /**
     * Checks if this range encloses the specified range.
     * 
     * This checks if the bounds of the specified range are within the bounds of this range.
     * An empty range encloses itself.
     * 
     * @param {LocalDateRange} other - the other range to check for, not null
     * @return {boolean} true if this range contains all dates in the other range
     */
    encloses(other) {
        requireNonNull(other, 'other');
        return this._start.compareTo(other._start) <= 0 && other._end.compareTo(this._end) <= 0;
    }

    /**
     * Checks if this range abuts the specified range.
     * 
     * The result is true if the end of this range is the start of the other, or vice versa.
     * An empty range does not abut itself.
     *
     * @param {LocalDateRange} other - the other range, not null
     * @return {boolean} true if this range abuts the other range
     */
    abuts(other) {
        requireNonNull(other, 'other');
        return this._end.equals(other._start) !== this._start.equals(other._end);
    }

    /**
     * Checks if this range is connected to the specified range.
     * 
     * The result is true if the two ranges have an enclosed range in common, even if that range is empty.
     * An empty range is connected to itself.
     * 
     * This is equivalent to `(overlaps(other) || abuts(other))`.
     *
     * @param {LocalDateRange} other - the other range, not null
     * @return {boolean} true if this range is connected to the other range
     */
    isConnected(other) {
        requireNonNull(other, 'other');
        return this.equals(other) || (this._start.compareTo(other._end) <= 0 && other._start.compareTo(this._end) <= 0);
    }

    /**
     * Checks if this range overlaps the specified range.
     * 
     * The result is true if the two ranges share some part of the time-line.
     * An empty range overlaps itself.
     * 
     * This is equivalent to `(isConnected(other) && !abuts(other))`.
     *
     * @param {LocalDateRange} other - the time range to compare to, null means a zero length range now
     * @return {boolean} true if the time ranges overlap
     */
    overlaps(other) {
        requireNonNull(other, 'other');
        return other.equals(this) || (this._start.compareTo(other._end) < 0 && other._start.compareTo(this._end) < 0);
    }

    //-----------------------------------------------------------------------
    /**
     * Calculates the range that is the intersection of this range and the specified range.
     * 
     * This finds the intersection of two ranges.
     * This throws an exception if the two ranges are not {@linkplain #isConnected(LocalDateRange) connected}.
     * 
     * @param {LocalDateRange} other - the other range to check for, not null
     * @return {LocalDateRange} the range that is the intersection of the two ranges
     * @throws {DateTimeException} if the ranges do not connect
     */
    intersection(other) {
        requireNonNull(other, 'other');
        if (this.isConnected(other) === false) {
            throw new DateTimeException(`Ranges do not connect: ${this} and ${other}`);
        }
        const cmpStart = this._start.compareTo(other._start);
        const cmpEnd = this._end.compareTo(other._end);
        if (cmpStart >= 0 && cmpEnd <= 0) {
            return this;
        } else if (cmpStart <= 0 && cmpEnd >= 0) {
            return other;
        } else {
            const newStart = (cmpStart >= 0 ? this._start : other._start);
            const newEnd = (cmpEnd <= 0 ? this._end : other._end);
            return LocalDateRange.of(newStart, newEnd);
        }
    }

    /**
     * Calculates the range that is the union of this range and the specified range.
     * 
     * This finds the union of two ranges.
     * This throws an exception if the two ranges are not {@linkplain #isConnected(LocalDateRange) connected}.
     * 
     * @param {LocalDateRange} other - the other range to check for, not null
     * @return {LocalDateRange} the range that is the union of the two ranges
     * @throws {DateTimeException} if the ranges do not connect
     */
    union(other) {
        requireNonNull(other, 'other');
        if (this.isConnected(other) === false) {
            throw new DateTimeException(`Ranges do not connect: ${this} and ${other}`);
        }
        const cmpStart = this._start.compareTo(other._start);
        const cmpEnd = this._end.compareTo(other._end);
        if (cmpStart >= 0 && cmpEnd <= 0) {
            return other;
        } else if (cmpStart <= 0 && cmpEnd >= 0) {
            return this;
        } else {
            const newStart = (cmpStart >= 0 ? other._start : this._start);
            const newEnd = (cmpEnd <= 0 ? other._end : this._end);
            return LocalDateRange.of(newStart, newEnd);
        }
    }

    /**
     * Calculates the smallest range that encloses this range and the specified range.
     * 
     * The result of this method will {@linkplain #encloses(LocalDateRange) enclose}
     * this range and the specified range.
     * 
     * @param {LocalDateRange} other - the other range to check for, not null
     * @return {LocalDateRange} the range that spans the two ranges
     */
    span(other) {
        requireNonNull(other, 'other');
        const cmpStart = this._start.compareTo(other._start);
        const cmpEnd = this._end.compareTo(other._end);
        const newStart = (cmpStart >= 0 ? other._start : this._start);
        const newEnd = (cmpEnd <= 0 ? other._end : this._end);
        return LocalDateRange.of(newStart, newEnd);
    }

    /**
     * Function overloading for {@link LocalDateRange.isAfter}
     * - if called with `LocalDate`, {@link LocalDateRange._isAfterLocalDate} is executed,
     * - if called with `LocalDateRange`, {@link LocalDateRange._isAfterLocalDateRange} is executed,
     * - otherwise throws IllegalArgumentException.
     *
     * @param {LocalDate|LocalDateRange} localDateOrLocalDateRange
     * @return {boolean}
     */
    isAfter(localDateOrLocalDateRange) {
        if (localDateOrLocalDateRange instanceof LocalDate) {
            return this._isAfterLocalDate(localDateOrLocalDateRange);
        }
        if (localDateOrLocalDateRange instanceof LocalDateRange) {
            return this._isAfterLocalDateRange(localDateOrLocalDateRange);
        }
        throw new IllegalArgumentException(`localDateOrLocalDateRange must be an instance of LocalDate or LocalDateRange but is ${localDateOrLocalDateRange.constructor.name}`);
    }

    /**
     * Function overloading for {@link LocalDateRange.isBefore}
     * - if called with `LocalDate`, {@link LocalDateRange._isBeforeLocalDate} is executed,
     * - if called with `LocalDateRange`, {@link LocalDateRange._isBeforeLocalDateRange} is executed,
     * - otherwise throws IllegalArgumentException.
     *
     * @param {LocalDate|LocalDateRange} localDateOrLocalDateRange
     * @return {boolean}
     */
    isBefore(localDateOrLocalDateRange) {
        if (localDateOrLocalDateRange instanceof LocalDate) {
            return this._isBeforeLocalDate(localDateOrLocalDateRange);
        }
        if (localDateOrLocalDateRange instanceof LocalDateRange) {
            return this._isBeforeLocalDateRange(localDateOrLocalDateRange);
        }
        throw new IllegalArgumentException(`localDateOrLocalDateRange must be an instance of LocalDate or LocalDateRange but is ${localDateOrLocalDateRange.constructor.name}`);
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this range is after the specified date.
     * 
     * The result is true if every date in this range is after the specified date.
     * An empty range behaves as though it is a date for comparison purposes.
     *
     * @param {LocalDate} date - the other date to compare to, not null
     * @return {boolean} true if the start of this range is after the specified date
     * @protected
     */
    _isAfterLocalDate(date) {
        return this._start.compareTo(date) > 0;
    }

    /**
     * Checks if this range is before the specified date.
     * 
     * The result is true if every date in this range is before the specified date.
     * An empty range behaves as though it is a date for comparison purposes.
     *
     * @param {LocalDate} date - the other date to compare to, not null
     * @return {boolean} true if the start of this range is before the specified date
     * @protected
     */
    _isBeforeLocalDate(date) {
        return this._end.compareTo(date) <= 0 && this._start.compareTo(date) < 0;
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this range is after the specified range.
     * 
     * The result is true if every date in this range is after every date in the specified range.
     * An empty range behaves as though it is a date for comparison purposes.
     *
     * @param {LocalDateRange} other - the other range to compare to, not null
     * @return {boolean} true if every date in this range is after every date in the other range
     * @protected
     */
    _isAfterLocalDateRange(other) {
        return this._start.compareTo(other._end) >= 0 && !other.equals(this);
    }

    /**
     * Checks if this range is before the specified range.
     * 
     * The result is true if every date in this range is before every date in the specified range.
     * An empty range behaves as though it is a date for comparison purposes.
     *
     * @param {LocalDateRange} range - the other range to compare to, not null
     * @return {boolean} true if every date in this range is before every date in the other range
     * @protected
     */
    _isBeforeLocalDateRange(range) {
        return this._end.compareTo(range._start) <= 0 && !range.equals(this);
    }

    //-----------------------------------------------------------------------
    /**
     * Obtains the length of this range in days.
     * 
     * This returns the number of days between the start and end dates.
     * Unbounded ranges return `Number.POSITIVE_INFINITY`.
     *
     * @return {number} the length in days, `Number.POSITIVE_INFINITY` if unbounded
     */
    lengthInDays() {
        if (this.isUnboundedStart() || this.isUnboundedEnd()) {
            return Number.POSITIVE_INFINITY;
        }
        return this._end.toEpochDay() - this._start.toEpochDay();
    }

    /**
     * Obtains the length of this range as a period.
     * 
     * This returns the {@link Period} between the start and end dates.
     * Unbounded ranges throw {@link ArithmeticException}.
     *
     * @return {Period} the period of the range
     * @throws {ArithmeticException} if the calculation exceeds the capacity of `Period`,
     *   or the range is unbounded
     */
    toPeriod() {
        if (this.isUnboundedStart() || this.isUnboundedEnd()) {
            throw new ArithmeticException('Unbounded range cannot be converted to a Period');
        }
        return Period.between(this._start, this._end);
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if this range is equal to another range.
     * 
     * Compares this `LocalDateRange` with another ensuring that the two dates are the same.
     * Only objects of type `LocalDateRange` are compared, other types return false.
     *
     * @param {*} obj - the object to check, null returns false
     * @return {boolean} true if this is equal to the other range
     */
    equals(obj) {
        if (this === obj) {
            return true;
        }
        if (obj instanceof LocalDateRange) {
            const other = obj;
            return this._start.equals(other._start) && this._end.equals(other._end);
        }
        return false;
    }

    /**
     * A hash code for this range.
     *
     * @return {number} a suitable hash code
     */
    hashCode() {
        return this._start.hashCode() ^ this._end.hashCode();
    }

    //-----------------------------------------------------------------------
    /**
     * Outputs this range as a `String`, such as `2007-12-03/2007-12-04`.
     * 
     * The output will be the ISO-8601 format formed by combining the
     * `toString()` methods of the two dates, separated by a forward slash.
     *
     * @return {string} a string representation of this date, not null
     */
    toString() {
        return `${this._start.toString()}/${this._end.toString()}`;
    }
}

export function _init() {
    /**
     * A range over the whole time-line.
     */
    LocalDateRange.ALL = new LocalDateRange(LocalDate.MIN, LocalDate.MAX);
}