Skip to content

Set Operations

  • The core functions, which every industry uses.

Use these functions to combine intervals, remove time from an interval, and total up how much time a set of intervals covers.

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.

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 removed

remove may be unsorted and overlapping. A removal that only touches from removes nothing.

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 time

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 join
intervalUnionDate("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.

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, joined
intervalUnionUnix(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 join
intervalXorTime("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*.