diff --git a/packages/core/src/__tests__/time.test.ts b/packages/core/src/__tests__/time.test.ts index f2d756c..7e2f7fb 100644 --- a/packages/core/src/__tests__/time.test.ts +++ b/packages/core/src/__tests__/time.test.ts @@ -16,6 +16,8 @@ import { type Timestamp } from '../classes' import { getDay, convertToDay } from '..' +const dayTimeZone = 'Europe/London' + const supportedTimezones: string[] = [ 'Europe/Andorra', 'Asia/Dubai', @@ -348,18 +350,22 @@ const supportedTimezones: string[] = [ 'Africa/Johannesburg' ] +function localeDate (date: Date, timezone: string): string { + return date.toLocaleDateString('en-US', { timeZone: timezone }) +} + function testGetDay (date: Date, timezone: string): void { const timestamp: Timestamp = getDay(date.getTime()) const convertedDate: Date = new Date(timestamp) - const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) - const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + const originalLocaleDate: string = localeDate(date, dayTimeZone) + const convertedLocaleDate: string = localeDate(convertedDate, timezone) expect(convertedLocaleDate).toEqual(originalLocaleDate) } function testConvertToDay (date: Date, timezone: string): void { const convertedDate: Date = convertToDay(date) - const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) - const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + const originalLocaleDate: string = localeDate(date, dayTimeZone) + const convertedLocaleDate: string = localeDate(convertedDate, timezone) expect(convertedLocaleDate).toEqual(originalLocaleDate) } @@ -389,10 +395,23 @@ describe('time', () => { 'dates are matched for time [h: %p, m: %p, s: %p, ms: %p]', (hours: number, minutes: number, seconds: number, milliSeconds: number) => { const date: Date = new Date() - const expectedDay: number = date.getDate() date.setHours(hours, minutes, seconds, milliSeconds) const convertedDate: Date = convertToDay(date) - expect(convertedDate.getDate()).toEqual(expectedDay) + expect(localeDate(convertedDate, dayTimeZone)).toEqual(localeDate(date, dayTimeZone)) + expect(convertedDate.getUTCHours()).toEqual(12) + expect(convertedDate.getUTCMinutes()).toEqual(0) + expect(convertedDate.getUTCSeconds()).toEqual(0) + expect(convertedDate.getUTCMilliseconds()).toEqual(0) } ) + + it.each([ + [new Date('2025-01-25T00:00:00.000Z'), '2025-01-25T12:00:00.000Z'], + [new Date('2024-06-12T23:30:00.000Z'), '2024-06-13T12:00:00.000Z'] + ])('normalizes the London calendar day to noon UTC for %p', (date: Date, expected: string) => { + const expectedTimestamp = Date.parse(expected) + + expect(convertToDay(date).getTime()).toEqual(expectedTimestamp) + expect(getDay(date.getTime())).toEqual(expectedTimestamp) + }) }) diff --git a/packages/core/src/time.ts b/packages/core/src/time.ts index 52569d8..8080922 100644 --- a/packages/core/src/time.ts +++ b/packages/core/src/time.ts @@ -14,20 +14,48 @@ // import { type Timestamp } from './classes' +const dayTimeZone = 'Europe/London' +const dayFormatter = new Intl.DateTimeFormat('en-US', { + timeZone: dayTimeZone, + year: 'numeric', + month: 'numeric', + day: 'numeric' +}) + +function parseCalendarPart (partName: string, partValue: string): number { + const value = Number(partValue) + if (!Number.isInteger(value)) { + throw new Error(`Unable to parse ${dayTimeZone} calendar ${partName}`) + } + return value +} + export function getDay (time: Timestamp): Timestamp { const date: Date = new Date(time) return convertToDay(date).getTime() } export function convertToDay (date: Date): Date { - const originalDay: number = date.getDate() - const convertedDate: Date = new Date(date) - // Set 12 AM UTC time, since it will be the same day in most timezones - convertedDate.setUTCHours(12, 0, 0, 0) - if (convertedDate.getDate() !== originalDay) { - convertedDate.setDate(originalDay) + let year: number | undefined + let month: number | undefined + let day: number | undefined + + for (const part of dayFormatter.formatToParts(date)) { + if (part.type === 'year') { + year = parseCalendarPart(part.type, part.value) + } else if (part.type === 'month') { + month = parseCalendarPart(part.type, part.value) + } else if (part.type === 'day') { + day = parseCalendarPart(part.type, part.value) + } + } + + if (year === undefined || month === undefined || day === undefined) { + throw new Error(`Unable to resolve ${dayTimeZone} calendar day`) } - return convertedDate + + // Set noon UTC, since it will be the same day in most timezones. + return new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0)) } export function getHour (time: Timestamp): Timestamp {