From 392e460962b03bb9b6ac3fd94392501d02cd5d29 Mon Sep 17 00:00:00 2001 From: Rup Sarmah Date: Tue, 21 Jul 2026 17:55:03 +0530 Subject: [PATCH] fix(analytics-utils): correct localTime offset hour for negative half-hour zones pad() floored before taking the absolute value, so localTime() built the timezone offset hour from Math.floor(tzo / 60). For negative half-hour zones Math.floor(-9.5) is -10, so UTC-09:30 (Pacific/Marquesas) rendered as -10:30 and Newfoundland -03:30 rendered as -04:30. The sign is already handled by the dif variable, so take the magnitude before the divide: pad(Math.abs(tzo) / 60). Minutes and positive/whole-hour zones are unchanged. Adds uvu regression coverage pinning process.env.TZ for Marquesas, Newfoundland, Kolkata (+05:30), New York and UTC. --- packages/analytics-utils/src/localTime.js | 2 +- .../analytics-utils/tests/localTime.test.js | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/analytics-utils/tests/localTime.test.js diff --git a/packages/analytics-utils/src/localTime.js b/packages/analytics-utils/src/localTime.js index f3a40927..55ba431a 100644 --- a/packages/analytics-utils/src/localTime.js +++ b/packages/analytics-utils/src/localTime.js @@ -7,7 +7,7 @@ export function localTime() { const d = new Date() const tzo = -d.getTimezoneOffset() const dif = tzo >= 0 ? '+' : '-' - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds())}${dif}${pad(tzo / 60)}:${pad(tzo % 60)}` + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds())}${dif}${pad(Math.abs(tzo) / 60)}:${pad(tzo % 60)}` } function pad(num) { diff --git a/packages/analytics-utils/tests/localTime.test.js b/packages/analytics-utils/tests/localTime.test.js new file mode 100644 index 00000000..03adb3f3 --- /dev/null +++ b/packages/analytics-utils/tests/localTime.test.js @@ -0,0 +1,47 @@ +import { test } from 'uvu' +import * as assert from 'uvu/assert' + +const { localTime } = await import('../src/localTime.js') + +/* The trailing 6 chars of the ISO string are the timezone offset e.g. "-09:30" */ +function offsetFor(tz) { + process.env.TZ = tz + return localTime().slice(-6) +} + +/* Correct reference offset for the current instant in the active TZ. + Used for zones whose offset shifts with daylight saving, so the + assertion stays valid whatever date the suite runs on. */ +function correctOffset() { + const tzo = -new Date().getTimezoneOffset() + const sign = tzo >= 0 ? '+' : '-' + const hours = Math.abs(Math.floor(Math.abs(tzo) / 60)) + const mins = Math.abs(Math.floor(tzo % 60)) + const p = (n) => (n < 10 ? '0' : '') + n + return `${sign}${p(hours)}:${p(mins)}` +} + +test('negative half hour zone UTC-09:30 (Marquesas) keeps the correct hour', () => { + // Regression: pad(tzo / 60) floored -9.5 to -10 and emitted "-10:30" + assert.is(offsetFor('Pacific/Marquesas'), '-09:30') +}) + +test('negative half hour zone Newfoundland stays off by zero', () => { + process.env.TZ = 'America/St_Johns' + assert.is(localTime().slice(-6), correctOffset()) +}) + +test('positive half hour zone Kolkata +05:30 is unaffected', () => { + assert.is(offsetFor('Asia/Kolkata'), '+05:30') +}) + +test('whole hour negative zone New York matches reference', () => { + process.env.TZ = 'America/New_York' + assert.is(localTime().slice(-6), correctOffset()) +}) + +test('UTC yields +00:00', () => { + assert.is(offsetFor('UTC'), '+00:00') +}) + +test.run()