import { describe, it } from "@effect/vitest" import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { DateTime, Duration, Effect, Option } from "effect" import { TestClock } from "effect/testing" const setTo2024NZ = TestClock.setTime(new Date("2023-12-31T11:00:00.000Z").getTime()) const assertSomeIso = (value: Option.Option, expected: string) => { const iso = Option.map(value, (value) => DateTime.formatIso(DateTime.toUtc(value))) assertSome(iso, expected) } const isDeno = "Deno" in globalThis describe("DateTime", () => { describe("mutate", () => { it.effect("should mutate the date", () => Effect.gen(function*() { const now = yield* DateTime.now const tomorrow = DateTime.mutate(now, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) const diff = DateTime.distance(now, tomorrow) deepStrictEqual(diff, Duration.fromInputUnsafe("1 day")) })) it.effect("preserves the named zone when mutating across DST", () => Effect.gen(function*() { yield* setTo2024NZ const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) const future = DateTime.mutate(now, (date) => { date.setUTCMonth(date.getUTCMonth() + 6) }) strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.mutate(future, (date) => { date.setUTCDate(date.getUTCDate() + 1) }) strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") })) }) describe("add", () => { it.effect("adds calendar days to a UTC DateTime", () => Effect.gen(function*() { const now = yield* DateTime.now const tomorrow = DateTime.add(now, { days: 1 }) const diff = DateTime.distance(now, tomorrow) deepStrictEqual(diff, Duration.fromInputUnsafe("1 day")) })) it("clamps to the last valid day when changing months", () => { const jan = DateTime.makeUnsafe({ year: 2023, month: 1, day: 31 }) let feb = DateTime.add(jan, { months: 1 }) strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") const mar = DateTime.makeUnsafe({ year: 2023, month: 3, day: 31 }) feb = DateTime.subtract(mar, { months: 1 }) strictEqual(feb.toJSON(), "2023-02-28T00:00:00.000Z") }) it.effect("preserves the named zone when adding calendar units across DST", () => Effect.gen(function*() { yield* setTo2024NZ const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) const future = DateTime.add(now, { months: 6 }) strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-06-30T12:00:00.000Z") strictEqual(DateTime.toDate(future).toISOString(), "2024-07-01T00:00:00.000Z") const plusOne = DateTime.add(future, { days: 1 }) strictEqual(DateTime.toDateUtc(plusOne).toISOString(), "2024-07-01T12:00:00.000Z") strictEqual(DateTime.toDate(plusOne).toISOString(), "2024-07-02T00:00:00.000Z") const minusOne = DateTime.subtract(plusOne, { days: 1 }) strictEqual(DateTime.toDateUtc(minusOne).toISOString(), "2024-06-30T12:00:00.000Z") strictEqual(DateTime.toDate(minusOne).toISOString(), "2024-07-01T00:00:00.000Z") })) it.effect("leap years", () => Effect.gen(function*() { yield* setTo2024NZ const now = DateTime.make({ year: 2024, month: 2, day: 29 }) assertSomeIso(now, "2024-02-29T00:00:00.000Z") const future = Option.map(now, (value) => DateTime.add(value, { years: 1 })) assertSome(Option.map(future, DateTime.formatIso), "2025-02-28T00:00:00.000Z") })) }) describe("endOf", () => { it("month", () => { const mar = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(mar, "month") strictEqual(end.toJSON(), "2024-03-31T23:59:59.999Z") }) it("feb leap year", () => { const feb = DateTime.makeUnsafe("2024-02-15T12:00:00.000Z") const end = DateTime.endOf(feb, "month") strictEqual(end.toJSON(), "2024-02-29T23:59:59.999Z") }) it("week", () => { const start = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week") strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") strictEqual(DateTime.getPartUtc(end, "weekDay"), 6) }) it("week last day", () => { const start = DateTime.makeUnsafe("2024-03-16T12:00:00.000Z") const end = DateTime.endOf(start, "week") strictEqual(end.toJSON(), "2024-03-16T23:59:59.999Z") }) it("week with options", () => { const start = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.endOf(start, "week", { weekStartsOn: 1 }) strictEqual(end.toJSON(), "2024-03-17T23:59:59.999Z") }) it.effect("returns the zoned end of the month", () => Effect.gen(function*() { yield* setTo2024NZ const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) const future = DateTime.endOf(now, "month") strictEqual(DateTime.toDateUtc(future).toISOString(), "2024-01-31T10:59:59.999Z") strictEqual(DateTime.toDate(future).toISOString(), "2024-01-31T23:59:59.999Z") })) }) describe("startOf", () => { it("month", () => { const mar = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(mar, "month") strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) it("month duplicated", () => { const mar = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(mar, "month").pipe( DateTime.startOf("month") ) strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) it("feb leap year", () => { const feb = DateTime.makeUnsafe("2024-02-15T12:00:00.000Z") const end = DateTime.startOf(feb, "month") strictEqual(end.toJSON(), "2024-02-01T00:00:00.000Z") }) it("week", () => { const start = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week") strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") strictEqual(DateTime.getPartUtc(end, "weekDay"), 0) }) it("week first day", () => { const start = DateTime.makeUnsafe("2024-03-10T12:00:00.000Z") const end = DateTime.startOf(start, "week") strictEqual(end.toJSON(), "2024-03-10T00:00:00.000Z") }) it("week with options", () => { const start = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") const end = DateTime.startOf(start, "week", { weekStartsOn: 1 }) strictEqual(end.toJSON(), "2024-03-11T00:00:00.000Z") }) }) describe("nearest", () => { it("month up", () => { const mar = DateTime.makeUnsafe("2024-03-16T12:00:00.000Z") const end = DateTime.nearest(mar, "month") strictEqual(end.toJSON(), "2024-04-01T00:00:00.000Z") }) it("month down", () => { const mar = DateTime.makeUnsafe("2024-03-16T11:00:00.000Z") const end = DateTime.nearest(mar, "month") strictEqual(end.toJSON(), "2024-03-01T00:00:00.000Z") }) it("second up", () => { const mar = DateTime.makeUnsafe("2024-03-20T12:00:00.500Z") const end = DateTime.nearest(mar, "second") strictEqual(end.toJSON(), "2024-03-20T12:00:01.000Z") }) it("second down", () => { const mar = DateTime.makeUnsafe("2024-03-20T12:00:00.400Z") const end = DateTime.nearest(mar, "second") strictEqual(end.toJSON(), "2024-03-20T12:00:00.000Z") }) }) describe.skipIf(isDeno)("format", () => { it.effect("full", () => Effect.gen(function*() { const now = yield* DateTime.now strictEqual( DateTime.format(now, { locale: "en-US", dateStyle: "full", timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" ) })) }) describe.skipIf(isDeno)("formatUtc", () => { it.effect("full", () => Effect.gen(function*() { const now = yield* DateTime.now strictEqual( DateTime.formatUtc(now, { locale: "en-US", dateStyle: "full", timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 AM Coordinated Universal Time" ) })) }) describe.skipIf(isDeno)("format zoned", () => { it.effect("full", () => Effect.gen(function*() { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) strictEqual( DateTime.format(now, { locale: "en-US", dateStyle: "full", timeStyle: "full" }), "Thursday, January 1, 1970 at 12:00:00 PM New Zealand Standard Time" ) })) it.effect.skipIf(isDeno)("long with offset", () => Effect.gen(function*() { const now = yield* DateTime.now const formatted = now.pipe( DateTime.setZoneOffset(10 * 60 * 60 * 1000), DateTime.format({ locale: "en-US", dateStyle: "long", timeStyle: "short" }) ) strictEqual(formatted, "January 1, 1970 at 10:00 AM") })) }) describe("fromParts", () => { it("partial", () => { const date = DateTime.makeUnsafe({ year: 2024, month: 12, day: 25 }) strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") }) it("month is set correctly", () => { const date = DateTime.makeUnsafe({ year: 2024 }) strictEqual(date.toJSON(), "2024-01-01T00:00:00.000Z") }) }) describe("setPartsUtc", () => { it("partial", () => { const date = DateTime.makeUnsafe({ year: 2024, month: 12, day: 25 }) strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") const updated = DateTime.setPartsUtc(date, { year: 2023, month: 1 }) strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") }) it("ignores time zones", () => { const date = DateTime.makeUnsafe({ year: 2024, month: 12, day: 25 }).pipe(DateTime.setZoneNamedUnsafe("Pacific/Auckland")) strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") const updated = DateTime.setPartsUtc(date, { year: 2023, month: 1 }) strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") }) }) describe("setParts", () => { it("partial", () => { const date = DateTime.makeUnsafe({ year: 2024, month: 12, day: 25 }) strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") const updated = DateTime.setParts(date, { year: 2023, month: 1 }) strictEqual(updated.toJSON(), "2023-01-25T00:00:00.000Z") }) it("accounts for time zone", () => { const date = DateTime.makeUnsafe({ year: 2024, month: 12, day: 25 }).pipe(DateTime.setZoneNamedUnsafe("Pacific/Auckland")) strictEqual(date.toJSON(), "2024-12-25T00:00:00.000Z") const updated = DateTime.setParts(date, { year: 2023, month: 6, hour: 12 }) strictEqual(updated.toJSON(), "2023-06-25T00:00:00.000Z") }) }) describe("formatIso", () => { it("full", () => { const now = DateTime.makeUnsafe("2024-03-15T12:00:00.000Z") strictEqual(DateTime.formatIso(now), "2024-03-15T12:00:00.000Z") }) }) describe("formatIsoOffset", () => { it.effect("correctly adds offset", () => Effect.gen(function*() { const now = yield* DateTime.nowInCurrentZone.pipe( DateTime.withCurrentZoneNamed("Pacific/Auckland") ) strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") })) }) describe("layerCurrentZoneNamed", () => { it.effect("correctly adds offset", () => Effect.gen(function*() { const now = yield* DateTime.nowInCurrentZone strictEqual(DateTime.formatIsoOffset(now), "1970-01-01T12:00:00.000+12:00") }).pipe( Effect.provide(DateTime.layerCurrentZoneNamed("Pacific/Auckland")) )) }) describe("removeTime", () => { it("removes time", () => { const dt = DateTime.makeZonedUnsafe("2024-01-01T01:00:00Z", { timeZone: "Pacific/Auckland", adjustForTimeZone: true }).pipe(DateTime.removeTime) strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") }) }) describe("makeZonedFromString", () => { it.effect("parses an instant with an offset and IANA zone", () => Effect.gen(function*() { const dt = DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]") assertSome(Option.map(dt, (value) => value.toJSON()), "2024-07-21T08:12:34.112Z") })) it.effect("parses an offset-only string as an Offset zone", () => Effect.gen(function*() { const dt = DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00") assertSome(Option.map(dt, (value) => value.zone._tag), "Offset") assertSome(Option.map(dt, (value) => value.toJSON()), "2024-07-21T08:12:34.112Z") })) it.effect("parses a UTC offset-only string as an Offset zone", () => Effect.gen(function*() { const dt = DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+00:00") assertSome(Option.map(dt, (value) => value.zone._tag), "Offset") assertSome(Option.map(dt, (value) => value.toJSON()), "2024-07-21T20:12:34.112Z") })) it.effect("roundtrips a bracketed IANA zone string", () => Effect.gen(function*() { const dt = Option.flatMap( DateTime.makeZonedFromString("2024-07-21T20:12:34.112546348+12:00[Pacific/Auckland]"), (d) => DateTime.makeZonedFromString(DateTime.formatIsoZoned(d)) ) assertSome(Option.map(dt, (value) => DateTime.zoneToString(value.zone)), "Pacific/Auckland") assertSome(Option.map(dt, (value) => value.toJSON()), "2024-07-21T08:12:34.112Z") })) }) it("parts equality", () => { const d1 = DateTime.makeUnsafe("2025-01-01") const d2 = DateTime.makeUnsafe("2025-01-01") deepStrictEqual(d1, d2) DateTime.toPartsUtc(d2) deepStrictEqual(d1, d2) }) // doesnt work in CI it.skip("makeZonedUnsafe no options", () => { const date = new Date("2024-07-21T20:12:34.112Z") ;(date as any).getTimezoneOffset = () => -60 const dt = DateTime.makeZonedUnsafe(date) deepStrictEqual(dt.zone, DateTime.zoneMakeOffset(60 * 60 * 1000)) }) describe("toUtc", () => { it.effect("with a Utc", () => Effect.gen(function*() { const dt = DateTime.makeUnsafe("2024-01-01T01:00:00Z") strictEqual(dt.toJSON(), "2024-01-01T01:00:00.000Z") })) it.effect("with a Zoned", () => Effect.gen(function*() { const dt = DateTime.makeZonedUnsafe("2024-01-01T01:00:00Z", { timeZone: "Pacific/Auckland", adjustForTimeZone: true }) strictEqual(dt.toJSON(), "2023-12-31T12:00:00.000Z") })) }) describe("nowAsDate", () => { it.effect("should return the current Date", () => Effect.gen(function*() { yield* setTo2024NZ const now = yield* DateTime.nowAsDate deepStrictEqual(now, new Date("2023-12-31T11:00:00.000Z")) })) }) describe("makeUnsafe", () => { it("treats strings without zone info as UTC", () => { const dt = DateTime.makeUnsafe("2024-01-01 01:00:00") strictEqual(dt.toJSON(), "2024-01-01T01:00:00.000Z") }) it("does not append Z to strings ending with Z", () => { const dt = DateTime.makeUnsafe("2026-01-27T17:14:06.000Z") strictEqual(dt.toJSON(), "2026-01-27T17:14:06.000Z") }) it("does not append Z to strings with explicit offset", () => { const dt = DateTime.makeUnsafe("2020-02-01T11:17:00+1100") strictEqual(dt.toJSON(), "2020-02-01T00:17:00.000Z") }) it("does not append Z to strings containing GMT", () => { const dt = DateTime.makeUnsafe("Tue, 27 Jan 2026 17:14:06 GMT") strictEqual(dt.toJSON(), "2026-01-27T17:14:06.000Z") }) }) describe("Disambiguation", () => { it.each<{ zone: string time: Partial description: string expectedResults: Record }>( [ { zone: "America/New_York", time: { year: 2024, month: 3, day: 10, hour: 2 }, description: "America/New_York 02:00 gap time during leap year (2024)", expectedResults: { compatible: "2024-03-10T07:00:00.000Z", earlier: "2024-03-10T06:00:00.000Z", later: "2024-03-10T07:00:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2024, month: 11, day: 3, hour: 1, minute: 30 }, description: "America/New_York 01:30 ambiguous time during leap year (2024)", expectedResults: { compatible: "2024-11-03T05:30:00.000Z", earlier: "2024-11-03T05:30:00.000Z", later: "2024-11-03T06:30:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 1 }, description: "America/New_York 01:00 before DST transition", expectedResults: { compatible: "2025-03-09T06:00:00.000Z", earlier: "2025-03-09T06:00:00.000Z", later: "2025-03-09T06:00:00.000Z", reject: "2025-03-09T06:00:00.000Z" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 1, minute: 59, second: 59 }, description: "America/New_York 01:59:59 last second before DST gap", expectedResults: { compatible: "2025-03-09T06:59:59.000Z", earlier: "2025-03-09T06:59:59.000Z", later: "2025-03-09T06:59:59.000Z", reject: "2025-03-09T06:59:59.000Z" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 2 }, description: "America/New_York 02:00 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-09T07:00:00.000Z", earlier: "2025-03-09T06:00:00.000Z", later: "2025-03-09T07:00:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 2, minute: 15 }, description: "America/New_York 02:15 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-09T07:15:00.000Z", earlier: "2025-03-09T06:15:00.000Z", later: "2025-03-09T07:15:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 2, minute: 30 }, description: "America/New_York 02:30 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-09T07:30:00.000Z", earlier: "2025-03-09T06:30:00.000Z", later: "2025-03-09T07:30:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 2, minute: 45 }, description: "America/New_York 02:45 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-09T07:45:00.000Z", earlier: "2025-03-09T06:45:00.000Z", later: "2025-03-09T07:45:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 3, day: 9, hour: 3 }, description: "America/New_York 03:00 first valid time after DST gap", expectedResults: { compatible: "2025-03-09T07:00:00.000Z", earlier: "2025-03-09T07:00:00.000Z", later: "2025-03-09T07:00:00.000Z", reject: "2025-03-09T07:00:00.000Z" } }, { zone: "America/New_York", time: { year: 2025, month: 11, day: 2, hour: 1, minute: 0, second: 0 }, description: "America/New_York 01:00:00 exact start of ambiguous period", expectedResults: { compatible: "2025-11-02T05:00:00.000Z", earlier: "2025-11-02T05:00:00.000Z", later: "2025-11-02T06:00:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 11, day: 2, hour: 1, minute: 30 }, description: "America/New_York 01:30 ambiguous time (DST fall back)", expectedResults: { compatible: "2025-11-02T05:30:00.000Z", earlier: "2025-11-02T05:30:00.000Z", later: "2025-11-02T06:30:00.000Z", reject: "REJECT" } }, { zone: "America/New_York", time: { year: 2025, month: 11, day: 2, hour: 1, minute: 59, second: 59 }, description: "America/New_York 01:59:59 last second of ambiguous period", expectedResults: { compatible: "2025-11-02T05:59:59.000Z", earlier: "2025-11-02T05:59:59.000Z", later: "2025-11-02T06:59:59.000Z", reject: "REJECT" } }, { zone: "Asia/Kathmandu", time: { year: 2025, month: 6, day: 15, hour: 12 }, description: "Asia/Kathmandu 12:00 unusual offset (UTC+05:45)", expectedResults: { compatible: "2025-06-15T06:15:00.000Z", earlier: "2025-06-15T06:15:00.000Z", later: "2025-06-15T06:15:00.000Z", reject: "2025-06-15T06:15:00.000Z" } }, { zone: "Australia/Melbourne", time: { year: 2025, month: 10, day: 5, hour: 2 }, description: "Australia/Melbourne 02:00 gap time (DST starts, spring forward)", expectedResults: { compatible: "2025-10-04T16:00:00.000Z", earlier: "2025-10-04T15:00:00.000Z", later: "2025-10-04T16:00:00.000Z", reject: "REJECT" } }, { zone: "Australia/Sydney", time: { year: 2025, month: 4, day: 6, hour: 1 }, description: "Australia/Sydney 01:00 normal timezone conversion", expectedResults: { compatible: "2025-04-05T14:00:00.000Z", earlier: "2025-04-05T14:00:00.000Z", later: "2025-04-05T14:00:00.000Z", reject: "2025-04-05T14:00:00.000Z" } }, { zone: "Australia/Sydney", time: { year: 2025, month: 4, day: 6, hour: 2, minute: 30 }, description: "Australia/Sydney 02:30 ambiguous time (DST ends, fall back)", expectedResults: { compatible: "2025-04-05T15:30:00.000Z", earlier: "2025-04-05T15:30:00.000Z", later: "2025-04-05T16:30:00.000Z", reject: "REJECT" } }, { zone: "Australia/Sydney", time: { year: 2025, month: 10, day: 5, hour: 2, minute: 30 }, description: "Australia/Sydney 02:30 gap time (DST starts, spring forward)", expectedResults: { compatible: "2025-10-04T16:30:00.000Z", earlier: "2025-10-04T15:30:00.000Z", later: "2025-10-04T16:30:00.000Z", reject: "REJECT" } }, { zone: "Europe/Athens", time: { year: 2024, month: 10, day: 27, hour: 3 }, description: "Europe/Athens 03:00 ambiguous time during leap year (2024)", expectedResults: { compatible: "2024-10-27T00:00:00.000Z", earlier: "2024-10-27T00:00:00.000Z", later: "2024-10-27T01:00:00.000Z", reject: "REJECT" } }, { zone: "Europe/Athens", time: { year: 2025, month: 3, day: 27, hour: 1 }, description: "Europe/Athens 01:00 normal time before DST", expectedResults: { compatible: "2025-03-26T23:00:00.000Z", earlier: "2025-03-26T23:00:00.000Z", later: "2025-03-26T23:00:00.000Z", reject: "2025-03-26T23:00:00.000Z" } }, { zone: "Europe/Athens", time: { year: 2025, month: 3, day: 30, hour: 1 }, description: "Europe/Athens 01:00 before DST transition (UTC+2)", expectedResults: { compatible: "2025-03-29T23:00:00.000Z", earlier: "2025-03-29T23:00:00.000Z", later: "2025-03-29T23:00:00.000Z", reject: "2025-03-29T23:00:00.000Z" } }, { zone: "Europe/Athens", time: { year: 2025, month: 3, day: 30, hour: 2, minute: 30 }, description: "Europe/Athens 02:30 normal time before DST transition", expectedResults: { compatible: "2025-03-30T00:30:00.000Z", earlier: "2025-03-30T00:30:00.000Z", later: "2025-03-30T00:30:00.000Z", reject: "2025-03-30T00:30:00.000Z" } }, { zone: "Europe/Athens", time: { year: 2025, month: 3, day: 30, hour: 3 }, description: "Europe/Athens 03:00 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-30T01:00:00.000Z", earlier: "2025-03-30T00:00:00.000Z", later: "2025-03-30T01:00:00.000Z", reject: "REJECT" } }, { zone: "Europe/Athens", time: { year: 2025, month: 3, day: 30, hour: 4 }, description: "Europe/Athens 04:00 normal time after DST transition", expectedResults: { compatible: "2025-03-30T01:00:00.000Z", earlier: "2025-03-30T01:00:00.000Z", later: "2025-03-30T01:00:00.000Z", reject: "2025-03-30T01:00:00.000Z" } }, { zone: "Europe/Athens", time: { year: 2025, month: 10, day: 26, hour: 3 }, description: "Europe/Athens 03:00 ambiguous time (DST fall back)", expectedResults: { compatible: "2025-10-26T00:00:00.000Z", earlier: "2025-10-26T00:00:00.000Z", later: "2025-10-26T01:00:00.000Z", reject: "REJECT" } }, { zone: "Europe/Berlin", time: { year: 2025, month: 10, day: 26, hour: 2, minute: 30 }, description: "Europe/Berlin 02:30 ambiguous time (DST fall back)", expectedResults: { compatible: "2025-10-26T00:30:00.000Z", earlier: "2025-10-26T00:30:00.000Z", later: "2025-10-26T01:30:00.000Z", reject: "REJECT" } }, { zone: "Europe/London", time: { year: 2024, month: 3, day: 31, hour: 1 }, description: "Europe/London 01:00 gap time during leap year (2024)", expectedResults: { compatible: "2024-03-31T01:00:00.000Z", earlier: "2024-03-31T00:00:00.000Z", later: "2024-03-31T01:00:00.000Z", reject: "REJECT" } }, { zone: "Europe/London", time: { year: 2025, month: 3, day: 29, hour: 1 }, description: "Europe/London 01:00 normal time day before DST", expectedResults: { compatible: "2025-03-29T01:00:00.000Z", earlier: "2025-03-29T01:00:00.000Z", later: "2025-03-29T01:00:00.000Z", reject: "2025-03-29T01:00:00.000Z" } }, { zone: "Europe/London", time: { year: 2025, month: 3, day: 30, hour: 1 }, description: "Europe/London 01:00 gap time (DST spring forward)", expectedResults: { compatible: "2025-03-30T01:00:00.000Z", earlier: "2025-03-30T00:00:00.000Z", later: "2025-03-30T01:00:00.000Z", reject: "REJECT" } }, { zone: "Europe/London", time: { year: 2025, month: 10, day: 26, hour: 1, minute: 30 }, description: "Europe/London 01:30 ambiguous time (DST fall back)", expectedResults: { compatible: "2025-10-26T00:30:00.000Z", earlier: "2025-10-26T00:30:00.000Z", later: "2025-10-26T01:30:00.000Z", reject: "REJECT" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 1, day: 15, hour: 12 }, description: "Pacific/Auckland 12:00 during DST period (NZDT, UTC+13)", expectedResults: { compatible: "2025-01-14T23:00:00.000Z", earlier: "2025-01-14T23:00:00.000Z", later: "2025-01-14T23:00:00.000Z", reject: "2025-01-14T23:00:00.000Z" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 4, day: 6, hour: 1, minute: 59 }, description: "Pacific/Auckland 01:59 last minute before DST ends", expectedResults: { compatible: "2025-04-05T12:59:00.000Z", earlier: "2025-04-05T12:59:00.000Z", later: "2025-04-05T12:59:00.000Z", reject: "2025-04-05T12:59:00.000Z" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 4, day: 6, hour: 2, minute: 30 }, description: "Pacific/Auckland 02:30 ambiguous time (DST ends, fall back)", expectedResults: { compatible: "2025-04-05T13:30:00.000Z", earlier: "2025-04-05T13:30:00.000Z", later: "2025-04-05T14:30:00.000Z", reject: "REJECT" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 4, day: 6, hour: 3 }, description: "Pacific/Auckland 03:00 normal time after DST ends", expectedResults: { compatible: "2025-04-05T15:00:00.000Z", earlier: "2025-04-05T15:00:00.000Z", later: "2025-04-05T15:00:00.000Z", reject: "2025-04-05T15:00:00.000Z" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 7, day: 15, hour: 12 }, description: "Pacific/Auckland 12:00 during standard time (NZST, UTC+12)", expectedResults: { compatible: "2025-07-15T00:00:00.000Z", earlier: "2025-07-15T00:00:00.000Z", later: "2025-07-15T00:00:00.000Z", reject: "2025-07-15T00:00:00.000Z" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 9, day: 28, hour: 1, minute: 59, second: 59 }, description: "Pacific/Auckland 01:59:59 last second before DST starts", expectedResults: { compatible: "2025-09-27T13:59:59.000Z", earlier: "2025-09-27T13:59:59.000Z", later: "2025-09-27T13:59:59.000Z", reject: "2025-09-27T13:59:59.000Z" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 9, day: 28, hour: 2, minute: 30 }, description: "Pacific/Auckland 02:30 gap time (DST starts, spring forward)", expectedResults: { compatible: "2025-09-27T14:30:00.000Z", earlier: "2025-09-27T13:30:00.000Z", later: "2025-09-27T14:30:00.000Z", reject: "REJECT" } }, { zone: "Pacific/Auckland", time: { year: 2025, month: 9, day: 28, hour: 3 }, description: "Pacific/Auckland 03:00 first valid time after DST gap", expectedResults: { compatible: "2025-09-27T14:00:00.000Z", earlier: "2025-09-27T14:00:00.000Z", later: "2025-09-27T14:00:00.000Z", reject: "2025-09-27T14:00:00.000Z" } }, { zone: "Pacific/Kiritimati", time: { year: 2025, month: 6, day: 15, hour: 12 }, description: "Pacific/Kiritimati 12:00 extreme positive offset (UTC+14)", expectedResults: { compatible: "2025-06-14T22:00:00.000Z", earlier: "2025-06-14T22:00:00.000Z", later: "2025-06-14T22:00:00.000Z", reject: "2025-06-14T22:00:00.000Z" } }, { zone: "Pacific/Marquesas", time: { year: 2025, month: 6, day: 15, hour: 12 }, description: "Pacific/Marquesas 12:00 unusual negative offset (UTC-09:30)", expectedResults: { compatible: "2025-06-15T21:30:00.000Z", earlier: "2025-06-15T21:30:00.000Z", later: "2025-06-15T21:30:00.000Z", reject: "2025-06-15T21:30:00.000Z" } } ] )( "should handle $description", ({ expectedResults, time, zone }) => { // Test normal strategies for (const strategy of ["compatible", "earlier", "later"] as const) { const result = DateTime.makeZoned(time, { timeZone: zone, adjustForTimeZone: true, disambiguation: strategy }) assertSomeIso(result, expectedResults[strategy]) } // Test default strategy const defaultResult = DateTime.makeZoned(time, { timeZone: zone, adjustForTimeZone: true }) assertSomeIso(defaultResult, expectedResults.compatible) // Test reject strategy const rejectResult = DateTime.makeZoned(time, { timeZone: zone, adjustForTimeZone: true, disambiguation: "reject" }) if (expectedResults.reject === "REJECT") { assertNone(rejectResult) } else { assertSomeIso(rejectResult, expectedResults.reject) } } ) }) })