import '#library/temporal.polyfill.js'; import { Immutable, Mutable, Serializable, StringTag } from '#library/decorator.library.js'; import { asArray, asError } from '#library/coercion.library.js'; import { getStorage, setStorage, isTestEnvironment } from '#library/storage.library.js'; import { secure, proxify, delegate, indexedArray, dynamicProxy } from '#library/proxy.library.js'; import { getContext, CONTEXT } from '#library/utility.library.js'; import { ownKeys, ownEntries, ownValues, unwrap, hasKey, hasOwn, hasAnyOf, hasNoneOf } from '#library/primitive.library.js'; import { getAccessors, omit } from '#library/reflection.library.js'; import { ifDefined } from '#library/object.library.js'; import { pad, trimAll } from '#library/string.library.js'; import { getType, cast } from '#library/type.library.js'; import { clone } from '#library/serialize.library.js'; import { isEmpty, isDefined, isUndefined, isString, isText, isObject, isPlainObject, isSymbol, isFunction, isClass, isCallable, isZonedDateTime, isDurationLike, isNumber } from '#library/assertion.library.js'; import { instant, getTemporalIds, normalizeUtcOffset } from '#library/temporal.library.js'; import { getDateTimeFormat, getHemisphere, canonicalLocales, resolveLocale, getISOWeekOfYear, getLC, getLI, getIntlNamespace, type ResolvedLocaleInfo, type TempoIntlNamespace } from '#library/international.library.js'; import { evaluate } from '#library/evaluation.library.js'; import { getStashedGeo, coerceGeo } from '#library/mapper.library.js'; import { Interval } from '#library/scheduling/interval.class.js'; import { Enum } from '#library/enumerate.library.js'; import { LOG } from '#library/logger.class.js'; import type { Secure } from '#library/type.library.js'; import { registerPlugin, interpret, ensureModule, type TempoPlugin } from './plugin/plugin.util.js' import { registerTerm, getTermRange } from './plugin/term/term.util.js'; import type { TermPlugin } from './plugin/term/term.type.js'; import { AliasEngine } from './engine/engine.alias.js'; import { PatternCompiler } from './engine/engine.pattern.js'; import { createMasterGuard } from './engine/engine.guard.js'; import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engine/engine.layout.js'; import { resolveConfig, resolveConfigSync } from './config/config.resolve.js'; import { $Identity, $Discover, $LogConfig } from '#library/symbol.library.js'; import { resolveMonthDay, setProperty, proto } from './support/support.util.js'; import { datePattern } from './support/support.default.js'; import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Token, Snippet, Layout, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $errored, $guard, $setDiscovery, logError, logDebug, logWarn, logTempo, setLogLevel, createCacheFacade } from '#tempo/support'; import { TEMPO_VERSION } from './tempo.version.js'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) declare module '#library/type.library.js' { interface TypeValueMap { Tempo: { type: 'Tempo', value: Tempo }; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** current execution context */ const Context = getContext(); /** */ const ClassStates = new WeakMap(); // shortcut functions to common Tempo properties / methods /** current timestamp (ts) */ export const getStamp = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).ts) as t.Params; /** create new Tempo */ export const getTempo = ((tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options)) as t.Params; /** format a Tempo */ export const fmtTempo = ((fmt: string, tempo: t.DateTime, options: t.Options) => new Tempo(tempo, options).format(fmt as any)) as Internal.Fmt; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ namespace Internal { // ...existing code... export type State = t.Internal.State; export type Parse = t.Internal.Parse; export type MatchResult = t.Internal.Match; export type Config = t.Internal.Config; export type Discovery = t.Internal.Discovery; export type Registry = t.Internal.Registry; export type DelegatorHost = t.Internal.DelegatorHost; export interface PluginContainer extends TempoPlugin { } export type Fmt = { // used for the fmtTempo() shortcut (fmt: F, tempo?: t.DateTime, options?: t.Options): string; (fmt: F, options: t.Options): string; (options: Intl.DateTimeFormatOptions & { timeZone?: string; calendar?: string }): string; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * # Tempo * A powerful wrapper around `Temporal.ZonedDateTime` for flexible parsing and intuitive manipulation of date-time objects. * Bridges the gap between raw string/number inputs and the strict requirements of the ECMAScript Temporal API. */ /** Tempo state for the global configuration */ let _global = {} as Internal.State; /** cache for next-available 'usr' Token key */ let _usrCount = 0; /** flag to prevent recursion during init */ const _lifecycle = { bootstrap: true, initialising: false, extendDepth: 0, ready: false }; const intervalProxyHandler: ProxyHandler = { construct(target, args: any[]) { const parse = (arg: t.DateTime | null) => arg === null ? null : (arg instanceof Tempo ? arg : new Tempo(arg)); return new (target as any)(parse(args[0]), parse(args[1])); } } /** * A powerful wrapper around `Temporal.ZonedDateTime` for flexible parsing and intuitive manipulation of date-time objects. * Bridges the gap between raw string/number inputs and the strict requirements of the ECMAScript Temporal API. */ @Serializable @Immutable @StringTag('Tempo') export class Tempo { /** Interval class for checking overlaps and bounds between Temporal points */ static Interval = cast(new Proxy(Interval, intervalProxyHandler)); /** Live registry enumerations */ static get enums() { return enums } /** Weekday names (short-form) */ static get WEEKDAY() { return enums.WEEKDAY } /** Weekday names (long-form) */ static get WEEKDAYS() { return enums.WEEKDAYS } /** Month names (short-form) */ static get MONTH() { return enums.MONTH } /** Month names (long-form) */ static get MONTHS() { return enums.MONTHS } /** Time durations as seconds (singular) */ static get DURATION() { return enums.DURATION } /** Time durations as milliseconds (plural) */ static get DURATIONS() { return enums.DURATIONS } /** Quarterly Seasons */ static get SEASON() { return enums.SEASON } /** Compass cardinal points */ static get COMPASS() { return enums.COMPASS } /** @deprecated Use Tempo.enums.ELEMENT */ static get ELEMENT() { return enums.ELEMENT } /** @deprecated Use Tempo.enums.FORMAT */ static get FORMAT() { return enums.FORMAT } /** @deprecated Use Tempo.enums.NUMBER */ static get NUMBER() { return enums.NUMBER } /** @deprecated Use Tempo.enums.ORDINAL */ static get ORDINAL() { return enums.ORDINAL } /** @deprecated Use Tempo.enums.TIMEZONE */ static get TIMEZONE() { return enums.TIMEZONE } /** @deprecated Use Tempo.enums.MONTH_DAY */ static get MONTH_DAY() { return enums.MONTH_DAY } /** @deprecated Use Tempo.enums.MODE */ static get MODE() { return enums.MODE } /** @deprecated Use Tempo.enums.CACHE */ static get CACHE() { return enums.CACHE } /** @deprecated Use Tempo.enums.LIMIT */ static get LIMIT() { return enums.LIMIT } /** @internal check if Tempo is currently initializing */ static get isInitializing() { return !_lifecycle.ready } /** @internal check if Tempo is currently extending */ static get isExtending() { return _lifecycle.extendDepth > 0 } static #versions: Record = { Tempo: TEMPO_VERSION }; /** the active versions of Tempo and any registered plugins or modules */ static get versions() { return Object.freeze({ ...Tempo.#versions }) as Readonly>; } /** the version of this Tempo build (stamped at build-time from package.json) */ static get version() { return Tempo.#versions['Tempo']; } /** high-performance in-memory cache facade for glossary and dynamic parse results */ static get cache() { return createCacheFacade(() => this[$Internal]()); } /** weakly-retained object cache facade for domain objects, ASTs, and plugin instances */ static get weakCache() { return this.cache.weak; } /** mutable list of registered term plugins */ static get #terms(): TermPlugin[] { return Tempo[$Internal]().pluginsDb.terms } /** mapping of terms to their resolved values */ static #termMap: Map = new Map(); /** tracks invalid locales already warned about to avoid log spam */ static #warnedLocales: Set = new Set(); /** @internal Master Guard predicate (implements RegExp-like interface) */static get [$guard]() { return (this[$Internal]() as any)[$guard] ?? { test: () => true }; } /** @internal */ static [$IsBase] = true; /** @internal Static access to global private state. */ static [$Internal]() { return ClassStates.get(this) ?? _global; } /** @internal brand check to distinguish Tempo objects from other objects */ get [$Identity](): true { return true } /** * Registers event or period aliases and updates the corresponding snippet pattern. * The {dt} layout combines date-related {snippets} (e.g. dd, mm -or- evt) into a pattern * against which a string can be tested. Because it includes a list of events (e.g. 'new_years' | 'xmas'), * we need to rebuild {dt} if the user adds a new event. * * @param shape - The internal state to update * @param kind - Type of alias to register ('evt' for events, 'per' for periods) * @param token - The snippet token to update * @param provided - Optional array of [key, value] pairs to register * @internal */ static [$setAliases](shape: Internal.State, kind: 'evt' | 'per', token: any, provided?: [string, any][]) { const field = kind === 'evt' ? 'event' : 'period'; const parent = proto(shape); const parentMap = parent.parse?.[field] ?? {}; // Identify local additions or overrides const entries = provided ?? ownEntries(shape.parse[field] as any, true).filter(([k, v]) => { return !(k in parentMap) || (shape.parse[field] as any)[k as string] !== parentMap[k as string]; }); // Sync legacy registry if provided directly if (provided) { if (!hasOwn(shape.parse, field)) (shape.parse as any)[field] = { ...(shape.parse as any)[field] }; provided.forEach(([k, v]) => { if (!hasOwn(shape.parse[field] as any, k)) (shape.parse[field] as any)[k as string] = v; }); } // If no local aliases, inherit the parent's engine (via prototype) and exit if (entries.length === 0 && !hasOwn(shape, 'aliasEngine')) return; // Use the correct alias engine: static for global, instance for local let engine = shape.aliasEngine; // If we have local aliases to register, we MUST have a local engine to avoid polluting the parent if (!hasOwn(shape, 'aliasEngine')) { engine = shape.aliasEngine = new AliasEngine({ parent: parent.aliasEngine, config: shape.config }); } const groups = engine!.registerAliases(kind, entries as any); if (groups) { const protoRegex = parent.parse?.snippet?.[token]?.source; if (groups !== protoRegex) { if (!hasOwn(shape.parse, 'snippet')) shape.parse.snippet = { ...shape.parse.snippet }; setProperty(shape.parse.snippet, token, new RegExp(groups)); } } else { if (hasOwn(shape.parse.snippet, token)) { if (!hasOwn(shape.parse, 'snippet')) shape.parse.snippet = { ...shape.parse.snippet }; delete (shape.parse.snippet as any)[token]; } } } /** @internal */ static [$setEvents](shape: Internal.State, provided?: [string, any][], rebuild = true) { this[$setAliases](shape, 'evt', Token.evt, provided); if (rebuild) setPatterns(shape); } /** @internal */ static [$setPeriods](shape: Internal.State, provided?: [string, any][], rebuild = true) { this[$setAliases](shape, 'per', Token.per, provided); if (rebuild) setPatterns(shape); } /** * Infers the hemisphere (north/south) using the timezone's daylight-savings setting. * * @param shape - The internal state * @param options - Configuration options containing sphere or timezone * @returns The resolved compass direction (north/south), or undefined if not determinable */ static #setSphere = (shape: Internal.State, options: t.Options): t.COMPASS | undefined => { const rawSphere = options.sphere ?? options.geo?.sphere; if (isDefined(rawSphere)) { const evaluatedSphere = evaluate(rawSphere); if (isDefined(evaluatedSphere)) return evaluatedSphere as t.COMPASS; } const geo = coerceGeo(options) ?? shape.config.geo ?? getStashedGeo(); if (isDefined(geo?.sphere)) return geo.sphere as t.COMPASS; if (isNumber(geo?.latitude)) return (geo.latitude > 0.001 ? 'north' : (geo.latitude < -0.001 ? 'south' : 'equator')) as t.COMPASS; const resolvedTz = options.timeZone ?? shape.config.timeZone; if (isDefined(resolvedTz) && String(resolvedTz).toLowerCase() !== 'utc') { const sphere = getHemisphere(String(resolvedTz)); if (isDefined(sphere)) return sphere; } return undefined; } /** * Determines if the configured timezone and locale prefer month-day-year date ordering. * * @param shape - The internal state containing timezone and locale configuration * @returns True if the timezone/locale prefers MDY format, false otherwise */ static #isMonthDay(shape: Internal.State) { const { timeZone, locale } = shape.config; const mdy = shape.parse.monthDay; const globalMdy = enums.MONTH_DAY as t.MonthDay; const rawLocale = Tempo.#locale(locale); if (!getLC(rawLocale) && !Tempo.#warnedLocales.has(rawLocale)) { Tempo.#warnedLocales.add(rawLocale); logWarn(`Invalid locale encountered in #isMonthDay: ${locale}. Falling back to en-US.`, shape.config); } const li = getLI(rawLocale); const baseName = li.baseName; const language = li.language ?? li.locale?.language; const tz = String(timeZone); const activeLocaleData = mdy.resolvedLocales?.find(l => l.locale === baseName || (language && l.locale === language)); return Boolean( (activeLocaleData?.timeZones?.includes(tz)) || (globalMdy.timezones?.[baseName]?.includes(tz)) || (language && globalMdy.timezones?.[language]?.includes(tz)) || ((asArray(globalMdy.locales).includes(baseName) || (language && asArray(globalMdy.locales).includes(language))) && li.timeZones.includes(tz)) ); } /** * swap parsing-order of layouts to suit different timeZones * this allows the parser to try to interpret '04012023' as Apr-01-2023 before trying 04-Jan-2023 */ static #swapLayout(shape: Internal.State) { const { layouts } = shape.parse.monthDay; if (isEmpty(layouts)) return; const isMonthDay = shape.parse.monthDay.isExplicit ? shape.parse.monthDay.active! : Tempo.#isMonthDay(shape); shape.parse.monthDay.active = isMonthDay; // ensure Token.dt matches the local monthDay preference const dt = isMonthDay ? datePattern.mdy : datePattern.dmy; if (shape.parse.layout[Token.dt] !== dt) shape.parse.layout = { ...shape.parse.layout, [Token.dt]: dt }; const layoutController = (shape.parse.planner.layoutOrder?.length ?? 0) > 0 ? { [DEFAULT_LAYOUT_CLASS]: [...shape.parse.planner.layoutOrder!] } : undefined; const layout = resolveLayoutOrder({ layout: shape.parse.layout, monthDayLayouts: layouts!, isMonthDay, ...ifDefined({ layoutController }), }); if (layout !== shape.parse.layout) shape.parse.layout = layout as Layout; logDebug(`Resolved layout order: ${getLayoutOrder(layout).join(' -> ')}`, shape.config); } /** * Gets the first canonical name of a supplied locale, falling back to navigator language. * * @param locale - Optional locale string or array of locale strings * @returns The canonical locale identifier */ static #locale = (locale?: string | readonly string[]) => { const global = Context.global; const primaryLocale = canonicalLocales(locale)[0]; return primaryLocale ?? global?.navigator?.languages?.[0] ?? // fallback to current first navigator.languages[] global?.navigator?.language ?? // else navigator.language Default.locale ?? // else default locale asArray(locale)[0] // cannot determine locale } /** * conform input of Snippet / Layout / Event / Period options * This is needed because we allow the user to flexibly provide detail as {[key]:val} or {[key]:val}[] or [key,val][] */ /** @internal */ static [$setConfig](shape: Internal.State, ...options: t.Options[]) { const providedOptions: t.Options = Object.assign({}, ...options); const storeKey = providedOptions.store; const mergedOptions: t.Options = storeKey ? Object.assign(Tempo.readStore(storeKey), providedOptions) : providedOptions; if (isEmpty(mergedOptions)) return; // Apply options using extendState const patternsDirty = extendState(shape, mergedOptions); let needsRebuild = patternsDirty; if (patternsDirty && shape.config.scope === 'local') { this[$buildGuard](shape); } // Side-effects if ((isDefined(mergedOptions.sphere) || isDefined((mergedOptions as any).latitude) || isDefined((mergedOptions as any).lat) || isDefined(mergedOptions.geo)) && !isFunction(mergedOptions.sphere)) { const newSphere = Tempo.#setSphere(shape, mergedOptions); if (isDefined(newSphere)) shape.config.sphere = newSphere; } const oldLayout = shape.parse.layout; Tempo.#swapLayout(shape); if (oldLayout !== shape.parse.layout) needsRebuild = true; if (isDefined(shape.parse.event)) { this[$setEvents](shape, undefined, false); needsRebuild = true; } if (isDefined(shape.parse.period)) { this[$setPeriods](shape, undefined, false); needsRebuild = true; } if (needsRebuild) { setPatterns(shape); this[$buildGuard](shape); } } /** support "Global Discovery" of user-options */ /** @internal */ static [$setDiscovery](shape: Internal.State, discovery?: Internal.Discovery) { if (!isObject(discovery)) return {} markConfig(discovery); // auto-mark the discovery object const isSandbox = shape !== _global; let opts: Record = evaluate(discovery.options, {}) ?? {}; // 1. Process TimeZones (normalize to lowercase for lookup) if (discovery.timeZones) { const tzs = Object.fromEntries( ownEntries(discovery.timeZones, true) .map(([k, v]) => [String(k).toLowerCase(), v]) ); if (isSandbox) opts = { ...opts, timeZones: tzs }; else registryUpdate('TIMEZONE', tzs); } // 1a. Process MDY settings if (discovery.monthDay) { let md = discovery.monthDay; if (md.timezones) { const zones = Object.fromEntries( ownEntries(md.timezones, true).map(([k, v]) => { const baseName = getLC(String(k))?.baseName; return [baseName ?? String(k), v]; }) ); if (isSandbox) md = { ...md, timezones: zones }; else registryUpdate('MONTH_DAY', { timezones: zones }); } if (!isSandbox) { if (md.locales) registryUpdate('MONTH_DAY', { locales: asArray(md.locales) }); if (md.layouts) registryUpdate('MONTH_DAY', { layouts: asArray(md.layouts) }); } opts = { ...opts, monthDay: md }; } // 1b. Process Internationalization if (discovery.intl) opts = { ...opts, intl: discovery.intl }; // 1c. Process Planner if (isObject(discovery.planner)) opts = { ...opts, planner: discovery.planner }; // 2. Process Terms if (discovery.terms) this.use(asArray(discovery.terms)); // 3. Process Registry let registryOpts = discovery.registry ?? {}; if (Object.keys(registryOpts).length > 0) { opts = { ...opts, registry: registryOpts }; if (!isSandbox) { if (registryOpts.formats) registryUpdate('FORMAT', registryOpts.formats); if (registryOpts.locales) registryUpdate('LOCALE', registryOpts.locales); if (registryOpts.numbers) registryUpdate('NUMBER', registryOpts.numbers); if (registryOpts.ordinals) registryUpdate('ORDINAL', registryOpts.ordinals); } } // 4. Process Plugins & Plugin Options if (discovery.pluginOptions && isPlainObject(discovery.pluginOptions)) opts = { ...opts, pluginOptions: isPlainObject(opts.pluginOptions) ? { ...opts.pluginOptions, ...discovery.pluginOptions } : discovery.pluginOptions }; /** @deprecated Providing configuration dictionaries under 'discovery.plugins' is deprecated. Use 'discovery.pluginOptions' instead. */ if (discovery.plugins && isPlainObject(discovery.plugins) && hasNoneOf(discovery.plugins, 'install', 'key')) { opts = { ...opts, plugins: isPlainObject(opts.plugins) ? { ...opts.plugins, ...discovery.plugins } : discovery.plugins, pluginOptions: isPlainObject(opts.pluginOptions) ? { ...discovery.plugins, ...opts.pluginOptions } : discovery.plugins, }; } // 5. Process Options if (discovery.ignore) { const ignore = evaluate(discovery.ignore); opts = { ...opts, ignore }; } const res = evaluate(opts, {}) ?? {}; if (shape === _global) { this[$buildGuard](); setPatterns(shape); } return res; } /** @internal */ static [$buildGuard](targetState?: Internal.State) { const state = targetState ?? this[$Internal](); const wordsList = [ ...Enum.keys(enums.NUMBER), ...Object.keys(state.config.registry?.numbers ?? {}), ...Enum.keys(enums.ORDINAL), ...Object.keys(state.config.registry?.ordinals ?? {}), ...Enum.keys(enums.WEEKDAY), ...Enum.keys(enums.WEEKDAYS), ...Enum.keys(enums.MONTH), ...Enum.keys(enums.MONTHS), ...Enum.keys(enums.DURATION), ...Enum.keys(enums.DURATIONS), ...Enum.keys(enums.TIMEZONE), ...(state.aliasEngine?.getAliases(undefined, true).map((a: any) => a.name) ?? []), ...ownKeys(state.parse.ignore), ...ownKeys(state.parse.snippet), ...ownKeys(state.parse.layout), ...[Token.slk], ...Tempo.#terms.map(t => t.key), ...Tempo.#terms.map(t => t.scope), ...Guard, ...(state.config.registry?.modifiers ? Object.values(state.config.registry.modifiers).flat() : []), ...ownValues(state.parse.layout, true).flatMap(val => { const src = (val as any) instanceof RegExp ? (val as any).source : (isString(val) ? val : ''); if (!src) return []; return src .replace(/\{[#]?[\w]+(?:\.[\w]+)*(?:\:[a-zA-Z]+)*\}/g, ' ') .replace(/\(\?<[\w]+>/g, ' ') .replace(/\\\\/g, '\u0001') .replace(/\\\|/g, '\u0000') .replace(/[\(\)\?\:\^\$\d\{\}\[\]]/g, ' ') .replace(/\\(.)/g, '$1') .replace(/\u0001/g, '\\') .split(/[\s\|]+/) .map((t: string) => t.replace(/\u0000/g, '|')) .filter(Boolean) .filter((t: string) => t !== '[' && t !== ']'); }) ]; const guard = createMasterGuard(wordsList); if (targetState) { targetState.parse.guard = guard as any; (targetState as any)[$guard] = guard; } else { (this[$Internal]() as any)[$guard] = guard; (this[$Internal]().parse as any).guard = guard; } if (!targetState && this[$Internal]() === _global) setPatterns(this[$Internal]()); } /** @internal resolve a global discovery config object by symbol key */ static #getConfig(sym: symbol) { const discovery = (globalThis as Record)[sym]; return proxify(omit({ ...discovery, scope: 'discovery' }, 'value')); } /** * Register discovery configuration with an optional discovery symbol. * * @param discovery - Discovery object defining options, formats, periods, or terms. * @param symbol - Optional symbol identifier under globalThis to attach discovery data. */ /** * Register discovery configuration with an optional discovery symbol. * * @param discovery - Discovery object defining options, formats, periods, or terms. * @param symbol - Optional symbol identifier under globalThis to attach discovery data. */ static use(discovery: t.Discovery, symbol?: symbol): typeof Tempo; /** * Register an individual plugin or plugin factory. * * @param plugin - The plugin function or definition to register. * @param options - Optional configuration for the plugin. */ static use(plugin: t.PluginEntry, options?: t.Options): typeof Tempo; /** * Register an array of plugins, terms, factories, or tuples. * * @param plugins - An array of plugins, terms, factories, or tuples to register. * @param options - Optional configuration for the plugins. */ static use(plugins: (t.PluginEntry | any)[], options?: t.Options): typeof Tempo; /** * Register multiple plugins or term extensions. * * @param args - A plugin, term, or list of extensions to register. */ static use(...args: any[]): typeof Tempo; static use(...args: any[]): typeof Tempo { const isPluginLike = (arg: any): boolean => Array.isArray(arg) || isFunction(arg) || hasAnyOf(arg, 'install', 'define', sym.$PluginType); const isPlainConfigDict = (arg: any) => isPlainObject(arg) && !ownKeys(arg).some(key => DISCOVERY.has(key as any)) && hasNoneOf(arg, 'name', 'key', 'install', 'define', sym.$PluginType) && ownKeys(arg).length > 0 && ownKeys(arg).every(k => isObject(arg[k])); const isPluginTuple = (entry: any): entry is [any, any] => Array.isArray(entry) && entry.length === 2 && !Array.isArray(entry[0]) && isPluginLike(entry[0]) && isObject(entry[1]) && !isPlainConfigDict(entry[1]) && !isPluginLike(entry[1]); const isOptionsArg = (arg: any) => isObject(arg) && !isPluginLike(arg) && !isPlainConfigDict(arg); const lastArg = args.at(-1); let options = (args.length > 1 && (isOptionsArg(lastArg) || isSymbol(lastArg))) ? args.pop() : undefined; const flattenPlugins = (arr: any[]): any[] => { const result: any[] = []; for (const item of arr) { if (isPluginTuple(item)) { result.push(item); } else if (Array.isArray(item)) { result.push(...flattenPlugins(item)); } else if (item != null) { result.push(item); } } return result; } const items = flattenPlugins(args); if (isEmpty(items)) return this; _lifecycle.extendDepth++; // increment the re-entrant nesting counter try { items.forEach(item => { let plugin = item; let callSiteOptions: any = undefined; // 1. Unpack tuple syntax: [Plugin, Options] if (isPluginTuple(item)) { plugin = item[0]; callSiteOptions = item[1]; } // 2. Resolve Factory Closures or Callable Plugins if (isFunction(plugin)) { if (plugin[sym.$PluginType] || isFunction(plugin.install) || isFunction(plugin.define)) { callSiteOptions = { ...((plugin as any).options ?? {}), ...(callSiteOptions ?? {}) }; } else if (plugin.length === 0 && callSiteOptions !== undefined) { // Potential factory function passed in tuple: [factoryFn, opts] const res = (plugin as any)(callSiteOptions); if (hasAnyOf(res, 'install', 'define', sym.$PluginType)) { plugin = res; callSiteOptions = { ...((res as any).options ?? {}), ...(callSiteOptions ?? {}) }; } } } else if (hasKey(plugin, 'options') && plugin.options) { callSiteOptions = { ...((plugin as any).options ?? {}), ...(callSiteOptions ?? {}) }; } let pluginType = (plugin as any)?.[sym.$PluginType] || (plugin as any)?.type; if (!pluginType && hasKey(plugin, 'define', isFunction) && hasKey(plugin, 'key')) pluginType = 'term'; if (!pluginType && hasKey(plugin, 'install', isFunction)) pluginType = 'plugin'; if (!pluginType && isFunction(plugin)) { // Legacy Standard Plugin registration callback: (Tempo, options, rehydrator) const state = this[$Internal](); const rt = getRuntime(); const installed = state.installed ?? rt.installed; // ScopedSet for sandboxes, global Set for Tempo if (installed.has(plugin)) return; installed.add(plugin); // mark as installed (BEFORE side-effects) registerPlugin(plugin, state); const pluginName = (plugin as any).name; const existingConfigOpts = isText(pluginName) ? (state.config.pluginOptions?.[pluginName] ?? state.config.plugins?.[pluginName]) : undefined; const resolvedOptions = { ...(isObject(existingConfigOpts) ? existingConfigOpts : {}), ...(isObject(options) ? options : {}), ...(isObject(callSiteOptions) ? callSiteOptions : {}) }; if (isText(pluginName) && pluginName !== 'anonymous' && !isEmpty(resolvedOptions)) { state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), [pluginName]: resolvedOptions }; } try { (plugin as any)(this, !isEmpty(resolvedOptions) ? resolvedOptions : options, (val: any) => new this(val)); } catch (e: any) { const msg = (e?.message ?? '').toLowerCase(); if (msg.includes('constructor') || msg.includes('class') || msg.includes('extensible') || msg.includes('read only') || isClass(plugin)) { logWarn(`Misidentified class or frozen sandbox in plugin registration: ${(plugin as any).name}`, state.config, e.stack ?? e); } else { throw e; } } return; } if (isObject(plugin) || isFunction(plugin)) { const assertContract = (isValid: boolean, msg: string) => { if (!isValid) { const config = this[$Internal]().config; if (config.catch) { logError(msg, config); return false; } throw new TypeError(msg); } return true; } switch (pluginType) { case 'plugin': case 'namespace': case 'module': { if (!assertContract(isFunction(plugin.install), `[Tempo] Invalid ${pluginType} object: missing 'install()' method.`)) return; const name = (plugin as any).name; const state = this[$Internal](); const rt = getRuntime(); const installed = state.installed ?? rt.installed; // ScopedSet for sandboxes, global Set for Tempo // Resolve options precedence: existingConfigOpts < options < callSiteOptions const existingConfigOpts = name ? (state.config.pluginOptions?.[name] ?? state.config.plugins?.[name]) : undefined; const resolvedOptions = { ...(isObject(existingConfigOpts) ? existingConfigOpts : {}), ...(isObject(options) ? options : {}), ...(isObject(callSiteOptions) ? callSiteOptions : {}) }; // Automatically synchronize resolved options to state.config.pluginOptions if (name && !isEmpty(resolvedOptions)) { state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), [name]: resolvedOptions } } const hasExplicitOptions = (isObject(callSiteOptions) && !isEmpty(callSiteOptions)) || (isObject(options) && !isEmpty(options)); if (name && installed.has(name)) { if (hasExplicitOptions) { registerPlugin(plugin, state); const installOpts = resolvedOptions; (plugin as any).install.call(this, this, installOpts); } else { logDebug(`Plugin already installed by name: ${name}`, state.config); } return; } if (!name && installed.has(plugin)) return; // identity-based dedup for unnamed plugins if (name) installed.add(name); else installed.add(plugin); registerPlugin(plugin, state); if (plugin.version && isString(name)) { let suffix = ''; if (pluginType === 'plugin') suffix = 'Plugin'; else if (pluginType === 'namespace') suffix = 'Namespace'; else if (pluginType === 'module') suffix = 'Module'; else suffix = 'Plugin'; Tempo.#versions[`${name}${name.endsWith(suffix) ? '' : suffix}`] = plugin.version; } const installOpts = !isEmpty(resolvedOptions) ? resolvedOptions : options; (plugin as any).install.call(this, this, installOpts); break; } case 'term': { if (!assertContract(isFunction(plugin.define), `[Tempo] Invalid term object: missing 'define()' method.`)) return; const config = plugin as TermPlugin; const state = this[$Internal](); const termName = config.scope || config.key; // Resolve options precedence: existingConfigOpts < options < callSiteOptions const existingConfigOpts = termName ? (state.config.pluginOptions?.[termName] ?? state.config.plugins?.[termName]) : undefined; const resolvedOptions = { ...(isObject(existingConfigOpts) ? existingConfigOpts : {}), ...(isObject(options) ? options : {}), ...(isObject(callSiteOptions) ? callSiteOptions : {}) }; // Automatically synchronize resolved options to state.config.pluginOptions if (termName && !isEmpty(resolvedOptions)) { state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), [termName]: resolvedOptions }; } const termConfig: TermPlugin = !isEmpty(resolvedOptions) ? { ...config, options: resolvedOptions } : config; if (Tempo.#termMap.get(config.key) === config) { if (!isEmpty(resolvedOptions)) { Tempo.#termMap.set(config.key, termConfig); if (config.scope) Tempo.#termMap.set(config.scope, termConfig); registerTerm(termConfig, this[$Internal]()); } return; } if (Tempo.#termMap.has(config.key)) { const existing = Tempo.#termMap.get(config.key); const rangesMatch = JSON.stringify(existing?.ranges) === JSON.stringify(config.ranges); if (existing?.scope === config.scope && existing?.description === config.description && rangesMatch) { logDebug(`[Tempo#extend] Duplicate term registration ignored for key: "${config.key}"`, state.config); if (!isEmpty(resolvedOptions)) { Tempo.#termMap.set(config.key, termConfig); if (config.scope) Tempo.#termMap.set(config.scope, termConfig); registerTerm(termConfig, this[$Internal]()); } return; } logError(`[Tempo#extend] Term collision on key: "${config.key}". Registration aborted.`, state.config); return; } if (config.scope && Tempo.#termMap.get(config.scope) === config) { /* continue */ } else if (config.scope && Tempo.#termMap.has(config.scope)) { const existingScope = Tempo.#termMap.get(config.scope); const rangesMatch = JSON.stringify(existingScope?.ranges) === JSON.stringify(config.ranges); if (existingScope?.key === config.key && existingScope?.description === config.description && rangesMatch) { /* continue */ } else { logError(`[Tempo#extend] Term collision on scope: "${config.scope}". Registration aborted.`, state.config); return; } } Tempo.#termMap.set(config.key, termConfig); if (config.scope) Tempo.#termMap.set(config.scope, termConfig); for (const alias of asArray(config.aliases)) if (!Tempo.#termMap.has(alias)) Tempo.#termMap.set(alias, termConfig); if (config.version) { const name = config.scope || config.key; Tempo.#versions[`${name}Term`] = config.version; } registerTerm(termConfig, this[$Internal]()); // 1a. sync with alias engine if (config.scope && config.ranges) { const type = config.scope === 'period' ? 'per' : (config.scope === 'event' ? 'evt' : undefined); if (type) { const aliases: [string, any][] = []; const monthKeys = Enum.keys(Tempo.MONTH); config.ranges.forEach(r => { if (r.key) { let val: string | undefined; if (isDefined(r.hour)) { if (Number.isInteger(r.hour) && r.hour >= 0 && r.hour <= 23) val = `${r.hour}:${pad(r.minute ?? 0)}`; } else if (r.month) { if (Number.isInteger(r.month) && r.month >= 1 && r.month <= 12) val = `${pad(r.day ?? 1)} ${monthKeys[r.month - 1]}`; } if (val) aliases.push([r.key, val]); } }); if (aliases.length > 0) { const state = this[$Internal](); if (type === 'per') this[$setPeriods](state, aliases); else if (type === 'evt') this[$setEvents](state, aliases); } } } break; } default: { // Safe fallback to Discovery Object parsing const discovery = item as any; const opts = this[$setDiscovery](this[$Internal](), discovery); if (!isEmpty(opts)) this[$setConfig](this[$Internal](), opts); // If a plain configuration dictionary was supplied directly in the plugins array (e.g. { ai: { timeout: 1000 } }) const isPlainConfigDict = isPlainObject(discovery) && !ownKeys(discovery).some(key => DISCOVERY.has(key as any)) && hasNoneOf(discovery, 'name', 'key', 'install'); if (isPlainConfigDict) { const state = this[$Internal](); /** @deprecated Supplying plain configuration dictionaries in the plugins array is deprecated. Use 'pluginOptions' instead. */ state.config.plugins = { ...(state.config.plugins ?? {}), ...discovery }; state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), ...discovery }; } if (discovery.plugins && (Array.isArray(discovery.plugins) || isFunction(discovery.plugins) || hasAnyOf(discovery.plugins, 'name', 'key', 'install'))) this.use(discovery.plugins); // only trigger init if we're assigning a new discovery object to a symbol if (ownKeys(item).some(key => DISCOVERY.has(key as any))) { const discoveryArg = (isSymbol(options) ? options : (options as any)?.discovery) ?? sym.$Tempo; const discoverySymbol = isString(discoveryArg) ? Symbol.for(discoveryArg) : discoveryArg; if ((globalThis as Record)[discoverySymbol as symbol] !== item) (globalThis as Record)[discoverySymbol as symbol] = item; this[$setConfig](this[$Internal](), { discovery: discoverySymbol }); } break; } } } }); } finally { _lifecycle.extendDepth--; // decrement the re-entrant nesting counter } if (_lifecycle.extendDepth === 0) { this[$buildGuard](); setPatterns(this[$Internal]()); // rebuild the global patterns } return this; } /** * @deprecated Use Tempo.use(...) instead. */ static extend(discovery: t.Discovery, symbol?: symbol): typeof Tempo; /** * @deprecated Use Tempo.use(...) instead. */ static extend(plugin: TempoPlugin, options?: t.Options): typeof Tempo; /** * @deprecated Use Tempo.use(...) instead. */ static extend(plugins: (TempoPlugin | TermPlugin | any)[], options?: t.Options): typeof Tempo; /** * @deprecated Use Tempo.use(...) instead. */ static extend(...args: any[]): typeof Tempo; /** * @deprecated Use Tempo.use(...) instead. */ static extend(...args: any[]): typeof Tempo { return (this.use as any)(...args); } /** * 🏭 Sandbox Factory Mode * Create a fresh, isolated Tempo subclass with its own configuration and registries. * Returns a new class that inherits from the caller, but maintains independent state. */ /** * 🏭 Sandbox Factory & Scoped Execution Mode * * 1. Factory Mode: Creates a fresh, isolated Tempo subclass with its own configuration and registries. * The returned class implements `Disposable` (`[Symbol.dispose]()`), allowing deterministic cleanup via `using`. * * 2. Scoped Execution Mode: When provided a callback, creates an isolated sandbox, executes the callback, * and automatically disposes the sandbox upon completion (supporting both synchronous and Promise returns). * * @example * ```ts * // Factory mode with `using` * { * using sb = Tempo.create({ discovery: 'test' }); * sb.use(MyPlugin); * } * * // Scoped callback mode * const result = Tempo.create((sb) => { * sb.use(MyPlugin); * return sb('2026-05-10').format(); * }); * ``` */ static create(options?: t.Options): typeof Tempo; static create(fn: (sandbox: typeof Tempo) => R): R; static create(options: t.Options, fn: (sandbox: typeof Tempo) => R): R; static create(arg1?: t.Options | ((sandbox: typeof Tempo) => R), arg2?: (sandbox: typeof Tempo) => R): (typeof Tempo) | R { let options: t.Options = {}; let fn: ((sandbox: typeof Tempo) => R) | undefined; if (isFunction(arg1)) { fn = arg1 as (sandbox: typeof Tempo) => R; } else if (isObject(arg1)) { options = arg1 as t.Options; if (isFunction(arg2)) fn = arg2; } const discovery = options.discovery; const normalizedDiscovery = (isObject(discovery) && !isSymbol(discovery)) || isUndefined(discovery) || (isSymbol(discovery) && !Symbol.keyFor(discovery)) ? Symbol('TempoSandbox') : (isString(discovery) ? Symbol.for(discovery) : discovery) as string | symbol; let disposed = false; const SandboxTempo = class extends (this as any) { static [Symbol.toStringTag] = 'TempoSandbox'; static [$IsBase] = false; static get isDisposed(): boolean { return disposed; } static [Symbol.dispose]() { if (disposed) return; disposed = true; if ((globalThis as any)[normalizedDiscovery as any] === data) delete (globalThis as any)[normalizedDiscovery as any]; ClassStates.delete(SandboxTempo as any); } } let data: any = { options: { ...options, discovery: normalizedDiscovery }, scope: 'sandbox' }; if (isObject(options.discovery) && !isSymbol(options.discovery)) data = { ...data, ...options.discovery as any }; (globalThis as any)[normalizedDiscovery as any] = data; const state = init(options, false, this[$Internal]()); state.config.discovery = normalizedDiscovery as any; ClassStates.set(SandboxTempo as any, state); setPatterns(state); // compile regex patterns for the isolated sandbox state // Apply configuration to the sandbox (SandboxTempo as any)[$setConfig](state, { scope: 'sandbox', discovery: normalizedDiscovery as any, catch: options.catch ?? false }, (SandboxTempo as any)[$setDiscovery](state, data), { ...options, discovery: normalizedDiscovery } ); const pluginsToInstall = options.plugins ?? data?.plugins; if (pluginsToInstall && (Array.isArray(pluginsToInstall) || isFunction(pluginsToInstall) || hasAnyOf(pluginsToInstall, 'name', 'key', 'install', sym.$PluginType, 'type'))) { let toInstall = pluginsToInstall; if (Array.isArray(pluginsToInstall)) { const isPlainConfigDict = (item: any) => isPlainObject(item) && !ownKeys(item).some(key => DISCOVERY.has(key as any)) && hasNoneOf(item, 'name', 'key', 'install', 'define', sym.$PluginType); const remaining: any[] = []; for (const item of pluginsToInstall) { if (isPlainConfigDict(item)) { state.config.plugins = { ...(state.config.plugins ?? {}), ...item }; state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), ...item }; } else { remaining.push(item); } } toInstall = remaining; } if (!isEmpty(toInstall)) (SandboxTempo as any).use(toInstall); } Object.freeze(SandboxTempo); const sb = cast(SandboxTempo); if (fn) { try { const res = fn(sb); if (res && isCallable((res as any).then)) { return Promise.resolve(res).finally(() => { (sb as any)[Symbol.dispose]?.(); }) as R; } (sb as any)[Symbol.dispose]?.(); return res; } catch (err) { (sb as any)[Symbol.dispose]?.(); throw err; } } return sb; } /** * Wait for engine readiness. * @returns The final status ('none' in Community Tempo) */ static async ready(): Promise { return 'none'; } /** * Automatically discovers and loads configuration from `tempo.config.*` * before initializing the engine. */ static async bootstrap(options?: { cwd?: string, configFile?: string }): Promise { const config = await resolveConfig(options); this.init(config || {}); await this.ready(); return this; } /** Reset Tempo to its default, built-in registration state */ @Mutable(isTestEnvironment) static init(options: t.Options = {}): typeof Tempo { if (_lifecycle.initialising) return this; _lifecycle.initialising = true; try { setLogLevel(options.debug ?? Default?.debug ?? LOG.Info); const rt = getRuntime(); const prevCache = rt.state?.cache; const isBase = !!this[$IsBase]; if (isBase) rt.state = undefined; const baseState = isBase ? undefined : Object.getPrototypeOf(this)[$Internal](); const state = init(options, isBase, baseState, prevCache); (state as any)._count = 0; if (isBase) { _global = state; } else { ClassStates.set(this, state); } // 1. Augment the parsing state (non-destructively) const parse = state.parse; parse.pattern ??= new Map(); parse.monthDay = resolveMonthDay(Default.monthDay, enums.MONTH_DAY); parse.planner.layoutOrder = asArray(Default.planner?.layoutOrder ?? (Default as any).layoutOrder); parse.planner.preFilter = Boolean(Default.planner?.preFilter ?? (Default as any).preFilter); parse.pivot ??= Default.pivot as any; parse.mode ??= Default.mode as any; parse.lazy = false; // 2. Establish context and keys const sys = getDateTimeFormat(); const config = state.config; const timeZone = evaluate(config.timeZone, sys.timeZone); const calendar = evaluate(config.calendar, sys.calendar); let discovery = options.discovery ?? Symbol.keyFor($Tempo) as string; const storeKey = options.store || config.store || Symbol.keyFor($Tempo) as string; // Normalize discovery to a symbol if it's an object to prevent leakage into config state if (isObject(discovery) && !isSymbol(discovery)) { const data = discovery; discovery = Symbol.for(`tempo.discovery.${_usrCount++}`); (globalThis as any)[discovery] = data; } const normalizedDiscovery = isString(discovery) ? Symbol.for(discovery) : (discovery as symbol); const userDiscovery = (globalThis as any)[normalizedDiscovery] as Internal.Discovery; // Resolve locale if missing or invalid const currentLocale = options.locale ?? config.locale; const locale = currentLocale ?? Tempo.#locale(); if (!hasOwn(config, 'get')) { Object.defineProperty(config, 'get', { value: function (key: string) { return this[key] }, enumerable: false, writable: true, configurable: true }); } _usrCount = 0; // reset user-key counter for (const key of Object.keys(Token)) // purge user-allocated Tokens if (key.startsWith('usr.')) // only remove 'usr.' prefixed keys delete Token[key]; Tempo.#termMap.clear(); // clear term lookup map registryReset(); // purge formats and numbers // 3. Apply configuration via unified setters (non-destructive merge) this[$setConfig](state, { calendar, timeZone, locale, discovery: normalizedDiscovery, registry: { formats: config.registry?.formats ?? config.formats ?? enums.FORMAT, locales: config.registry?.locales ?? config.locales ?? proxify(STATE.LOCALE, true, true) }, scope: 'global', catch: options.catch ?? config.catch ?? false }, { store: storeKey, discovery: normalizedDiscovery, scope: 'global' }, this.readStore(storeKey), // allow for storage-values to overwrite this[$setDiscovery](state, state.pluginsDb as any), // persistent library extensions this[$setDiscovery](state, userDiscovery), // user Discovery (Configuration bootstrapping) options, // explicit options from the call ) setLogLevel(state.config.debug ?? options.debug ?? Default?.debug ?? LOG.Info); const isInstallable = (item: any) => Array.isArray(item) || isFunction(item) || hasAnyOf(item, 'name', 'key', 'install', sym.$PluginType, 'type'); const isPlainConfigDict = (item: any) => isPlainObject(item) && !ownKeys(item).some(key => DISCOVERY.has(key as any)) && hasNoneOf(item, 'name', 'key', 'install', 'define', sym.$PluginType); const extractConfigDicts = (plugins: any): any => { if (!Array.isArray(plugins)) return plugins; const remaining: any[] = []; for (const item of plugins) { if (isPlainConfigDict(item)) { state.config.plugins = { ...(state.config.plugins ?? {}), ...item }; state.config.pluginOptions = { ...(state.config.pluginOptions ?? {}), ...item }; } else { remaining.push(item); } } return remaining; }; if (userDiscovery?.plugins && isInstallable(userDiscovery.plugins)) this.use(extractConfigDicts(userDiscovery.plugins)); if (options.plugins && isInstallable(options.plugins)) this.use(extractConfigDicts(options.plugins)); if (Context.type === CONTEXT.Browser || state.config.debug === LOG.Debug) logDebug('Tempo:', this.config, state.config); setPatterns(state); // rebuild the global patterns (Master Guard etc) _lifecycle.ready = true; return this; } catch (err) { _lifecycle.ready = false; throw err; } finally { _lifecycle.initialising = false; _lifecycle.bootstrap = false; } } /** explicitly enable/disable "catch" mode for internal errors */ static setCatchMode(catchMode: boolean): typeof Tempo { this[$setConfig](this[$Internal](), { catch: catchMode }); return this; } /** @internal unload a module by name (experimental internal use only) */ static unloadModule(name: string): typeof Tempo { const rt = getRuntime(); delete rt.modules[name]; rt.installed.delete(name); const baseName = name.endsWith('Module') ? name.slice(0, -6) : name; rt.installed.delete(baseName); return this; } /** @internal check if a module is loaded */ static hasModule(name: string): boolean { const rt = getRuntime(); const mod = name === 'term' ? 'TermsModule' : name; return isDefined(rt.modules[mod]) || rt.installed.has(mod) || this[$Internal]().pluginsDb.plugins.some(p => p.name === mod); } /** @internal Reads options from persistent storage (e.g., localStorage). */ static readStore(key: string = this[$Internal]().config.store || (Symbol.keyFor($Tempo) as string)) { return getStorage(key, {}); } /** @internal Writes configuration into persistent storage. */ static writeStore(config?: t.Options, key: string = this[$Internal]().config.store || (Symbol.keyFor($Tempo) as string)) { return setStorage(key, config); } /** @internal lookup or registers a new `Symbol` for a given key. */ static getSymbol(key?: string | symbol) { if (isUndefined(key)) { const usr = `usr.${++_usrCount}`; // allocate a prefixed 'user' key return Token[usr] = Symbol(usr); // add to Symbol register } if (isSymbol(key)) return key; if (isString(key) && key.includes('.')) { const description = key.split('.').at(-1)!; // use last segment as description return Token[key as keyof typeof Token] ??= Symbol(description); } return Token[key as keyof typeof Token] ?? Symbol.for(`$Tempo.${key}`); } /** translates {layout} into an anchored, case-insensitive RegExp. */ static regexp(layout: string | RegExp, snippet?: Snippet) { const state = this[$Internal](); if (!state.patternCompiler || state.patternCompiler.state !== state) state.patternCompiler = new PatternCompiler({ state }); return state.patternCompiler.compileRegExp(layout, snippet as any); } /** Compares two `Tempo` instances or date-time values. Returns -1 (earlier), 0 (equal), or 1 (later). */ static compare(tempo1?: t.DateTime | t.Options, tempo2?: t.DateTime | t.Options) { const host = hasKey(this, $Identity) ? this : Tempo; const one = host.isTempo(tempo1) ? tempo1 : new host(tempo1 as t.DateTime), two = host.isTempo(tempo2) ? tempo2 : new host(tempo2 as t.DateTime); return Number((one.nano > two.nano) || -(one.nano < two.nano)) + 0; } /** global Tempo configuration */ static get config() { const state = this[$Internal](); // Create an object that only contains CONFIG-specific defaults as its prototype, // preventing parse-related keys (like planner, monthDay) from leaking into the config state. const configDefaults = Object.fromEntries(Object.entries(Default).filter(([key]) => Enum.has(enums.CONFIG, key))); const out = Object.create(configDefaults); const descriptors = omit(Object.getOwnPropertyDescriptors(state.config), 'value', 'anchor', 'result'); Object.defineProperties(out, descriptors); Object.defineProperty(out, 'toJSON', // bare-bones: only show global overrides { value: () => Object.fromEntries(Object.entries(out)),// proxify sees own toJSON, skips allObject enumerable: false, configurable: true }); Object.defineProperty(out, sym.$Inspect, { value: () => Object.fromEntries(Object.entries(out)), enumerable: false, configurable: true }); return dynamicProxy(proxify(out)) as t.Internal.Config; } /** global discovery configuration */ static get discovery() { const discovery = this.config.discovery; const sym = isString(discovery) ? Symbol.for(discovery) : discovery; return Tempo.#getConfig(sym as symbol); } /** * Retrieves the merged options for this Tempo class, combining defaults, storage, discovery, and config. * @returns The complete options object for Tempo initialization */ static get options() { const keyFor = this.config.store ?? Symbol.keyFor($Tempo) as string; const storage = proxify(Object.assign({ key: keyFor, scope: 'storage' }, omit(Tempo.readStore(keyFor), 'value'))); return Object.assign({}, this.default, storage, this.discovery, this.config); } /** Creates a new `Tempo` instance. */ static from(options?: t.Options): Tempo; static from(tempo: t.DateTime | undefined, options?: t.Options): Tempo; static from(tempo?: t.DateTime | t.Options, options?: t.Options) { return new this(tempo as NonNullable, options); } /** * Gets the current timestamp in the specified unit. * @param unit - The time unit ('ss', 'ms', 'us'). * @returns The current timestamp as a number (for 'ss', 'ms', 'us') */ static now(unit: Exclude): number; /** * Gets the current timestamp in the specified unit. * @param unit - The time unit ('ss', 'ms', 'us', 'ns'). Defaults to 'ns' (nanoseconds). * @returns The current timestamp as a number (for 'ss', 'ms', 'us') or bigint (for 'ns') */ static now(unit?: Extract): bigint; static now(unit?: Tempo.TimeStamp): number | bigint { const inst = instant(); switch (unit) { case 'ss': return Math.trunc(inst.epochMilliseconds / 1_000); case 'ms': return inst.epochMilliseconds; case 'us': return Number(inst.epochNanoseconds / 1_000n); case 'ns': default: return inst.epochNanoseconds; } } static #getEpoch(inst: { epochMilliseconds: number, epochNanoseconds: bigint }): Readonly<{ ss: number; ms: number; us: number; ns: bigint }> { return secure({ /** seconds since epoch */ ss: Math.trunc(inst.epochMilliseconds / 1_000), /** milliseconds since epoch */ ms: inst.epochMilliseconds, /** microseconds since epoch */ us: Number(inst.epochNanoseconds / 1_000n), /** nanoseconds since epoch */ ns: inst.epochNanoseconds, }); } /** static units since Unix epoch */ static get epoch(): Readonly<{ ss: number; ms: number; us: number; ns: bigint }> { return Tempo.#getEpoch(instant()); } /** get the current system Instant */ static get instant() { return Temporal.Instant.fromEpochNanoseconds(Tempo.now()) } /** static Tempo.terms (registry) */ static get terms(): Secure[]> & Readonly>> { const list = Tempo.#terms.map(({ define, resolve, ...rest }) => { const item: any = { ...rest }; const aliasesSet = new Set(); for (const a of asArray(item.aliases)) if (a !== item.key) aliasesSet.add(a); if (item.scope && item.scope !== item.key) aliasesSet.add(item.scope); if (aliasesSet.size > 0) item.aliases = Array.from(aliasesSet); return item; }); // treats `Tempo.terms` as array-like and indexable by key, scope, or alias. return cast[]> & Readonly>>>( indexedArray(list, key => list.find((t: any) => t.key === key || t.scope === key || asArray(t.aliases).includes(key))) ); } /** static Tempo.registry */ static get registry() { return this.config.registry; } /** Internationalization namespace (info, locale, and cultural metadata) for the global locale */ static get intl(): TempoIntlNamespace { return getIntlNamespace(Tempo.#locale(this.config.locale), this.config.timeZone); } /** static Tempo properties getter */ static get properties(): Secure { return secure(getAccessors(Tempo) .filter(acc => getType(acc) !== 'Symbol') as string[]); // omit any Symbol properties } /** Tempo initial default settings */ static get default(): Readonly { return cast>(Object.freeze({ ...Default, scope: 'default', timeZone: Default.timeZone || enums.TIMEZONE.utc })); } /** * configuration governing the static 'rules' used when parsing t.DateTime argument */ static get parse() { const parse = this[$Internal]().parse; const planner = { /** Layout order for parsing */ layoutOrder: [...(parse.planner?.layoutOrder ?? [])], /** Pre-filter flag for parsing */ preFilter: parse.planner?.preFilter ?? false } return secure({ ...omit({ ...parse }, 'token', 'planner'), // spread primitives like {pivot} /** Snippet registry for parsing shortcuts */ snippet: { ...parse.snippet }, // spread nested objects /** Layout registry for date/time patterns */ layout: { ...parse.layout }, /** Event registry for special dates */ event: { ...parse.event }, /** Period registry for time-of-day aliases */ period: { ...parse.period }, /** Ignore list for noise words */ ignore: { ...parse.ignore }, /** Month-day disambiguation settings */ monthDay: clone(parse.monthDay), /** Planner configuration for layout selection */ planner, }); } /** iterate over Tempo properties */ static [Symbol.iterator]() { return Tempo.properties[Symbol.iterator](); // static Iterator over array of 'getters' } /** release global config and reset library to defaults */ static [Symbol.dispose]() { Tempo.init() } /** static Tempo.ignores (registry) */ static get ignores(): Secure { return secure(ownKeys(this[$Internal]().parse.ignore, true) as string[]); } /** allow instanceof to work across module boundaries via the local brand symbol */ /** @internal */ static [$Identity] = true; /** * Checks if an instance is a Tempo object using the brand symbol. * Allows instanceof to work across module boundaries. * @param instance - The instance to check * @returns True if the instance is a Tempo object */ static [Symbol.hasInstance](instance: any): boolean { return hasKey(instance, $Identity); } /** check if a supplied variable is a valid Tempo instance */ static isTempo(instance?: any): instance is Tempo { return instance instanceof Tempo; } static { // Static initialization block to sequence the bootstrap phase // Define the reactive register hook getRuntime().setHook($Register, (plugin: TempoPlugin | TempoPlugin[]) => { if (!Tempo.isExtending) Tempo.use(plugin) }); onRegistryReset(() => { const state = (Tempo as any)[sym.$Internal](); // 🏛️ Clear the root engine to avoid stale state and collision warnings during hard resets if (state.aliasEngine && state.aliasEngine.depth === 0) { state.aliasEngine.clear('evt'); state.aliasEngine.clear('per'); } (Tempo as any)[$buildGuard](); (Tempo as any)[$setEvents](state, undefined, false); (Tempo as any)[$setPeriods](state, undefined, false); setPatterns(state); }); const syncConfig = resolveConfigSync(); Tempo.init(syncConfig || {}); // synchronously initialize the library getRuntime().logger = logTempo; } /** constructor tempo */ #tempo?: t.DateTime; /** constructor options */ #options = {} as t.Options; /** instantiation Temporal Instant */ #instant?: Temporal.Instant; /** underlying Temporal ZonedDateTime */ #zdt!: Temporal.ZonedDateTime; /** memoized instance properties */ #memo: { tz?: string; cal?: string; sphere?: t.COMPASS | undefined; geo?: t.GeoConfig | undefined; fmt?: Record } = {}; /** indicator that the instance failed to parse */ #errored = false; /** temporary anchor used during parsing */ #anchor: Temporal.ZonedDateTime | undefined; /** mapping of terms to their resolved values */ #term?: any; /** a collection of parse rule-matches */ #matches: Internal.MatchResult[] | undefined; /** current parsing depth to manage state isolation */ #parseDepth = 0; /** current mutation depth to manage infinite recursion */#mutateDepth = 0; /** instance values to complement static values */ #local = { /** instance configuration */ config: cast({ [$LogConfig]: true }), /** instance parse rules (only populated if provided) */ parse: { result: [] as Internal.MatchResult[] } as Internal.Parse } as Internal.State; /** @internal internal key for signaling pre-errored state in constructor */ static [$errored] = $errored; /** @internal */ static [TermError](config: Internal.Config, term: string): void { const hint = Tempo.#terms.length === 0 ? ". (No term plugins are registered—did you forget to call Tempo.use(TermsModule)?)" : ""; const msg = `Unknown Term identifier: ${term}${hint}`; logError(msg, config); } /** * @internal Internal access to instance private state. * This surface is not part of the public contract and is subject to change. */ [$Internal]() { const self: Tempo = unwrap(this); const out = { get zdt() { return self.#zdt }, set zdt(val: any) { self.#zdt = val }, get errored() { return self.#errored }, set errored(val: any) { self.#errored = val }, get parseDepth() { return self.#parseDepth }, set parseDepth(val: any) { self.#parseDepth = val }, get mutateDepth() { return self.#mutateDepth }, set mutateDepth(val: any) { self.#mutateDepth = val }, get matches() { return self.#matches }, set matches(val: any) { self.#matches = val }, get anchor() { return self.#anchor }, set anchor(val: any) { self.#anchor = val }, get options() { return self.#options }, get tempo() { return self.#tempo }, get now() { return self.#now }, config: self.#local.config, parse: self.#local.parse, userProvidedKeys: self.#local.userProvidedKeys, pluginsDb: self.#local.pluginsDb, aliasEngine: self.#local.aliasEngine, _id: (self.#local as any)._id, tempoInstance: self, CONFIG: enums.CONFIG, PARSE: enums.PARSE, ZONED_DATE_TIME: enums.ZONED_DATE_TIME } return Object.setPrototypeOf(out, self.#local); } /** allow for auto-convert of Tempo to BigInt, Number or String */ [Symbol.toPrimitive](hint?: 'string' | 'number' | 'default') { switch (hint) { case 'string': return this.toString(); // iso 8601 string case 'number': return this.epoch.ms; // Unix epoch (milliseconds) default: return this.nano; // Unix epoch (nanoseconds) } } /** iterate over instance formats */ [Symbol.iterator]() { return ownEntries(this.fmt, true)[Symbol.iterator](); // instance Iterator over tuple of FormatType[] } /** * Instantiates a new `Tempo` object with configuration only. * * @param options - Configuration options for this specific instance. */ constructor(options?: t.Options); /** * Instantiates a new `Tempo` object with a value. * * @param tempo - The date-time value to parse. * @param options - Configuration options for this specific instance. */ constructor(tempo: t.DateTime, options?: t.Options); constructor(tempo?: t.DateTime | t.Options, options: t.Options = {}) { [this.#tempo, this.#options] = this.#swap(tempo, options);// swap arguments around if (isZonedDateTime(this.#tempo)) this.#zdt = this.#tempo; this.#setLocal(this.#options); // parse local options this.#options = this.#local.options ?? {}; this.#anchor = evaluate(this.#options.anchor); if (!this.#zdt && isObject(this.#tempo) && isDurationLike(this.#tempo)) { this.#zdt = this.#resolveAnchorBasis(this.#anchor) // relative shorthand for "anchor (or now) plus duration" .add(this.#tempo as Temporal.DurationLike); } const input = String(this.#tempo ?? ''); const guard = this.#local.parse.guard || (this.constructor as typeof Tempo)[$guard]; const { mode } = this.#local.parse; // 🏛️ Initialization Strategy ('auto' | 'strict' | 'defer') if (mode === enums.MODE.Defer) this.#local.parse.lazy = true; else if (mode === enums.MODE.Strict) this.#local.parse.lazy = false; else if (isString(this.#tempo) && isText(input) && guard.test(trimAll(input))) this.#local.parse.lazy = true; // auto-switch to lazy-mode for valid strings // 🧬 Unified State Hand-off (from clone / mutate) const handoff = (this.#options as any)[$Internal]; if (isObject(handoff)) { this.#errored = handoff.errored ?? false; this.#mutateDepth = handoff.mutateDepth ?? 0; this.#parseDepth = 0; this.#matches = handoff.matches; } else if ((this.#options as any)[$errored]) { this.#errored = true; } if (!this.#local.parse.lazy) this.#resolve(); // attempt to interpret immediately (if not lazy) return Object.freeze(this) as this; } /** explicitly enable/disable "catch" mode for this instance */ setCatchMode(catchMode: boolean): this { setProperty(this.#local.config, 'catch', catchMode); return this; } /** @internal unload a module by name (experimental internal use only) */ unloadModule(name: string): this { (this.constructor as typeof Tempo).unloadModule(name); return this; } /** @internal check if a module is loaded */ hasModule(name: string): boolean { return (this.constructor as typeof Tempo).hasModule(name); } /** returns a [timezone, calendar] tuple derived from the underlying date-time. */ #temporalIds(): [string, string] { if (!this.#memo.tz || !this.#memo.cal) { [this.#memo.tz, this.#memo.cal] = getTemporalIds(this.toDateTime()); } return [this.#memo.tz, this.#memo.cal]; } /** Resolve the instance to a Temporal.ZonedDateTime (with optional callback) */ #resolve(cb?: (zdt: Temporal.ZonedDateTime) => T): T | Temporal.ZonedDateTime { if (!this.#zdt) { try { const skip = [this.#local.parse.format, this.#local.parse.term, this.#local.parse.result] .filter(isDefined); this.#zdt = this.#parse(this.#tempo as t.DateTime, this.#anchor); if (isUndefined(this.#zdt)) { this.#errored = true; const msg = `Tempo parse returned undefined for: ${String(this.#tempo)}`; logError(msg, this.#local.config); this.#zdt = this.#now.toZonedDateTimeISO('UTC'); } secure(this.#local.config); secure(this.#local.parse, new WeakSet(skip)); } catch (err) { this.#errored = true; // mark as errored const msg = `Cannot create Tempo: ${(err as Error).message}\n${(err as Error).stack}`; if (this.#local.config.catch === true) { logError(msg, this.#local.config); // log as error if in catch-mode this.#zdt = this.#now.toZonedDateTimeISO('UTC'); } else { logError((err as Error), this.#local.config, msg); // log as error then re-throw throw err; } } } const zdt = isZonedDateTime(this.#zdt) ? this.#zdt : this.#now.toZonedDateTimeISO('UTC'); return cb?.(zdt) ?? zdt; } #setLazy(target: any, name: PropertyKey | undefined, define: (keyOnly: boolean) => any, isKeyOnly = false) { if (isDefined(name) && isDefined(define)) { const desc = Object.getOwnPropertyDescriptor(target, name); if (desc) return ('value' in desc) ? () => desc.value : desc.get; let guard = false; let memo: any; let set = false; const get = () => { if (guard) return undefined; // recursion guard guard = true; try { if (!set) { try { memo = define.call(this, isKeyOnly); // evaluate the property } catch (e: any) { const msg = (e?.message ?? '').toLowerCase(); if (msg.includes('constructor') || msg.includes('class') || (e instanceof TypeError) || isClass(define)) { logWarn(`Misidentified class in delegator evaluate: ${String(define)}`, this.#local.config, e.stack ?? e); memo = define; } else { throw e; } } set = true; // Promote to own property on target for subsequent calls (memoization) if extensible if (Reflect.isExtensible(target)) Object.defineProperty(target, name, { value: memo, enumerable: true, configurable: true, writable: false }); } return memo; } finally { guard = false; } } // shadowing chain (only if extensible) if (Reflect.isExtensible(target)) Object.defineProperty(target, name, { get, enumerable: true, configurable: true }); return get; // return getter closure } return undefined; } /** create a Proxy-based delegator that registers lazy properties on-demand */ #setDelegator(host: Internal.DelegatorHost) { const target = Object.create(null); const proxy = delegate(target, (key) => { if (key === $Discover) return this.#discover(host, target); if (!isString(key)) return; // discovery phase switch (host) { case 'fmt': { if (!ensureModule(this, 'FormatModule')) return undefined; const allFormats = Object.assign({}, enums.FORMAT, this.#local.config.registry?.formats); if (isDefined(allFormats[key])) return this.#setLazy(target, key, () => this.format(key as t.Format))?.(); return undefined; } case 'term': { if (!ensureModule(this, 'TermsModule')) return undefined; const term = Tempo.#termMap.get(key); if (term) { const isKeyOnly = key === term.key ? Boolean(term.scope) : key !== term.scope; const define = (keyOnly: boolean) => { try { const result = term.define.call(this, keyOnly); const res = Array.isArray(result) ? getTermRange(this, result, keyOnly) : result; return isObject(res) ? secure(res) : res; } catch (err: unknown) { const error = asError(err); if (error.message.includes('Class constructor')) { logWarn(`Misidentified class in term definition: ${key}`, this.#local.config, error.stack ?? error); } else { throw error; } } return undefined; } return this.#setLazy(target, key, define, isKeyOnly)?.(); } return undefined; } default: { const _exhaustive: never = host; return undefined; } } }, true); // Eager support during construction if (!this.#local.parse.lazy) this.#discover(host, target); return proxy; } #discover(host: Internal.DelegatorHost, target: any) { if (!_lifecycle.ready) return; switch (host) { case 'fmt': { if (!ensureModule(this, 'FormatModule')) return; const allFormats = Object.assign({}, enums.FORMAT, this.#local.config.registry?.formats); ownKeys(allFormats).forEach(key => { if (isString(key)) this.#setLazy(target, key, () => this.format(key as t.Format)); }); break; } case 'term': { if (!ensureModule(this, 'TermsModule')) return; Tempo.#terms.forEach(term => { const define = (keyOnly: boolean, anchor?: any, alias?: string) => { try { const result = term.define ? term.define.call(this, keyOnly, anchor, alias) : undefined; const res = Array.isArray(result) ? getTermRange(this, result, keyOnly, anchor) : result; return isObject(res) ? secure(res) : res; } catch (err: unknown) { const error = asError(err); if (error.message.includes('Class constructor')) { logWarn(`Misidentified class in term discovery: ${term.key}`, this.#local.config, error.stack ?? error); } else { throw error; } } return undefined; } // If the term has a scope, register key as shortform (isKeyOnly=true → returns key string) // and scope as longform (isKeyOnly=false → returns full Range with label). // If the term has NO scope, the key IS the only accessor — register it as longform // (isKeyOnly=false) so format() can access res.label on the returned Range. this.#setLazy(target, term.key, (isKey: boolean) => define(isKey, this.toDateTime(), term.key), !!term.scope); if (term.scope) this.#setLazy(target, term.scope, (isKey: boolean) => define(isKey, this.toDateTime(), term.scope), false); for (const alias of asArray(term.aliases)) { if (alias !== term.scope) this.#setLazy(target, alias, (isKey: boolean) => define(isKey, this.toDateTime(), alias), true); } }); break; } default: { const _exhaustive: never = host; break; } } } /** 4-digit year (e.g., 2024) */ get yy() { return this.toDateTime().year } /** Era string for the calendar (e.g., 'ce', 'bce') */ get era() { return this.toDateTime().era; } /** Year within the era (positive integer) */ get eon() { return this.toDateTime().eraYear; } /** * Year within the era (positive integer). * @deprecated Use `eon` (Tempo canonical) or `zdt.eraYear` instead. To be removed in v5.0.0. */ get eraYear() { return this.toDateTime().eraYear; } /** @internal 4-digit ISO week-numbering year */ get yw() { return getISOWeekOfYear(this.toDateTime()).yearOfWeek; } /** 4-digit ISO week-numbering year */ get yow() { return getISOWeekOfYear(this.toDateTime()).yearOfWeek; } /** @internal ISO week number of the year */ get wy() { return getISOWeekOfYear(this.toDateTime()).weekOfYear as t.wy; } /** ISO week number of the year */ get woy() { return getISOWeekOfYear(this.toDateTime()).weekOfYear as t.wy; } /** * ISO week number of the year. * @deprecated Use `woy` (Tempo canonical) or `zdt.weekOfYear` instead. To be removed in v5.0.0. */ get ww() { return getISOWeekOfYear(this.toDateTime()).weekOfYear as t.wy; } /** Month number: Jan=1, Dec=12 */ get mm() { return this.toDateTime().month as t.mm } /** Day of the month (1-31) */ get dd() { return this.toDateTime().day as t.dd } /** * Day of the month (1-31). * @deprecated Use `dd` (Tempo canonical) or `zdt.day` instead. To be removed in v5.0.0. */ get day() { return this.toDateTime().day as t.dd } /** Internationalization namespace (info, locale, and cultural metadata) */ get intl(): TempoIntlNamespace { return getIntlNamespace(this.locale, this.tz); } /** Hour of the day (0-23) */ get hh() { return this.toDateTime().hour as t.hh } /** Minutes of the hour (0-59) */ get mi() { return this.toDateTime().minute as t.mi } /** Seconds of the minute (0-59) */ get ss() { return this.toDateTime().second as t.ss } /** Milliseconds of the second (0-999) */ get ms() { return this.toDateTime().millisecond as t.ms } /** Microseconds of the millisecond (0-999) */ get us() { return this.toDateTime().microsecond as t.us } /** Nanoseconds of the microsecond (0-999) */ get ns() { return this.toDateTime().nanosecond as t.ns } /** Fractional seconds (e.g., 0.123456789) */ get ff() { return +(`0.${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`) } /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return this.#temporalIds()[0] } /** Temporal Calendar ID (e.g., 'iso8601' | 'gregory') */ get cal() { return this.#temporalIds()[1] } /** Resolved BCP 47 locale (e.g., 'en-US') */ get locale(): string { return Tempo.#locale(this.#local.config.locale ?? (this as any)[$Internal]().config.locale) } /** Resolved geographic coordinates object ({ latitude, longitude, ... }) */ get geo(): Readonly | undefined { if (hasOwn(this.#memo, 'geo')) return this.#memo.geo; const res = this.#local.config.geo ?? (this as any)[$Internal]().config.geo ?? getStashedGeo(); return (this.#memo.geo = res ? Object.freeze({ ...res }) : undefined); } /** Resolved hemisphere ('north' | 'south' | undefined) */get sphere(): t.COMPASS | undefined { if (hasOwn(this.#memo, 'sphere')) return this.#memo.sphere; const globalTz = (this as any)[$Internal]().config.timeZone; const hasInstanceTzOverride = isDefined(this.tz) && String(this.tz).toLowerCase() !== 'utc' && (isUndefined(globalTz) || String(this.tz).toLowerCase() !== String(globalTz).toLowerCase()); const lat = this.geo?.latitude; const geoSphere = this.geo?.sphere; const res = evaluate( hasOwn(this.#local.options, 'sphere') ? this.#local.options?.sphere : undefined, geoSphere, isNumber(lat) ? (lat > 0.001 ? 'north' : (lat < -0.001 ? 'south' : 'equator')) : undefined, hasInstanceTzOverride ? () => getHemisphere(String(this.tz)) : undefined, hasOwn(this.#local.config, 'sphere') ? this.#local.config.sphere : undefined, () => (isDefined(this.tz) && String(this.tz).toLowerCase() !== 'utc' ? getHemisphere(String(this.tz)) : undefined), (this as any)[$Internal]().config.sphere, Default.sphere ); return (this.#memo.sphere = res as t.COMPASS | undefined); } /** Unix timestamp (defaults to milliseconds) */ get ts() { return this.epoch[(this.#local.config.timeStamp ?? 'ms') as t.TimeStamp] } /** Short month name (e.g., 'Jan') */ get mmm() { return Tempo.MONTH.keyOf(this.toDateTime().month as t.Month) } /** Full month name (e.g., 'January') */ get mon() { return Tempo.MONTHS.keyOf(this.toDateTime().month as t.Month) } /** Short weekday name (e.g., 'Mon') */ get www() { return Tempo.WEEKDAY.keyOf(this.toDateTime().dayOfWeek as t.Weekday) } /** Full weekday name (e.g., 'Monday') */ get wkd() { return Tempo.WEEKDAYS.keyOf(this.toDateTime().dayOfWeek as t.Weekday) } /** iso weekday number: Mon=1, Sun=7 */ get dow() { return this.toDateTime().dayOfWeek as t.Weekday } /** Day of year: 1-366 */ get doy() { return this.toDateTime().dayOfYear } /** Total days in the current month, using the instance calendar. */ get dim() { return this.toDateTime().daysInMonth } /** Integer Julian day number for the local ISO calendar date, ignoring time of day (1970-01-01 = 2440588). */ get jj() { const pd = this.toDateTime().toPlainDate().withCalendar('iso8601'); return 2440588 + Math.floor(pd.since(Temporal.PlainDate.from('1970-01-01'), { largestUnit: 'day' }).days); } /** Nanoseconds since Unix epoch (BigInt) */ get nano() { return this.toDateTime().epochNanoseconds } /** Standard ISO 8601 string in UTC */ get iso() { return this.toDateTime().toInstant().toString() } /** `true` if the underlying date-time is valid. */ get isValid() { return this.#resolve(zdt => !this.#errored && isZonedDateTime(zdt)); } /** list of registered terms and their available range keys */ get terms(): Readonly> { const base: Record = {}; Tempo.terms.forEach(term => { const source = (term as any).ranges || (term as any).groups || []; // check both ranges and groups const list = Array.isArray(source) ? source : Object.values(source).flat(Infinity) as any[]; const ranges = [...new Set(list.map(r => r.key).filter(isString))]; // collect unique range keys base[term.key] = ranges; }); return delegate(base, (key) => { if (!isString(key)) return undefined; const term = Tempo.terms.find((t: any) => t.key === key || t.scope === key || asArray(t.aliases).includes(key)); return term ? base[term.key] : undefined; }); } /** current range key for every registered term */ get ranges(): Readonly> { const base: Record = {}; Tempo.terms.forEach(term => { const val = (this as Tempo).term[term.key]; // access the term-delegate (forces evaluation) if (isString(val)) base[term.key] = val; }); return delegate(base, (key) => { if (!isString(key)) return undefined; const term = Tempo.terms.find((t: any) => t.key === key || t.scope === key || asArray(t.aliases).includes(key)); return term ? base[term.key] : undefined; }); } /** current Tempo configuration */ get config(): Readonly { const global = (this as any)[$Internal]().config; const out = markConfig(Object.create(global)); Object.entries(this.#local.config).forEach(([k, v]) => setProperty(out, k, v)); Object.defineProperty(out, 'sphere', { get: () => this.sphere, enumerable: true, configurable: true }); Object.defineProperty(out, 'toJSON', { value: () => Object.fromEntries( // bare-bones: only show local overrides Object.entries(out)), // proxify sees own toJSON, skips allObject enumerable: false, configurable: true }); return dynamicProxy(proxify(out)) as Readonly; } /** Instance-specific parse rules (merged with global) */ get parse(): Internal.Parse { const self: Tempo = unwrap(this); self.#resolve(); // Return a shadowed view so we can safely inject matches without breaking the freeze on the original state const out = Object.create(self.#local.parse); // Explicitly surface key debug properties as "own" properties for better visibility in consoles/REPLs const keys = ['anchor', 'isAnchored', 'mode', 'lazy', 'pivot'] as const; for (const key of keys) { if (self.#local.parse[key] !== undefined) Object.defineProperty(out, key, { value: self.#local.parse[key], enumerable: true, configurable: true }); } // Always surface the result (prioritizing current operation matches) Object.defineProperty(out, 'result', { value: self.#matches ?? self.#local.parse.result ?? [], enumerable: true, configurable: true }); return out as t.Internal.Parse; } /** returns the underlying Temporal.ZonedDateTime */ get zdt(): Temporal.ZonedDateTime { return this.toDateTime(); } /** Keyed results for all resolved terms */ get term(): TempoTermRegistry { return this.#term ??= this.#setDelegator('term'); } /** Formatted results for all pre-defined format codes */ get fmt(): Record { return this.#memo.fmt ??= this.#setDelegator('fmt'); } /** units since epoch for this date-time instance */ get epoch() { return Tempo.#getEpoch(this.toDateTime()); } /** * @Immutable class decorators wrap the class but leave internal lexical bindings pointing to the original, undecorated class. * To ensure new instances returned by instance methods are properly frozen, * we must instantiate internally from the decorated wrapper (which is bound to `this.constructor`) * rather than using `new Tempo(..)`. */ /** @internal */ get #Tempo() { return this.constructor as typeof Tempo; } /** @internal */ get #now(): Temporal.Instant { return this.#instant ??= instant(); } /** apply a custom format. */ format(fmt?: any, options?: any): string { return this.#resolve(() => interpret(this, 'FormatModule', () => `{${String(fmt)}}`, false, fmt, options)) as string; } /** time duration until another date-time */ until(dateTimeOrOpts?: t.DateTime | t.Options, opts?: t.Options): t.Duration; until(unit: t.Unit, opts?: t.Options): number; until(dateTimeOrOpts: t.DateTime | t.Options, unit: t.Unit): number; until(arg0?: any, arg1?: any): any { return this.#resolve(() => interpret(this, 'DurationModule', undefined, false, 'until', arg0, arg1) ?? this); } /** time elapsed since another date-time */ since(dateTimeOrOpts?: t.DateTime | t.Options, opts?: t.Options): t.Duration; since(unit: t.Unit, opts?: t.Options): number; since(dateTimeOrOpts: t.DateTime | t.Options, unit: t.Unit): number; since(arg0?: any, arg1?: any): any { return this.#resolve(() => interpret(this, 'DurationModule', undefined, false, 'since', arg0, arg1) ?? this); } /** returns a new `Tempo` with specific duration added. */add(tempo?: t.MutateAdd, options?: t.Options): Tempo { return this.#resolve(() => interpret(this, 'MutateModule', 'add', false, tempo, options) ?? this); } /** @hidden */ plus(tempo?: t.MutateAdd, options?: t.Options): Tempo { return this.add(tempo, options); } /** returns a new `Tempo` with specific duration subtracted. */subtract(tempo?: t.MutateAdd, options?: t.Options): Tempo { return this.#resolve(() => interpret(this, 'MutateModule', 'subtract', false, tempo, options) ?? this); } /** @hidden */ sub(tempo?: t.MutateAdd, options?: t.Options): Tempo { return this.subtract(tempo, options); } /** @hidden */ minus(tempo?: t.MutateAdd, options?: t.Options): Tempo { return this.subtract(tempo, options); } /** returns a new `Tempo` with specific offsets. */ set(tempo?: t.MutateSet, options?: t.Options): Tempo { return this.#resolve(() => interpret(this, 'MutateModule', 'set', false, tempo, options) ?? this); } /** * Returns a clone of the current `Tempo` instance. * @deprecated Tempo instances are immutable. Use `Tempo.from(instance)` instead. */ clone(): Tempo { return this.#Tempo.from(this, this.config); } /** returns the underlying Temporal.ZonedDateTime */ toDateTime() { return this.#resolve() as Temporal.ZonedDateTime; } /** returns a Temporal.PlainDate representation */ toPlainDate() { return this.toDateTime().toPlainDate() } /** returns a Temporal.PlainTime representation */ toPlainTime() { return this.toDateTime().toPlainTime() } /** returns a Temporal.PlainDateTime representation */ toPlainDateTime() { return this.toDateTime().toPlainDateTime() } /** returns the underlying Temporal.Instant */ toInstant() { return this.toDateTime().toInstant() } /** the current system time localized to this instance. */toNow() { const [tz, cal] = getTemporalIds(this.#local.config.timeZone, this.#local.config.calendar); return instant().toZonedDateTimeISO(tz).withCalendar(cal); } /** the date-time as a standard `Date` object. */ toDate() { return new Date(this.toDateTime().round({ smallestUnit: enums.ELEMENT.ms }).epochMilliseconds) } /** Custom JSON serialization for `JSON.stringify`. */ toJSON() { return { ...this.#local.config, value: this.toString() } } /** iso8601 string representation of the date-time. */ toString() { return (this.isValid && !this.#errored) ? this.toPlainDateTime().toString({ calendarName: 'never' }) : String(this.#tempo ?? ''); } /** setup local 'config' and 'parse' rules (prototype-linked to global) */ #setLocal(rawOptions: t.Options = {}) { const classState = (this.constructor as any)[$Internal](); const storeKey = rawOptions.store; const storeOptions = storeKey ? (this.constructor as any).readStore(storeKey) : {}; const options: t.Options = { ...storeOptions, ...rawOptions }; this.#local = Object.create(classState); const self = unwrap(this); this.#local.config = markConfig(Object.create(classState.config)); if (classState.config.registry) this.#local.config.registry = Object.create(classState.config.registry); Object.assign(this.#local.config, { scope: 'local' }); // Evaluate and snapshot dynamic context suppliers for this specific Tempo instance const rawTz = options.timeZone ?? (options as any).timezone ?? (options as any).TimeZone; const explicitTz = evaluate(rawTz); const evaluatedTz = explicitTz ?? evaluate(classState.config.timeZone); let resolvedZone: string | undefined; if (isDefined(evaluatedTz)) { const tzString = isObject(evaluatedTz) ? getTemporalIds(evaluatedTz)[0] : String(evaluatedTz); const zone = tzString.toLowerCase(); resolvedZone = (this.constructor as any).timeZones?.[zone] ?? classState.config.timeZones?.[zone] ?? enums.TIMEZONE[zone] ?? normalizeUtcOffset(tzString); setProperty(this.#local.config, 'timeZone', resolvedZone); } const rawCal = options.calendar ?? (options as any).Calendar; const explicitCal = evaluate(rawCal); const evaluatedCal = explicitCal ?? evaluate(classState.config.calendar); if (isDefined(evaluatedCal)) setProperty(this.#local.config, 'calendar', String(evaluatedCal)); let finalLocale: string | readonly string[] | undefined; const rawLoc = options.locale ?? (options as any).Locale; const explicitLoc = evaluate(rawLoc); const evaluatedLoc = explicitLoc ?? evaluate(classState.config.locale); if (isDefined(evaluatedLoc)) { finalLocale = resolveLocale(evaluatedLoc); if (finalLocale) setProperty(this.#local.config, 'locale', finalLocale); } this.#local.userProvidedKeys = new Set(); let explicitSphere: t.COMPASS | undefined; let evalSphere: t.COMPASS | undefined; const rawSphere = options.sphere ?? (options as any).Sphere ?? options.geo?.sphere; if (isDefined(rawSphere)) { explicitSphere = evaluate(rawSphere) as t.COMPASS; evalSphere = explicitSphere; setProperty(this.#local.config, 'sphere', evalSphere); this.#local.userProvidedKeys.add('sphere'); } else if (classState.userProvidedKeys?.has('sphere')) { evalSphere = evaluate(classState.config.sphere); setProperty(this.#local.config, 'sphere', evalSphere); } const explicitGeo = coerceGeo(options); if (isDefined(explicitGeo)) { setProperty(this.#local.config, 'geo', explicitGeo); this.#local.userProvidedKeys.add('geo'); if (isUndefined(evalSphere) && isDefined(explicitGeo.sphere)) { evalSphere = explicitGeo.sphere as t.COMPASS; setProperty(this.#local.config, 'sphere', evalSphere); } } else if (classState.userProvidedKeys?.has('geo')) { setProperty(this.#local.config, 'geo', classState.config.geo); } const optionsSnapshot = { ...options, ...(isDefined(explicitTz) ? { timeZone: resolvedZone } : {}), ...(isDefined(explicitCal) ? { calendar: String(evaluatedCal) } : {}), ...(isDefined(explicitLoc) ? { locale: finalLocale } : {}), ...(isDefined(explicitSphere) ? { sphere: explicitSphere } : {}), ...(isDefined(explicitGeo) ? { geo: explicitGeo } : {}), }; this.#local.options = optionsSnapshot; this.#local.parse = markConfig(Object.create(classState.parse)); this.#local.parse.event = { ...classState.parse.event }; this.#local.parse.period = { ...classState.parse.period }; this.#local.parse.snippet = { ...classState.parse.snippet }; this.#local.userProvidedKeys = new Set(classState.userProvidedKeys); this.#local.parse.planner = { ...(classState.parse.planner.layoutOrder ? { layoutOrder: [...asArray(classState.parse.planner.layoutOrder)] } : {}), ...(isDefined(classState.parse.planner.preFilter) ? { preFilter: Boolean(classState.parse.planner.preFilter) } : {}), }; this.#local.parse.monthDay = { ...classState.parse.monthDay, locales: [...asArray(classState.parse.monthDay.locales)], layouts: [...asArray(classState.parse.monthDay.layouts)], timezones: { ...classState.parse.monthDay.timezones }, ...(classState.parse.monthDay.resolvedLocales ? { resolvedLocales: classState.parse.monthDay.resolvedLocales.map((l: any) => ({ ...l, timeZones: [...l.timeZones] })) } : {}) }; setProperty(this.#local.parse, 'result', [...(options.result ?? [])]); Object.defineProperty(this.#local, 'tempoInstance', { // Link this instance to its state for static alias access value: self, writable: false, configurable: true, enumerable: false }); (this.constructor as any)[$setConfig](this.#local, optionsSnapshot); // set #local config } /** parse DateTime input */ #parse(tempo: t.DateTime, dateTime?: Temporal.ZonedDateTime, term?: string): Temporal.ZonedDateTime { if (!ensureModule(this, 'ParseModule', true)) return this.#fallbackParse(tempo, dateTime, term); const res = interpret(this, 'ParseModule', 'parse', false, tempo, dateTime, term); if (isUndefined(res)) { const msg = `ParseModule error. Could not parse ${String(tempo)}`; logError(msg, this.#local.config); return undefined as any; } if (hasOwn(res, 'value')) { return res.value ?? (undefined as any); } return res; } #fallbackParse(tempo: t.DateTime, dateTime?: Temporal.ZonedDateTime, term?: string): Temporal.ZonedDateTime { if (isZonedDateTime(tempo)) return tempo; if (isTempo(tempo)) return tempo.toDateTime(); if (isString(tempo)) { try { const tz = (this.#local.config.timeZone as string) ?? 'UTC'; if ((tempo as string).includes('[')) return Temporal.ZonedDateTime.from(tempo as string); return Temporal.PlainDateTime.from(tempo as string).toZonedDateTime(tz); } catch { /* ignore and throw below */ } } if (isUndefined(tempo) || isEmpty(tempo)) return dateTime ?? instant().toZonedDateTimeISO(this.#local.config.timeZone); const msg = 'Tempo ParseModule not loaded. Did you forget to Tempo.use(ParseModule)?'; logError(msg, this.#local.config); return undefined as any; } #resolveAnchorBasis(anchor: unknown): Temporal.ZonedDateTime { if (isTempo(anchor)) return anchor.toDateTime(); if (isZonedDateTime(anchor)) return anchor; if (isDefined(anchor)) return new (this.constructor as typeof Tempo)(anchor as any, this.#options).toDateTime(); return this.#now.toZonedDateTimeISO(this.#local.config.timeZone); } /** resolve constructor / method arguments */ #swap(tempo?: t.DateTime | t.Options, options: t.Options = {}): [t.DateTime | undefined, t.Options] { if (isTempo(tempo)) // preserve parse result history when creating new instance from an existing one return [tempo, Object.assign({ result: [...tempo.parse.result] }, options)]; let mappedTempo = tempo as t.DateTime; if (isObject(tempo) && isDurationLike(tempo)) { const record = { ...cast>(tempo) }; let hasMapped = false; for (const [k, v] of Object.entries(record)) { const resolved = Enum.has(enums.ELEMENT, k) ? `${enums.ELEMENT[k as t.Element]}s` : undefined; if (resolved) { record[resolved] = v; delete record[k]; hasMapped = true; } } if (hasMapped) mappedTempo = record as t.DateTime; } return this.#isOptions(mappedTempo) ? [mappedTempo.value, Object.assign({}, mappedTempo)] : [mappedTempo, options]; } /** check if we've been given a Tempo Options object */ #isOptions(arg: any): arg is t.Options { if (!isPlainObject(arg)) return false; const keys = ownKeys(arg); // if it contains any 'mutation' keys, then it's not (just) an options object if (keys.some(key => Enum.has(enums.MUTATION, key))) return false; // 2. If it contains any recognized Date/Time value keys (e.g. year, month, day, hours, minutes), it's likely an input value if (keys.some(key => (Enum.has(enums.ZONED_DATE_TIME, key) && !Enum.has(enums.CONFIG, key)) || Enum.has(enums.DURATIONS, key))) return false; // 3. If it contains any recognized Config or Parse keys, it's definitely an options object if (keys.some(key => Enum.has(enums.CONFIG, key) || Enum.has(enums.PARSE, key))) return true; // 4. Otherwise, it is a plain object (possibly empty), so we treat it as an options object return true; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Keyed lookup surface for resolved term values returned by Tempo.term. */ export interface TempoTermRegistry { [key: string]: any; } export namespace Tempo { /** Represents any value that can be parsed as a date/time */ export type DateTime = t.DateTime; /** String or RegExp pattern for matching dates/times */ export type Pattern = t.Pattern; /** Function or string logic for event/period evaluation */ export type Logic = t.Logic; /** Pair of values for date ranges */ export type Pair = t.Pair; /** Regex capture groups from pattern matching */ export type Groups = t.Groups; /** Base configuration options for Tempo */ export interface BaseOptions extends t.Internal.BaseOptions { } /** Full configuration options for Tempo */ export type Options = t.Options; /** Formatting options for date/time display */ export interface FormatOptions extends t.FormatOptions { } /** Configuration to use for #until() and #since() argument */ export type Unit = t.Unit; /** Options for calculating duration until/since another date */ export type Until = t.Until; /** Mutation operations for date/time modification */ export type Mutate = t.Mutate; /** Set operations for setting specific date/time values */ export type Set = t.MutateSet; /** Add operations for adding durations to date/time */ export type Add = t.MutateAdd; /** Collection of format string templates */ export type Formats = t.Formats; /** A single format string template name */ export type Format = t.Format; /** Registry of format strings */ export type FormatRegistry = t.FormatRegistry; /** Temporal direction modifier (next, last, this, etc.) */ export type Modifier = t.Modifier; /** Relative temporal expression (e.g., 'next week', '2 days ago') */ export type Relative = t.Relative; /** Month numeric value (1-12) */ export type mm = t.mm; /** Hour numeric value (0-23) */ export type hh = t.hh; /** Minute numeric value (0-59) */ export type mi = t.mi; /** Second numeric value (0-59) */ export type ss = t.ss; /** Millisecond numeric value */ export type ms = t.ms; /** Microsecond numeric value */ export type us = t.us; /** Nanosecond numeric value */ export type ns = t.ns; /** ISO week-year numeric value */ export type wy = t.wy; /** @hidden prefer `wy` */ export type ww = t.wy; /** Timestamp precision unit (ss, ms, us, ns) */ export type TimeStamp = t.Internal.TimeStamp; /** Temporal duration object */ export type Duration = t.Duration; /** Weekday enumeration (short form) */ export type WEEKDAY = t.WEEKDAY; /** Weekday enumeration (long form) */ export type WEEKDAYS = t.WEEKDAYS; /** Month enumeration (short form) */ export type MONTH = t.MONTH; /** Month enumeration (long form) */ export type MONTHS = t.MONTHS; /** Duration in seconds */ export type DURATION = t.DURATION; /** Duration in milliseconds */ export type DURATIONS = t.DURATIONS; /** Cardinal directions */ export type COMPASS = t.COMPASS; /** Calendar seasons */ export type SEASON = t.SEASON; /** Date-time element tokens */ export type ELEMENT = t.ELEMENT; /** Weekday numeric value */ export type Weekday = t.Weekday; /** Month numeric value */ export type Month = t.Month; /** Date-time element name */ export type Element = t.Element; /** * Helper function signature for creating Tempo instances with flexible parameter order. * @template T Return type */ export interface Params extends t.Params { } }