Skip to content

Containment and Overlap

  • The core functions, which every industry uses.

Use these functions for three questions: “is this instant in the range?”, “do these two ranges share any time?” and “what part of this range falls inside that one?”

intervalsOverlap(a, b) is a.start < b.end && b.start < a.end. It is true exactly when intersectIntervals(a, b) is not null, so the two can never disagree:

import { intersectIntervals, intervalsOverlap } from "@northguild/gmt";
const morning = { start: "2024-01-01T09:00:00Z", end: "2024-01-01T13:00:00Z" };
const afternoon = { start: "2024-01-01T12:00:00Z", end: "2024-01-01T18:00:00Z" };
const evening = { start: "2024-01-01T18:00:00Z", end: "2024-01-01T22:00:00Z" };
intervalsOverlap(morning, afternoon); // true
intersectIntervals(morning, afternoon);
// { start: "2024-01-01T12:00:00Z", end: "2024-01-01T13:00:00Z" }
intervalsOverlap(afternoon, evening); // false — they touch at 18:00 and share no instant
intersectIntervals(afternoon, evening); // null

Empty intervals (start === end) follow the same rule:

  • An empty interval strictly inside another overlaps it, and the intersection is that empty interval.
  • An empty interval at an edge does not overlap.

intervalContains(interval, t) is start ≤ t < end:

import { intervalContains } from "@northguild/gmt";
const morning = { start: "2024-01-01T09:00:00Z", end: "2024-01-01T13:00:00Z" };
intervalContains(morning, "2024-01-01T09:00:00Z"); // true
intervalContains(morning, "2024-01-01T13:00:00Z"); // false — the end is excluded
intervalContains(morning, "2024-01-01T04:00:00-05:00"); // true — the same instant as 09:00Z

clampInterval(interval, bounds) is intersectIntervals(interval, bounds) under a name that reads better when bounds is a fixed window such as a business day:

import { clampInterval } from "@northguild/gmt";
const businessDay = { start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" };
clampInterval({ start: "2024-01-01T08:00:00Z", end: "2024-01-01T12:00:00Z" }, businessDay);
// { start: "2024-01-01T09:00:00Z", end: "2024-01-01T12:00:00Z" }

The output endpoints are always the caller’s own strings. When both arguments spell the same instant differently, such as 04:00:00-05:00 and 09:00:00Z, the first argument’s spelling wins. No specification covers this tie. GMT chose it because a rule based on position is deterministic without inventing a canonical spelling.

The positional intervalContains*, intervalsOverlap* and intervalIntersection* functions in plain/, utc/, zoned/ and unix/ follow the same half-open rule, so the end is outside and touching intervals do not overlap:

import {
intervalContainsDate,
intervalContainsUtc,
intervalIntersectionDate,
intervalsOverlapDate,
} from "@northguild/gmt";
intervalContainsUtc("2024-01-01T09:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T17:00:00Z");
// false — the end is excluded
intervalsOverlapDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01");
// false — the first interval ends before 1 July
intervalIntersectionDate("2024-01-01", "2024-07-01", "2024-04-01", "2025-01-01");
// { start: "2024-04-01", end: "2024-07-01" }
intervalIntersectionDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01");
// null — touching intervals share no date

With a fourth argument, intervalContains* tests an inner interval instead of a point, and intervalEngulfs* is the same test under another name. The inner interval must lie within the outer one and overlap it, so it may end at the outer end, but an empty inner interval at end is not contained:

intervalContainsDate("2024-01-01", "2024-12-31", "2024-03-01", "2024-09-01"); // true
intervalContainsDate("2024-01-01", "2025-01-01", "2024-07-01", "2025-01-01"); // true — same end
intervalContainsDate("2024-01-01", "2025-01-01", "2025-01-01", "2025-01-01"); // false — empty, at the end

intervalAbuts* is true when one interval’s end equals the other’s start, in either order. Any gap, even one nanosecond, means they do not abut, and an empty interval abuts nothing:

import { intervalAbutsDate, intervalAbutsUtc } from "@northguild/gmt";
intervalAbutsDate("2024-01-01", "2024-07-01", "2024-07-01", "2025-01-01"); // true
intervalAbutsDate("2024-01-01", "2024-06-30", "2024-07-01", "2024-12-31"); // false — 30 June is in neither
intervalAbutsUtc(
"2024-01-01T09:00:00Z",
"2024-01-01T12:00:00Z",
"2024-01-01T12:00:00.000000001Z",
"2024-01-01T17:00:00Z",
); // false — 1 ns apart

Each positional function exists in six variants: Date, Time, DateTime, Utc, Zoned and Unix. The examples above use Date and Utc. These show the others, one call each. The Unix variants take epoch numbers (milliseconds by default), and Time has no calendar, so it compares times of day.

Containment of a point, then of an inner interval:

import {
intervalContainsDateTime,
intervalContainsTime,
intervalContainsUnix,
intervalContainsZoned,
intervalEngulfsDate,
intervalEngulfsDateTime,
intervalEngulfsTime,
intervalEngulfsUnix,
intervalEngulfsUtc,
intervalEngulfsZoned,
} from "@northguild/gmt";
intervalContainsTime("09:00:00", "17:00:00", "12:00:00"); // true
intervalContainsDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-01T12:00:00"); // true
intervalContainsUnix(0, 1700000000, 170000000); // true
intervalContainsZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-12-31T23:59:59+00:00[UTC]", "2024-06-15T12:00:00+00:00[UTC]"); // true
intervalContainsTime("09:00:00", "17:00:00", "10:00:00", "16:00:00"); // true — inner interval
intervalEngulfsDate("2024-01-01", "2024-12-31", "2024-06-01", "2024-07-01"); // true
intervalEngulfsDate("2024-01-01", "2024-12-31", "2024-01-01", "2024-12-31"); // true — equal intervals
intervalEngulfsDate("2024-06-01", "2024-07-01", "2024-01-01", "2024-12-31"); // false — B is larger than A
intervalEngulfsTime("09:00:00", "17:00:00", "10:00:00", "12:00:00"); // true
intervalEngulfsDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-01T06:00:00", "2024-01-01T12:00:00"); // true
intervalEngulfsUnix(0, 1700000000, 1000000, 2000000); // true
intervalEngulfsUtc("2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z", "2024-06-01T00:00:00Z", "2024-07-01T00:00:00Z"); // true
intervalEngulfsZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-12-31T23:59:59+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-07-01T00:00:00+00:00[UTC]"); // true

intervalEngulfs* asks whether B lies within A and overlaps it. B may share A’s start or end, but an empty B at A’s end is not engulfed. It is the same test as the four-argument intervalContains*.

Overlap and intersection:

import {
intervalIntersectionDateTime,
intervalIntersectionTime,
intervalIntersectionUnix,
intervalIntersectionUtc,
intervalIntersectionZoned,
intervalsOverlapDateTime,
intervalsOverlapTime,
intervalsOverlapUnix,
intervalsOverlapUtc,
intervalsOverlapZoned,
} from "@northguild/gmt";
intervalsOverlapTime("09:00:00", "12:00:00", "11:00:00", "14:00:00"); // true
intervalsOverlapDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-01T12:00:00", "2024-01-03T00:00:00"); // true
intervalsOverlapUnix(0, 1700000000, 1000000, 2000000); // true
intervalsOverlapUtc("2024-01-01T09:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T17:00:00Z", "2024-01-01T18:00:00Z"); // false — touching at 17:00
intervalsOverlapZoned("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]"); // true
intervalIntersectionTime("09:00:00", "12:00:00", "11:00:00", "14:00:00");
// { start: "11:00:00", end: "12:00:00" }
intervalIntersectionDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-01T12:00:00", "2024-01-03T00:00:00");
// { start: "2024-01-01T12:00:00", end: "2024-01-02T00:00:00" }
intervalIntersectionUnix(0, 1700000000, 1000000, 2000000);
// { start: 1000000, end: 2000000 } — B lies inside A
intervalIntersectionUtc("2024-01-01T00:00:00Z", "2024-07-01T00:00:00Z", "2024-04-01T00:00:00Z", "2025-01-01T00:00:00Z");
// { start: "2024-04-01T00:00:00Z", end: "2024-07-01T00:00:00Z" }
intervalIntersectionZoned("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-03-01T00:00:00+00:00[UTC]", end: "2024-06-01T00:00:00+00:00[UTC]" }

Adjacency in the remaining variants:

import {
intervalAbutsDateTime,
intervalAbutsTime,
intervalAbutsUnix,
intervalAbutsZoned,
} from "@northguild/gmt";
intervalAbutsTime("09:00:00", "12:00:00", "12:00:00", "17:00:00"); // true
intervalAbutsDateTime("2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-02T00:00:00", "2024-01-03T00:00:00"); // true
intervalAbutsUnix(0, 1000000, 1000000, 2000000); // true
intervalAbutsZoned("2024-01-01T00:00:00+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-06-01T00:00:00+00:00[UTC]", "2024-09-01T00:00:00+00:00[UTC]"); // true

intervalAbuts* is Allen’s “meets”: one interval’s end equals the other’s start, so the two share no instant and leave no gap. A gap of even one epoch unit means they do not abut:

intervalAbutsUnix(0, 1000000, 1000001, 2000000); // false — one millisecond apart

All of these return the sentinel for the same invalid input: false for the boolean checks and null for the intersections. 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) all count.