import { toDisplayString } from '../extensions/temporal-extensions'; import { isNullOrEmpty } from './is-null-or-empty'; const isDigit = (c: string): boolean => c >= '0' && c <= '9'; export default class TemporalUtils { /* * Returns the number of milliseconds for this date since the epoch, which is defined as the midnight at the beginning of January 1, 1970, UTC. */ static dateNowMs(): number { // eslint-disable-next-line no-restricted-syntax return new Date().getTime(); } static isSerializedDate(str: string): boolean { if (str == null || str.length < 10) { return false; } // Check if its YYYY-MM-DD if (!( isDigit(str[0]) && isDigit(str[1]) && isDigit(str[2]) && isDigit(str[3]) && str[4] === '-' && isDigit(str[5]) && isDigit(str[6]) && str[7] === '-' && isDigit(str[8]) && isDigit(str[9]) )) { return false; } if (str.length > 10) { return isDigit(str[11]) && isDigit(str[12]) && str[13] === ':' && isDigit(str[14]) && isDigit(str[15]) && str[16] === ':' && isDigit(str[17]) && isDigit(str[18]); } else { // Its YYYY-MM-DD return true; } } static fromDate(d: Date): Temporal.PlainDateTime { if (d == null) { return null; } return Temporal.PlainDateTime.from({ year: d.getFullYear(), month: d.getMonth() + 1, // JS Date months are 0-based day: d.getDate(), hour: d.getHours(), minute: d.getMinutes(), second: d.getSeconds(), }); } static fromString(val: string): Temporal.PlainDateTime { if (isNullOrEmpty(val)) { return null; } return Temporal.PlainDateTime.from(val.split('Z')[0]); } static fromEpochMs(val: number): Temporal.PlainDateTime { const MS_PER_DAY = 86_400_000; let daysSinceEpoch = Math.floor(val / MS_PER_DAY); let msInDay = val - daysSinceEpoch * MS_PER_DAY; if (msInDay < 0) { msInDay += MS_PER_DAY; daysSinceEpoch -= 1; } // Convert daysSinceEpoch → Y-M-D const z = daysSinceEpoch + 719468; const era = Math.floor(z / 146097); const doe = z - era * 146097; const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365); const y = yoe + era * 400; const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100)); const mp = Math.floor((5 * doy + 2) / 153); const day = doy - Math.floor((153 * mp + 2) / 5) + 1; const month = mp + (mp < 10 ? 3 : -9); const year = y + (month <= 2 ? 1 : 0); // Compute time from milliseconds in day const hour = Math.floor(msInDay / 3_600_000); const minute = Math.floor((msInDay % 3_600_000) / 60_000); const second = Math.floor((msInDay % 60_000) / 1000); const millisecond = msInDay % 1000; return new Temporal.PlainDateTime( year, month, day, hour, minute, second, millisecond, 0, 0, ); } /** * Reinterprets a UTC-stored PlainDateTime as a UTC instant, shifts it to the * target timezone, and formats it for display. Backend timestamps are stored as * UTC wall-clocks with no zone info, so they MUST be converted before display. * * @param value - the UTC-stored PlainDateTime (returns '-' for null/undefined) * @param options.showTime - include the time component (default false) * @param options.timeZone - IANA zone to render in; defaults to the browser's zone */ static utcToLocalDisplay( value: Temporal.PlainDateTime | null | undefined, options?: { showTime?: boolean; timeZone?: string }, ): string { if (value == null) { return '-'; } const targetZone = options?.timeZone ?? Temporal.Now.timeZoneId(); const local = value.toZonedDateTime('UTC').withTimeZone(targetZone).toPlainDateTime(); return local[toDisplayString]({ showTime: options?.showTime ?? false }); } }