Skip to content

Validation

  • The core functions, which every industry uses.

GMT’s public functions never throw on invalid input — they return a sentinel. But when you need to reject invalid input at a system boundary (a form, an API parameter, a config file), use these validators to check first.

import { isValidDate, isValidTime, isValidDateTime } from "@northguild/gmt";
isValidDate("2024-03-15"); // true
isValidDate("2024-02-30"); // false — invalid day
isValidTime("14:30:45"); // true
isValidTime("25:00:00"); // false — invalid hour
isValidDateTime("2024-03-15T14:30:45"); // true
import { isValidTimeZone, isValidZonedDateTime } from "@northguild/gmt/zoned";
isValidTimeZone("America/New_York"); // true
isValidTimeZone("Invalid/Zone"); // false
isValidZonedDateTime("2024-03-15T14:30:45[America/New_York]"); // true
isValidZonedDateTime("2024-02-30T14:30:45[America/New_York]"); // false

isValidZonedDateTime accepts [u-ca=iso8601] and rejects any other calendar annotation, so a function that has not opted in to calendars fails closed. To check a value that carries a calendar, use isValidCalendarZonedDateTime. The calendar annotation must follow the zone:

import { isValidCalendarZonedDateTime, isValidUtc } from "@northguild/gmt";
isValidCalendarZonedDateTime("2024-02-24T14:30:00-05:00[America/New_York][u-ca=hebrew]"); // true
isValidCalendarZonedDateTime("2024-02-24T14:30:00-05:00[u-ca=hebrew][America/New_York]"); // false — the zone comes first
isValidUtc("2024-01-01T00:00:00Z[u-ca=hebrew]"); // true — a UTC string's calendar annotation is ignored

See Calendar Systems for the rules on calendar annotations.

isValidUtc is true for a UTC instant in ISO 8601 extended form with an upper-case T and Z. That includes four forms outside RFC 3339, which other readers do not all take: a time without seconds, a comma before the fraction, an expanded ±YYYYYY year and an RFC 9557 annotation. When the string goes on to a database, a JSON client or any reader that is not Temporal, check it with isValidUtcTimestamp:

import { isValidUtc, isValidUtcTimestamp } from "@northguild/gmt";
isValidUtcTimestamp("2026-10-06T12:00:00Z"); // true
isValidUtcTimestamp("2026-10-06T12:00:00.123456789Z"); // true
isValidUtc("2026-10-06T12:00:00Z[Europe/Paris]"); // true
isValidUtcTimestamp("2026-10-06T12:00:00Z[Europe/Paris]"); // false — the annotation
isValidUtcTimestamp("2026-10-06T12:00:00,5Z"); // false — comma before the fraction
isValidUtcTimestamp("-005000-01-01T00:00:00Z"); // false — expanded year
isValidUtcTimestamp("2026-10-06T12:00Z"); // false — no seconds
isValidUtcTimestamp("2026-02-30T00:00:00Z"); // false — February has no 30th

It accepts one spelling of the RFC 3339 date-time: a four-digit year, an upper-case T, seconds, an optional fraction of one to nine digits, and an upper-case Z with nothing after it. A reader may still refuse or round a string that is true here: PostgreSQL has no year 0000 and keeps microseconds. See Nanosecond Precision. To accept the other RFC 3339 spellings, numeric offsets included, use isValidRfc3339DateTime.

import { hasDaylightSaving, getDstTransitions } from "@northguild/gmt/zoned";
hasDaylightSaving("America/New_York", { at: "2024-01-15T12:00:00Z" }); // true
hasDaylightSaving("Asia/Tokyo", { at: "2024-01-15T12:00:00Z" }); // false
getDstTransitions("America/New_York", 2024);
// [
// { instant: "2024-03-10T07:00:00Z", offsetBefore: "-05:00", offsetAfter: "-04:00" },
// { instant: "2024-11-03T06:00:00Z", offsetBefore: "-04:00", offsetAfter: "-05:00" }
// ]
getDstTransitions("Asia/Tokyo", 2024); // [] — no transitions
getDstTransitions("Invalid/Zone", 2024); // [] — invalid input

hasDaylightSaving is true when the zone is in daylight time at at, or a daylight period begins less than 365 days after it. An invalid zone, an invalid at and a fixed offset return false:

import { hasDaylightSaving } from "@northguild/gmt/zoned";
hasDaylightSaving("Invalid/Zone", { at: "2024-01-15T12:00:00Z" }); // false

at is an ISO 8601 instant string. With it, the answer does not depend on the day the code runs. Without it, the reference is the current instant. See Zoned Operations for the rule behind both functions.

Each fixed interchange format has a validator that is true exactly when its parser returns a value. A pattern proves the shape alone:

import {
isValidHttpDate,
isValidRfc2822DateTime,
isValidRfc3339DateTime,
isValidSqlDateTime,
} from "@northguild/gmt";
isValidRfc3339DateTime("2023-02-29T10:00:00Z"); // false — 2023 has no 29 February
isValidRfc2822DateTime("Sat, 15 Mar 2024 14:30:00 -0400"); // false — a Friday
isValidHttpDate("Sun Nov 6 08:49:37 1994"); // true — asctime-date
isValidHttpDate("Fri, 15 Mar 2024 24:00:00 GMT"); // false — hour 24
isValidSqlDateTime("2024-03-15 14:30:00"); // true

See Parsing for the formats themselves.

import { isValidDateUnit, isValidTimeUnit } from "@northguild/gmt";
isValidDateUnit("day"); // true
isValidTimeUnit("hour"); // true
isValidDateUnit("invalid"); // false

Interval validation (positional args, start ≤ end)

Section titled “Interval validation (positional args, start ≤ end)”
import {
isValidDateInterval,
isValidTimeInterval,
isValidDateTimeInterval,
} from "@northguild/gmt";
isValidDateInterval("2024-01-01", "2024-12-31"); // true
isValidDateInterval("2024-12-31", "2024-01-01"); // false — start > end
isValidTimeInterval("09:00:00", "17:00:00"); // true
isValidDateTimeInterval("2024-01-01T10:00:00", "2024-12-31T23:59:59"); // true

There are isValidUtcInterval, isValidUnixInterval, and isValidZonedInterval siblings. isValidZonedInterval takes two zoned datetimes:

import { isValidZonedInterval } from "@northguild/gmt";
isValidZonedInterval("2024-01-01T10:00:00+00:00[UTC]", "2024-12-31T23:59:59+00:00[UTC]"); // true
isValidZonedInterval("2024-12-31T10:00:00+00:00[UTC]", "2024-01-01T10:00:00+00:00[UTC]"); // false — start > end

All return false on invalid input: a wrong type, a malformed string, a leap second, an inverted interval, mixed kinds for the plain interval validators, or an epoch outside the unix/ grammar (not a safe integer or a digit string). An interval whose start equals its end is valid: it is empty.

Range validators take { value1, value2, options? } — a different shape from the interval validators above:

import { isValidDateRange, isValidTimeRange, isValidZonedRange } from "@northguild/gmt";
isValidDateRange({ value1: "2024-01-01", value2: "2024-12-31" }); // true
isValidTimeRange({ value1: "09:00:00", value2: "17:00:00" }); // true
isValidZonedRange({
value1: "2024-01-01T10:00:00+00:00[UTC]",
value2: "2024-12-31T23:59:59+00:00[UTC]",
}); // true

A range requires value1 strictly before value2. Pass options: { allowEqual: true } to accept two equal values:

isValidTimeRange({ value1: "12:00:00", value2: "12:00:00" }); // false — equal values are not a range
isValidTimeRange({ value1: "12:00:00", value2: "12:00:00", options: { allowEqual: true } }); // true

The other range validators (isValidDateTimeRange, isValidUtcRange, isValidUnixRange) take the same object, and isValidDateRange and isValidZonedRange accept allowEqual too.

Don’t confuse the two: isValidDateInterval takes positional (start, end) args; isValidDateRange takes an object { value1, value2 }.