diff --git a/supabase/functions/diff-schedule/deno.lock b/supabase/functions/diff-schedule/deno.lock index 9af6c8a4..873d62c6 100644 --- a/supabase/functions/diff-schedule/deno.lock +++ b/supabase/functions/diff-schedule/deno.lock @@ -2,7 +2,8 @@ "version": "5", "specifiers": { "jsr:@std/assert@1": "1.0.19", - "jsr:@std/internal@^1.0.12": "1.0.13" + "jsr:@std/internal@^1.0.12": "1.0.13", + "npm:@date-fns/tz@1.5.0": "1.5.0" }, "jsr": { "@std/assert@1.0.19": { @@ -15,6 +16,11 @@ "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" } }, + "npm": { + "@date-fns/tz@1.5.0": { + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==" + } + }, "redirects": { "https://esm.sh/@supabase/node-fetch@^2.6.13?target=denonext": "https://esm.sh/@supabase/node-fetch@2.6.15?target=denonext", "https://esm.sh/@supabase/node-fetch@^2.6.14?target=denonext": "https://esm.sh/@supabase/node-fetch@2.6.15?target=denonext", diff --git a/supabase/functions/diff-schedule/helpers.test.ts b/supabase/functions/diff-schedule/helpers.test.ts index a9fbae36..22378305 100644 --- a/supabase/functions/diff-schedule/helpers.test.ts +++ b/supabase/functions/diff-schedule/helpers.test.ts @@ -37,3 +37,19 @@ Deno.test("localToUtc converts midnight correctly", () => { const result = localToUtc("2026-07-11", "00:00", "Europe/Lisbon"); assertEquals(result, "2026-07-10T23:00:00.000Z"); }); + +Deno.test("localToUtc resolves a spring-forward wall time inside the skipped hour", () => { + // Lisbon clocks jump from 01:00 to 02:00 local at 2026-03-29T01:00:00Z, so + // "01:30" never occurs. @date-fns/tz's TZDate resolves a wall time inside + // the skipped hour using the pre-transition (+00:00) offset, i.e. as if + // DST had not yet started. + const result = localToUtc("2026-03-29", "01:30", "Europe/Lisbon"); + assertEquals(result, "2026-03-29T01:30:00.000Z"); +}); + +Deno.test("localToUtc resolves a fall-back wall time inside the repeated hour", () => { + // Lisbon clocks fall back from 02:00 to 01:00 local at 2026-10-25T01:00:00Z, + // so "01:30" occurs twice. Resolve to the later (post-transition) instant. + const result = localToUtc("2026-10-25", "01:30", "Europe/Lisbon"); + assertEquals(result, "2026-10-25T01:30:00.000Z"); +}); diff --git a/supabase/functions/diff-schedule/helpers.ts b/supabase/functions/diff-schedule/helpers.ts index 1c298afa..a2df9163 100644 --- a/supabase/functions/diff-schedule/helpers.ts +++ b/supabase/functions/diff-schedule/helpers.ts @@ -1,3 +1,5 @@ +import { TZDate } from "npm:@date-fns/tz@1.5.0"; + export function toSlug(name: string): string { return name .toLowerCase() @@ -21,14 +23,10 @@ export function localToUtc( timeStr: string, timezone: string, ): string { - const localIso = `${dateStr}T${timeStr}:00`; - const naiveUtc = new Date(localIso + "Z"); - // sv-SE locale gives "YYYY-MM-DD HH:MM:SS" — unambiguously parseable as UTC - const localInTz = new Date( - naiveUtc.toLocaleString("sv-SE", { timeZone: timezone }) + "Z", - ); - const offsetMs = naiveUtc.getTime() - localInTz.getTime(); - return new Date(naiveUtc.getTime() + offsetMs).toISOString(); + const [year, month, day] = dateStr.split("-").map(Number); + const [hour, minute] = timeStr.split(":").map(Number); + const zoned = new TZDate(year, month - 1, day, hour, minute, 0, timezone); + return new Date(+zoned).toISOString(); } export function utcToLocalDate(utcIso: string, timezone: string): string {