From 2798ba65ce0fd7196871d3195b92d237f8242622 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Sun, 4 Aug 2024 13:27:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20fallback=20behaviour=20for?= =?UTF-8?q?=20unrecognized=20timezones=20(#1241)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/utils/date-time-utils.ts | 63 ++++++++++++++++----- apps/web/src/utils/date-time-utilts.test.ts | 18 ++++-- apps/web/src/utils/dayjs.tsx | 7 +-- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/apps/web/src/utils/date-time-utils.ts b/apps/web/src/utils/date-time-utils.ts index f5a4871a6..76fba8c9b 100644 --- a/apps/web/src/utils/date-time-utils.ts +++ b/apps/web/src/utils/date-time-utils.ts @@ -19,24 +19,59 @@ export function parseIanaTimezone(timezone: string): { } export function getBrowserTimeZone() { - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - return resolveGeographicTimeZone(timeZone); + const timeZone = dayjs.tz.guess(); + return normalizeTimeZone(timeZone); } -export function resolveGeographicTimeZone(timezone: string) { - const tz = supportedTimeZones.find((tz) => tz === timezone); +function getTimeZoneOffset(timeZone: string) { + try { + return dayjs().tz(timeZone).utcOffset(); + } catch (e) { + console.error(`Failed to resolve timezone ${timeZone}`); + return 0; + } +} + +function isFixedOffsetTimeZone(timeZone: string) { + return ( + timeZone.toLowerCase().startsWith("etc") || + timeZone.toLowerCase().startsWith("gmt") || + timeZone.toLowerCase().startsWith("utc") + ); +} + +/** + * Given a timezone, this function returns a normalized timezone + * that is supported by the application. If the timezone is not + * recognized, it will return a timezone in the same continent + * with the same offset. + * @param timeZone + * @returns + */ +export function normalizeTimeZone(timeZone: string) { + let tz = supportedTimeZones.find((tz) => tz === timeZone); + + if (tz) { + return tz; + } + + const timeZoneOffset = getTimeZoneOffset(timeZone); + + if (!isFixedOffsetTimeZone(timeZone)) { + // Find a timezone in the same continent with the same offset + const [continent] = timeZone.split("/"); + const sameContinentTimeZones = supportedTimeZones.filter((tz) => + tz.startsWith(continent), + ); + tz = sameContinentTimeZones.find((tz) => { + return dayjs().tz(tz, true).utcOffset() === timeZoneOffset; + }); + } if (!tz) { - // find nearest timezone with the same offset - let offset = 0; - try { - offset = dayjs().tz(timezone).utcOffset(); - } catch (e) { - console.error(`Failed to resolve timezone ${timezone}`); - } - return supportedTimeZones.find((tz) => { - return dayjs().tz(tz, true).utcOffset() === offset; - })!; + tz = supportedTimeZones.find((tz) => { + return getTimeZoneOffset(tz) === timeZoneOffset; + })!; // We assume there has to be a timezone with the same offset } return tz; diff --git a/apps/web/src/utils/date-time-utilts.test.ts b/apps/web/src/utils/date-time-utilts.test.ts index a9d3a69b4..4205faf8e 100644 --- a/apps/web/src/utils/date-time-utilts.test.ts +++ b/apps/web/src/utils/date-time-utilts.test.ts @@ -5,34 +5,40 @@ import { describe, expect, it } from "vitest"; import { supportedTimeZones } from "@/utils/supported-time-zones"; -import { resolveGeographicTimeZone } from "./date-time-utils"; +import { normalizeTimeZone } from "./date-time-utils"; dayjs.extend(utc); dayjs.extend(timezone); -describe("resolveGeographicTimezone", () => { +describe("Normalize Time Zone", () => { it("should return same timezone when given a geographic timezone", () => { - const browserTimeZone = resolveGeographicTimeZone("Europe/London"); + const browserTimeZone = normalizeTimeZone("Europe/London"); // Assert that the browser time zone is one of the supported time zones expect(browserTimeZone).toBe("Europe/London"); }); it("should return a supported timezone when given a fixed offset timezone", () => { - const browserTimeZone = resolveGeographicTimeZone("Etc/GMT-1"); + const browserTimeZone = normalizeTimeZone("Etc/GMT-1"); // Assert that the browser time zone is one of the supported time zones expect(supportedTimeZones.includes(browserTimeZone)).toBe(true); }); it("should return a supported timezone when given GMT", () => { - const browserTimeZone = resolveGeographicTimeZone("GMT"); + const browserTimeZone = normalizeTimeZone("GMT"); // Assert that the browser time zone is one of the supported time zones expect(supportedTimeZones.includes(browserTimeZone)).toBe(true); }); it("should return a supported timezone when given UTC", () => { - const browserTimeZone = resolveGeographicTimeZone("UTC"); + const browserTimeZone = normalizeTimeZone("UTC"); // Assert that the browser time zone is one of the supported time zones expect(supportedTimeZones.includes(browserTimeZone)).toBe(true); }); + + it("should return a valid timezone in the same continent when not recognized", () => { + const browserTimeZone = normalizeTimeZone("Asia/Kolkata"); + + expect(browserTimeZone).toBe("Asia/Calcutta"); + }); }); diff --git a/apps/web/src/utils/dayjs.tsx b/apps/web/src/utils/dayjs.tsx index 41d2d4610..5bd0435ca 100644 --- a/apps/web/src/utils/dayjs.tsx +++ b/apps/web/src/utils/dayjs.tsx @@ -17,10 +17,7 @@ import * as React from "react"; import { useAsync } from "react-use"; import { usePreferences } from "@/contexts/preferences"; -import { - getBrowserTimeZone, - resolveGeographicTimeZone, -} from "@/utils/date-time-utils"; +import { getBrowserTimeZone, normalizeTimeZone } from "@/utils/date-time-utils"; import { useRequiredContext } from "../components/use-required-context"; @@ -213,7 +210,7 @@ export const DayjsProvider: React.FunctionComponent<{ const preferredTimeZone = React.useMemo( () => config?.timeZone - ? resolveGeographicTimeZone(config?.timeZone) + ? normalizeTimeZone(config?.timeZone) : getBrowserTimeZone(), [config?.timeZone], );