@formatjs/intl-datetimeformat
Version:
Intl.DateTimeFormat polyfill
121 lines (120 loc) • 4.41 kB
JavaScript
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
};
}