UNPKG

@burglekitt/gmt

Version:

Temporal-based date and time utilities with timezone support and polyfill integration

412 lines (284 loc) 16.5 kB
--- name: compare-dates description: > Compare date values for ordering. Use isAfterDate, isBeforeDate, areDatesEqual for comparisons. Use areDatesEqualBy/areDateTimesEqualBy/areZonedEqualBy/ areUnixEqualBy/areUtcEqualBy to compare two values at a given calendar unit ("same month?"). Use isWeekend/isZonedWeekend for locale-aware weekend checks (weekend days vary by locale). Use isBusinessDay for fixed ISO Mon-Fri checks (locale-agnostic, matches addBusinessDays). Use getLocaleDayOfWeek/getLocaleZonedDayOfWeek for a locale-relative day-of-week index (0 = first day of week). Use isRelativeDay/isThisUnit/isPast/isFuture (plus isZoned* variants) for now-relative predicates ("is this today", "is this overdue") — depend on system clock/timeZone unless using the zoned variants with an explicit timeZone. Use nextWeekday/previousWeekday for the next/previous occurrence of an ISO day of week ("next Friday"). Returns false/null/"" on invalid input. sources: - 'burglekitt/gmt:packages/gmt/src/plain/compare/index.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/compare/index.ts' - 'burglekitt/gmt:packages/gmt/src/unix/compare/index.ts' - 'burglekitt/gmt:packages/gmt/src/utc/compare/index.ts' - 'burglekitt/gmt:packages/gmt/src/plain/calculate/getLocaleDayOfWeek.ts' - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/getLocaleZonedDayOfWeek.ts' - 'burglekitt/gmt:packages/gmt/src/plain/calculate/nextWeekday.ts' - 'burglekitt/gmt:packages/gmt/src/plain/calculate/previousWeekday.ts' metadata: type: core library: '@burglekitt/gmt' library_version: '1.14.1' --- # Compare Dates Use this skill when you need to compare date values for ordering. ## Setup ```ts import { isAfterDate, isBeforeDate, areDatesEqual } from "@burglekitt/gmt"; import { isBetweenDate } from "@burglekitt/gmt"; ``` ## Core Patterns ### Check if date is after another ```ts const result = isAfterDate("2024-03-20", "2024-03-15"); // true ``` ### Check if date is before another ```ts const result = isBeforeDate("2024-03-10", "2024-03-15"); // true ``` ### Check if dates are equal ```ts import { areDatesEqual } from "@burglekitt/gmt"; const result = areDatesEqual("2024-03-15", "2024-03-15"); // true ``` ### Check if two values are equal at a given unit (same month, same year, ...) ```ts import { areDatesEqualBy } from "@burglekitt/gmt"; areDatesEqualBy("2024-03-15", "2024-03-20", "month"); // true areDatesEqualBy("2023-03-15", "2024-03-15", "month"); // false — same month, different year areDatesEqualBy("2024-03-11", "2024-03-17", "week"); // true (default weekStartsOn: "monday") ``` `areDatesEqualBy`/`areDateTimesEqualBy`/`areZonedEqualBy`/`areUnixEqualBy`/`areUtcEqualBy` are the namespace variants of this pattern (Decision 5 — one parameterized function per namespace instead of a `isSameDay`/`isSameMonth`/`isSameYear`/... family). Equality is measured by comparing the *start of that unit* for each value, so a coarser unit like `"month"` implicitly requires every unit above it (year) to match too — see the Common Mistakes entry below. `areZonedEqualBy` compares each value's own local calendar fields (its own time zone), not the underlying instant. Migrating from date-fns: | date-fns | GMT | | ----------------------------- | ------------------------------------- | | `isSameDay(a, b)` | `areDatesEqualBy(a, b, "day")` | | `isSameWeek(a, b, opts)` | `areDatesEqualBy(a, b, "week", opts)` | | `isSameMonth(a, b)` | `areDatesEqualBy(a, b, "month")` | | `isSameYear(a, b)` | `areDatesEqualBy(a, b, "year")` | | `isSameHour(a, b)` | `areDateTimesEqualBy(a, b, "hour")` | | `isSameMinute(a, b)` | `areDateTimesEqualBy(a, b, "minute")` | | `isSameSecond(a, b)` | `areDateTimesEqualBy(a, b, "second")` | ### Check if date is between two dates ```ts import { isBetweenDate } from "@burglekitt/gmt"; const result = isBetweenDate("2024-03-15", "2024-03-10", "2024-03-20"); // true ``` ### Compare datetime values ```ts import { isAfterDateTime, isBeforeDateTime, areDateTimesEqual } from "@burglekitt/gmt"; const after = isAfterDateTime("2024-03-15T14:30:45", "2024-03-15T14:30:00"); // true const before = isBeforeDateTime("2024-03-15T14:30:00", "2024-03-15T14:30:45"); // true const equal = areDateTimesEqual("2024-03-15T14:30:45", "2024-03-15T14:30:45"); // true ``` ### Compare time values ```ts import { isAfterTime, isBeforeTime, areTimesEqual } from "@burglekitt/gmt"; const after = isAfterTime("14:30:45", "14:30:00"); // true const before = isBeforeTime("14:30:00", "14:30:45"); // true const equal = areTimesEqual("14:30:45", "14:30:45"); // true ``` ### Check if a date falls on a weekend (locale-aware) ```ts import { isWeekend, isZonedWeekend } from "@burglekitt/gmt"; isWeekend("2024-02-03", "en-US"); // true (Saturday, en-US weekend is Sat/Sun) isWeekend("2024-02-02", "he-IL"); // true (Friday, he-IL weekend is Fri/Sat) isZonedWeekend("2024-02-04T10:00:00+02:00[Asia/Jerusalem]", "he-IL"); // false (Sunday isn't part of he-IL's weekend) ``` `isZonedWeekend` checks the `ZonedDateTime`'s own local calendar day — no separate timezone conversion needed. ### Check if a date is a business day (fixed MonFri) ```ts import { isBusinessDay } from "@burglekitt/gmt"; isBusinessDay("2024-02-05"); // true (Monday) isBusinessDay("2024-02-10"); // false (Saturday) isBusinessDay("2024-02-04"); // false (Sunday) ``` `isBusinessDay` uses the fixed ISO MondayFriday boundary (Mon=1 Fri=5) — locale-agnostic and with no holiday calendar. It matches the boundary that `addBusinessDays`/`subtractBusinessDays` use, and is the locale-agnostic complement to `isWeekend` (which resolves weekend days per locale via `Intl.Locale`'s `weekInfo`). ### Get the locale-relative day-of-week index ```ts import { getLocaleDayOfWeek, getLocaleZonedDayOfWeek } from "@burglekitt/gmt"; getLocaleDayOfWeek("2024-02-25", "en-US"); // 0 (Sunday = first day of en-US week) getLocaleDayOfWeek("2024-02-26", "en-US"); // 1 (Monday) getLocaleDayOfWeek("2024-02-26", "fr-FR"); // 0 (Monday = first day of fr-FR week) getLocaleDayOfWeek("2024-02-24", "he-IL"); // 0 (Saturday = first day of he-IL week) getLocaleZonedDayOfWeek("2024-02-25T12:00:00+00:00[UTC]", "en-US"); // 0 getLocaleZonedDayOfWeek("2024-02-26T12:00:00+00:00[UTC]", "fr-FR"); // 0 ``` `getLocaleDayOfWeek` returns `null` on invalid input. `getLocaleZonedDayOfWeek` reads the `ZonedDateTime`'s local calendar day — no separate timezone conversion needed. ### Check now-relative predicates (today, this month, past, future) ```ts import { isRelativeDay, isThisUnit, isPast, isFuture } from "@burglekitt/gmt"; isRelativeDay("2024-03-15", 0); // "is today" — true if today is 2024-03-15 isRelativeDay("2024-03-14", -1); // "is yesterday" — true if today is 2024-03-15 isRelativeDay("2024-03-16", 1); // "is tomorrow" — true if today is 2024-03-15 isThisUnit("2024-03-15", "month"); // true if today is any day in March 2024 isThisUnit("2024-02-26", "week", "fr-FR"); // locale-aware week boundary (fr-FR starts Monday) isPast("2024-03-14"); // true if today is 2024-03-15 (strictly before, not on-or-before) isFuture("2024-03-16"); // true if today is 2024-03-15 (strictly after, not on-or-before) ``` `isRelativeDay` subsumes `isToday`/`isYesterday`/`isTomorrow` (`offsetDays: 0`/`-1`/`1`); `isThisUnit` subsumes `isThisWeek`/`isThisMonth`/`isThisYear` (Decision 5, `context/roadmap/issues/J.md`). All four compare against `getToday()`, so they depend on the **system clock and system timeZone** — see the Common Mistakes entry below before using these in a server or test context. Zoned counterparts — `isZonedRelativeDay`, `isZonedThisUnit`, `isZonedPast`, `isZonedFuture` — take a `ZonedDateTime` string and resolve "today"/"now" in *that value's own* timeZone, making them deterministic regardless of the host's system timeZone: ```ts import { isZonedRelativeDay, isZonedThisUnit, isZonedPast, isZonedFuture } from "@burglekitt/gmt"; isZonedRelativeDay("2024-03-15T10:00:00-04:00[America/New_York]", 0); // "today" in America/New_York, not the host's timeZone isZonedThisUnit("2024-03-15T10:00:00-04:00[America/New_York]", "month"); // isZonedPast/isZonedFuture compare the exact instant (not just the calendar // day) against Temporal.Now.instant() — the zoned counterpart carries a // full time-of-day, unlike the plain, day-only isPast/isFuture. isZonedPast("2020-01-01T00:00:00Z[UTC]"); // true isZonedFuture("2999-01-01T00:00:00Z[UTC]"); // true ``` ### Find the next/previous occurrence of a weekday ```ts import { nextWeekday, previousWeekday } from "@burglekitt/gmt"; nextWeekday("2024-03-13", 5); // "2024-03-15" (Wednesday -> next Friday) nextWeekday("2024-03-15", 5); // "2024-03-22" (already Friday -> advances a full week by default) nextWeekday("2024-03-15", 5, { inclusive: true }); // "2024-03-15" (already Friday -> returned as-is) previousWeekday("2024-03-13", 5); // "2024-03-08" (Wednesday -> previous Friday) previousWeekday("2024-03-15", 5); // "2024-03-08" (already Friday -> goes back a full week by default) ``` `dayOfWeek` uses Temporal's ISO numbering (1 = Monday 7 = Sunday), the same as `getDayOfWeek`/`parseDayOfWeekFromDate`. These replace date-fns's sixteen `next*`/`previous*` functions with two parameterized calls (Decision 5, `context/roadmap/issues/J.md`): | date-fns | GMT | | ------------------ | ------------------------------ | | `nextMonday(v)` | `nextWeekday(v, 1)` | | `nextFriday(v)` | `nextWeekday(v, 5)` | | `nextDay(v, n)` | `nextWeekday(v, n)` | | `previousFriday(v)`| `previousWeekday(v, 5)` | | `previousDay(v, n)`| `previousWeekday(v, n)` | `date-fns`'s `lastDayOfMonth` is already covered by `endOfDate(value, "month")` — not a gap this pair fills. ## Common Mistakes ### HIGH Using string comparison for dates Wrong: ```ts const isAfter = "2024-03-20" > "2024-03-15"; // true but fragile ``` Correct: ```ts import { isAfterDate } from "@burglekitt/gmt"; const isAfter = isAfterDate("2024-03-20", "2024-03-15"); // true ``` Source: AGENTS.md — Use Temporal for proper comparison ### MEDIUM Not handling invalid input Wrong: ```ts const result = isAfterDate("invalid", "2024-03-15"); // Assume result is always boolean if (result) { // false, not throwing process(); } ``` Correct: ```ts import { isAfterDate, isValidDate } from "@burglekitt/gmt"; const dateA = "invalid"; const dateB = "2024-03-15"; if (!isValidDate(dateA) || !isValidDate(dateB)) { throw new Error("Invalid date"); } const result = isAfterDate(dateA, dateB); ``` Source: packages/gmt/src/plain/compare/isAfterDate.ts — Returns false on invalid ### MEDIUM Comparing different date formats Wrong: ```ts const result = isAfterDate("2024-3-5", "2024-03-15"); // may be incorrect ``` Correct: ```ts // Ensure canonical format before comparison import { isAfterDate } from "@burglekitt/gmt"; const result = isAfterDate("2024-03-05", "2024-03-15"); ``` Source: Temporal.PlainDate.from() — canonicalizes input ### MEDIUM Assuming weekends are always Saturday/Sunday Wrong: ```ts import { getDayOfWeek } from "@burglekitt/gmt"; const day = getDayOfWeek(); // ISO day of week, always Monday-start const isWeekendDay = day === 6 || day === 7; // wrong for he-IL/ar-SA (Fri/Sat) ``` Correct: ```ts import { isWeekend } from "@burglekitt/gmt"; const isWeekendDay = isWeekend("2024-02-02", "he-IL"); // true — Friday is part of he-IL's weekend ``` Most locales use Saturday/Sunday, but `he-IL`/`ar-SA` use Friday/Saturday — `isWeekend`/`isZonedWeekend` resolve this per-locale via `Intl.Locale`'s `weekInfo` instead of hardcoding a day pair. Source: packages/gmt/src/plain/compare/isWeekend.ts — locale-aware via Intl.Locale weekInfo ### MEDIUM Confusing isBusinessDay with isWeekend Wrong: ```ts const isWeekendDay = isBusinessDay("2024-02-02"); // false (it's a Friday) // then assume isBusinessDay captures locale-specific weekend days ``` Correct: ```ts import { isWeekend } from "@burglekitt/gmt"; // isBusinessDay is fixed ISO Mon–Fri (no locale lookup, no holidays) isBusinessDay("2024-02-02"); // true (Friday) // For locale-aware weekend detection (e.g. he-IL Fri/Sat), use isWeekend isWeekend("2024-02-02", "he-IL"); // true (Friday is part of he-IL's weekend) ``` `isBusinessDay` uses a fixed MondayFriday boundary and never consults `Intl.Locale`'s `weekInfo`; it does not account for holidays. Do not reach for it when you need locale-aware weekend detection — that's `isWeekend`/`isZonedWeekend`'s job. Source: packages/gmt/src/plain/compare/isBusinessDay.ts — fixed ISO MonFri, locale-agnostic ### MEDIUM Assuming "same month" ignores the year Wrong: ```ts import { areDatesEqualBy } from "@burglekitt/gmt"; // Assuming this checks "is it March, regardless of year" const bothMarch = areDatesEqualBy("2023-03-15", "2024-03-15", "month"); // false, not true ``` Correct: ```ts import { areDatesEqualBy } from "@burglekitt/gmt"; // "Same month" means the same month AND year — matches date-fns's isSameMonth // and Luxon's dt.hasSame(other, "month") areDatesEqualBy("2024-03-01", "2024-03-31", "month"); // true (same month, same year) // To check "same month-of-year regardless of year", compare the month field directly import { parseMonthFromDate } from "@burglekitt/gmt"; const sameMonthOfYear = parseMonthFromDate("2023-03-15") === parseMonthFromDate("2024-03-15"); // true ``` `areDatesEqualBy(a, b, unit)` compares the start-of-`unit` boundary for each value, so any unit implicitly requires every coarser unit above it to match too — this is deliberate (Decision 5/6, `context/roadmap/issues/J.md`) and matches date-fns/Luxon precedent, not a GMT-specific quirk. Source: packages/gmt/src/plain/compare/areDatesEqualBy.ts — start-of-unit comparison ### HIGH Assuming isRelativeDay/isThisUnit/isPast/isFuture are timeZone-independent Wrong: ```ts import { isRelativeDay } from "@burglekitt/gmt"; // Assuming "today" means the same thing everywhere const isDueToday = isRelativeDay(dueDate, 0); ``` Correct: ```ts import { isZonedRelativeDay } from "@burglekitt/gmt"; // "Today" is resolved in the value's own timeZone — deterministic // regardless of the host machine's system timeZone const isDueToday = isZonedRelativeDay(dueZonedDateTime, 0); ``` `isRelativeDay`/`isThisUnit`/`isPast`/`isFuture` depend on the **system clock and system timeZone** (they compare against `getToday()`). The same call returns different answers on hosts in different timeZones at the same instant — `isRelativeDay("2024-03-15", 0)` can be true in one timeZone and false in another 24 hours apart (e.g. `Pacific/Apia` vs. `Pacific/Niue`). Callers needing determinism — server-side rendering, tests, scheduled jobs — should use the zoned variants (`isZonedRelativeDay`, `isZonedThisUnit`, `isZonedPast`, `isZonedFuture`) with an explicit timeZone, or compare against an explicit reference with `areDatesEqualBy`/`isBeforeDate`/`isAfterDate`. Source: packages/gmt/src/plain/compare/isRelativeDay.ts, packages/gmt/src/plain/get/getToday.ts — system clock/timeZone dependency ### MEDIUM Assuming nextWeekday/previousWeekday return the input when it's already on the target day Wrong: ```ts import { nextWeekday } from "@burglekitt/gmt"; // Assuming a Friday input returns itself when asking for "next Friday" const result = nextWeekday("2024-03-15", 5); // "2024-03-22", not "2024-03-15" ``` Correct: ```ts import { nextWeekday } from "@burglekitt/gmt"; // Pass { inclusive: true } to return the input as-is when it already falls on dayOfWeek const result = nextWeekday("2024-03-15", 5, { inclusive: true }); // "2024-03-15" ``` `options.inclusive` defaults to `false`, matching date-fns: a `value` already on `dayOfWeek` advances a full week rather than returning itself. Same default applies to `previousWeekday`. Source: packages/gmt/src/plain/calculate/nextWeekday.ts, packages/gmt/src/plain/calculate/previousWeekday.ts — inclusive defaults to false ## References - [Full compare API](references/compare-api.md) - [Temporal.PlainDate comparison](https://tc39.es/proposal-temporal/docs/plaindate.html#comparison)