Interval Basics
An interval is a start and an end. GMT has two families of interval functions. They
differ in shape and value types, and share one boundary model: half-open [start, end).
interval/ (the standard) | positional plain/ utc/ zoned/ unix/ functions | |
|---|---|---|
| Shape | Interval records: { start, end } | positional strings: (aStart, aEnd, bStart, bEnd) |
| Endpoints | instant strings with an offset, any zone | one value type per suffix (Date, Time, DateTime, Utc, Zoned, Unix) |
| Boundary model | half-open [start, end) | half-open [start, end) |
| Touching intervals | do not overlap | do not overlap; they abut |
| Output strings | the caller’s own strings | re-serialised values |
The half-open standard
Section titled “The half-open standard”An instant t is inside [start, end) when start ≤ t < end. The start belongs to
the interval, and the end does not. This is the convention of SQL:2011’s closed-open
application-time PERIOD, RFC 5545’s non-inclusive DTEND, and Dijkstra’s EWD831.
Under it, a container that gates out at exactly 17:00 has not used a day of the next
window, and two consecutive shifts never double-count the instant they meet.
import { intervalContains, intervalsOverlap } from "@northguild/gmt";
const shift = { start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" };
intervalContains(shift, "2024-01-01T09:00:00Z"); // true — start is insideintervalContains(shift, "2024-01-01T17:00:00Z"); // false — end is not
intervalsOverlap(shift, { start: "2024-01-01T17:00:00Z", end: "2024-01-01T18:00:00Z" });// false — touching intervals share no instantEndpoints are instants
Section titled “Endpoints are instants”Both endpoints must be instant strings: an offset (Z or ±HH:MM) is required, and a
bracketed IANA zone is optional. As with a TC39 Temporal.Instant, they compare by
exact time only, so the two endpoints may name different zones. Leap seconds are
rejected, and a [u-ca=…] calendar annotation is ignored, as Temporal.Instant.from
ignores it.
The offset fixes the instant. Some zones had an offset with seconds in their history, and an
offset written to the minute may be that offset rounded: -00:45[Africa/Monrovia] stands for
−00:44:30. GMT reads it as the instant the zone gives, so a zoned string GMT wrote reads back
as the same moment, from the first instant of the range to the last. The one exception comes
from Temporal itself. A wall time repeated inside a sub-minute offset change, written with a
minute-rounded offset, reads as its first pass; 1952-10-15T23:59:59-11:20[Pacific/Niue] is
one example, and several zones have such a change in their history.
A plain date or a zoneless datetime has no position on the timeline until you name a
zone. Resolve it to an instant first. For local-calendar edges such as a local day or a
trading session, build them with floorToZone
or bucketRange.
Validate with isValidInterval
Section titled “Validate with isValidInterval”import { isValidInterval } from "@northguild/gmt";
isValidInterval({ start: "2024-01-01T09:00:00Z", end: "2024-01-01T17:00:00Z" }); // trueisValidInterval({ start: "2024-01-01T09:00:00Z", end: "2024-01-01T09:00:00Z" }); // true — emptyisValidInterval({ start: "2024-01-01T17:00:00Z", end: "2024-01-01T09:00:00Z" }); // false — invertedisValidInterval({ start: "2024-01-01T09:00:00", end: "2024-01-01T17:00:00Z" }); // false — no offsetstart === end is a valid empty interval. It has a position but contains no
instant. An inverted interval is invalid input, and every function returns its
sentinel for it rather than swapping the endpoints.
Return conventions
Section titled “Return conventions”Nothing in either family throws on bad input.
interval/ function | Returns | Invalid input |
|---|---|---|
isValidInterval, intervalsOverlap, intervalContains | boolean | false |
intersectIntervals, clampInterval | Interval | null | null |
mergeIntervals, subtractIntervals, splitIntervalAt | Interval[] | [] |
sumIntervals | ISO 8601 duration string | "" |
[] is both a legitimate answer (a fully covered subtraction, for example) and the
array sentinel. When the difference matters, check the inputs with isValidInterval.
| Positional family | Returns | Invalid input |
|---|---|---|
isValid*Interval, intervalContains*, intervalsOverlap*, intervalAbuts*, intervalEngulfs* | boolean | false |
intervalIntersection*, intervalUnion*, intervalFromDuration* | { start, end } | null | null |
intervalDifference*, intervalXor*, intervalXorAll*, mergeIntervals*, splitIntervalByUnit*, intervalDivideEqually*, intervalSplitAt* | Array<{ start, end }> | [] |
intervalCount*, intervalLength*, intervalOverlappingDays* | number | null | null |
Every positional function returns its sentinel for the same causes: a wrong type, a malformed
string, a leap second, an inverted interval (start > end), or an epoch outside the unix/
grammar (not a safe integer or a digit string). The plain interval validators also reject
mixed kinds, such as a date paired with a time.
The positional functions
Section titled “The positional functions”The positional functions read an interval as half-open too (since 1.16.0; most families were
closed before). For a Date interval, end is the first day after the period:
["2024-01-01", "2024-07-01") is the first half of 2024. Their validators take (start, end)
and require start <= end:
import { isValidDateInterval, isValidDateRange, isValidTimeInterval, isValidTimeRange, isValidZonedInterval, isValidZonedRange,} from "@northguild/gmt";
isValidDateInterval("2024-01-01", "2024-12-31"); // trueisValidDateInterval("2024-12-31", "2024-01-01"); // false — invertedisValidTimeInterval("09:00:00", "17:00:00"); // trueisValidZonedInterval("2024-01-01T10:00:00+00:00[UTC]", "2024-12-31T23:59:59+00:00[UTC]"); // true
// Range validators are a different shape: an object with value1 / value2isValidDateRange({ value1: "2024-01-01", value2: "2024-12-31" }); // trueisValidTimeRange({ value1: "09:00:00", value2: "17:00:00" }); // trueisValidZonedRange({ value1: "2024-01-01T10:00:00+00:00[UTC]", value2: "2024-12-31T23:59:59+00:00[UTC]" }); // trueThe interval validators accept start === end, an empty interval. The range validators
require value1 strictly before value2 unless you pass options: { allowEqual: true }.
The full set of twelve, with Utc and Unix variants, is on the
Validation page.
intervalFromDuration* builds a positional interval from one point and an ISO 8601
duration, anchored at either end. Calendar units resolve against the point itself:
import { intervalFromDurationDate, intervalFromDurationZoned } from "@northguild/gmt";
intervalFromDurationDate("2024-01-01", "P1M", "start");// { start: "2024-01-01", end: "2024-02-01" }
intervalFromDurationDate("2024-02-01", "P1M", "end");// { start: "2024-01-01", end: "2024-02-01" }
intervalFromDurationZoned("2024-03-09T02:30:00-05:00[America/New_York]", "P1D", "start");// { start: "2024-03-09T02:30:00-05:00[America/New_York]", end: "2024-03-10T03:30:00-04:00[America/New_York]" }// — the spring-forward day is 23 hours longEach variant is shown once below. The time-of-day variant has no calendar, so it takes a duration in exact units only:
import { intervalFromDurationDateTime, intervalFromDurationTime, intervalFromDurationUnix, intervalFromDurationUtc,} from "@northguild/gmt";
intervalFromDurationDateTime("2024-01-01T09:00:00", "PT90M", "start");// { start: "2024-01-01T09:00:00", end: "2024-01-01T10:30:00" }
intervalFromDurationUtc("2024-01-01T09:00:00Z", "PT8H", "end");// { start: "2024-01-01T01:00:00Z", end: "2024-01-01T09:00:00Z" }
intervalFromDurationUnix(0, "PT1H", "start");// { start: 0, end: 3600000 }
intervalFromDurationUnix(0, "PT1H", "start", { epochUnit: "seconds" });// { start: 0, end: 3600 }
intervalFromDurationTime("09:00:00", "PT8H", "start");// { start: "09:00:00", end: "17:00:00" }
intervalFromDurationTime("12:00:00", "P1D", "start");// null — PlainTime has no calendar, so a day cannot be resolvedintervalFromDurationTime returns null for a duration with a nonzero years, months, weeks or
days part. It takes no options argument, as addTime takes none. intervalFromDurationZoned
accepts the same disambiguation and overflow options as addZoned, and
intervalFromDurationUnix accepts addUnix’s epochUnit, timeZone and overflow options.
Calendar units (years, months, weeks) resolve against the point itself, so no separate
relativeTo is needed.
A negative duration that inverts the span returns null, and so does an overflow: "reject"
result:
intervalFromDurationDate("2024-01-01", "-P1M", "start"); // null — invertedintervalFromDurationDate("2024-01-31", "P1M", "start", { overflow: "reject" }); // null — 31 February does not existBoth families agree at every boundary, so a positional result and an interval/ result can
be combined. For instant-typed values, intervalsOverlapUtc, intervalIntersectionUtc,
mergeIntervalsUtc, intervalUnionUtc and intervalDifferenceUtc call the interval/
functions directly.
See also
Section titled “See also”- Containment and Overlap: overlap, containment, intersection, clamping
- Set Operations: merge, subtract, sum covered time, and the positional union, difference and xor
- Splitting and Counting: split at boundaries, split by unit, count, measure length
intervalreferenceIntervaltypeisValidDateIntervalreference