mirror of
https://github.com/swc-project/swc.git
synced 2024-12-27 23:56:32 +03:00
605 lines
19 KiB
TypeScript
605 lines
19 KiB
TypeScript
// Loaded from https://unpkg.com/luxon@1.25.0/src/interval.js
|
||
|
||
|
||
import DateTime, { friendlyDateTime } from "./datetime.js";
|
||
import Duration, { friendlyDuration } from "./duration.js";
|
||
import Settings from "./settings.js";
|
||
import { InvalidArgumentError, InvalidIntervalError } from "./errors.js";
|
||
import Invalid from "./impl/invalid.js";
|
||
|
||
const INVALID = "Invalid Interval";
|
||
|
||
// checks if the start is equal to or before the end
|
||
function validateStartEnd(start, end) {
|
||
if (!start || !start.isValid) {
|
||
return Interval.invalid("missing or invalid start");
|
||
} else if (!end || !end.isValid) {
|
||
return Interval.invalid("missing or invalid end");
|
||
} else if (end < start) {
|
||
return Interval.invalid(
|
||
"end before start",
|
||
`The end of an interval must be after its start, but you had start=${start.toISO()} and end=${end.toISO()}`
|
||
);
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}. Conceptually, it's a container for those two endpoints, accompanied by methods for creating, parsing, interrogating, comparing, transforming, and formatting them.
|
||
*
|
||
* Here is a brief overview of the most commonly used methods and getters in Interval:
|
||
*
|
||
* * **Creation** To create an Interval, use {@link fromDateTimes}, {@link after}, {@link before}, or {@link fromISO}.
|
||
* * **Accessors** Use {@link start} and {@link end} to get the start and end.
|
||
* * **Interrogation** To analyze the Interval, use {@link count}, {@link length}, {@link hasSame}, {@link contains}, {@link isAfter}, or {@link isBefore}.
|
||
* * **Transformation** To create other Intervals out of this one, use {@link set}, {@link splitAt}, {@link splitBy}, {@link divideEqually}, {@link merge}, {@link xor}, {@link union}, {@link intersection}, or {@link difference}.
|
||
* * **Comparison** To compare this Interval to another one, use {@link equals}, {@link overlaps}, {@link abutsStart}, {@link abutsEnd}, {@link engulfs}.
|
||
* * **Output** To convert the Interval into other representations, see {@link toString}, {@link toISO}, {@link toISODate}, {@link toISOTime}, {@link toFormat}, and {@link toDuration}.
|
||
*/
|
||
export default class Interval {
|
||
/**
|
||
* @private
|
||
*/
|
||
constructor(config) {
|
||
/**
|
||
* @access private
|
||
*/
|
||
this.s = config.start;
|
||
/**
|
||
* @access private
|
||
*/
|
||
this.e = config.end;
|
||
/**
|
||
* @access private
|
||
*/
|
||
this.invalid = config.invalid || null;
|
||
/**
|
||
* @access private
|
||
*/
|
||
this.isLuxonInterval = true;
|
||
}
|
||
|
||
/**
|
||
* Create an invalid Interval.
|
||
* @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent
|
||
* @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
|
||
* @return {Interval}
|
||
*/
|
||
static invalid(reason, explanation = null) {
|
||
if (!reason) {
|
||
throw new InvalidArgumentError("need to specify a reason the Interval is invalid");
|
||
}
|
||
|
||
const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);
|
||
|
||
if (Settings.throwOnInvalid) {
|
||
throw new InvalidIntervalError(invalid);
|
||
} else {
|
||
return new Interval({ invalid });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.
|
||
* @param {DateTime|Date|Object} start
|
||
* @param {DateTime|Date|Object} end
|
||
* @return {Interval}
|
||
*/
|
||
static fromDateTimes(start, end) {
|
||
const builtStart = friendlyDateTime(start),
|
||
builtEnd = friendlyDateTime(end);
|
||
|
||
const validateError = validateStartEnd(builtStart, builtEnd);
|
||
|
||
if (validateError == null) {
|
||
return new Interval({
|
||
start: builtStart,
|
||
end: builtEnd
|
||
});
|
||
} else {
|
||
return validateError;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create an Interval from a start DateTime and a Duration to extend to.
|
||
* @param {DateTime|Date|Object} start
|
||
* @param {Duration|Object|number} duration - the length of the Interval.
|
||
* @return {Interval}
|
||
*/
|
||
static after(start, duration) {
|
||
const dur = friendlyDuration(duration),
|
||
dt = friendlyDateTime(start);
|
||
return Interval.fromDateTimes(dt, dt.plus(dur));
|
||
}
|
||
|
||
/**
|
||
* Create an Interval from an end DateTime and a Duration to extend backwards to.
|
||
* @param {DateTime|Date|Object} end
|
||
* @param {Duration|Object|number} duration - the length of the Interval.
|
||
* @return {Interval}
|
||
*/
|
||
static before(end, duration) {
|
||
const dur = friendlyDuration(duration),
|
||
dt = friendlyDateTime(end);
|
||
return Interval.fromDateTimes(dt.minus(dur), dt);
|
||
}
|
||
|
||
/**
|
||
* Create an Interval from an ISO 8601 string.
|
||
* Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.
|
||
* @param {string} text - the ISO string to parse
|
||
* @param {Object} [opts] - options to pass {@link DateTime.fromISO} and optionally {@link Duration.fromISO}
|
||
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
||
* @return {Interval}
|
||
*/
|
||
static fromISO(text, opts) {
|
||
const [s, e] = (text || "").split("/", 2);
|
||
if (s && e) {
|
||
let start, startIsValid;
|
||
try {
|
||
start = DateTime.fromISO(s, opts);
|
||
startIsValid = start.isValid;
|
||
} catch (e) {
|
||
startIsValid = false;
|
||
}
|
||
|
||
let end, endIsValid;
|
||
try {
|
||
end = DateTime.fromISO(e, opts);
|
||
endIsValid = end.isValid;
|
||
} catch (e) {
|
||
endIsValid = false;
|
||
}
|
||
|
||
if (startIsValid && endIsValid) {
|
||
return Interval.fromDateTimes(start, end);
|
||
}
|
||
|
||
if (startIsValid) {
|
||
const dur = Duration.fromISO(e, opts);
|
||
if (dur.isValid) {
|
||
return Interval.after(start, dur);
|
||
}
|
||
} else if (endIsValid) {
|
||
const dur = Duration.fromISO(s, opts);
|
||
if (dur.isValid) {
|
||
return Interval.before(end, dur);
|
||
}
|
||
}
|
||
}
|
||
return Interval.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
|
||
}
|
||
|
||
/**
|
||
* Check if an object is an Interval. Works across context boundaries
|
||
* @param {object} o
|
||
* @return {boolean}
|
||
*/
|
||
static isInterval(o) {
|
||
return (o && o.isLuxonInterval) || false;
|
||
}
|
||
|
||
/**
|
||
* Returns the start of the Interval
|
||
* @type {DateTime}
|
||
*/
|
||
get start() {
|
||
return this.isValid ? this.s : null;
|
||
}
|
||
|
||
/**
|
||
* Returns the end of the Interval
|
||
* @type {DateTime}
|
||
*/
|
||
get end() {
|
||
return this.isValid ? this.e : null;
|
||
}
|
||
|
||
/**
|
||
* Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.
|
||
* @type {boolean}
|
||
*/
|
||
get isValid() {
|
||
return this.invalidReason === null;
|
||
}
|
||
|
||
/**
|
||
* Returns an error code if this Interval is invalid, or null if the Interval is valid
|
||
* @type {string}
|
||
*/
|
||
get invalidReason() {
|
||
return this.invalid ? this.invalid.reason : null;
|
||
}
|
||
|
||
/**
|
||
* Returns an explanation of why this Interval became invalid, or null if the Interval is valid
|
||
* @type {string}
|
||
*/
|
||
get invalidExplanation() {
|
||
return this.invalid ? this.invalid.explanation : null;
|
||
}
|
||
|
||
/**
|
||
* Returns the length of the Interval in the specified unit.
|
||
* @param {string} unit - the unit (such as 'hours' or 'days') to return the length in.
|
||
* @return {number}
|
||
*/
|
||
length(unit = "milliseconds") {
|
||
return this.isValid ? this.toDuration(...[unit]).get(unit) : NaN;
|
||
}
|
||
|
||
/**
|
||
* Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.
|
||
* Unlike {@link length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'
|
||
* asks 'what dates are included in this interval?', not 'how many days long is this interval?'
|
||
* @param {string} [unit='milliseconds'] - the unit of time to count.
|
||
* @return {number}
|
||
*/
|
||
count(unit = "milliseconds") {
|
||
if (!this.isValid) return NaN;
|
||
const start = this.start.startOf(unit),
|
||
end = this.end.startOf(unit);
|
||
return Math.floor(end.diff(start, unit).get(unit)) + 1;
|
||
}
|
||
|
||
/**
|
||
* Returns whether this Interval's start and end are both in the same unit of time
|
||
* @param {string} unit - the unit of time to check sameness on
|
||
* @return {boolean}
|
||
*/
|
||
hasSame(unit) {
|
||
return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval has the same start and end DateTimes.
|
||
* @return {boolean}
|
||
*/
|
||
isEmpty() {
|
||
return this.s.valueOf() === this.e.valueOf();
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval's start is after the specified DateTime.
|
||
* @param {DateTime} dateTime
|
||
* @return {boolean}
|
||
*/
|
||
isAfter(dateTime) {
|
||
if (!this.isValid) return false;
|
||
return this.s > dateTime;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval's end is before the specified DateTime.
|
||
* @param {DateTime} dateTime
|
||
* @return {boolean}
|
||
*/
|
||
isBefore(dateTime) {
|
||
if (!this.isValid) return false;
|
||
return this.e <= dateTime;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval contains the specified DateTime.
|
||
* @param {DateTime} dateTime
|
||
* @return {boolean}
|
||
*/
|
||
contains(dateTime) {
|
||
if (!this.isValid) return false;
|
||
return this.s <= dateTime && this.e > dateTime;
|
||
}
|
||
|
||
/**
|
||
* "Sets" the start and/or end dates. Returns a newly-constructed Interval.
|
||
* @param {Object} values - the values to set
|
||
* @param {DateTime} values.start - the starting DateTime
|
||
* @param {DateTime} values.end - the ending DateTime
|
||
* @return {Interval}
|
||
*/
|
||
set({ start, end } = {}) {
|
||
if (!this.isValid) return this;
|
||
return Interval.fromDateTimes(start || this.s, end || this.e);
|
||
}
|
||
|
||
/**
|
||
* Split this Interval at each of the specified DateTimes
|
||
* @param {...[DateTime]} dateTimes - the unit of time to count.
|
||
* @return {[Interval]}
|
||
*/
|
||
splitAt(...dateTimes) {
|
||
if (!this.isValid) return [];
|
||
const sorted = dateTimes
|
||
.map(friendlyDateTime)
|
||
.filter(d => this.contains(d))
|
||
.sort(),
|
||
results = [];
|
||
let { s } = this,
|
||
i = 0;
|
||
|
||
while (s < this.e) {
|
||
const added = sorted[i] || this.e,
|
||
next = +added > +this.e ? this.e : added;
|
||
results.push(Interval.fromDateTimes(s, next));
|
||
s = next;
|
||
i += 1;
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Split this Interval into smaller Intervals, each of the specified length.
|
||
* Left over time is grouped into a smaller interval
|
||
* @param {Duration|Object|number} duration - The length of each resulting interval.
|
||
* @return {[Interval]}
|
||
*/
|
||
splitBy(duration) {
|
||
const dur = friendlyDuration(duration);
|
||
|
||
if (!this.isValid || !dur.isValid || dur.as("milliseconds") === 0) {
|
||
return [];
|
||
}
|
||
|
||
let { s } = this,
|
||
added,
|
||
next;
|
||
|
||
const results = [];
|
||
while (s < this.e) {
|
||
added = s.plus(dur);
|
||
next = +added > +this.e ? this.e : added;
|
||
results.push(Interval.fromDateTimes(s, next));
|
||
s = next;
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Split this Interval into the specified number of smaller intervals.
|
||
* @param {number} numberOfParts - The number of Intervals to divide the Interval into.
|
||
* @return {[Interval]}
|
||
*/
|
||
divideEqually(numberOfParts) {
|
||
if (!this.isValid) return [];
|
||
return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts);
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval overlaps with the specified Interval
|
||
* @param {Interval} other
|
||
* @return {boolean}
|
||
*/
|
||
overlaps(other) {
|
||
return this.e > other.s && this.s < other.e;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval's end is adjacent to the specified Interval's start.
|
||
* @param {Interval} other
|
||
* @return {boolean}
|
||
*/
|
||
abutsStart(other) {
|
||
if (!this.isValid) return false;
|
||
return +this.e === +other.s;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval's start is adjacent to the specified Interval's end.
|
||
* @param {Interval} other
|
||
* @return {boolean}
|
||
*/
|
||
abutsEnd(other) {
|
||
if (!this.isValid) return false;
|
||
return +other.e === +this.s;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval engulfs the start and end of the specified Interval.
|
||
* @param {Interval} other
|
||
* @return {boolean}
|
||
*/
|
||
engulfs(other) {
|
||
if (!this.isValid) return false;
|
||
return this.s <= other.s && this.e >= other.e;
|
||
}
|
||
|
||
/**
|
||
* Return whether this Interval has the same start and end as the specified Interval.
|
||
* @param {Interval} other
|
||
* @return {boolean}
|
||
*/
|
||
equals(other) {
|
||
if (!this.isValid || !other.isValid) {
|
||
return false;
|
||
}
|
||
|
||
return this.s.equals(other.s) && this.e.equals(other.e);
|
||
}
|
||
|
||
/**
|
||
* Return an Interval representing the intersection of this Interval and the specified Interval.
|
||
* Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.
|
||
* Returns null if the intersection is empty, meaning, the intervals don't intersect.
|
||
* @param {Interval} other
|
||
* @return {Interval}
|
||
*/
|
||
intersection(other) {
|
||
if (!this.isValid) return this;
|
||
const s = this.s > other.s ? this.s : other.s,
|
||
e = this.e < other.e ? this.e : other.e;
|
||
|
||
if (s > e) {
|
||
return null;
|
||
} else {
|
||
return Interval.fromDateTimes(s, e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Return an Interval representing the union of this Interval and the specified Interval.
|
||
* Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.
|
||
* @param {Interval} other
|
||
* @return {Interval}
|
||
*/
|
||
union(other) {
|
||
if (!this.isValid) return this;
|
||
const s = this.s < other.s ? this.s : other.s,
|
||
e = this.e > other.e ? this.e : other.e;
|
||
return Interval.fromDateTimes(s, e);
|
||
}
|
||
|
||
/**
|
||
* Merge an array of Intervals into a equivalent minimal set of Intervals.
|
||
* Combines overlapping and adjacent Intervals.
|
||
* @param {[Interval]} intervals
|
||
* @return {[Interval]}
|
||
*/
|
||
static merge(intervals) {
|
||
const [found, final] = intervals.sort((a, b) => a.s - b.s).reduce(
|
||
([sofar, current], item) => {
|
||
if (!current) {
|
||
return [sofar, item];
|
||
} else if (current.overlaps(item) || current.abutsStart(item)) {
|
||
return [sofar, current.union(item)];
|
||
} else {
|
||
return [sofar.concat([current]), item];
|
||
}
|
||
},
|
||
[[], null]
|
||
);
|
||
if (final) {
|
||
found.push(final);
|
||
}
|
||
return found;
|
||
}
|
||
|
||
/**
|
||
* Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.
|
||
* @param {[Interval]} intervals
|
||
* @return {[Interval]}
|
||
*/
|
||
static xor(intervals) {
|
||
let start = null,
|
||
currentCount = 0;
|
||
const results = [],
|
||
ends = intervals.map(i => [{ time: i.s, type: "s" }, { time: i.e, type: "e" }]),
|
||
flattened = Array.prototype.concat(...ends),
|
||
arr = flattened.sort((a, b) => a.time - b.time);
|
||
|
||
for (const i of arr) {
|
||
currentCount += i.type === "s" ? 1 : -1;
|
||
|
||
if (currentCount === 1) {
|
||
start = i.time;
|
||
} else {
|
||
if (start && +start !== +i.time) {
|
||
results.push(Interval.fromDateTimes(start, i.time));
|
||
}
|
||
|
||
start = null;
|
||
}
|
||
}
|
||
|
||
return Interval.merge(results);
|
||
}
|
||
|
||
/**
|
||
* Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals.
|
||
* @param {...Interval} intervals
|
||
* @return {[Interval]}
|
||
*/
|
||
difference(...intervals) {
|
||
return Interval.xor([this].concat(intervals))
|
||
.map(i => this.intersection(i))
|
||
.filter(i => i && !i.isEmpty());
|
||
}
|
||
|
||
/**
|
||
* Returns a string representation of this Interval appropriate for debugging.
|
||
* @return {string}
|
||
*/
|
||
toString() {
|
||
if (!this.isValid) return INVALID;
|
||
return `[${this.s.toISO()} – ${this.e.toISO()})`;
|
||
}
|
||
|
||
/**
|
||
* Returns an ISO 8601-compliant string representation of this Interval.
|
||
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
||
* @param {Object} opts - The same options as {@link DateTime.toISO}
|
||
* @return {string}
|
||
*/
|
||
toISO(opts) {
|
||
if (!this.isValid) return INVALID;
|
||
return `${this.s.toISO(opts)}/${this.e.toISO(opts)}`;
|
||
}
|
||
|
||
/**
|
||
* Returns an ISO 8601-compliant string representation of date of this Interval.
|
||
* The time components are ignored.
|
||
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
||
* @return {string}
|
||
*/
|
||
toISODate() {
|
||
if (!this.isValid) return INVALID;
|
||
return `${this.s.toISODate()}/${this.e.toISODate()}`;
|
||
}
|
||
|
||
/**
|
||
* Returns an ISO 8601-compliant string representation of time of this Interval.
|
||
* The date components are ignored.
|
||
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
||
* @param {Object} opts - The same options as {@link DateTime.toISO}
|
||
* @return {string}
|
||
*/
|
||
toISOTime(opts) {
|
||
if (!this.isValid) return INVALID;
|
||
return `${this.s.toISOTime(opts)}/${this.e.toISOTime(opts)}`;
|
||
}
|
||
|
||
/**
|
||
* Returns a string representation of this Interval formatted according to the specified format string.
|
||
* @param {string} dateFormat - the format string. This string formats the start and end time. See {@link DateTime.toFormat} for details.
|
||
* @param {Object} opts - options
|
||
* @param {string} [opts.separator = ' – '] - a separator to place between the start and end representations
|
||
* @return {string}
|
||
*/
|
||
toFormat(dateFormat, { separator = " – " } = {}) {
|
||
if (!this.isValid) return INVALID;
|
||
return `${this.s.toFormat(dateFormat)}${separator}${this.e.toFormat(dateFormat)}`;
|
||
}
|
||
|
||
/**
|
||
* Return a Duration representing the time spanned by this interval.
|
||
* @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration.
|
||
* @param {Object} opts - options that affect the creation of the Duration
|
||
* @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
|
||
* @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }
|
||
* @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }
|
||
* @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }
|
||
* @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }
|
||
* @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }
|
||
* @return {Duration}
|
||
*/
|
||
toDuration(unit, opts) {
|
||
if (!this.isValid) {
|
||
return Duration.invalid(this.invalidReason);
|
||
}
|
||
return this.e.diff(this.s, unit, opts);
|
||
}
|
||
|
||
/**
|
||
* Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes
|
||
* @param {function} mapFn
|
||
* @return {Interval}
|
||
* @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())
|
||
* @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))
|
||
*/
|
||
mapEndpoints(mapFn) {
|
||
return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e));
|
||
}
|
||
}
|