954 lines
34 KiB
TypeScript
954 lines
34 KiB
TypeScript
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<DateTime.DateTime>, 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<DateTime.DateTime.Parts>
|
|
description: string
|
|
expectedResults: Record<DateTime.Disambiguation, string>
|
|
}>(
|
|
[
|
|
{
|
|
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)
|
|
}
|
|
}
|
|
)
|
|
})
|
|
})
|