UNPKG

@js-joda/extra

Version:

additional date-time classes that complement those in js-joda

548 lines (514 loc) 22.7 kB
/* * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos * @license BSD-3-Clause (see LICENSE in the root directory of this source tree) */ import { DateTimeException, DateTimeParseException, Duration, IllegalArgumentException, Instant, ZonedDateTime } from '@js-joda/core'; // TODO: hm... is this a good idea?? copied from joda currently, could we add a js-joda-utils module?? import { requireNonNull, requireInstance } from './assert'; /** * An immutable interval of time between two instants. * * An interval represents the time on the time-line between two {@link Instant}s. * The class stores the start and end instants, with the start inclusive and the end exclusive. * The end instant is always greater than or equal to the start instant. * * The {@link Duration} of an interval can be obtained, but is a separate concept. * An interval is connected to the time-line, whereas a duration is not. * * Intervals are not comparable. To compare the length of two intervals, it is * generally recommended to compare their durations. * */ export class Interval { //----------------------------------------------------------------------- /** * function overloading for {@link Interval.of} * - If called without arguments, then {@link Interval.ofInstantInstant} is executed. * - If called with 1 arguments and first argument is an instance of ZoneId, then {@link Interval.ofInstantDuration} is executed. * - Otherwise {@link Interval.ofInstantDuration} is executed. * * @param {Instant} startInstant * @param {Instant|Duration} endInstantOrDuration * @return {Interval} */ static of(startInstant, endInstantOrDuration) { if (endInstantOrDuration instanceof Duration) { return Interval.ofInstantDuration(startInstant, endInstantOrDuration); } else { return Interval.ofInstantInstant(startInstant, endInstantOrDuration); } } /** * Obtains an instance of `Interval` from the start and end instant. * * The end instant must not be before the start instant. * * @param {Instant} startInclusive - the start instant, inclusive, MIN_DATE treated as unbounded, not null * @param {Instant} endExclusive - the end instant, exclusive, MAX_DATE treated as unbounded, not null * @return {Interval} the half-open interval, not null * @throws {DateTimeException} if the end is before the start * @protected */ static ofInstantInstant(startInclusive, endExclusive) { requireNonNull(startInclusive, 'startInclusive'); requireNonNull(endExclusive, 'endExclusive'); requireInstance(startInclusive, Instant, 'startInclusive'); requireInstance(endExclusive, Instant, 'endExclusive'); if (endExclusive.isBefore(startInclusive)) { throw new DateTimeException('End instant must on or after start instant'); } return new Interval(startInclusive, endExclusive); } /** * Obtains an instance of `Interval` from the start and a duration. * * The end instant is calculated as the start plus the duration. * The duration must not be negative. * * @param {Instant} startInclusive - the start instant, inclusive, not null * @param {Duration} duration - the duration from the start to the end, not null * @return {Interval} the interval, not null * @throws {DateTimeException} if the end is before the start, * or if the duration addition cannot be made * @throws {ArithmeticException} if numeric overflow occurs when adding the duration * @protected */ static ofInstantDuration(startInclusive, duration) { requireNonNull(startInclusive, 'startInclusive'); requireNonNull(duration, 'duration'); requireInstance(startInclusive, Instant, 'startInclusive'); requireInstance(duration, Duration, 'duration'); if (duration.isNegative()) { throw new DateTimeException('Duration must not be zero or negative'); } return new Interval(startInclusive, startInclusive.plus(duration)); } //----------------------------------------------------------------------- /** * Obtains an instance of `Interval` from a text string such as * `2007-12-03T10:15:30Z/2007-12-04T10:15:30Z`, where the end instant is exclusive. * * The string must consist of one of the following three formats: * - a representations of an {@link ZonedDateTime}, followed by a forward slash, * followed by a representation of a {@link ZonedDateTime} * - a representation of an {@link ZonedDateTime}, followed by a forward slash, * followed by a representation of a {@link Duration} * - a representation of a {@link Duration}, followed by a forward slash, * followed by a representation of an {@link ZonedDateTime} * * NOTE: in contrast to the threeten-extra base we are not using `OffsetDateTime` but `ZonedDateTime` to parse * the string, this does not change the format but adds the possibility to optionally specify a zone * * @param {string} text - the text to parse, not null * @return {Interval} the parsed interval, not null * @throws {DateTimeParseException} if the text cannot be parsed */ static parse(text) { requireNonNull(text, 'text'); if (!(typeof text === 'string')) { throw new IllegalArgumentException(`text must be a string, but is ${text.constructor.name}`); } for (let i = 0; i < text.length; i += 1) { if (text.charAt(i) === '/') { const firstChar = text.charAt(0); if (firstChar === 'P' || firstChar === 'p') { // duration followed by instant const duration = Duration.parse(text.substring(0, i)); const end = ZonedDateTime.parse(text.substring(i + 1, text.length)).toInstant(); return Interval.of(end.minus(duration), end); } else { // instant followed by instant or duration const start = ZonedDateTime.parse(text.substring(0, i)).toInstant(); if (i + 1 < text.length) { const c = text.charAt(i + 1); if (c === 'P' || c === 'p') { const duration = Duration.parse(text.substring(i + 1, text.length)); return Interval.of(start, start.plus(duration)); } } const end = ZonedDateTime.parse(text.substring(i + 1, text.length)).toInstant(); return Interval.of(start, end); } } } throw new DateTimeParseException('Interval cannot be parsed, no forward slash found', text, 0); } //----------------------------------------------------------------------- /** * Constructor. * * @param {Instant} startInclusive - the start instant, inclusive, validated not null * @param {Instant} endExclusive - the end instant, exclusive, validated not null * @private */ constructor(startInclusive, endExclusive) { this._start = startInclusive; this._end = endExclusive; } //----------------------------------------------------------------------- /** * Gets the start of this time interval, inclusive. * * This will return {@link Instant#MIN} if the range is unbounded at the start. * In this case, the range includes all dates into the far-past. * * @return {Instant} the start of the time interval */ start() { return this._start; } /** * Gets the end of this time interval, exclusive. * * This will return {@link Instant#MAX} if the range is unbounded at the end. * In this case, the range includes all dates into the far-future. * * @return {Instant} the end of the time interval, exclusive */ end() { return this._end; } //----------------------------------------------------------------------- /** * Checks if the range is empty. * * An empty range occurs when the start date equals the inclusive end date. * * @return {boolean} true if the range is empty */ isEmpty() { return this._start.equals(this._end); } /** * Checks if the start of the interval is unbounded. * * @return {boolean} true if start is unbounded */ isUnboundedStart() { return this._start.equals(Instant.MIN); } /** * Checks if the end of the interval is unbounded. * * @return {boolean} true if end is unbounded */ isUnboundedEnd() { return this._end.equals(Instant.MAX); } //----------------------------------------------------------------------- /** * Returns a copy of this range with the specified start instant. * * @param {Instant} start - the start instant for the new interval, not null * @return {Interval} an interval with the end from this interval and the specified start * @throws {DateTimeException} if the resulting interval has end before start */ withStart(start) { return Interval.of(start, this._end); } /** * Returns a copy of this range with the specified end instant. * * @param {Instant} end - the end instant for the new interval, not null * @return {Interval} an interval with the start from this interval and the specified end * @throws {DateTimeException} if the resulting interval has end before start */ withEnd(end) { return Interval.of(this._start, end); } //----------------------------------------------------------------------- /** * Checks if this interval contains the specified instant. * * This checks if the specified instant is within the bounds of this interval. * If this range has an unbounded start then `contains(Instant.MIN)` returns true. * If this range has an unbounded end then `contains(Instant.MAX)` returns true. * If this range is empty then this method always returns false. * * @param {Instant} instant - the instant, not null * @return {boolean} true if this interval contains the instant */ contains(instant) { requireNonNull(instant, 'instant'); requireInstance(instant, Instant, 'instant'); return this._start.compareTo(instant) <= 0 && (instant.compareTo(this._end) < 0 || this.isUnboundedEnd()); } /** * Checks if this interval encloses the specified interval. * * This checks if the bounds of the specified interval are within the bounds of this interval. * An empty interval encloses itself. * * @param {Interval} other - the other interval, not null * @return {boolean} true if this interval contains the other interval */ encloses(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); return this._start.compareTo(other.start()) <= 0 && other.end().compareTo(this._end) <= 0; } /** * Checks if this interval abuts the specified interval. * * The result is true if the end of this interval is the start of the other, or vice versa. * An empty interval does not abut itself. * * @param {Interval} other - the other interval, not null * @return {boolean} true if this interval abuts the other interval */ abuts(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); return !this._end.equals(other.start()) !== !this._start.equals(other.end()); } /** * Checks if this interval is connected to the specified interval. * * The result is true if the two intervals have an enclosed interval in common, even if that interval is empty. * An empty interval is connected to itself. * * This is equivalent to `(overlaps(other) || abuts(other))`. * * @param {Interval} other - the other interval, not null * @return {boolean} true if this interval is connected to the other interval */ isConnected(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); return this.equals(other) || (this._start.compareTo(other.end()) <= 0 && other.start().compareTo(this._end) <= 0); } /** * Checks if this interval overlaps the specified interval. * * The result is true if the the two intervals share some part of the time-line. * An empty interval overlaps itself. * * This is equivalent to `(isConnected(other) && !abuts(other))`. * * @param {Interval} other - the time interval to compare to, null means a zero length interval now * @return {boolean} true if the time intervals overlap */ overlaps(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); return other.equals(this) || (this._start.compareTo(other.end()) < 0 && other.start().compareTo(this._end) < 0); } //----------------------------------------------------------------------- /** * Calculates the interval that is the intersection of this interval and the specified interval. * * This finds the intersection of two intervals. * This throws an exception if the two intervals are not {@linkplain #isConnected(Interval) connected}. * * @param {Interval} other - the other interval to check for, not null * @return {Interval} the interval that is the intersection of the two intervals * @throws {DateTimeException} if the intervals do not connect */ intersection(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); if (this.isConnected(other) === false) { throw new DateTimeException(`Intervals 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 Interval.of(newStart, newEnd); } } /** * Calculates the interval that is the union of this interval and the specified interval. * * This finds the union of two intervals. * This throws an exception if the two intervals are not {@linkplain #isConnected(Interval) connected}. * * @param {Interval} other - the other interval to check for, not null * @return {Interval} the interval that is the union of the two intervals * @throws {DateTimeException} if the intervals do not connect */ union(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, 'other'); if (this.isConnected(other) === false) { throw new DateTimeException(`Intervals 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 Interval.of(newStart, newEnd); } } /** * Calculates the smallest interval that encloses this interval and the specified interval. * * The result of this method will {@linkplain #encloses(Interval) enclose} * this interval and the specified interval. * * @param {Interval} other - the other interval to check for, not null * @return {Interval} the interval that spans the two intervals */ span(other) { requireNonNull(other, 'other'); requireInstance(other, Interval, '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 Interval.of(newStart, newEnd); } //------------------------------------------------------------------------- /** * Function overloading for {@link Interval#isAfter} * - If called with an Instant, then {@link Interval#isAfterInstant} is executed. * - Otherwise {@link Interval#isAfterInterval} is executed. * * @param {Instant|Interval} instantOrInterval * @return {boolean} */ isAfter(instantOrInterval) { if (instantOrInterval instanceof Instant) { return this.isAfterInstant(instantOrInterval); } else { return this.isAfterInterval(instantOrInterval); } } /** * Function overloading for {@link Interval#isBefore} * - If called with an Instant, then {@link Interval#isBeforeInstant} is executed. * - Otherwise {@link Interval#isBeforeInterval} is executed. * * @param {Instant|Interval} instantOrInterval * @return {boolean} */ isBefore(instantOrInterval) { if (instantOrInterval instanceof Instant) { return this.isBeforeInstant(instantOrInterval); } else { return this.isBeforeInterval(instantOrInterval); } } /** * Checks if this interval is after the specified instant. * * The result is true if the this instant starts after the specified instant. * An empty interval behaves as though it is an instant for comparison purposes. * * @param {Instant} instant - the other instant to compare to, not null * @return {boolean} true if the start of this interval is after the specified instant */ isAfterInstant(instant) { return this._start.compareTo(instant) > 0; } /** * Checks if this interval is before the specified instant. * * The result is true if the this instant ends before the specified instant. * Since intervals do not include their end points, this will return true if the * instant equals the end of the interval. * An empty interval behaves as though it is an instant for comparison purposes. * * @param {Instant} instant - the other instant to compare to, not null * @return {boolean} true if the start of this interval is before the specified instant */ isBeforeInstant(instant) { return this._end.compareTo(instant) <= 0 && this._start.compareTo(instant) < 0; } //------------------------------------------------------------------------- /** * Checks if this interval is after the specified interval. * * The result is true if the this instant starts after the end of the specified interval. * Since intervals do not include their end points, this will return true if the * instant equals the end of the interval. * An empty interval behaves as though it is an instant for comparison purposes. * * @param {Interval} interval - the other interval to compare to, not null * @return {boolean} true if this instant is after the specified instant */ isAfterInterval(interval) { return this._start.compareTo(interval.end()) >= 0 && !interval.equals(this); } /** * Checks if this interval is before the specified interval. * * The result is true if the this instant ends before the start of the specified interval. * Since intervals do not include their end points, this will return true if the * two intervals abut. * An empty interval behaves as though it is an instant for comparison purposes. * * @param {Interval} interval - the other interval to compare to, not null * @return {boolean} true if this instant is before the specified instant */ isBeforeInterval(interval) { return this._end.compareTo(interval.start()) <= 0 && !interval.equals(this); } //----------------------------------------------------------------------- /** * Obtains the duration of this interval. * * An `Interval` is associated with two specific instants on the time-line. * A `Duration` is simply an amount of time, separate from the time-line. * * @return {Duration} the duration of the time interval * @throws {ArithmeticException} if the calculation exceeds the capacity of `Duration` */ toDuration() { return Duration.between(this._start, this._end); } //----------------------------------------------------------------------- /** * Checks if this interval is equal to another interval. * * Compares this `Interval` with another ensuring that the two instants are the same. * Only objects of type `Interval` 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 interval */ equals(obj) { if (this === obj) { return true; } if (obj instanceof Interval) { return this._start.equals(obj.start()) && this._end.equals(obj.end()); } return false; } /** * A hash code for this interval. * * @return {number} a suitable hash code */ hashCode() { // eslint-disable-next-line no-bitwise return this._start.hashCode() ^ this._end.hashCode(); } //----------------------------------------------------------------------- /** * Outputs this interval as a `String`, such as `2007-12-03T10:15:30/2007-12-04T10:15:30`. * * The output will be the ISO-8601 format formed by combining the * `toString()` methods of the two instants, separated by a forward slash. * * @return {string} a string representation of this instant, not null */ toString() { return `${this._start.toString()}/${this._end.toString()}`; } } export function _init() { Interval.ALL = Interval.of(Instant.MIN, Instant.MAX); }