Skip to content

Commit 4e9442f

Browse files
committed
fix: offset() returns NaN for dates with non 4-digit years
- Rewrite relativeTime() to use Date.UTC() instead of reconstructing ISO strings - Add era retrieval from Intl.DateTimeFormat to correctly handle BCE years - Use setUTCFullYear(year, month, day) to avoid leap day normalization issues Closes #77
1 parent f89f840 commit 4e9442f

2 files changed

Lines changed: 28 additions & 22 deletions

File tree

src/__tests__/offset.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ describe("offset", () => {
3131
"+1100"
3232
)
3333
})
34+
it("can determine the offset with non 4-digit year dates", () => {
35+
expect(offset("0001-01-01", "UTC", "UTC")).toBe("+00:00")
36+
expect(offset("0001-01-01")).not.toContain("NaN")
37+
expect(offset("0200-06-15", "UTC", "UTC")).toBe("+00:00")
38+
expect(offset("0200-06-15")).not.toContain("NaN")
39+
expect(offset("0001-01-01T00:00:00Z", "UTC", "Etc/GMT+5")).toBe("-05:00")
40+
expect(offset("0001-01-01T00:00:00Z", "UTC", "Etc/GMT-5")).toBe("+05:00")
41+
expect(offset("0000-01-01T00:00:00Z", "UTC", "UTC")).toBe("+00:00")
42+
expect(offset("0000-01-01T00:00:00Z", "UTC", "Etc/GMT+5")).toBe("-05:00")
43+
})
3444
it("can determine the offset to a non full-hour offset timezone", () => {
3545
expect(offset("2023-02-22", "Europe/London", "Pacific/Chatham")).toBe(
3646
"+13:45"

src/offset.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { date } from "./date"
2-
import { normStr, secsToOffset, TimezoneToken } from "./common"
2+
import { secsToOffset, TimezoneToken } from "./common"
33
import { deviceTZ } from "./deviceTZ"
4-
import type { DateInput, MaybeDateInput } from "./types"
4+
import type { MaybeDateInput } from "./types"
55

66
/**
77
* Converts a date object from one timezone to that same time in UTC. This is
@@ -11,31 +11,27 @@ import type { DateInput, MaybeDateInput } from "./types"
1111
*/
1212
function relativeTime(d: Date, timeZone: string): Date {
1313
const utcParts = new Intl.DateTimeFormat("en-US", {
14+
era: "short",
1415
year: "numeric",
15-
month: "2-digit",
16-
day: "2-digit",
17-
hour: "2-digit",
18-
minute: "2-digit",
19-
second: "2-digit",
16+
month: "numeric",
17+
day: "numeric",
18+
hour: "numeric",
19+
minute: "numeric",
20+
second: "numeric",
2021
timeZone,
2122
hourCycle: "h23",
22-
})
23-
.formatToParts(d)
24-
.map(normStr)
25-
const parts: {
26-
year?: string
27-
month?: string
28-
day?: string
29-
hour?: string
30-
minute?: string
31-
second?: string
32-
} = {}
23+
}).formatToParts(d)
24+
const p: Record<string, string> = {}
3325
utcParts.forEach((part) => {
34-
parts[part.type as keyof typeof parts] = part.value
26+
if (part.type !== "literal") p[part.type] = part.value
3527
})
36-
return new Date(
37-
`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z`
38-
)
28+
// BC year N in Intl = ISO year (1 - N), e.g. 1 BC = year 0, 2 BC = year -1
29+
const year = p.era === "BC" ? 1 - Number(p.year) : Number(p.year)
30+
const result = new Date(Date.UTC(0, 0, 1, Number(p.hour), Number(p.minute), Number(p.second)))
31+
// setUTCFullYear with year, month, day together avoids Date.UTC's 0-99 year mapping
32+
// and ensures leap day validation uses the correct year
33+
result.setUTCFullYear(year, Number(p.month) - 1, Number(p.day))
34+
return result
3935
}
4036

4137
/**

0 commit comments

Comments
 (0)