From 5e58ccdd37565b94e1769e8658b2df3ebd7ad22a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 1 May 2025 16:57:10 +0100 Subject: [PATCH 1/3] Fix for the DateTime ISO function --- .../app/components/primitives/DateTime.tsx | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 11a7ab990e..b0ee408294 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -98,7 +98,52 @@ export function formatDateTime( } export function formatDateTimeISO(date: Date, timeZone: string): string { - return new Date(date.toLocaleString("en-US", { timeZone })).toISOString(); + // Special handling for UTC + if (timeZone === "UTC") { + return date.toISOString(); + } + + // Get the offset in minutes for the specified timezone + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "shortOffset", + }); + const tzOffset = + formatter + .formatToParts(date) + .find((p) => p.type === "timeZoneName") + ?.value.replace("GMT", "") ?? ""; + + // Format the offset properly as ±HH:mm + const offsetNum = parseInt(tzOffset); + const offsetHours = Math.abs(Math.floor(offsetNum)).toString().padStart(2, "0"); + const sign = offsetNum >= 0 ? "+" : "-"; + const formattedOffset = `${sign}${offsetHours}:00`; + + // Format the date parts + const dateFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + + const parts = dateFormatter.formatToParts(date); + const dateParts: Record = {}; + parts.forEach((part) => { + dateParts[part.type] = part.value; + }); + + // Format: YYYY-MM-DDThh:mm:ss.sss±hh:mm + const isoString = + `${dateParts.year}-${dateParts.month}-${dateParts.day}T` + + `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.000${formattedOffset}`; + + return isoString; } // New component that only shows date when it changes From 5cf6fdf335a3725f141dabb3077b5d288782b27c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 1 May 2025 17:13:11 +0100 Subject: [PATCH 2/3] Fix implementation and added unit tests --- .../app/components/primitives/DateTime.tsx | 43 +++++++---------- apps/webapp/test/components/DateTime.test.ts | 48 +++++++++++++++++++ 2 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 apps/webapp/test/components/DateTime.test.ts diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index b0ee408294..d34dcb4dab 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -103,24 +103,7 @@ export function formatDateTimeISO(date: Date, timeZone: string): string { return date.toISOString(); } - // Get the offset in minutes for the specified timezone - const formatter = new Intl.DateTimeFormat("en-US", { - timeZone, - timeZoneName: "shortOffset", - }); - const tzOffset = - formatter - .formatToParts(date) - .find((p) => p.type === "timeZoneName") - ?.value.replace("GMT", "") ?? ""; - - // Format the offset properly as ±HH:mm - const offsetNum = parseInt(tzOffset); - const offsetHours = Math.abs(Math.floor(offsetNum)).toString().padStart(2, "0"); - const sign = offsetNum >= 0 ? "+" : "-"; - const formattedOffset = `${sign}${offsetHours}:00`; - - // Format the date parts + // Get the date parts in the target timezone const dateFormatter = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", @@ -132,18 +115,26 @@ export function formatDateTimeISO(date: Date, timeZone: string): string { hour12: false, }); - const parts = dateFormatter.formatToParts(date); - const dateParts: Record = {}; - parts.forEach((part) => { - dateParts[part.type] = part.value; + // Get the timezone offset for this specific date + const timeZoneFormatter = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "longOffset", }); + const dateParts = Object.fromEntries( + dateFormatter.formatToParts(date).map(({ type, value }) => [type, value]) + ); + + const timeZoneParts = timeZoneFormatter.formatToParts(date); + const offset = + timeZoneParts.find((part) => part.type === "timeZoneName")?.value.replace("GMT", "") || + "+00:00"; + // Format: YYYY-MM-DDThh:mm:ss.sss±hh:mm - const isoString = + return ( `${dateParts.year}-${dateParts.month}-${dateParts.day}T` + - `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.000${formattedOffset}`; - - return isoString; + `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.000${offset}` + ); } // New component that only shows date when it changes diff --git a/apps/webapp/test/components/DateTime.test.ts b/apps/webapp/test/components/DateTime.test.ts new file mode 100644 index 0000000000..a0e510ada5 --- /dev/null +++ b/apps/webapp/test/components/DateTime.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { formatDateTimeISO } from "~/components/primitives/DateTime"; + +describe("formatDateTimeISO", () => { + it("should format UTC dates with Z suffix", () => { + const date = new Date("2025-04-29T14:01:19.000Z"); + const result = formatDateTimeISO(date, "UTC"); + expect(result).toBe("2025-04-29T14:01:19.000Z"); + }); + + describe("British Time (Europe/London)", () => { + it("should format with +01:00 during BST (summer)", () => { + // BST - British Summer Time (last Sunday in March to last Sunday in October) + const summerDate = new Date("2025-07-15T14:01:19.000Z"); + const result = formatDateTimeISO(summerDate, "Europe/London"); + expect(result).toBe("2025-07-15T15:01:19.000+01:00"); + }); + + it("should format with +00:00 during GMT (winter)", () => { + // GMT - Greenwich Mean Time (winter) + const winterDate = new Date("2025-01-15T14:01:19.000Z"); + const result = formatDateTimeISO(winterDate, "Europe/London"); + expect(result).toBe("2025-01-15T14:01:19.000+00:00"); + }); + }); + + describe("US Pacific Time (America/Los_Angeles)", () => { + it("should format with -07:00 during PDT (summer)", () => { + // PDT - Pacific Daylight Time (second Sunday in March to first Sunday in November) + const summerDate = new Date("2025-07-15T14:01:19.000Z"); + const result = formatDateTimeISO(summerDate, "America/Los_Angeles"); + expect(result).toBe("2025-07-15T07:01:19.000-07:00"); + }); + + it("should format with -08:00 during PST (winter)", () => { + // PST - Pacific Standard Time (winter) + const winterDate = new Date("2025-01-15T14:01:19.000Z"); + const result = formatDateTimeISO(winterDate, "America/Los_Angeles"); + expect(result).toBe("2025-01-15T06:01:19.000-08:00"); + }); + }); + + it("should preserve milliseconds", () => { + const date = new Date("2025-04-29T14:01:19.123Z"); + const result = formatDateTimeISO(date, "UTC"); + expect(result).toBe("2025-04-29T14:01:19.123Z"); + }); +}); From 04e1b340562fe8b7411fdc2bb79229a22d539007 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 1 May 2025 17:31:19 +0100 Subject: [PATCH 3/3] Preserve milliseconds properly --- apps/webapp/app/components/primitives/DateTime.tsx | 4 +++- apps/webapp/test/components/DateTime.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index d34dcb4dab..7d75bc7351 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -133,7 +133,9 @@ export function formatDateTimeISO(date: Date, timeZone: string): string { // Format: YYYY-MM-DDThh:mm:ss.sss±hh:mm return ( `${dateParts.year}-${dateParts.month}-${dateParts.day}T` + - `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.000${offset}` + `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${String( + date.getMilliseconds() + ).padStart(3, "0")}${offset}` ); } diff --git a/apps/webapp/test/components/DateTime.test.ts b/apps/webapp/test/components/DateTime.test.ts index a0e510ada5..103f416eb9 100644 --- a/apps/webapp/test/components/DateTime.test.ts +++ b/apps/webapp/test/components/DateTime.test.ts @@ -45,4 +45,10 @@ describe("formatDateTimeISO", () => { const result = formatDateTimeISO(date, "UTC"); expect(result).toBe("2025-04-29T14:01:19.123Z"); }); + + it("should preserve milliseconds, not UTC", () => { + const date = new Date("2025-04-29T14:01:19.123Z"); + const result = formatDateTimeISO(date, "Europe/London"); + expect(result).toBe("2025-04-29T15:01:19.123+01:00"); + }); });