import '#library/temporal.polyfill.js'; import { pad, toTitleCase } from '#library/string.library.js'; import { deepMerge } from '#library/object.library.js'; import { suffix, toRoman } from '#library/number.library.js'; import { isString, isObject, isZonedDateTime, isInstant, isPlainDate, isPlainDateTime, isUndefined, isDefined, isFunction, isSafeKey, isNullish, RE_DIGITS, isDigits, RE_INTEGER } from '#library/assertion.library.js'; import { evaluate, evaluateString } from '#library/evaluation.library.js'; import { formatDayPeriod, getDTF, getPR, getISOWeekOfYear, getLocaleWeekOfYear, getLanguage, getLI, canonicalLocales, localizeDigits, isolateBidi } from '#library/international.library.js'; import { delegator } from '#library/proxy.library.js'; import { hasOwn } from '#library/primitive.library.js'; import { cast } from '#library/type.library.js'; import { isTempo, enums, Match, getRuntime, $Internal, TermHook, hasNativeFormatBraces } from '#tempo/support'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; import { findTermPlugin } from '../plugin/term/term.util.js'; import type { FormatOptions, ValidateFormat, TempoFormatTokens } from '../tempo.type.js'; import type { Tempo } from '../tempo.class.js'; declare module '../tempo.class.js' { interface Tempo { /** applies a format to the instance. */ format(options: FormatOptions): string; /** * Applies a format to the instance. * Format strings are validated at compile time — any unrecognised `{token}` * will produce an IDE error showing the bad token name. * @see {@link TempoFormatTokens} to extend the token set. */ format( fmt: string extends S ? S // variable string — no validation, accept as-is : string extends ValidateFormat ? S // ValidateFormat is `string` → all tokens valid, accept : ValidateFormat, // ValidateFormat is an error literal → mismatch forces IDE error options?: any ): string; /** applies a format to the instance (zero-argument — returns a pre-built format proxy). */ format(): string; } } const RE_SIGNED_DIGITS = /^-?\d+$/; const RE_H12_TAG = /\{h12[^}]*\}/; const RE_H12_GLOBAL = /\{h12[^}]*\}/g; const RE_MI_GLOBAL = /\{mi[^}]*\}/g; const RE_SS_GLOBAL = /\{ss[^}]*\}/g; const RE_MS_GLOBAL = /\{ms[^}]*\}/g; const RE_US_GLOBAL = /\{us[^}]*\}/g; const RE_NS_GLOBAL = /\{ns[^}]*\}/g; const RE_FF_GLOBAL = /\{ff[^}]*\}/g; const RE_SPACE_MODIFIER = /:space/g; const RE_FORMAT_BRACES = new RegExp(Match.formatBraces.source, 'g'); const RE_DOT = /\./g; const RE_HASH_PREFIX = /^#/; const RE_COLON = /:/g; /** * Resolves dot-delimited namespace tokens (e.g. '{geo.city}', '{custom.tag.name}') * against a Tempo instance with safety guards and graceful fallback. */ function resolveNamespaceToken(obj: unknown, token: string): string { const parts = token.split('.'); if (!parts.every(isSafeKey)) return `{${token}}`; const [root, ...rest] = parts; let curr = (obj as any)[root]; if (isNullish(curr)) { return root === 'geo' ? '' : `{${token}}`; } for (const key of rest) { if (isNullish(curr)) return ''; curr = curr[key]; } if (isNullish(curr) || isFunction(curr)) return ''; return isObject(curr) ? String(curr.label ?? curr.key ?? curr.name ?? curr.value ?? curr) : String(curr); } /** * Resolves localized translations from a nested registry.locales dictionary, * cascading from deep hierarchical token path -> leaf property -> flat value. */ function resolveLocaleFromDictionary( dict: Record | undefined, tokenPath: string[], rawValue: string, locale?: string ): string | undefined { if (!isObject(dict)) return undefined; // 1. Hierarchical traversal: dict.geo.sphere['south'] let curr: any = dict; for (const segment of tokenPath) { if (!hasOwn(curr, segment)) { curr = undefined; break; } curr = curr[segment]; } if (hasOwn(curr, rawValue)) return evaluateString(curr[rawValue], locale); // 2. Leaf property: dict.sphere['south'] const leaf = tokenPath.at(-1); if (leaf && hasOwn(dict, leaf) && hasOwn(dict[leaf], rawValue)) return evaluateString(dict[leaf][rawValue], locale); // 3. Flat match: dict['south'] if (hasOwn(dict, rawValue)) return evaluateString(dict[rawValue], locale); return undefined; } /** * Standalone Formatter * Returns a formatted string from a Temporal.ZonedDateTime or Tempo instance. * * @example * import { format } from '@magmacomputing/tempo/format'; * const str = format(Temporal.Now.zonedDateTimeISO(), '{yyyy}-{mm}-{dd}'); * * // Proxy usage (zero or one argument) * const weekDate = format(zdt).weekDate; * const stamp = format().logStamp; // defaults to 'Now' */ export function format(obj?: any): any; export function format(obj: any, options: FormatOptions): string; export function format(obj: any, fmt: string | symbol, options?: any): string; /** * Formats a supported date-time value using a named format, template, or internationalization options. * Scoped day/week time fields count wall-clock units from local midnight or Monday, independent of offset transitions. * The roman modifier converts integers 1-3999 and leaves other values unchanged. * * @param obj - The value to format; omitted values use the current date and time. * @param fmt - A registered format name or literal format template. * @param options - Formatting and configuration overrides. * @returns The formatted date-time string, a format proxy when no format or options are supplied, or an empty string * when a format is supplied and the resolved value is not a ZonedDateTime. * @throws Error when a requested dialect formatter is unavailable. Temporal input conversion errors, * formatter or custom token callback errors, and Intl errors when no fallback succeeds also propagate. */ export function format(obj?: any, fmt?: any, options?: any): any { const state = getRuntime().state; const baseConfig = isTempo(obj) ? obj.config : state?.config; let config = baseConfig; if (isObject(fmt) && isUndefined(options)) { options = fmt; fmt = undefined; } if (options) { config = { ...baseConfig, ...options }; if (options.intl) config.intl = deepMerge(baseConfig?.intl || {}, options.intl); if (options.registry) { config.registry = { ...baseConfig?.registry, ...options.registry }; if (options.registry.formats) config.registry.formats = { ...baseConfig?.registry?.formats, ...options.registry.formats }; if (options.registry.locales) config.registry.locales = { ...baseConfig?.registry?.locales, ...options.registry.locales }; if (options.registry.tokens) config.registry.tokens = { ...baseConfig?.registry?.tokens, ...options.registry.tokens }; } } const formats = Object.assign({}, enums.FORMAT, config?.registry?.formats); const tz = config?.timeZone ?? 'UTC'; let zdt: any; switch (true) { case isTempo(obj): zdt = (obj as any).toDateTime(); break; case isZonedDateTime(obj): zdt = obj; break; case isInstant(obj): zdt = (obj as any).toZonedDateTimeISO(tz); break; case isString(obj): zdt = (obj as any).includes('[') ? Temporal.ZonedDateTime.from(obj as any) : ((obj as any).includes('T') ? Temporal.PlainDateTime.from(obj as any).toZonedDateTime(tz) : Temporal.PlainDate.from(obj as any).toZonedDateTime(tz)); break; case isPlainDateTime(obj): case isPlainDate(obj): zdt = (obj as any).toZonedDateTime(tz); break; case obj === undefined: zdt = Temporal.Now.zonedDateTimeISO(tz); break; default: zdt = obj; } if (config?.timeZone && zdt?.timeZoneId && zdt.timeZoneId !== config.timeZone) { try { zdt = zdt.withTimeZone(config.timeZone); } catch (e) { } } if (config?.calendar && zdt?.calendarId && zdt.calendarId !== config.calendar) { try { zdt = zdt.withCalendar(config.calendar); } catch (e) { } } if (isUndefined(fmt)) { if (options) { try { // Defensive performance boost: Once V8 natively supports Temporal, this memoized instance will succeed. return getDTF(config?.locale, options).format(zdt); } catch (e) { // Fallback: Node < 22 lacks native Temporal support in Intl, so we format the epoch. const fallbackOptions = { timeZone: zdt.timeZoneId, ...options }; if (zdt.calendarId !== 'iso8601' && !fallbackOptions.calendar) fallbackOptions.calendar = zdt.calendarId; return getDTF(config?.locale, fallbackOptions).format(zdt.epochMilliseconds); } } return delegator(formats, (prop) => format(zdt, prop)); } if (!isZonedDateTime(zdt)) return ''; const isNamedFormat = isString(fmt) && hasOwn(formats, fmt); const dialect = evaluate(options?.dialect ?? config?.dialect); if (dialect && isString(fmt) && !isNamedFormat && !hasNativeFormatBraces(fmt, dialect)) { const TempoClass = getRuntime().modules['Tempo'] ?? (obj as any)?.constructor; const dialectsRegistry = config?.registry?.dialects ?? (getRuntime() as any).dialects ?? (getRuntime().modules as any)?.['DialectsModule'] ?? (globalThis as any)[Symbol.for('Tempo.dialects')] ?? (TempoClass as any)?.dialectsRegistry; const formatter = dialectsRegistry?.format ?? dialectsRegistry?.[dialect]?.format; if (isFunction(formatter)) return formatter(zdt, String(fmt), { ...config, dialect }); throw new Error(`[Tempo] Formatting with dialect '${dialect}' requires '@magmacomputing/tempo-plugin-dialects'.`); } let template = (isString(fmt) && formats && (fmt as string in formats)) ? (formats as Record)[fmt as string] : String(fmt); // auto-meridiem: if {h12} is present and {mer} is absent, append it after the last time component if (template.includes('{h12') && !template.includes('{mer')) { const hMatch = template.match(RE_H12_TAG); let merMod = ''; let skipMeridiem = false; if (hMatch) { const modifiers = hMatch[0].toLowerCase(); if (modifiers.includes('raw')) skipMeridiem = true; if (modifiers.includes('upper')) merMod += ':upper'; if (modifiers.includes('lower')) merMod += ':lower'; if (modifiers.includes('dots')) merMod += ':dots'; if (modifiers.includes('locale')) merMod += ':locale'; if (modifiers.includes('space')) merMod += ':space'; } if (!skipMeridiem) { const lastSearch = (rgx: RegExp) => { const matches = [...template.matchAll(rgx)]; return matches.at(-1)?.index ?? -1; } const hIndex = lastSearch(RE_H12_GLOBAL); const miIndex = lastSearch(RE_MI_GLOBAL); const ssIndex = lastSearch(RE_SS_GLOBAL); const subIndex = Math.max( lastSearch(RE_MS_GLOBAL), lastSearch(RE_US_GLOBAL), lastSearch(RE_NS_GLOBAL), lastSearch(RE_FF_GLOBAL) ); const index = Math.max(hIndex, miIndex, ssIndex, subIndex); if (index !== -1) { const end = template.indexOf('}', index) + 1; const prefix = merMod.includes(':space') ? ' ' : ''; const cleanMod = merMod.replace(RE_SPACE_MODIFIER, ''); template = template.slice(0, end) + `${prefix}{mer${cleanMod}}` + template.slice(end); } } } const li = getLI(canonicalLocales(config?.locale)[0]); const result = template.replace(RE_FORMAT_BRACES, (_match: string, fullToken: string) => { let [token, ...modifiers] = fullToken.split(':'); const normMods = modifiers.map(m => m.toLowerCase()); if (token === 'tzd') token = 'tz'; // @deprecated, will remove in v5.0.0 let res: any; switch (token) { case 'yyyy': res = pad(zdt.year, 4); break; case 'yy': res = pad(zdt.year % 100); break; case 'yw': case 'yow': { const yow = normMods.includes('locale') ? getLocaleWeekOfYear(zdt, li).yearOfWeek : getISOWeekOfYear(zdt).yearOfWeek; res = normMods.includes('short') ? pad(Math.abs(yow) % 100) : pad(yow, 4); break; } case 'ww': case 'wy': case 'woy': { if (normMods.includes('cal')) { const w = Math.ceil(zdt.dayOfYear / 7); res = pad(normMods.includes('zero') || normMods.includes('0') ? w - 1 : w); } else if (normMods.includes('month')) { const w = Math.ceil(zdt.day / 7); res = (normMods.includes('zero') || normMods.includes('0') ? w - 1 : w).toString(); } else if (normMods.includes('locale')) { const w = getLocaleWeekOfYear(zdt, li).weekOfYear; res = pad(normMods.includes('zero') || normMods.includes('0') ? w - 1 : w); } else { const w = getISOWeekOfYear(zdt).weekOfYear; res = pad(normMods.includes('zero') || normMods.includes('0') ? w - 1 : w); } break; } case 'wom': { const w = Math.ceil(zdt.day / 7); res = (normMods.includes('zero') || normMods.includes('0') ? w - 1 : w).toString(); break; } case 'yyww': case 'yywy': case 'yywoy': { const { weekOfYear, yearOfWeek } = normMods.includes('locale') ? getLocaleWeekOfYear(zdt, li) : getISOWeekOfYear(zdt); res = pad(yearOfWeek, 4) + pad(weekOfYear); break; } case 'era': { const dtfOptions = (config?.intl?.dateTimeFormat || {}) as any; res = getDTF(config?.locale, { timeZone: zdt.timeZoneId, calendar: zdt.calendarId, era: dtfOptions.era ?? 'short' }).formatToParts(zdt.epochMilliseconds).find(p => p.type === 'era')?.value ?? ''; break; } case 'eon': res = (zdt.eraYear ?? Math.abs(zdt.year <= 0 ? zdt.year - 1 : zdt.year)).toString(); break; case 'cen': case 'century': res = pad(Math.abs(Math.floor(zdt.year / 100)), 2); break; case 'dim': res = zdt.daysInMonth.toString(); break; case 'jj': case 'julian': { const pd = zdt.toPlainDate().withCalendar('iso8601'); const epochDays = Math.floor(pd.since(Temporal.PlainDate.from('1970-01-01'), { largestUnit: 'day' }).days); res = (2440588 + epochDays).toString(); break; } case 'mm': { const m = (normMods.includes('zero') || normMods.includes('0')) ? zdt.month - 1 : zdt.month; res = pad(m); break; } case 'mon': res = enums.MONTHS.keyOf(zdt.month as any); break; case 'mmm': res = enums.MONTH.keyOf(zdt.month as any); break; case 'dd': case 'day': { if (normMods.includes('year')) { const d = (normMods.includes('zero') || normMods.includes('0')) ? zdt.dayOfYear - 1 : zdt.dayOfYear; res = pad(d, 3); } else if (normMods.includes('week')) { res = (normMods.includes('zero') || normMods.includes('0')) ? (zdt.dayOfWeek % 7).toString() : zdt.dayOfWeek.toString(); } else { const d = (normMods.includes('zero') || normMods.includes('0')) ? zdt.day - 1 : zdt.day; res = token === 'day' ? d.toString() : pad(d); } break; } case 'dow': { const isZero = normMods.includes('zero') || normMods.includes('0'); const isLocale = normMods.includes('locale'); if (isLocale) { const firstDay = li.firstDay; const localDow = ((zdt.dayOfWeek - firstDay + 7) % 7) + 1; res = (isZero ? localDow - 1 : localDow).toString(); } else if (isZero) { res = (zdt.dayOfWeek % 7).toString(); } else { res = zdt.dayOfWeek.toString(); } break; } case 'doy': { const d = (normMods.includes('zero') || normMods.includes('0')) ? zdt.dayOfYear - 1 : zdt.dayOfYear; res = pad(d, 3); break; } case 'wkd': res = enums.WEEKDAYS.keyOf(zdt.dayOfWeek as any); break; case 'www': res = enums.WEEKDAY.keyOf(zdt.dayOfWeek as any); break; case 'h24': case 'hh': { if (normMods.includes('week')) { res = ((zdt.dayOfWeek - 1) * 24 + zdt.hour).toString(); } else if (normMods.includes('locale') && token === 'hh') { const hc = li.hourCycle; if (hc === 'h12') { res = pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); } else if (hc === 'h11') { res = pad(zdt.hour % 12); } else if (hc === 'h24') { res = pad(zdt.hour === 0 ? 24 : zdt.hour); } else { res = pad(zdt.hour); } } else { res = pad(zdt.hour); } break; } case 'h12': res = pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); break; case 'mer': res = zdt.hour >= 12 ? 'pm' : 'am'; break; case 'mi': { if (normMods.includes('day')) { res = (zdt.hour * 60 + zdt.minute).toString(); } else if (normMods.includes('week')) { res = (((zdt.dayOfWeek - 1) * 1440) + (zdt.hour * 60 + zdt.minute)).toString(); } else { res = pad(zdt.minute); } break; } case 'ss': { if (normMods.includes('day')) { res = (zdt.hour * 3600 + zdt.minute * 60 + zdt.second).toString(); } else if (normMods.includes('week')) { res = (((zdt.dayOfWeek - 1) * 86400) + (zdt.hour * 3600 + zdt.minute * 60 + zdt.second)).toString(); } else { res = pad(zdt.second); } break; } case 'ms': { if (normMods.includes('day')) { res = ((zdt.hour * 3600 + zdt.minute * 60 + zdt.second) * 1000 + zdt.millisecond).toString(); } else { res = pad(zdt.millisecond, 3); } break; } case 'us': res = pad(zdt.microsecond, 3); break; case 'ns': { if (normMods.includes('day')) { const totalNs = BigInt((zdt.hour * 3600 + zdt.minute * 60 + zdt.second) * 1000 + zdt.millisecond) * 1000000n + BigInt(zdt.microsecond * 1000 + zdt.nanosecond); res = totalNs.toString(); } else { res = pad(zdt.nanosecond, 3); } break; } case 'ff': res = `${pad(zdt.millisecond, 3)}${pad(zdt.microsecond, 3)}${pad(zdt.nanosecond, 3)}`; break; case 'dmy': case 'mdy': case 'ymd': { const isShort = normMods.includes('short') || normMods.includes('yy') || normMods.includes('year'); // :yy & :year @deprecated, will remove in v5.0.0 const y = pad(isShort ? zdt.year % 100 : zdt.year, isShort ? 2 : 4); const m = pad(zdt.month); const d = pad(zdt.day); if (token === 'dmy') res = `${d}${m}${y}`; else if (token === 'mdy') res = `${m}${d}${y}`; else res = `${y}${m}${d}`; break; } case 'hms': res = `${pad(zdt.hour)}${pad(zdt.minute)}${pad(zdt.second)}`; break; case 'time': { if (normMods.includes('locale')) { const is12 = li.hourCycle === 'h12' || li.hourCycle === 'h11'; const h12 = zdt.hour % 12; const h = li.hourCycle === 'h11' ? pad(h12) : li.hourCycle === 'h12' ? pad(h12 || 12) : li.hourCycle === 'h24' ? pad(zdt.hour || 24) : pad(zdt.hour); const m = pad(zdt.minute); const s = pad(zdt.second); if (is12) { const dtOptions = config?.intl?.dateTimeFormat ?? {}; const mer = formatDayPeriod(zdt.epochMilliseconds, config?.locale, { ...dtOptions, hour: 'numeric', hour12: true, timeZone: tz }) || (zdt.hour >= 12 ? 'pm' : 'am'); res = `${h}:${m}:${s} ${mer}`; } else { res = `${h}:${m}:${s}`; } } else { res = `${pad(zdt.hour)}:${pad(zdt.minute)}:${pad(zdt.second)}`; } break; } case 'ts': res = ((config?.timeStamp ?? 'ms') === 'ss') ? Math.trunc(zdt.epochMilliseconds / 1000).toString() : zdt.epochMilliseconds.toString(); break; case 'nano': res = zdt.epochNanoseconds.toString(); break; case 'tz': res = zdt.timeZoneId; break; case 'cal': res = zdt.calendarId; break; default: { const customTokens = (config?.registry as any)?.tokens ?? {}; const customTokenFn = customTokens[token]; if (hasOwn(customTokens, token) && isFunction(customTokenFn)) { res = customTokenFn(zdt, { modifiers, config }); } else if (token.startsWith('#') && isTempo(obj)) { const termName = token.slice(1).split('.')[0]; const plugin = findTermPlugin(termName, (obj.constructor as any)[$Internal]?.()); const pluginHook = (plugin as any)?.[TermHook.format]; let hookRes: any; if (isFunction(pluginHook)) hookRes = pluginHook.call(plugin, token, cast(obj)); if (!isDefined(hookRes)) { const instanceHook = ((obj as any)?.term?.[termName] as any)?.[TermHook.format]; if (isFunction(instanceHook)) hookRes = instanceHook.call(((obj as any)?.term?.[termName]), token, cast(obj)); } if (isDefined(hookRes)) { res = hookRes; break; } const termObj = cast(obj).term[token.slice(1)]; res = isObject(termObj) ? (termObj.label ?? termObj.key ?? `{${token}}`) : (termObj ?? `{${token}}`); } else if (token.includes('.')) { if (token.startsWith('intl.')) { res = resolveNamespaceToken({ intl: { ...li, info: li, locale: li.locale } }, token); } else if (isTempo(obj)) { res = resolveNamespaceToken(obj, token); } else { res = `{${token}}`; } } else { res = `{${token}}`; } break; } } if (res === `{${token}}` || modifiers.length === 0) return res; let canonicalTerm: string | undefined; if (token.startsWith('#') && isTempo(obj)) { const termVal = cast(obj).term[token.slice(1)]; if (isObject(termVal)) { canonicalTerm = String(termVal.key ?? termVal.id ?? res); } else if (isDefined(termVal)) { canonicalTerm = String(termVal); } } const dtOptions = config?.intl?.dateTimeFormat ?? {}; for (const mod of modifiers) { switch (mod.toLowerCase()) { case 'lower': res = String(res).toLocaleLowerCase(config?.locale); break; case 'upper': res = String(res).toLocaleUpperCase(config?.locale); break; case 'title': res = toTitleCase(String(res), config?.locale); break; case 'ord': case 'nth': { const val = parseInt(String(res), 10); const lang = getLanguage(config?.locale); const dict = config?.registry?.locales?.[lang]?.['ordinal']; if (isObject(dict)) { const pr = getPR(config?.locale, { type: 'ordinal' }); const category = pr.select(val); res = `${val}${dict[category] ?? dict.other ?? suffix(val)}`; } else if (isFunction(dict)) { res = dict(val); } else { res = suffix(val); } break; } case 'raw': if (RE_DIGITS.test(String(res))) res = BigInt(String(res)).toString(); break; case 'roman': { const roman = toRoman(Number(res)); if (roman) res = roman; break; } case 'dots': { if (token === 'mer' || token === 'era') res = String(res).replace(RE_DOT, '').split('').join('.') + '.'; break; } case 'locale': { try { const lang = getLanguage(config?.locale); const dict = config?.registry?.locales?.[lang]; const tokenPath = token.replace(RE_HASH_PREFIX, '').split('.'); const valStr = canonicalTerm ?? String(res); // 1. Check user-defined registry.locales (hierarchical, leaf, or flat) let registered = resolveLocaleFromDictionary(dict, tokenPath, valStr, config?.locale); if (!isDefined(registered) && canonicalTerm && canonicalTerm !== String(res)) registered = resolveLocaleFromDictionary(dict, tokenPath, String(res), config?.locale); if (isDefined(registered)) { res = registered; } else if (token.startsWith('#') && isTempo(obj)) { // 2. Fall back to Term Plugin bundled dictionary const termKey = token.slice(1); const termName = termKey.split('.')[0]; const plugin = findTermPlugin(termName, (obj.constructor as any)[$Internal]()); if (plugin) { let locRes: any; const flatGroups = Array.isArray(plugin.groups) ? plugin.groups : (isObject(plugin.groups) ? Object.values(plugin.groups).flat() : []); const group = flatGroups.find((g: any) => g.key === valStr); if (group && isObject(group.locale)) locRes = group.locale[lang] ?? group.locale.en; if (isDefined(locRes)) res = evaluateString(locRes, config?.locale); } } else { // 3. Fall back to native Intl for core date tokens const tzOpts = { ...dtOptions, timeZone: zdt.timeZoneId, calendar: zdt.calendarId }; if (token === 'mon') res = getDTF(config?.locale, { ...tzOpts, month: 'long' }).format(zdt.epochMilliseconds); else if (token === 'mmm') res = getDTF(config?.locale, { ...tzOpts, month: 'short' }).format(zdt.epochMilliseconds); else if (token === 'wkd') res = getDTF(config?.locale, { ...tzOpts, weekday: 'long' }).format(zdt.epochMilliseconds); else if (token === 'www') res = getDTF(config?.locale, { ...tzOpts, weekday: 'short' }).format(zdt.epochMilliseconds); else if (token === 'mer') { const period = formatDayPeriod(zdt.epochMilliseconds, config?.locale, { ...dtOptions, hour: 'numeric', hour12: true, timeZone: tz }); if (period) res = period; } } } catch (e) { // Fallback to the current base token string } break; } case 'short': case 'long': case 'shortoffset': case 'longoffset': case 'shortgeneric': case 'longgeneric': { const modKey = mod.toLowerCase(); if (token === 'tz') { if (normMods.includes('offset') || normMods.includes('offsetcompact')) { // Handled by case 'offset' / 'offsetcompact' } else { const styleMap: Record = { short: 'short', long: 'long', shortoffset: 'shortOffset', longoffset: 'longOffset', shortgeneric: 'shortGeneric', longgeneric: 'longGeneric' }; const parts = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, timeZoneName: styleMap[modKey] }).formatToParts(zdt.epochMilliseconds); res = parts.find(p => p.type === 'timeZoneName')?.value ?? zdt.timeZoneId; } } else if (token === 'mon' || token === 'mmm') { if (modKey === 'short' || modKey === 'long') res = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, calendar: zdt.calendarId, month: modKey }).format(zdt.epochMilliseconds); } else if (token === 'wkd' || token === 'www') { if (modKey === 'short' || modKey === 'long') res = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, calendar: zdt.calendarId, weekday: modKey }).format(zdt.epochMilliseconds); } else if (token === 'era') { if (modKey === 'short' || modKey === 'long') { const parts = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, calendar: zdt.calendarId, era: modKey }).formatToParts(zdt.epochMilliseconds); res = parts.find(p => p.type === 'era')?.value ?? res; } } break; } case 'offset': case 'offsetcompact': { if (token === 'tz') { const sep = (mod.toLowerCase() === 'offsetcompact' || normMods.includes('compact')) ? '' : ':'; const isUtc = zdt.offset === '+00:00' || zdt.offset === '-00:00' || zdt.offset === 'Z' || zdt.offsetNanoseconds === 0; const sign = zdt.offset.startsWith('-') ? '-' : '+'; const cleanOffset = (zdt.offset.startsWith('+') || zdt.offset.startsWith('-')) ? zdt.offset.slice(1) : zdt.offset === 'Z' ? '00:00' : zdt.offset; const [hStr = '00', mStr = '00', sStr = '00'] = cleanOffset.split(':'); if ((normMods.includes('utc') || normMods.includes('z')) && isUtc) { // :utc canon, :z @internal alias res = 'Z'; } else if (normMods.includes('hh') || normMods.includes('hour')) { res = `${sign}${hStr}`; } else if (normMods.includes('mi') || normMods.includes('min')) { res = mStr; } else if (normMods.includes('ss') || normMods.includes('sec')) { res = `${sign}${hStr}${sep}${mStr}${sep}${sStr}`; } else if (normMods.includes('short')) { if (mStr === '00' && sStr === '00') { res = `${sign}${hStr}`; } else { res = `${sign}${hStr}${sep}${mStr}`; } } else { res = `${sign}${hStr}${sep}${mStr}`; } } break; } case 'compact': if (token !== 'tz' || (!normMods.includes('offset') && !normMods.includes('offsetcompact'))) res = String(res).replace(RE_COLON, ''); break; case 'offsetshort': // @deprecated, will remove in v5.0.0 if (token === 'tz') res = zdt.offset.endsWith(':00') ? zdt.offset.slice(0, -3) : zdt.offset; break; case 'z': // @deprecated, will remove in v5.0.0; use Tempo Dialects (LDML / Moment / Strftime) or canonical {tz:short}, {tz:offset}, etc. if (token === 'tz') { if (!normMods.includes('offset') && !normMods.includes('offsetcompact')) res = zdt.offset.endsWith(':00') ? zdt.offset.slice(0, -3) : zdt.offset; } break; case 'zz': // @deprecated, will remove in v5.0.0; use Tempo Dialects (LDML / Moment / Strftime) or canonical {tz:offset} if (token === 'tz') res = zdt.offset; break; case 'zzz': // @deprecated, will remove in v5.0.0; use Tempo Dialects (LDML / Moment / Strftime) or canonical {tz:offsetcompact} if (token === 'tz') res = zdt.offset.replace(':', ''); break; case 'zzzz': // @deprecated, will remove in v5.0.0; use Tempo Dialects (LDML / Moment / Strftime) or canonical {tz:short} if (token === 'tz') { const parts = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, timeZoneName: 'short' }).formatToParts(zdt.epochMilliseconds); res = parts.find(p => p.type === 'timeZoneName')?.value ?? zdt.timeZoneId; } break; case 'zzzzz': // @deprecated, will remove in v5.0.0; use Tempo Dialects (LDML / Moment / Strftime) or canonical {tz:long} if (token === 'tz') { const parts = getDTF(config?.locale, { ...dtOptions, timeZone: zdt.timeZoneId, timeZoneName: 'long' }).formatToParts(zdt.epochMilliseconds); res = parts.find(p => p.type === 'timeZoneName')?.value ?? zdt.timeZoneId; } break; case 'space': { if (!normMods.some(m => RE_INTEGER.test(m) && m !== '0')) { const defaultWidth = (token === 'doy' || (token === 'dd' && normMods.includes('year'))) ? 3 : (token === 'ss' && normMods.includes('day')) ? 5 : 2; const strVal = String(res); if (RE_SIGNED_DIGITS.test(strVal)) { const unpadded = (strVal.startsWith('-')) ? '-' + BigInt(strVal.slice(1)).toString() : BigInt(strVal).toString(); res = (unpadded.startsWith('-')) ? '-' + unpadded.slice(1).padStart(Math.max(1, defaultWidth - 1), ' ') : unpadded.padStart(defaultWidth, ' '); } } break; } case 'zero': case '0': case 'day': case 'week': case 'month': case 'year': case 'cal': break; default: { if (RE_INTEGER.test(mod) && mod !== '0') { const width = parseInt(mod, 10); const strVal = String(res); if (RE_SIGNED_DIGITS.test(strVal)) { if (token === 'ff') { res = width > 0 ? strVal.slice(0, width) : strVal; } else if (width > 0) { if (normMods.includes('space')) { const unpadded = (strVal.startsWith('-')) ? '-' + BigInt(strVal.slice(1)).toString() : BigInt(strVal).toString(); res = (unpadded.startsWith('-')) ? '-' + unpadded.slice(1).padStart(Math.max(1, width - 1), ' ') : unpadded.padStart(width, ' '); } else { res = (strVal.startsWith('-')) ? '-' + strVal.slice(1).padStart(Math.max(1, width - 1), '0') : strVal.padStart(width, '0'); } } else if (width < 0) { res = strVal.slice(width); } } } break; } } } if (normMods.includes('locale')) { if (li.numberingSystem && li.numberingSystem !== 'latn') res = localizeDigits(res, li.numberingSystem); if (li.direction === 'rtl' && (token === 'mon' || token === 'mmm' || token === 'wkd' || token === 'www' || token.startsWith('#') || token === 'time')) res = isolateBidi(res, 'rtl'); } return res; }); return result as any; } /** * Format Module Plugin */ // @ts-ignore export const FormatModule: Tempo.Module = defineInterpreterModule('FormatModule', function (this: Tempo, fmt: any, options?: any) { if (!this.isValid) return '' as unknown as any; return format(this, fmt, options); });