1 | import DateTime, { friendlyDateTime } from "./datetime.js";
|
2 | import Duration, { friendlyDuration } from "./duration.js";
|
3 | import Settings from "./settings.js";
|
4 | import { InvalidArgumentError, InvalidIntervalError } from "./errors.js";
|
5 | import Invalid from "./impl/invalid.js";
|
6 |
|
7 | const INVALID = "Invalid Interval";
|
8 |
|
9 | // checks if the start is equal to or before the end
|
10 | function validateStartEnd(start, end) {
|
11 | if (!start || !start.isValid) {
|
12 | return Interval.invalid("missing or invalid start");
|
13 | } else if (!end || !end.isValid) {
|
14 | return Interval.invalid("missing or invalid end");
|
15 | } else if (end < start) {
|
16 | return Interval.invalid(
|
17 | "end before start",
|
18 | `The end of an interval must be after its start, but you had start=${start.toISO()} and end=${end.toISO()}`
|
19 | );
|
20 | } else {
|
21 | return null;
|
22 | }
|
23 | }
|
24 |
|
25 | /**
|
26 | * 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.
|
27 | *
|
28 | * Here is a brief overview of the most commonly used methods and getters in Interval:
|
29 | *
|
30 | * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.
|
31 | * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.
|
32 | * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame}, {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.
|
33 | * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually}, {@link Interval#merge}, {@link Interval#xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.
|
34 | * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}
|
35 | * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toISO}, {@link Interval#toISODate}, {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.
|
36 | */
|
37 | export default class Interval {
|
38 | /**
|
39 | * @private
|
40 | */
|
41 | constructor(config) {
|
42 | /**
|
43 | * @access private
|
44 | */
|
45 | this.s = config.start;
|
46 | /**
|
47 | * @access private
|
48 | */
|
49 | this.e = config.end;
|
50 | /**
|
51 | * @access private
|
52 | */
|
53 | this.invalid = config.invalid || null;
|
54 | /**
|
55 | * @access private
|
56 | */
|
57 | this.isLuxonInterval = true;
|
58 | }
|
59 |
|
60 | /**
|
61 | * Create an invalid Interval.
|
62 | * @param {string} reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent
|
63 | * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
|
64 | * @return {Interval}
|
65 | */
|
66 | static invalid(reason, explanation = null) {
|
67 | if (!reason) {
|
68 | throw new InvalidArgumentError("need to specify a reason the Interval is invalid");
|
69 | }
|
70 |
|
71 | const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);
|
72 |
|
73 | if (Settings.throwOnInvalid) {
|
74 | throw new InvalidIntervalError(invalid);
|
75 | } else {
|
76 | return new Interval({ invalid });
|
77 | }
|
78 | }
|
79 |
|
80 | /**
|
81 | * Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.
|
82 | * @param {DateTime|Date|Object} start
|
83 | * @param {DateTime|Date|Object} end
|
84 | * @return {Interval}
|
85 | */
|
86 | static fromDateTimes(start, end) {
|
87 | const builtStart = friendlyDateTime(start),
|
88 | builtEnd = friendlyDateTime(end);
|
89 |
|
90 | const validateError = validateStartEnd(builtStart, builtEnd);
|
91 |
|
92 | if (validateError == null) {
|
93 | return new Interval({
|
94 | start: builtStart,
|
95 | end: builtEnd,
|
96 | });
|
97 | } else {
|
98 | return validateError;
|
99 | }
|
100 | }
|
101 |
|
102 | /**
|
103 | * Create an Interval from a start DateTime and a Duration to extend to.
|
104 | * @param {DateTime|Date|Object} start
|
105 | * @param {Duration|Object|number} duration - the length of the Interval.
|
106 | * @return {Interval}
|
107 | */
|
108 | static after(start, duration) {
|
109 | const dur = friendlyDuration(duration),
|
110 | dt = friendlyDateTime(start);
|
111 | return Interval.fromDateTimes(dt, dt.plus(dur));
|
112 | }
|
113 |
|
114 | /**
|
115 | * Create an Interval from an end DateTime and a Duration to extend backwards to.
|
116 | * @param {DateTime|Date|Object} end
|
117 | * @param {Duration|Object|number} duration - the length of the Interval.
|
118 | * @return {Interval}
|
119 | */
|
120 | static before(end, duration) {
|
121 | const dur = friendlyDuration(duration),
|
122 | dt = friendlyDateTime(end);
|
123 | return Interval.fromDateTimes(dt.minus(dur), dt);
|
124 | }
|
125 |
|
126 | /**
|
127 | * Create an Interval from an ISO 8601 string.
|
128 | * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.
|
129 | * @param {string} text - the ISO string to parse
|
130 | * @param {Object} [opts] - options to pass {@link DateTime.fromISO} and optionally {@link Duration.fromISO}
|
131 | * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
132 | * @return {Interval}
|
133 | */
|
134 | static fromISO(text, opts) {
|
135 | const [s, e] = (text || "").split("/", 2);
|
136 | if (s && e) {
|
137 | let start, startIsValid;
|
138 | try {
|
139 | start = DateTime.fromISO(s, opts);
|
140 | startIsValid = start.isValid;
|
141 | } catch (e) {
|
142 | startIsValid = false;
|
143 | }
|
144 |
|
145 | let end, endIsValid;
|
146 | try {
|
147 | end = DateTime.fromISO(e, opts);
|
148 | endIsValid = end.isValid;
|
149 | } catch (e) {
|
150 | endIsValid = false;
|
151 | }
|
152 |
|
153 | if (startIsValid && endIsValid) {
|
154 | return Interval.fromDateTimes(start, end);
|
155 | }
|
156 |
|
157 | if (startIsValid) {
|
158 | const dur = Duration.fromISO(e, opts);
|
159 | if (dur.isValid) {
|
160 | return Interval.after(start, dur);
|
161 | }
|
162 | } else if (endIsValid) {
|
163 | const dur = Duration.fromISO(s, opts);
|
164 | if (dur.isValid) {
|
165 | return Interval.before(end, dur);
|
166 | }
|
167 | }
|
168 | }
|
169 | return Interval.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
|
170 | }
|
171 |
|
172 | /**
|
173 | * Check if an object is an Interval. Works across context boundaries
|
174 | * @param {object} o
|
175 | * @return {boolean}
|
176 | */
|
177 | static isInterval(o) {
|
178 | return (o && o.isLuxonInterval) || false;
|
179 | }
|
180 |
|
181 | /**
|
182 | * Returns the start of the Interval
|
183 | * @type {DateTime}
|
184 | */
|
185 | get start() {
|
186 | return this.isValid ? this.s : null;
|
187 | }
|
188 |
|
189 | /**
|
190 | * Returns the end of the Interval
|
191 | * @type {DateTime}
|
192 | */
|
193 | get end() {
|
194 | return this.isValid ? this.e : null;
|
195 | }
|
196 |
|
197 | /**
|
198 | * Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.
|
199 | * @type {boolean}
|
200 | */
|
201 | get isValid() {
|
202 | return this.invalidReason === null;
|
203 | }
|
204 |
|
205 | /**
|
206 | * Returns an error code if this Interval is invalid, or null if the Interval is valid
|
207 | * @type {string}
|
208 | */
|
209 | get invalidReason() {
|
210 | return this.invalid ? this.invalid.reason : null;
|
211 | }
|
212 |
|
213 | /**
|
214 | * Returns an explanation of why this Interval became invalid, or null if the Interval is valid
|
215 | * @type {string}
|
216 | */
|
217 | get invalidExplanation() {
|
218 | return this.invalid ? this.invalid.explanation : null;
|
219 | }
|
220 |
|
221 | /**
|
222 | * Returns the length of the Interval in the specified unit.
|
223 | * @param {string} unit - the unit (such as 'hours' or 'days') to return the length in.
|
224 | * @return {number}
|
225 | */
|
226 | length(unit = "milliseconds") {
|
227 | return this.isValid ? this.toDuration(...[unit]).get(unit) : NaN;
|
228 | }
|
229 |
|
230 | /**
|
231 | * Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.
|
232 | * Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'
|
233 | * asks 'what dates are included in this interval?', not 'how many days long is this interval?'
|
234 | * @param {string} [unit='milliseconds'] - the unit of time to count.
|
235 | * @return {number}
|
236 | */
|
237 | count(unit = "milliseconds") {
|
238 | if (!this.isValid) return NaN;
|
239 | const start = this.start.startOf(unit),
|
240 | end = this.end.startOf(unit);
|
241 | return Math.floor(end.diff(start, unit).get(unit)) + 1;
|
242 | }
|
243 |
|
244 | /**
|
245 | * Returns whether this Interval's start and end are both in the same unit of time
|
246 | * @param {string} unit - the unit of time to check sameness on
|
247 | * @return {boolean}
|
248 | */
|
249 | hasSame(unit) {
|
250 | return this.isValid ? this.isEmpty() || this.e.minus(1).hasSame(this.s, unit) : false;
|
251 | }
|
252 |
|
253 | /**
|
254 | * Return whether this Interval has the same start and end DateTimes.
|
255 | * @return {boolean}
|
256 | */
|
257 | isEmpty() {
|
258 | return this.s.valueOf() === this.e.valueOf();
|
259 | }
|
260 |
|
261 | /**
|
262 | * Return whether this Interval's start is after the specified DateTime.
|
263 | * @param {DateTime} dateTime
|
264 | * @return {boolean}
|
265 | */
|
266 | isAfter(dateTime) {
|
267 | if (!this.isValid) return false;
|
268 | return this.s > dateTime;
|
269 | }
|
270 |
|
271 | /**
|
272 | * Return whether this Interval's end is before the specified DateTime.
|
273 | * @param {DateTime} dateTime
|
274 | * @return {boolean}
|
275 | */
|
276 | isBefore(dateTime) {
|
277 | if (!this.isValid) return false;
|
278 | return this.e <= dateTime;
|
279 | }
|
280 |
|
281 | /**
|
282 | * Return whether this Interval contains the specified DateTime.
|
283 | * @param {DateTime} dateTime
|
284 | * @return {boolean}
|
285 | */
|
286 | contains(dateTime) {
|
287 | if (!this.isValid) return false;
|
288 | return this.s <= dateTime && this.e > dateTime;
|
289 | }
|
290 |
|
291 | /**
|
292 | * "Sets" the start and/or end dates. Returns a newly-constructed Interval.
|
293 | * @param {Object} values - the values to set
|
294 | * @param {DateTime} values.start - the starting DateTime
|
295 | * @param {DateTime} values.end - the ending DateTime
|
296 | * @return {Interval}
|
297 | */
|
298 | set({ start, end } = {}) {
|
299 | if (!this.isValid) return this;
|
300 | return Interval.fromDateTimes(start || this.s, end || this.e);
|
301 | }
|
302 |
|
303 | /**
|
304 | * Split this Interval at each of the specified DateTimes
|
305 | * @param {...DateTime} dateTimes - the unit of time to count.
|
306 | * @return {Array}
|
307 | */
|
308 | splitAt(...dateTimes) {
|
309 | if (!this.isValid) return [];
|
310 | const sorted = dateTimes
|
311 | .map(friendlyDateTime)
|
312 | .filter((d) => this.contains(d))
|
313 | .sort(),
|
314 | results = [];
|
315 | let { s } = this,
|
316 | i = 0;
|
317 |
|
318 | while (s < this.e) {
|
319 | const added = sorted[i] || this.e,
|
320 | next = +added > +this.e ? this.e : added;
|
321 | results.push(Interval.fromDateTimes(s, next));
|
322 | s = next;
|
323 | i += 1;
|
324 | }
|
325 |
|
326 | return results;
|
327 | }
|
328 |
|
329 | /**
|
330 | * Split this Interval into smaller Intervals, each of the specified length.
|
331 | * Left over time is grouped into a smaller interval
|
332 | * @param {Duration|Object|number} duration - The length of each resulting interval.
|
333 | * @return {Array}
|
334 | */
|
335 | splitBy(duration) {
|
336 | const dur = friendlyDuration(duration);
|
337 |
|
338 | if (!this.isValid || !dur.isValid || dur.as("milliseconds") === 0) {
|
339 | return [];
|
340 | }
|
341 |
|
342 | let { s } = this,
|
343 | idx = 1,
|
344 | next;
|
345 |
|
346 | const results = [];
|
347 | while (s < this.e) {
|
348 | const added = this.start.plus(dur.mapUnits((x) => x * idx));
|
349 | next = +added > +this.e ? this.e : added;
|
350 | results.push(Interval.fromDateTimes(s, next));
|
351 | s = next;
|
352 | idx += 1;
|
353 | }
|
354 |
|
355 | return results;
|
356 | }
|
357 |
|
358 | /**
|
359 | * Split this Interval into the specified number of smaller intervals.
|
360 | * @param {number} numberOfParts - The number of Intervals to divide the Interval into.
|
361 | * @return {Array}
|
362 | */
|
363 | divideEqually(numberOfParts) {
|
364 | if (!this.isValid) return [];
|
365 | return this.splitBy(this.length() / numberOfParts).slice(0, numberOfParts);
|
366 | }
|
367 |
|
368 | /**
|
369 | * Return whether this Interval overlaps with the specified Interval
|
370 | * @param {Interval} other
|
371 | * @return {boolean}
|
372 | */
|
373 | overlaps(other) {
|
374 | return this.e > other.s && this.s < other.e;
|
375 | }
|
376 |
|
377 | /**
|
378 | * Return whether this Interval's end is adjacent to the specified Interval's start.
|
379 | * @param {Interval} other
|
380 | * @return {boolean}
|
381 | */
|
382 | abutsStart(other) {
|
383 | if (!this.isValid) return false;
|
384 | return +this.e === +other.s;
|
385 | }
|
386 |
|
387 | /**
|
388 | * Return whether this Interval's start is adjacent to the specified Interval's end.
|
389 | * @param {Interval} other
|
390 | * @return {boolean}
|
391 | */
|
392 | abutsEnd(other) {
|
393 | if (!this.isValid) return false;
|
394 | return +other.e === +this.s;
|
395 | }
|
396 |
|
397 | /**
|
398 | * Return whether this Interval engulfs the start and end of the specified Interval.
|
399 | * @param {Interval} other
|
400 | * @return {boolean}
|
401 | */
|
402 | engulfs(other) {
|
403 | if (!this.isValid) return false;
|
404 | return this.s <= other.s && this.e >= other.e;
|
405 | }
|
406 |
|
407 | /**
|
408 | * Return whether this Interval has the same start and end as the specified Interval.
|
409 | * @param {Interval} other
|
410 | * @return {boolean}
|
411 | */
|
412 | equals(other) {
|
413 | if (!this.isValid || !other.isValid) {
|
414 | return false;
|
415 | }
|
416 |
|
417 | return this.s.equals(other.s) && this.e.equals(other.e);
|
418 | }
|
419 |
|
420 | /**
|
421 | * Return an Interval representing the intersection of this Interval and the specified Interval.
|
422 | * Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.
|
423 | * Returns null if the intersection is empty, meaning, the intervals don't intersect.
|
424 | * @param {Interval} other
|
425 | * @return {Interval}
|
426 | */
|
427 | intersection(other) {
|
428 | if (!this.isValid) return this;
|
429 | const s = this.s > other.s ? this.s : other.s,
|
430 | e = this.e < other.e ? this.e : other.e;
|
431 |
|
432 | if (s >= e) {
|
433 | return null;
|
434 | } else {
|
435 | return Interval.fromDateTimes(s, e);
|
436 | }
|
437 | }
|
438 |
|
439 | /**
|
440 | * Return an Interval representing the union of this Interval and the specified Interval.
|
441 | * Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.
|
442 | * @param {Interval} other
|
443 | * @return {Interval}
|
444 | */
|
445 | union(other) {
|
446 | if (!this.isValid) return this;
|
447 | const s = this.s < other.s ? this.s : other.s,
|
448 | e = this.e > other.e ? this.e : other.e;
|
449 | return Interval.fromDateTimes(s, e);
|
450 | }
|
451 |
|
452 | /**
|
453 | * Merge an array of Intervals into a equivalent minimal set of Intervals.
|
454 | * Combines overlapping and adjacent Intervals.
|
455 | * @param {Array} intervals
|
456 | * @return {Array}
|
457 | */
|
458 | static merge(intervals) {
|
459 | const [found, final] = intervals
|
460 | .sort((a, b) => a.s - b.s)
|
461 | .reduce(
|
462 | ([sofar, current], item) => {
|
463 | if (!current) {
|
464 | return [sofar, item];
|
465 | } else if (current.overlaps(item) || current.abutsStart(item)) {
|
466 | return [sofar, current.union(item)];
|
467 | } else {
|
468 | return [sofar.concat([current]), item];
|
469 | }
|
470 | },
|
471 | [[], null]
|
472 | );
|
473 | if (final) {
|
474 | found.push(final);
|
475 | }
|
476 | return found;
|
477 | }
|
478 |
|
479 | /**
|
480 | * Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.
|
481 | * @param {Array} intervals
|
482 | * @return {Array}
|
483 | */
|
484 | static xor(intervals) {
|
485 | let start = null,
|
486 | currentCount = 0;
|
487 | const results = [],
|
488 | ends = intervals.map((i) => [
|
489 | { time: i.s, type: "s" },
|
490 | { time: i.e, type: "e" },
|
491 | ]),
|
492 | flattened = Array.prototype.concat(...ends),
|
493 | arr = flattened.sort((a, b) => a.time - b.time);
|
494 |
|
495 | for (const i of arr) {
|
496 | currentCount += i.type === "s" ? 1 : -1;
|
497 |
|
498 | if (currentCount === 1) {
|
499 | start = i.time;
|
500 | } else {
|
501 | if (start && +start !== +i.time) {
|
502 | results.push(Interval.fromDateTimes(start, i.time));
|
503 | }
|
504 |
|
505 | start = null;
|
506 | }
|
507 | }
|
508 |
|
509 | return Interval.merge(results);
|
510 | }
|
511 |
|
512 | /**
|
513 | * Return an Interval representing the span of time in this Interval that doesn't overlap with any of the specified Intervals.
|
514 | * @param {...Interval} intervals
|
515 | * @return {Array}
|
516 | */
|
517 | difference(...intervals) {
|
518 | return Interval.xor([this].concat(intervals))
|
519 | .map((i) => this.intersection(i))
|
520 | .filter((i) => i && !i.isEmpty());
|
521 | }
|
522 |
|
523 | /**
|
524 | * Returns a string representation of this Interval appropriate for debugging.
|
525 | * @return {string}
|
526 | */
|
527 | toString() {
|
528 | if (!this.isValid) return INVALID;
|
529 | return `[${this.s.toISO()} – ${this.e.toISO()})`;
|
530 | }
|
531 |
|
532 | /**
|
533 | * Returns an ISO 8601-compliant string representation of this Interval.
|
534 | * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
535 | * @param {Object} opts - The same options as {@link DateTime#toISO}
|
536 | * @return {string}
|
537 | */
|
538 | toISO(opts) {
|
539 | if (!this.isValid) return INVALID;
|
540 | return `${this.s.toISO(opts)}/${this.e.toISO(opts)}`;
|
541 | }
|
542 |
|
543 | /**
|
544 | * Returns an ISO 8601-compliant string representation of date of this Interval.
|
545 | * The time components are ignored.
|
546 | * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
547 | * @return {string}
|
548 | */
|
549 | toISODate() {
|
550 | if (!this.isValid) return INVALID;
|
551 | return `${this.s.toISODate()}/${this.e.toISODate()}`;
|
552 | }
|
553 |
|
554 | /**
|
555 | * Returns an ISO 8601-compliant string representation of time of this Interval.
|
556 | * The date components are ignored.
|
557 | * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
558 | * @param {Object} opts - The same options as {@link DateTime.toISO}
|
559 | * @return {string}
|
560 | */
|
561 | toISOTime(opts) {
|
562 | if (!this.isValid) return INVALID;
|
563 | return `${this.s.toISOTime(opts)}/${this.e.toISOTime(opts)}`;
|
564 | }
|
565 |
|
566 | /**
|
567 | * Returns a string representation of this Interval formatted according to the specified format string.
|
568 | * @param {string} dateFormat - the format string. This string formats the start and end time. See {@link DateTime.toFormat} for details.
|
569 | * @param {Object} opts - options
|
570 | * @param {string} [opts.separator = ' – '] - a separator to place between the start and end representations
|
571 | * @return {string}
|
572 | */
|
573 | toFormat(dateFormat, { separator = " – " } = {}) {
|
574 | if (!this.isValid) return INVALID;
|
575 | return `${this.s.toFormat(dateFormat)}${separator}${this.e.toFormat(dateFormat)}`;
|
576 | }
|
577 |
|
578 | /**
|
579 | * Return a Duration representing the time spanned by this interval.
|
580 | * @param {string|string[]} [unit=['milliseconds']] - the unit or units (such as 'hours' or 'days') to include in the duration.
|
581 | * @param {Object} opts - options that affect the creation of the Duration
|
582 | * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
|
583 | * @example Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }
|
584 | * @example Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }
|
585 | * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }
|
586 | * @example Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }
|
587 | * @example Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }
|
588 | * @return {Duration}
|
589 | */
|
590 | toDuration(unit, opts) {
|
591 | if (!this.isValid) {
|
592 | return Duration.invalid(this.invalidReason);
|
593 | }
|
594 | return this.e.diff(this.s, unit, opts);
|
595 | }
|
596 |
|
597 | /**
|
598 | * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes
|
599 | * @param {function} mapFn
|
600 | * @return {Interval}
|
601 | * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())
|
602 | * @example Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))
|
603 | */
|
604 | mapEndpoints(mapFn) {
|
605 | return Interval.fromDateTimes(mapFn(this.s), mapFn(this.e));
|
606 | }
|
607 | }
|