Interval Operations
GMT has two families of interval functions. The half-open interval/ module
(intervalsOverlap, intersectIntervals, subtractIntervals, sumIntervals, …) takes
{ start, end } records of instant strings. The positional functions in plain/,
utc/, zoned/ and unix/ take separate strings. Both families read an interval as
half-open [start, end). Every output shown here is what the function returns.
Mistakes
Section titled “Mistakes”Passing the last day of a period as a Date interval's end
A positional Date interval is half-open like every other interval: end is the first day after the period, not the last day in it. An end of 2024-06-30 leaves 30 June out, so it does not touch an interval starting on 1 July. Add one day to the last day.
Wrong
intervalAbutsDate("2024-01-01", "2024-06-30", "2024-07-01", "2024-12-31");// expected true — actually false: 30 June lies in neither intervalRight
import { addDate, intervalAbutsDate } from "@northguild/gmt";
const end = addDate("2024-06-30", { days: 1 }); // "2024-07-01"intervalAbutsDate("2024-01-01", end, "2024-07-01", "2025-01-01"); // trueExpecting touching intervals to overlap
Both interval families are half-open: an interval's end is not inside it, so two intervals that touch share no instant. They abut, their intersection is null, and their union joins them.
Wrong
intervalsOverlapUtc("2024-01-01T09:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T18:00:00Z");// expected true — actually falseRight
import { intervalAbutsUtc } from "@northguild/gmt";
intervalAbutsUtc("2024-01-01T09:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T18:00:00Z");// true — the first ends where the second startsStepping one unit around a cut by hand
intervalDifference* and intervalXor* cut exactly at the removal's start and end, as subtractIntervals does. Adding or subtracting a nanosecond (or a day, or an epoch unit) to the pieces leaves a gap that sumIntervals counts as missing time.
Wrong
// The pieces already meet the lunch break exactly; do not nudge themconst morningEnd = "2024-01-01T11:59:59.999999999Z";Right
import { intervalDifferenceUtc } from "@northguild/gmt";
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" }]Passing plain dates or zoneless datetimes to interval/
interval/ endpoints are instants: an offset (Z or ±HH:MM) is required. A plain date or a local datetime has no position on the timeline until a zone is named, so every function returns its sentinel for it.
Wrong
intervalsOverlap({ start: "2024-01-01", end: "2024-01-02" }, { start: "2024-01-01T12:00:00", end: "2024-01-03T00:00:00" });// false — both are invalid input, not a real answerRight
import { floorToZone, intervalsOverlap } from "@northguild/gmt";
// Build local-day edges as instants firstconst day = {start: floorToZone("2024-01-01T12:00:00Z", "day", "America/New_York"), // "2024-01-01T05:00:00Z"end: floorToZone("2024-01-02T12:00:00Z", "day", "America/New_York"), // "2024-01-02T05:00:00Z"};Treating [] from mergeIntervals or subtractIntervals as an error
[] is a legitimate result — a fully covered subtraction, or a list of only empty intervals — and it is also the invalid-input sentinel. Check the inputs with isValidInterval when the difference matters.
Wrong
const left = subtractIntervals(shift, removals);if (left.length === 0) throw new Error("bad input"); // wrong when the removals cover the shiftRight
import { isValidInterval, subtractIntervals } from "@northguild/gmt";
if (!isValidInterval(shift) || !removals.every(isValidInterval)) {// genuinely invalid input}const left = subtractIntervals(shift, removals); // [] now means fully coveredExpecting sumIntervals to return days
sumIntervals returns covered time with hours as the largest unit — exactly Temporal.Instant.prototype.until with largestUnit hour. An instant has no calendar, so a day is not a fixed length and is never used.
Wrong
sumIntervals([{ start: "2024-01-01T00:00:00Z", end: "2024-01-03T01:30:00Z" }]);// expected "P2DT1H30M"Right
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" — New York's spring-forward daysumIntervals([]); // "PT0S"Expecting splitIntervalByUnit slices to be equal
splitIntervalByUnitDate cuts fixed amount × unit steps from the start, so the final slice is whatever remains and can be shorter. For n pieces of equal length, use intervalDivideEquallyDate.
Wrong
splitIntervalByUnitDate("2024-03-15", "2024-03-20", "day", 2);// [{ start: "2024-03-15", end: "2024-03-17" }, { start: "2024-03-17", end: "2024-03-19" },// { start: "2024-03-19", end: "2024-03-20" }] — the last slice is 1 dayRight
intervalDivideEquallyDate("2024-03-15", "2024-03-20", 2);// [{ start: "2024-03-15", end: "2024-03-18" }, { start: "2024-03-18", end: "2024-03-20" }]Using intervalCount where you meant a length
intervalCount* counts the calendar units the half-open interval touches; intervalLength* measures how long it is. They agree only when the interval starts on a unit boundary and is a whole number of units long.
Wrong
intervalCountDateTime("2024-01-01T23:59:00", "2024-01-02T00:01:00", "day");// 2 — used as a duration, but the interval is two minutes longRight
import { intervalLengthDateTime } from "@northguild/gmt";
intervalLengthDateTime("2024-01-01T23:59:00", "2024-01-02T00:01:00", "day");// 0.001388888888888889Expecting a zero-length interval to count the unit it sits in
intervalCount* counts the units the half-open [start, end) touches. A zero-length interval holds no instant, so it counts 0 in every unit, even mid-month. Before 1.16.0 it counted 1 when it sat mid-unit.
Wrong
intervalCountDate("2024-01-15", "2024-01-15", "month"); // expecting 1Right
intervalCountDate("2024-01-15", "2024-01-15", "month"); // 0 — an empty interval touches no monthPassing an options object to intervalFromDuration*
intervalFromDuration* takes the value, an ISO 8601 duration and an anchor of start or end. Calendar units resolve against the value itself — there is no relativeTo option. A negative duration returns null rather than an inverted interval.
Wrong
intervalFromDurationDate("2024-03-15", "P1M", { relativeTo: "2024-03-15" });Right
import { intervalFromDurationDate } from "@northguild/gmt";
intervalFromDurationDate("2024-03-15", "P1M", "start"); // { start: "2024-03-15", end: "2024-04-15" }intervalFromDurationDate("2024-01-31", "P1M", "start"); // { start: "2024-01-31", end: "2024-02-29" }intervalFromDurationDate("2024-03-15", "-P10D", "start"); // nullReading intervalOverlappingDays* as an intersection object
intervalOverlappingDays* returns a number: how many distinct calendar dates hold an instant of the half-open intersection. Touching intervals share no date. For the shared span itself, use intervalIntersection* or intersectIntervals.
Wrong
const shared = intervalOverlappingDaysDate("2024-01-01", "2024-07-01", "2024-04-01", "2025-01-01");shared.start; // undefined — shared is 91Right
intervalOverlappingDaysDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01"); // 0 — touchingintervalIntersectionDate("2024-01-01", "2024-07-01", "2024-04-01", "2025-01-01");// { start: "2024-04-01", end: "2024-07-01" }Calling list functions with flat arguments, or without a suffix
mergeIntervals*, intervalXorAll* and every interval/ list function take one array of { start, end } records. The positional list functions carry a type suffix — there is no bare intervalXorAll.
Wrong
intervalXorAll(a, b);Right
import { intervalXorAllDate, mergeIntervals } from "@northguild/gmt";
intervalXorAllDate([a, b, c]); // positional, Date valuesmergeIntervals([x, y, z]); // half-open, instant stringsSee also
Section titled “See also”- Intervals guide
intervalsOverlapreferencesubtractIntervalsreferencesumIntervalsreferenceisValidIntervalreferenceintervalsOverlapDatereferenceintervalAbutsDatereferenceintervalDifferenceUtcreferenceintervalCountDatereferenceintervalLengthDatereferenceintervalFromDurationDatereferenceintervalOverlappingDaysDatereferencesplitIntervalByUnitDatereferenceintervalXorAllDatereference