Refactor hafasDateTime to use Intl.DateTimeFormat for Vienna TZ
This commit is contained in:
@@ -182,34 +182,123 @@ describe("getTimezoneOffsetMinutes (via parseHafasTime)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("hafasDateTime", () => {
|
describe("hafasDateTime", () => {
|
||||||
it("formats a Date into HAFAS date and time strings", () => {
|
/*
|
||||||
const input = new Date("2024-11-15T14:30:00");
|
* hafasDateTime is the inverse of parseHafasTime: it takes a JavaScript
|
||||||
const { date, time } = hafasDateTime(input);
|
* Date (UTC instant) and returns the Vienna-local HAFAS date/time strings.
|
||||||
expect(date).toBe("20241115");
|
* The roundtrip hafasDateTime(parseHafasTime(d, t)) === {date: d, time: t}
|
||||||
expect(time).toBe("143000000");
|
* must hold for every valid Vienna local time.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("roundtrip through parseHafasTime", () => {
|
||||||
|
function roundtrip(dateStr: string, timeStr: string) {
|
||||||
|
const parsed = parseHafasTime(dateStr, timeStr);
|
||||||
|
const { date, time } = hafasDateTime(parsed);
|
||||||
|
return { date, time };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("roundtrips standard CET times", () => {
|
||||||
|
expect(roundtrip("20240115", "080000")).toEqual({
|
||||||
|
date: "20240115",
|
||||||
|
time: "080000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20240201", "000000")).toEqual({
|
||||||
|
date: "20240201",
|
||||||
|
time: "000000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20241231", "235900")).toEqual({
|
||||||
|
date: "20241231",
|
||||||
|
time: "235900000",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips standard CEST times", () => {
|
||||||
|
expect(roundtrip("20240715", "080000")).toEqual({
|
||||||
|
date: "20240715",
|
||||||
|
time: "080000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20240801", "000000")).toEqual({
|
||||||
|
date: "20240801",
|
||||||
|
time: "000000000",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips spring DST transition (Mar 31, 2024)", () => {
|
||||||
|
expect(roundtrip("20240331", "010000")).toEqual({
|
||||||
|
date: "20240331",
|
||||||
|
time: "010000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20240331", "030000")).toEqual({
|
||||||
|
date: "20240331",
|
||||||
|
time: "030000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20240331", "120000")).toEqual({
|
||||||
|
date: "20240331",
|
||||||
|
time: "120000000",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips fall DST transition (Oct 27, 2024)", () => {
|
||||||
|
expect(roundtrip("20241027", "000000")).toEqual({
|
||||||
|
date: "20241027",
|
||||||
|
time: "000000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20241027", "023000")).toEqual({
|
||||||
|
date: "20241027",
|
||||||
|
time: "023000000",
|
||||||
|
});
|
||||||
|
expect(roundtrip("20241027", "120000")).toEqual({
|
||||||
|
date: "20241027",
|
||||||
|
time: "120000000",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("roundtrips leap year", () => {
|
||||||
|
expect(roundtrip("20240229", "120000")).toEqual({
|
||||||
|
date: "20240229",
|
||||||
|
time: "120000000",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pads single-digit month/day/hour/minute", () => {
|
describe("UTC-based correctness", () => {
|
||||||
const input = new Date("2024-01-02T03:04:05");
|
it("converts a UTC instant to correct Vienna HAFAS strings (CET)", () => {
|
||||||
const { date, time } = hafasDateTime(input);
|
// Jan 15 07:00 UTC = Jan 15 08:00 Vienna (CET)
|
||||||
expect(date).toBe("20240102");
|
const utc = new Date(Date.UTC(2024, 0, 15, 7, 0, 0));
|
||||||
expect(time).toBe("030405000");
|
const { date, time } = hafasDateTime(utc);
|
||||||
});
|
expect(date).toBe("20240115");
|
||||||
|
expect(time).toBe("080000000");
|
||||||
|
});
|
||||||
|
|
||||||
it("roundtrips through parseHafasTime for a standard CET date", () => {
|
it("converts a UTC instant to correct Vienna HAFAS strings (CEST)", () => {
|
||||||
// Note: hafasDateTime uses local-timezone getters, so we construct
|
// Jul 15 06:00 UTC = Jul 15 08:00 Vienna (CEST)
|
||||||
// a Date in a known state. parseHafasTime then converts the Vienna
|
const utc = new Date(Date.UTC(2024, 6, 15, 6, 0, 0));
|
||||||
// local time to UTC. This is not a perfect inverse, but we can
|
const { date, time } = hafasDateTime(utc);
|
||||||
// at least verify the format is valid.
|
expect(date).toBe("20240715");
|
||||||
const originalDate = new Date(Date.UTC(2024, 0, 15, 7, 0, 0)); // 2024-01-15 07:00 UTC = 08:00 CET
|
expect(time).toBe("080000000");
|
||||||
const { date, time } = hafasDateTime(originalDate);
|
});
|
||||||
|
|
||||||
// hafasDateTime reads local TZ components — on a UTC machine:
|
it("handles midnight wraparound (UTC 23:00 → Vienna +1 day 00:00)", () => {
|
||||||
const parsed = parseHafasTime(date, time);
|
// Jan 14 23:00 UTC = Jan 15 00:00 Vienna (CET)
|
||||||
|
const utc = new Date(Date.UTC(2024, 0, 14, 23, 0, 0));
|
||||||
|
const { date, time } = hafasDateTime(utc);
|
||||||
|
expect(date).toBe("20240115");
|
||||||
|
expect(time).toBe("000000000");
|
||||||
|
});
|
||||||
|
|
||||||
// The parsed result must be a valid Date (not NaN)
|
it("handles midnight wraparound (Vienna midnight is still previous UTC day)", () => {
|
||||||
expect(Number.isNaN(parsed.getTime())).toBe(false);
|
// Vienna Jan 1 00:00 CET = Dec 31 23:00 UTC
|
||||||
expect(parsed.getFullYear()).toBeGreaterThanOrEqual(2023);
|
const utc = new Date(Date.UTC(2023, 11, 31, 23, 0, 0));
|
||||||
expect(parsed.getFullYear()).toBeLessThanOrEqual(2025);
|
const { date, time } = hafasDateTime(utc);
|
||||||
|
expect(date).toBe("20240101");
|
||||||
|
expect(time).toBe("000000000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pads single-digit components correctly", () => {
|
||||||
|
// Jan 2 08:03:07 Vienna (CET) = Jan 2 07:03:07 UTC
|
||||||
|
const utc = new Date(Date.UTC(2024, 0, 2, 7, 3, 7));
|
||||||
|
const { date, time } = hafasDateTime(utc);
|
||||||
|
expect(date).toBe("20240102");
|
||||||
|
expect(time).toBe("080307000");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+35
-2
@@ -71,12 +71,45 @@ export function parseHafasTime(dateStr: string, timeStr: string): Date {
|
|||||||
return new Date(Date.UTC(y, mo, d, h, m, s, 0) - 60 * 60_000);
|
return new Date(Date.UTC(y, mo, d, h, m, s, 0) - 60 * 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: extract date/time components of an instant in a given IANA timezone
|
||||||
|
// via Intl.DateTimeFormat. Returns { year, month (1-based), day, hour, minute, second }.
|
||||||
|
function getDateTimeParts(instant: Date, tz: string) {
|
||||||
|
const fmt = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: tz,
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
second: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = fmt.formatToParts(instant);
|
||||||
|
const get = (type: string) => parseInt(parts.find((p) => p.type === type)!.value, 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: get("year"),
|
||||||
|
month: get("month"), // 1-based
|
||||||
|
day: get("day"),
|
||||||
|
hour: get("hour"),
|
||||||
|
minute: get("minute"),
|
||||||
|
second: get("second"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build HAFAS date and time strings from a JavaScript Date.
|
* Build HAFAS date and time strings from a JavaScript Date.
|
||||||
|
*
|
||||||
|
* Components are extracted in Europe/Vienna so the output is always a valid
|
||||||
|
* Vienna-local HAFAS timestamp, regardless of the server's timezone.
|
||||||
|
* This is the inverse of parseHafasTime: hafasDateTime(parseHafasTime(d, t))
|
||||||
|
* will return { date: d, time: t } for every valid Vienna date/time pair.
|
||||||
*/
|
*/
|
||||||
export function hafasDateTime(date: Date): { date: string; time: string } {
|
export function hafasDateTime(date: Date): { date: string; time: string } {
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
const d = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`;
|
const p = getDateTimeParts(date, "Europe/Vienna");
|
||||||
const t = `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}000`;
|
const d = `${p.year}${pad(p.month)}${pad(p.day)}`;
|
||||||
|
const t = `${pad(p.hour)}${pad(p.minute)}${pad(p.second)}000`;
|
||||||
return { date: d, time: t };
|
return { date: d, time: t };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user