Set Operations
Use these functions to combine intervals, remove time from an interval, and total up how much time a set of intervals covers.
Merge a list
Section titled “Merge a list”mergeIntervals collapses a list into the fewest sorted intervals that do not overlap.
Input order does not matter. Because the intervals are half-open, touching intervals
join. Intervals separated by even one nanosecond stay apart:
import { mergeIntervals } from "@northguild/gmt";
mergeIntervals([ { start: "2024-01-01T13:00:00Z", end: "2024-01-01T17:00:00Z" }, { start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" }, { start: "2024-01-01T12:00:00Z", end: "2024-01-01T13:00:00Z" },]);// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" }]An empty interval that touches or lies inside a merged run is absorbed into it. One that touches nothing is dropped, since it covers no time.
Subtract windows
Section titled “Subtract windows”subtractIntervals(from, remove) returns the parts of from that no removal covers.
Cuts land exactly where a removal starts and ends, with no one-unit step:
import { subtractIntervals } from "@northguild/gmt";
const shift = { start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" };
subtractIntervals(shift, [{ start: "2024-01-01T12:00:00Z", end: "2024-01-01T13:00:00Z" }]);// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" },// { start: "2024-01-01T13:00:00Z", end: "2024-01-01T17:00:00Z" }]
subtractIntervals(shift, [{ start: "2024-01-01T08:00:00Z", end: "2024-01-01T18:00:00Z" }]);// [] — everything removedremove may be unsorted and overlapping. A removal that only touches from removes
nothing.
Sum covered time
Section titled “Sum covered time”sumIntervals returns the length of the union as an exact ISO 8601 duration. Overlapping
time counts once, and empty intervals add nothing:
import { sumIntervals } from "@northguild/gmt";
sumIntervals([ { start: "2024-01-01T09:00:00Z", end: "2024-01-01T13:00:00Z" }, { start: "2024-01-01T12:00:00Z", end: "2024-01-01T17:00:00Z" },]); // "PT8H" — the overlapping hour counts once
sumIntervals([{ start: "2024-01-01T00:00:00Z", end: "2024-01-03T01:30:00Z" }]); // "PT49H30M"sumIntervals([{ start: "2024-03-10T05:00:00Z", end: "2024-03-11T04:00:00Z" }]); // "PT23H"sumIntervals([]); // "PT0S"Hours are the largest unit. The output is exactly what TC39 Temporal.Instant.prototype.until
returns with largestUnit: "hour", because an instant has no calendar to say how long a day
is. That is also why the New York day across the spring-forward change is PT23H. The total
is summed in bigint nanoseconds, so it stays exact at any size.
Composed together, these answer the question every realm asks: how much of this interval fell inside the allowed windows?
const breaks = [{ start: "2024-01-01T12:00:00Z", end: "2024-01-01T13:00:00Z" }];
sumIntervals(subtractIntervals(shift, breaks)); // "PT7H" — worked timeThe positional functions
Section titled “The positional functions”The pairwise intervalUnion*, intervalDifference* and intervalXor*, and the list forms
mergeIntervals* and intervalXorAll*, are half-open too. Touching intervals join, and
every cut lands exactly on an input’s own start or end, with no one-unit step:
import { intervalDifferenceDate, intervalDifferenceUtc, intervalUnionDate, intervalXorAllDate, intervalXorDate, mergeIntervalsDate,} from "@northguild/gmt";
intervalUnionDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01");// { start: "2024-01-01", end: "2025-01-01" } — touching intervals joinintervalUnionDate("2024-01-01", "2024-06-30", "2024-07-01", "2024-12-31");// null — 30 June lies in neither interval
intervalDifferenceDate("2024-01-01", "2024-12-31", "2024-06-01", "2024-07-01");// [{ start: "2024-01-01", end: "2024-06-01" }, { start: "2024-07-01", end: "2024-12-31" }]
intervalDifferenceUtc("2024-01-01T09:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T12:00:00Z", "2024-01-01T13:00:00Z");// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" },// { start: "2024-01-01T13:00:00Z", end: "2024-01-01T17:00:00Z" }]
intervalXorDate("2024-01-01", "2024-07-01", "2024-04-01", "2025-01-01");// [{ start: "2024-01-01", end: "2024-04-01" }, { start: "2024-07-01", end: "2025-01-01" }]
mergeIntervalsDate([ { start: "2024-01-01", end: "2024-01-10" }, { start: "2024-01-05", end: "2024-01-15" },]);// [{ start: "2024-01-01", end: "2024-01-15" }]
intervalXorAllDate([ { start: "2024-01-01", end: "2024-01-10" }, { start: "2024-01-05", end: "2024-01-15" }, { start: "2024-01-08", end: "2024-01-20" },]);// [{ start: "2024-01-01", end: "2024-01-05" }, { start: "2024-01-08", end: "2024-01-10" }, { start: "2024-01-15", end: "2024-01-20" }]intervalXorAll* returns the maximal runs covered by an odd number of the inputs, sorted by
start, so two identical intervals cancel to []. intervalDifferenceUtc above returns the
same pieces as subtractIntervals on the same shift: they meet the lunch break exactly, and
together with it they cover the shift with nothing counted twice.
One call for each value kind
Section titled “One call for each value kind”The positional set functions exist in six variants: Date, Time, DateTime, Utc, Zoned
and Unix. The examples above use Date and Utc. These show the others, one call each.
import { intervalDifferenceDateTime, intervalDifferenceTime, intervalDifferenceUnix, intervalDifferenceZoned, intervalUnionDateTime, intervalUnionTime, intervalUnionUnix, intervalUnionUtc, intervalUnionZoned,} from "@northguild/gmt";
intervalUnionTime("09:00:00", "12:00:00", "11:00:00", "14:00:00");// { start: "09:00:00", end: "14:00:00" }intervalUnionDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-02T00:00:00", "2024-01-03T00:00:00");// { start: "2024-01-01T00:00:00", end: "2024-01-03T00:00:00" } — touching, joinedintervalUnionUnix(0, 1700000000, 1000000, 2000000);// { start: 0, end: 1700000000 }intervalUnionUtc("2024-01-01T00:00:00Z", "2024-07-01T00:00:00Z", "2024-04-01T00:00:00Z", "2025-01-01T00:00:00Z");// { start: "2024-01-01T00:00:00Z", end: "2025-01-01T00:00:00Z" }intervalUnionZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-03-01T00:00:00+00:00[UTC]", "2024-09-01T00:00:00+00:00[UTC]");// { start: "2024-01-01T00:00:00+00:00[UTC]", end: "2024-09-01T00:00:00+00:00[UTC]" }
intervalDifferenceTime("09:00:00", "17:00:00", "12:00:00", "13:00:00");// [{ start: "09:00:00", end: "12:00:00" }, { start: "13:00:00", end: "17:00:00" }]intervalDifferenceDateTime("2024-01-01T00:00:00", "2024-01-03T00:00:00", "2024-01-01T12:00:00", "2024-01-02T00:00:00");// [{ start: "2024-01-01T00:00:00", end: "2024-01-01T12:00:00" }, { start: "2024-01-02T00:00:00", end: "2024-01-03T00:00:00" }]intervalDifferenceUnix(0, 1700000000, 1000000, 2000000);// [{ start: 0, end: 1000000 }, { start: 2000000, end: 1700000000 }]intervalDifferenceZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-03-01T00:00:00+00:00[UTC]", "2024-09-01T00:00:00+00:00[UTC]");// [{ start: "2024-01-01T00:00:00+00:00[UTC]", end: "2024-03-01T00:00:00+00:00[UTC]" }]intervalXor* is the symmetric difference: the parts covered by exactly one of the two
intervals. Touching intervals share no instant, so their runs join into one:
import { intervalXorAllUnix, intervalXorAllUtc, intervalXorDate, intervalXorDateTime, intervalXorTime, intervalXorUnix, intervalXorUtc, intervalXorZoned, mergeIntervalsTime, mergeIntervalsUnix, mergeIntervalsUtc,} from "@northguild/gmt";
intervalXorDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01");// [{ start: "2024-01-01", end: "2025-01-01" }] — touching intervals share no date, so the runs joinintervalXorTime("09:00:00", "13:00:00", "12:00:00", "17:00:00");// [{ start: "09:00:00", end: "12:00:00" }, { start: "13:00:00", end: "17:00:00" }]intervalXorDateTime("2024-01-01T00:00:00", "2024-01-03T00:00:00", "2024-01-02T00:00:00", "2024-01-04T00:00:00");// [{ start: "2024-01-01T00:00:00", end: "2024-01-02T00:00:00" }, { start: "2024-01-03T00:00:00", end: "2024-01-04T00:00:00" }]intervalXorUnix(0, 1700000000, 1000000, 2000000);// [{ start: 0, end: 1000000 }, { start: 2000000, end: 1700000000 }]intervalXorUtc("2024-01-01T09:00:00Z", "2024-01-01T13:00:00Z", "2024-01-01T12:00:00Z", "2024-01-01T17:00:00Z");// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" }, { start: "2024-01-01T13:00:00Z", end: "2024-01-01T17:00:00Z" }]intervalXorZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-03-01T00:00:00+00:00[UTC]", "2024-09-01T00:00:00+00:00[UTC]");// [{ start: "2024-01-01T00:00:00+00:00[UTC]", end: "2024-03-01T00:00:00+00:00[UTC]" }, { start: "2024-06-01T00:00:00+00:00[UTC]", end: "2024-09-01T00:00:00+00:00[UTC]" }]
mergeIntervalsTime([{ start: "09:00:00", end: "12:00:00" }, { start: "11:00:00", end: "14:00:00" }]);// [{ start: "09:00:00", end: "14:00:00" }]mergeIntervalsUnix([{ start: 0, end: 10 }, { start: 5, end: 20 }]);// [{ start: 0, end: 20 }]mergeIntervalsUtc([{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" }, { start: "2024-01-01T12:00:00Z", end: "2024-01-01T17:00:00Z" }]);// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" }]
intervalXorAllUnix([{ start: 0, end: 10 }, { start: 5, end: 15 }]);// [{ start: 0, end: 5 }, { start: 10, end: 15 }]intervalXorAllUtc([{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T13:00:00Z" }, { start: "2024-01-01T12:00:00Z", end: "2024-01-01T17:00:00Z" }]);// [{ start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" }, { start: "2024-01-01T13:00:00Z", end: "2024-01-01T17:00:00Z" }]For exactly two intervals, intervalXorAll* gives the same result as intervalXor*. The
list forms mergeIntervals* and intervalXorAll* take one array of { start, end } records
instead of two flat intervals. They return [] for an empty list.
A Zoned, Unix or other positional function returns its sentinel for a wrong type, a
malformed string, a leap second, an inverted interval, or an epoch outside the unix/
grammar (not a safe integer or a digit string): null for intervalUnion*, and [] for
intervalDifference*, intervalXor*, mergeIntervals* and intervalXorAll*.
See also
Section titled “See also”- Interval Basics: the half-open standard, validation, return conventions
- Containment and Overlap: overlap, intersection, clamping
mergeIntervalsreferencesubtractIntervalsreferencesumIntervalsreferenceintervalDifferenceDatereferencemergeIntervalsDatereference