@formatjs/ecma402-abstract
Version:
A collection of implementation for ECMAScript abstract operations
91 lines (90 loc) • 3.5 kB
JavaScript
// Cached regex patterns for performance
const OFFSET_TIMEZONE_PREFIX_REGEX = /^[+-]/;
const OFFSET_TIMEZONE_FORMAT_REGEX = /^([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?(?:\.(\d{1,9}))?$/;
const TRAILING_ZEROS_REGEX = /0+$/;
/**
* IsTimeZoneOffsetString ( offsetString )
* https://tc39.es/ecma262/#sec-istimezoneoffsetstring
*
* Simplified check to determine if a string is a UTC offset identifier.
*
* @param offsetString - The string to check
* @returns true if offsetString starts with '+' or '-'
*/
function IsTimeZoneOffsetString(offsetString) {
// 1. If offsetString does not start with '+' or '-', return false
return OFFSET_TIMEZONE_PREFIX_REGEX.test(offsetString);
}
/**
* ParseTimeZoneOffsetString ( offsetString )
* https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
*
* Parses a UTC offset string and returns its canonical representation.
* Normalizes various formats (±HH, ±HHMM, ±HH:MM, etc.) to ±HH:MM format.
*
* @param offsetString - The UTC offset string to parse
* @returns The canonical offset string in ±HH:MM format (with :SS.sss if non-zero)
*/
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 (validated by IsValidTimeZoneName)
if (!match) {
return offsetString;
}
// 3. Extract components from parseResult
const sign = match[1];
const hours = match[2];
const minutes = match[3] ? match[3] : "00";
const seconds = match[4];
const fractional = match[5];
// 4. Build canonical format: ±HH:MM
let canonical = `${sign}${hours}:${minutes}`;
// 5. If seconds are present and non-zero (or fractional present), include them
if (seconds && (parseInt(seconds, 10) !== 0 || fractional)) {
canonical += `:${seconds}`;
// 6. If fractional seconds present, include them (trim trailing zeros)
if (fractional) {
const trimmedFractional = fractional.replace(TRAILING_ZEROS_REGEX, "");
if (trimmedFractional) {
canonical += `.${trimmedFractional}`;
}
}
}
// 7. Return canonical representation
return canonical;
}
/**
* CanonicalizeTimeZoneName ( timeZone )
* https://tc39.es/ecma402/#sec-canonicalizetimezonename
*
* Extended to support UTC offset time zones per ECMA-402 PR #788 (ES2026).
* Returns the canonical and case-regularized form of a timezone identifier.
*
* @param tz - The timezone identifier to canonicalize
* @param implDetails - Implementation details containing timezone data
* @returns The canonical timezone identifier
*/
export function CanonicalizeTimeZoneName(tz, { zoneNames, uppercaseLinks }) {
// 1. If IsTimeZoneOffsetString(timeZone) is true, then
// a. Return ParseTimeZoneOffsetString(timeZone)
// Per ECMA-402 PR #788, UTC offset identifiers are canonicalized
if (IsTimeZoneOffsetString(tz)) {
return ParseTimeZoneOffsetString(tz);
}
// 2. Let ianaTimeZone be the String value of the Zone or Link name
// in the IANA Time Zone Database that is an ASCII-case-insensitive
// match of timeZone
const uppercasedTz = tz.toUpperCase();
const uppercasedZones = zoneNames.reduce((all, z) => {
all[z.toUpperCase()] = z;
return all;
}, {});
const ianaTimeZone = uppercaseLinks[uppercasedTz] || uppercasedZones[uppercasedTz];
// 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", return "UTC"
if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") {
return "UTC";
}
// 4. Return ianaTimeZone
return ianaTimeZone;
}