UNPKG

@formatjs/intl-datetimeformat

Version:
121 lines (120 loc) 4.41 kB
import { DateFromTime, HourFromTime, MinFromTime, MonthFromTime, SecFromTime, WeekDay, YearFromTime, invariant, msFromTime } from "@formatjs/ecma402-abstract"; // Cached regex patterns for performance const OFFSET_TIMEZONE_PREFIX_REGEX = /^[+-]/; const OFFSET_TIMEZONE_FORMAT_REGEX = /^([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?(?:\.(\d{1,9}))?$/; /** * IsTimeZoneOffsetString ( offsetString ) * https://tc39.es/ecma262/#sec-istimezoneoffsetstring * * Determines if a string is a UTC offset identifier. * * @param offsetString - The string to check * @returns true if offsetString is a UTC offset format */ function IsTimeZoneOffsetString(offsetString) { return OFFSET_TIMEZONE_PREFIX_REGEX.test(offsetString); } /** * ParseTimeZoneOffsetString ( offsetString ) * https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring * * Parses a UTC offset string and returns the offset in milliseconds. * This is used to calculate the timezone offset for ToLocalTime. * * Supports formats: ±HH, ±HHMM, ±HH:MM, ±HH:MM:SS, ±HH:MM:SS.sss * * @param offsetString - The UTC offset string to parse (e.g., "+01:00") * @returns The offset in milliseconds */ function ParseTimeZoneOffsetString(offsetString) { // 1. Let parseResult be ParseText(offsetString, UTCOffset) const match = OFFSET_TIMEZONE_FORMAT_REGEX.exec(offsetString); // 2. Assert: parseResult is not a List of errors if (!match) { return 0; } // 3. Extract components from parseResult const sign = match[1] === "+" ? 1 : -1; const hours = parseInt(match[2], 10); const minutes = match[3] ? parseInt(match[3], 10) : 0; const seconds = match[4] ? parseInt(match[4], 10) : 0; const fractionalStr = match[5] || "0"; // 4. Convert fractional seconds (nanoseconds) to milliseconds // Pad to 9 digits and divide by 1000000 // Use manual padding for compatibility (padEnd is ES2017) const paddedFractional = (fractionalStr + "000000000").slice(0, 9); const fractional = parseInt(paddedFractional, 10) / 1e6; // 5. Calculate total offset in milliseconds // offset = sign × (hours × 3600000 + minutes × 60000 + seconds × 1000 + fractional) const offsetMs = sign * (hours * 36e5 + minutes * 6e4 + seconds * 1e3 + fractional); // 6. Return offset in milliseconds return offsetMs; } /** * GetNamedTimeZoneOffsetNanoseconds ( timeZone, t ) * Similar to abstract operation in ECMA-262, adapted for IANA timezone data. * Extended to support UTC offset time zones per ECMA-402 PR #788. * * Returns the timezone offset in milliseconds (not nanoseconds for this impl) * and DST flag for the given timezone at time t. * * @param t - Time value in milliseconds since epoch * @param timeZone - The timezone identifier * @param tzData - IANA timezone database * @returns Tuple of [offset in milliseconds, inDST boolean] */ function getApplicableZoneData(t, timeZone, tzData) { // 1. If IsTimeZoneOffsetString(timeZone) is true, then // a. Let offsetNs be ParseTimeZoneOffsetString(timeZone) // b. Return offsetNs (no DST for offset timezones) if (IsTimeZoneOffsetString(timeZone)) { const offsetMs = ParseTimeZoneOffsetString(timeZone); return [offsetMs, false]; } // 2. Let timeZoneData be the IANA Time Zone Database entry for timeZone const zoneData = tzData[timeZone]; // 3. If no data available, treat as UTC (0 offset, no DST) if (!zoneData) { return [0, false]; } // 4. Find the applicable transition for time t let i = 0; let offset = 0; let dst = false; for (; i <= zoneData.length; i++) { if (i === zoneData.length || zoneData[i][0] * 1e3 > t) { ; [, , offset, dst] = zoneData[i - 1]; break; } } // 5. Return offset in milliseconds and DST flag return [offset * 1e3, dst]; } /** * https://tc39.es/ecma402/#sec-tolocaltime * @param t * @param calendar * @param timeZone */ export function ToLocalTime(t, calendar, timeZone, { tzData }) { invariant(calendar === "gregory", "We only support Gregory calendar right now"); const [timeZoneOffset, inDST] = getApplicableZoneData(t.toNumber(), timeZone, tzData); const tz = t.plus(timeZoneOffset).toNumber(); const year = YearFromTime(tz); return { weekday: WeekDay(tz), era: year < 0 ? "BC" : "AD", year, relatedYear: undefined, yearName: undefined, month: MonthFromTime(tz), day: DateFromTime(tz), hour: HourFromTime(tz), minute: MinFromTime(tz), second: SecFromTime(tz), millisecond: msFromTime(tz), inDST, timeZoneOffset }; }