From 07bd1f28d102167d45b2429f50a64fef4af189ca Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 28 Nov 2023 22:34:34 -0800 Subject: [PATCH] time_zone_util: Avoid relying on timeZoneName: "longOffset". MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safari doesn’t support it yet. Signed-off-by: Anders Kaseorg --- web/src/time_zone_util.ts | 43 ++++++++++++++++++++------------ web/tests/time_zone_util.test.js | 4 +++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/web/src/time_zone_util.ts b/web/src/time_zone_util.ts index 70f4901fb6..9b0def2f62 100644 --- a/web/src/time_zone_util.ts +++ b/web/src/time_zone_util.ts @@ -1,30 +1,41 @@ -import assert from "minimalistic-assert"; +const parsable_formats = new Map(); -const offset_formats = new Map(); - -function get_offset_format(time_zone: string): Intl.DateTimeFormat { - let format = offset_formats.get(time_zone); +function get_parsable_format(time_zone: string): Intl.DateTimeFormat { + let format = parsable_formats.get(time_zone); if (format === undefined) { format = new Intl.DateTimeFormat("en-US", { - timeZoneName: "longOffset", + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hourCycle: "h23", timeZone: time_zone, }); - offset_formats.set(time_zone, format); + parsable_formats.set(time_zone, format); } return format; } /** Get the given time zone's offset in milliseconds at the given date. */ export function get_offset(date: number | Date, time_zone: string): number { - const offset_string = get_offset_format(time_zone) - .formatToParts(date) - .find((part) => part.type === "timeZoneName")!.value; - if (offset_string === "GMT") { - return 0; - } - const m = /^GMT(([+-])\d\d):(\d\d)$/.exec(offset_string); - assert(m !== null, offset_string); - return (Number(m[1]) * 60 + Number(m[2] + m[3])) * 60000; + const parts = Object.fromEntries( + get_parsable_format(time_zone) + .formatToParts(date) + .map((part) => [part.type, part.value]), + ); + return ( + Date.UTC( + Number(parts.year), + Number(parts.month) - 1, + Number(parts.day), + Number(parts.hour), + Number(parts.minute), + Number(parts.second), + ) - + (Number(date) - (Number(date) % 1000)) + ); } /** Get the start of the day for the given date in the given time zone. */ diff --git a/web/tests/time_zone_util.test.js b/web/tests/time_zone_util.test.js index 7f5c7b3ab4..2254a7e6a6 100644 --- a/web/tests/time_zone_util.test.js +++ b/web/tests/time_zone_util.test.js @@ -23,6 +23,7 @@ const chatham_dst_begin = new Date("2022-09-24T14:00Z"); const chatham_dst_end = new Date("2023-04-01T14:00Z"); const kiritimati = "Pacific/Kiritimati"; const kiritimati_date_skip = new Date("1994-12-31T10:00Z"); +const fractional_date = new Date("2023-01-02T03:04:05.678Z"); run_test("get_offset", () => { assert.equal(get_offset(ny_new_year, "UTC"), 0); @@ -37,6 +38,9 @@ run_test("get_offset", () => { assert.equal(get_offset(chatham_dst_end, chatham), (12 * 60 + 45) * 60000); assert.equal(get_offset(pre(kiritimati_date_skip), kiritimati), -10 * 60 * 60000); assert.equal(get_offset(kiritimati_date_skip, kiritimati), 14 * 60 * 60000); + assert.equal(get_offset(fractional_date, "UTC"), 0); + assert.equal(get_offset(fractional_date, ny), -5 * 60 * 60000); + assert.equal(get_offset(fractional_date, kiritimati), 14 * 60 * 60000); }); run_test("start_of_day", () => {