@magmacomputing/tempo 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,53 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
36
+ if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
37
+ return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
38
+ };
39
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
40
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
41
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
42
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
43
+ };
44
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
45
+ if (kind === "m") throw new TypeError("Private method is not writable");
46
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
47
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
48
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
49
+ };
50
+ import './temporal.polyfill.js';
1
51
  // #region library modules~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2
52
  import { Logify } from './logify.class.js';
3
53
  import { ifDefined } from './object.library.js';
@@ -51,1357 +101,1340 @@ const Context = getContext(); // get current execution context
51
101
  * })
52
102
  * ```
53
103
  */
54
- @Serializable
55
- @Immutable
56
- export class Tempo {
57
- // #region Static enum properties~~~~~~~~~~~~~~~~~~~~~~~~~
58
- /** Weekday names (short-form) */ static get WEEKDAY() { return enums.WEEKDAY; }
59
- /** Weekday names (long-form) */ static get WEEKDAYS() { return enums.WEEKDAYS; }
60
- /** Month names (short-form) */ static get MONTH() { return enums.MONTH; }
61
- /** Month names (long-form) */ static get MONTHS() { return enums.MONTHS; }
62
- /** Time durations as seconds (singular) */ static get DURATION() { return enums.DURATION; }
63
- /** Time durations as milliseconds (plural) */ static get DURATIONS() { return enums.DURATIONS; }
64
- /** Quarterly Seasons */ static get SEASON() { return enums.SEASON; }
65
- /** Compass cardinal points */ static get COMPASS() { return enums.COMPASS; }
66
- /** Tempo to Temporal DateTime Units map */ static get ELEMENT() { return enums.ELEMENT; }
67
- /** Pre-configured format {name -> string} pairs */ static get FORMAT() { return enums.FORMAT; }
68
- /** some useful Dates */ static get LIMIT() { return enums.LIMIT; }
69
- // #endregion
70
- // #region Static private properties~~~~~~~~~~~~~~~~~~~~~~
71
- static #dbg = new Logify('Tempo', {
72
- debug: Default?.debug ?? false,
73
- catch: Default?.catch ?? false
74
- });
75
- static #global = {};
76
- static #pending = void 0; // collect the parse rule-match results
77
- static #usrCount = 0; // cache for next-available 'usr' Token key
78
- // #endregion
79
- // #region Static private methods~~~~~~~~~~~~~~~~~~~~~~~~~
80
- //** prototype handlers */
81
- /** return the Prototype parent of an object */ static #proto(obj) { return Object.getPrototypeOf(obj); }
82
- /** test object has own property with the given key */ static #hasOwn(obj, key) { return Object.hasOwn(obj, key); }
83
- /** test object is Frozen */ static #isFrozen(obj) { return isDefined(obj) && Object.isFrozen(obj); }
84
- /** return whether the shape is 'local' or 'global' */ static #isLocal(shape) { return shape.config.scope === 'local'; }
85
- /** create an object based on a prototype */ static #create(obj, name) { return Object.create(Tempo.#proto(obj)[name]); }
86
- /**
87
- * {dt} is a layout that combines date-related {snippets} (e.g. dd, mm -or- evt) into a pattern against which a string can be tested.
88
- * because it will also include a list of events (e.g. 'new_years' | 'xmas'), we need to rebuild {dt} if the user adds a new event
89
- */
90
- // TODO: check all Layouts which reference "{evt}" and update them
91
- static #setEvents(shape) {
92
- const events = ownEntries(shape.parse.event, true);
93
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'event') && !Tempo.#hasOwn(shape.parse, 'isMonthDay'))
94
- return; // no local change needed
95
- const src = shape.config.scope.substring(0, 1); // 'g'lobal or 'l'ocal
96
- const groups = events
97
- .map(([pat, _], idx) => `(?<${src}evt${idx}>${pat})`) // assign a number to the pattern
98
- .join('|'); // make an 'Or' pattern for the event-keys
99
- if (groups) {
100
- const protoEvt = Tempo.#proto(shape.parse.snippet)[Token.evt]?.source;
101
- if (!Tempo.#isLocal(shape) || groups !== protoEvt) {
102
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'snippet'))
103
- shape.parse.snippet = Tempo.#create(shape.parse, 'snippet');
104
- Object.defineProperty(shape.parse.snippet, Token.evt, {
105
- value: new RegExp(groups),
106
- enumerable: true,
107
- writable: true,
108
- configurable: true
109
- });
110
- }
104
+ let Tempo = (() => {
105
+ var _a, _Tempo_dbg, _Tempo_global, _Tempo_pending, _Tempo_usrCount, _Tempo_proto, _Tempo_hasOwn, _Tempo_isFrozen, _Tempo_isLocal, _Tempo_create, _Tempo_setEvents, _Tempo_setPeriods, _Tempo_setSphere, _Tempo_isMonthDay, _Tempo_swapLayout, _Tempo_prefix, _Tempo_locale, _Tempo_setConfig, _Tempo_mdyLocales, _Tempo_setPatterns;
106
+ let _classDecorators = [Serializable, Immutable];
107
+ let _classDescriptor;
108
+ let _classExtraInitializers = [];
109
+ let _classThis;
110
+ var Tempo = _a = class {
111
+ static { _classThis = this; }
112
+ static { __setFunctionName(this, "Tempo"); }
113
+ static {
114
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
115
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
116
+ Tempo = _classThis = _classDescriptor.value;
117
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
111
118
  }
112
- if (shape.parse.isMonthDay) {
113
- const protoDt = Tempo.#proto(shape.parse.layout)[Token.dt];
114
- const localDt = '{mm}{sep}?{dd}({sep}?{yy})?|{mod}?({evt})';
115
- if (!Tempo.#isLocal(shape) || localDt !== protoDt) {
116
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'layout'))
117
- shape.parse.layout = Tempo.#create(shape.parse, 'layout');
118
- Object.defineProperty(shape.parse.layout, Token.dt, {
119
- value: localDt,
120
- enumerable: true,
121
- writable: true,
122
- configurable: true
123
- });
124
- }
119
+ // #region Static enum properties~~~~~~~~~~~~~~~~~~~~~~~~~
120
+ /** Weekday names (short-form) */ static get WEEKDAY() { return enums.WEEKDAY; }
121
+ /** Weekday names (long-form) */ static get WEEKDAYS() { return enums.WEEKDAYS; }
122
+ /** Month names (short-form) */ static get MONTH() { return enums.MONTH; }
123
+ /** Month names (long-form) */ static get MONTHS() { return enums.MONTHS; }
124
+ /** Time durations as seconds (singular) */ static get DURATION() { return enums.DURATION; }
125
+ /** Time durations as milliseconds (plural) */ static get DURATIONS() { return enums.DURATIONS; }
126
+ /** Quarterly Seasons */ static get SEASON() { return enums.SEASON; }
127
+ /** Compass cardinal points */ static get COMPASS() { return enums.COMPASS; }
128
+ /** Tempo to Temporal DateTime Units map */ static get ELEMENT() { return enums.ELEMENT; }
129
+ /** Pre-configured format {name -> string} pairs */ static get FORMAT() { return enums.FORMAT; }
130
+ /** some useful Dates */ static get LIMIT() { return enums.LIMIT; }
131
+ static {
132
+ // #endregion
133
+ // #region Static private properties~~~~~~~~~~~~~~~~~~~~~~
134
+ _Tempo_dbg = { value: new Logify('Tempo', {
135
+ debug: Default?.debug ?? false,
136
+ catch: Default?.catch ?? false
137
+ }) };
125
138
  }
126
- }
127
- /**
128
- * {tm} is a layout that combines time-related snippets (hh, mi, ss, ff, mer -or- per) into a pattern against which a string can be tested.
129
- * because it will also include a list of periods (e.g. 'midnight' | 'afternoon' ), we need to rebuild {tm} if the user adds a new period
130
- */
131
- // TODO: check all Layouts which reference "{per}" and update them
132
- static #setPeriods(shape) {
133
- const periods = ownEntries(shape.parse.period, true);
134
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'period'))
135
- return; // no local change needed
136
- const src = shape.config.scope.substring(0, 1); // 'g'lobal or 'l'ocal
137
- const groups = periods
138
- .map(([pat, _], idx) => `(?<${src}per${idx}>${pat})`) // {pattern} is the 1st element of the tuple
139
- .join('|'); // make an 'or' pattern for the period-keys
140
- if (groups) {
141
- const protoPer = Tempo.#proto(shape.parse.snippet)[Token.per]?.source;
142
- if (!Tempo.#isLocal(shape) || groups !== protoPer) {
143
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'snippet'))
144
- shape.parse.snippet = Tempo.#create(shape.parse, 'snippet');
145
- Object.defineProperty(shape.parse.snippet, Token.per, {
146
- value: new RegExp(groups),
147
- enumerable: true,
148
- writable: true,
149
- configurable: true
139
+ static {
140
+ _Tempo_global = { value: {} };
141
+ }
142
+ static {
143
+ _Tempo_pending = { value: void 0 }; // collect the parse rule-match results
144
+ }
145
+ static {
146
+ _Tempo_usrCount = { value: 0 }; // cache for next-available 'usr' Token key
147
+ }
148
+ static {
149
+ /** try to infer hemisphere using the timezone's daylight-savings setting */
150
+ _Tempo_setSphere = { value: (shape, options) => {
151
+ if (isUndefined(shape.config.timeZone) || __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, options, 'sphere'))
152
+ return shape.config.sphere; // already specified or no timeZone to calculate from
153
+ const zdt = Temporal.Now.zonedDateTimeISO(shape.config.timeZone);
154
+ const jan = zdt.with({ day: 1, month: 1 }).offsetNanoseconds;
155
+ const jun = zdt.with({ day: 1, month: 6 }).offsetNanoseconds;
156
+ const dst = Math.sign(jan - jun); // timeZone offset difference between Jan and Jun
157
+ switch (dst) {
158
+ case -1:
159
+ return Tempo.COMPASS.North; // clock moves backward in Northern hemisphere
160
+ case 1:
161
+ return Tempo.COMPASS.South; // clock moves forward in Southern hemisphere
162
+ case 0:
163
+ return void 0; // timeZone does not observe DST
164
+ default:
165
+ return Default.sphere ?? Tempo.COMPASS.North; // timeZone does not observe DST
166
+ }
167
+ } };
168
+ }
169
+ static {
170
+ /** properCase week-day / calendar-month */
171
+ _Tempo_prefix = { value: (str) => toProperCase(str.substring(0, 3)) };
172
+ }
173
+ static {
174
+ /** get first Canonical name of a supplied locale */
175
+ _Tempo_locale = { value: (locale) => {
176
+ let language;
177
+ try { // lookup locale
178
+ language = Intl.getCanonicalLocales(locale.replace('_', '-'))[0];
179
+ }
180
+ catch (error) { } // catch unknown locale
181
+ const global = Context.global;
182
+ return language ??
183
+ global?.navigator?.languages?.[0] ?? // fallback to current first navigator.languages[]
184
+ global?.navigator?.language ?? // else navigator.language
185
+ Default.locale ?? // else default locale
186
+ locale; // cannot determine locale
187
+ } };
188
+ }
189
+ // #endregion Static private methods
190
+ // #region Static public methods~~~~~~~~~~~~~~~~~~~~~~~~~~
191
+ /**
192
+ * Initializes the global default configuration for all subsequent `Tempo` instances.
193
+ *
194
+ * Settings are inherited in this priority:
195
+ * 1. Reasonable library defaults (defined in tempo.config.js)
196
+ * 2. Persistent storage (e.g. localStorage), if available.
197
+ * 3. `options` provided to this method.
198
+ *
199
+ * @param options - Configuration overrides to apply globally.
200
+ * @returns The resolved global configuration.
201
+ */
202
+ static init(options = {}) {
203
+ if (isEmpty(options)) { // if no options supplied, reset all
204
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config = {}; // remove previous config
205
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse = {
206
+ snippet: { ...Snippet },
207
+ layout: { ...Layout },
208
+ event: { ...Event },
209
+ period: { ...Period },
210
+ mdyLocales: __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_mdyLocales).call(Tempo, Default.mdyLocales),
211
+ mdyLayouts: asArray(Default.mdyLayouts),
212
+ }; // remove previous parsing rules
213
+ for (const key of Object.keys(Token)) // purge user-allocated Tokens
214
+ if (key.startsWith('usr.')) // only remove 'usr.' prefixed keys
215
+ delete Token[key];
216
+ __classPrivateFieldSet(Tempo, _classThis, 0, "f", _Tempo_usrCount); // reset user-key counter
217
+ const dateTime = Intl.DateTimeFormat().resolvedOptions();
218
+ Object.assign(__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config, {
219
+ calendar: dateTime.calendar,
220
+ timeZone: dateTime.timeZone,
221
+ locale: __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_locale).call(Tempo), // get from browser, if possible
150
222
  });
223
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setConfig).call(Tempo, __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global), { store: STORAGEKEY, scope: 'global' }, Default, // set Tempo defaults
224
+ Tempo.readStore());
225
+ }
226
+ else {
227
+ // if (options.store !== STORAGEKEY)
228
+ // Tempo.#setConfig(Tempo.#global, Tempo.readStore(options.store)); // user-defined local storage
229
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setConfig).call(Tempo, __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global), options); // overload with init() argument (options)
151
230
  }
231
+ if (Context.type === CONTEXT.Browser || options.debug === true)
232
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).info(Tempo.config, 'Tempo:', __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config);
233
+ return __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config;
152
234
  }
153
- }
154
- /** try to infer hemisphere using the timezone's daylight-savings setting */
155
- static #setSphere = (shape, options) => {
156
- if (isUndefined(shape.config.timeZone) || Tempo.#hasOwn(options, 'sphere'))
157
- return shape.config.sphere; // already specified or no timeZone to calculate from
158
- const zdt = Temporal.Now.zonedDateTimeISO(shape.config.timeZone);
159
- const jan = zdt.with({ day: 1, month: 1 }).offsetNanoseconds;
160
- const jun = zdt.with({ day: 1, month: 6 }).offsetNanoseconds;
161
- const dst = Math.sign(jan - jun); // timeZone offset difference between Jan and Jun
162
- switch (dst) {
163
- case -1:
164
- return Tempo.COMPASS.North; // clock moves backward in Northern hemisphere
165
- case 1:
166
- return Tempo.COMPASS.South; // clock moves forward in Southern hemisphere
167
- case 0:
168
- return void 0; // timeZone does not observe DST
169
- default:
170
- return Default.sphere ?? this.COMPASS.North; // timeZone does not observe DST
235
+ /**
236
+ * Reads `Tempo` options from persistent storage (e.g., localStorage).
237
+ * @returns The stored configuration or an empty object.
238
+ */
239
+ static readStore(key = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config.store) {
240
+ return getStorage(key, {});
171
241
  }
172
- };
173
- /** determine if we have a {timeZone} which prefers {mdy} date-order */
174
- static #isMonthDay(shape) {
175
- const monthDay = [...asArray(Tempo.#global.parse.mdyLocales)];
176
- if (Tempo.#isLocal(shape) && Tempo.#hasOwn(shape.parse, 'mdyLocales'))
177
- monthDay.push(...shape.parse.mdyLocales); // append local mdyLocales (not overwrite global)
178
- return monthDay.some(mdy => mdy.timeZones?.includes(shape.config.timeZone));
179
- }
180
- /**
181
- * swap parsing-order of layouts to suit different timeZones
182
- * this allows the parser to try to interpret '04012023' as Apr-01-2023 before trying 04-Jan-2023
183
- */
184
- static #swapLayout(shape) {
185
- const layouts = ownEntries(shape.parse.layout); // get entries of Layout Record
186
- const swap = shape.parse.mdyLayouts; // get the swap-tuple
187
- let chg = false; // no need to rebuild, if no change
188
- swap
189
- .forEach(([dmy, mdy]) => {
190
- const idx1 = layouts.findIndex(([key]) => key.description === dmy); // 1st swap element exists in {layouts}
191
- const idx2 = layouts.findIndex(([key]) => key.description === mdy); // 2nd swap element exists in {layouts}
192
- if (idx1 === -1 || idx2 === -1)
193
- return; // no pair to swap
194
- const swap1 = (idx1 < idx2) && shape.parse.isMonthDay; // we prefer {mdy} and the 1st tuple was found earlier than the 2nd
195
- const swap2 = (idx1 > idx2) && !shape.parse.isMonthDay; // we dont prefer {mdy} and the 1st tuple was found later than the 2nd
196
- if (swap1 || swap2) { // since {layouts} is an array, ok to swap by-reference
197
- [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
198
- chg = true;
199
- }
200
- });
201
- if (chg)
202
- shape.parse.layout = Object.fromEntries(layouts); // rebuild Layout in new parse order
203
- }
204
- /** properCase week-day / calendar-month */
205
- static #prefix = (str) => toProperCase(str.substring(0, 3));
206
- /** get first Canonical name of a supplied locale */
207
- static #locale = (locale) => {
208
- let language;
209
- try { // lookup locale
210
- language = Intl.getCanonicalLocales(locale.replace('_', '-'))[0];
242
+ /**
243
+ * Writes the provided configuration into persistent storage.
244
+ * @param config - The options to save.
245
+ */
246
+ static writeStore(config, key = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config.store) {
247
+ return setStorage(key, config);
211
248
  }
212
- catch (error) { } // catch unknown locale
213
- const global = Context.global;
214
- return language ??
215
- global?.navigator?.languages?.[0] ?? // fallback to current first navigator.languages[]
216
- global?.navigator?.language ?? // else navigator.language
217
- Default.locale ?? // else default locale
218
- locale; // cannot determine locale
219
- };
220
- /**
221
- * conform input of Snippet / Layout / Event / Period options
222
- * This is needed because we allow the user to flexibly provide detail as {[key]:val} or {[key]:val}[] or [key,val][]
223
- * for example:
224
- ```
225
- Tempo.init({ snippet: {'yy': /20\d{2}/, 'mm': /[0-9|1|2]\d/ } })
226
- Tempo.init({ layout: {'ddmm': '{dd}{sep}?{mm}'} })
227
- Tempo.init({ layout: '{wkd}' }) (can be a single string)
228
- Tempo.init({ layout: '({wkd})? {tm}' }) (or a string combination of snippets)
229
- Tempo.init({ layout: new Map([['name1', '{wkd} {yy}']], ['name2', '{mm}{sep}{dd}']]]) })
230
- Tempo.init({ layout: [ {name1: '{wkd} {yy}'}, {name2: '{mm}{sep}{dd}'} ]
231
-
232
- Tempo.init({event: {'canada ?day': '01-Jun', 'aus(tralia)? ?day': '26-Jan'} })
233
- Tempo.init({period: [{'morning tea': '09:30' }, {'elevenses': '11:00' }]})
234
- new Tempo('birthday', {event: [["birth(day)?", "20-May"], ["anniversary", "01-Jul"] ]})
235
- ```
236
- */
237
- static #setConfig(shape, ...options) {
238
- /** helper to normalize snippet/layout Options into the target Config */
239
- const collect = (target, value, convert) => {
240
- const itm = asType(value);
241
- target ??= {};
242
- switch (itm.type) {
243
- case 'Object':
244
- ownEntries(itm.value)
245
- .forEach(([k, v]) => target[Tempo.getSymbol(k)] = convert(v));
246
- break;
247
- case 'String':
248
- case 'RegExp':
249
- target[Tempo.getSymbol()] = convert(itm.value);
250
- break;
251
- case 'Array':
252
- itm.value.forEach(elm => collect(target, elm, convert));
253
- break;
249
+ /**
250
+ * looks-up or registers a new `Symbol` for a given key.
251
+ * auto-maintains the `Token` registry for internal consistency.
252
+ *
253
+ * @param key - The description for which to retrieve/create a Symbol.
254
+ */
255
+ static getSymbol(key) {
256
+ var _b, _c;
257
+ if (isUndefined(key)) {
258
+ const usr = `usr.${__classPrivateFieldSet(_b = Tempo, _classThis, (_c = __classPrivateFieldGet(_b, _classThis, "f", _Tempo_usrCount), ++_c), "f", _Tempo_usrCount)}`; // allocate a prefixed 'user' key
259
+ return Token[usr] = Symbol(usr); // add to Symbol register
254
260
  }
255
- };
256
- const mergedOptions = Object.assign({}, ...options);
257
- ownEntries(mergedOptions)
258
- .forEach(([optKey, optVal]) => {
259
- if (isUndefined(optVal))
260
- return; // skip undefined values
261
- const arg = asType(optVal);
262
- switch (optKey) {
263
- case 'snippet':
264
- case 'layout':
265
- case 'event':
266
- case 'period':
267
- // lazy-shadowing: only create local object if it doesn't already exist on local shape
268
- if (!Tempo.#hasOwn(shape.parse, optKey))
269
- shape.parse[optKey] = Tempo.#create(shape.parse, optKey);
270
- const rule = shape.parse[optKey];
271
- if (['snippet', 'layout'].includes(optKey)) {
272
- collect(rule, arg.value, v => optKey === 'snippet'
273
- ? isRegExp(v) ? v : new RegExp(v)
274
- : isRegExp(v) ? v.source : v);
275
- }
276
- else {
277
- asArray(arg.value)
278
- .forEach(elm => ownEntries(elm).forEach(([key, val]) => rule[key] = val));
261
+ if (isSymbol(key)) {
262
+ const name = key.description ?? Cipher.randomKey(); // get Symbol description, else allocate random string
263
+ return Token[name] ??= key;
264
+ }
265
+ if (isDefined(Token[key])) // already registered (internal)
266
+ return Token[key]; // return existing Symbol
267
+ const usr = `usr.${key}`;
268
+ if (isDefined(Token[usr])) // already registered (user)
269
+ return Token[usr]; // return existing Symbol
270
+ const description = key
271
+ .split(Match.separator)
272
+ .filter(s => !isEmpty(s)).pop() || key;
273
+ return Token[usr] = Symbol(description); // add to Symbol register
274
+ }
275
+ /**
276
+ * translates a {layout} string into an anchored, case-insensitive regular expression.
277
+ * supports placeholder expansion using the {snippet} registry (e.g., `{yy}`, `{mm}`).
278
+ */
279
+ static regexp(layout, snippet) {
280
+ // helper function to replace {name} placeholders with their corresponding snippets
281
+ function matcher(str) {
282
+ let source = isRegExp(str) ? str.source : str;
283
+ if (isRegExpLike(source)) // string that looks like a RegExp
284
+ source = source.substring(1, source.length - 1); // remove the leading/trailing "/"
285
+ if (source.startsWith('^') && source.endsWith('$'))
286
+ source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $)
287
+ return source.replace(Match.braces, (match, name) => {
288
+ const token = Tempo.getSymbol(name); // get the symbol for this {name}
289
+ let snip = snippet?.[token]?.source // get the snippet source (custom)
290
+ ?? Snippet[token]?.source // else get the snippet source (global)
291
+ ?? Layout[token]; // else get the layout source
292
+ if (isNullish(snip) && name.includes('.')) { // if no definition found, try fallback
293
+ const prefix = name.split('.')[0]; // get the base token name
294
+ const pToken = Tempo.getSymbol(prefix);
295
+ snip = snippet?.[pToken]?.source
296
+ ?? Snippet[pToken]?.source
297
+ ?? Layout[pToken];
298
+ if (snip) {
299
+ const safeName = name.replace(/\./g, '_'); // e.g. aaa.bbb -> aaa_bbb
300
+ snip = `(?<${safeName}>${snip.replace(Match.captures, '(?:$2)')})`;
301
+ }
279
302
  }
280
- break;
281
- case 'mdyLocales':
282
- shape.parse[optKey] = Tempo.#mdyLocales(arg.value);
283
- break;
284
- case 'mdyLayouts': // these are the 'layouts' that need to swap parse-order
285
- shape.parse[optKey] = asArray(arg.value);
286
- break;
287
- case 'timeZone':
288
- const zone = String(arg.value).toLowerCase();
289
- shape.config.timeZone = TimeZone[zone] ?? arg.value;
290
- break;
291
- default: // else just add to config
292
- Object.assign(shape.config, { [optKey]: optVal });
293
- break;
303
+ return (isNullish(snip) || snip === match) // if no definition found,
304
+ ? match // return the original match
305
+ : matcher(snip); // else recurse to see if snippet contains embedded "{}" pairs
306
+ });
294
307
  }
295
- });
296
- const isMonthDay = Tempo.#isMonthDay(shape);
297
- if (isMonthDay !== Tempo.#proto(shape.parse).isMonthDay) // this will always set on 'global', conditionally on 'local'
298
- shape.parse.isMonthDay = isMonthDay;
299
- shape.config.sphere = Tempo.#setSphere(shape, mergedOptions);
300
- if (isDefined(shape.parse.mdyLayouts))
301
- Tempo.#swapLayout(shape);
302
- if (isDefined(shape.parse.event))
303
- Tempo.#setEvents(shape);
304
- if (isDefined(shape.parse.period))
305
- Tempo.#setPeriods(shape);
306
- Tempo.#setPatterns(shape); // setup Regex DateTime patterns
307
- }
308
- /** setup mdy TimeZones, using Intl.Locale */
309
- // The google-apps-script types package provides its own Intl.Locale interface that doesn't include getTimeZones(),
310
- // and it takes priority over the ESNext.Intl augmentation in tsconfig.
311
- // The "(mdy as any).getTimeZones?.()" can be replaced with "mdy.getTimeZones()" after google-apps-script is corrected
312
- static #mdyLocales(value) {
313
- return asArray(value)
314
- .map(mdy => new Intl.Locale(mdy))
315
- .map(mdy => ({ locale: mdy.baseName, timeZones: mdy.getTimeZones?.() ?? [] }));
316
- }
317
- /** build RegExp patterns */
318
- static #setPatterns(shape) {
319
- const snippet = shape.parse.snippet;
320
- // if local and no snippet or layout overrides, we can just use the prototype's patterns
321
- if (Tempo.#isLocal(shape) && !Tempo.#hasOwn(shape.parse, 'snippet') && !Tempo.#hasOwn(shape.parse, 'layout'))
322
- return;
323
- // ensure we have our own Map to mutate (shadow if local)
324
- if (!Tempo.#hasOwn(shape.parse, 'pattern'))
325
- shape.parse.pattern = new Map();
326
- shape.parse.pattern.clear(); // reset {pattern} Map
327
- for (const [sym, layout] of ownEntries(shape.parse.layout, true))
328
- shape.parse.pattern.set(sym, Tempo.regexp(layout, snippet));
329
- }
330
- // #endregion Static private methods
331
- // #region Static public methods~~~~~~~~~~~~~~~~~~~~~~~~~~
332
- /**
333
- * Initializes the global default configuration for all subsequent `Tempo` instances.
334
- *
335
- * Settings are inherited in this priority:
336
- * 1. Reasonable library defaults (defined in tempo.config.js)
337
- * 2. Persistent storage (e.g. localStorage), if available.
338
- * 3. `options` provided to this method.
339
- *
340
- * @param options - Configuration overrides to apply globally.
341
- * @returns The resolved global configuration.
342
- */
343
- static init(options = {}) {
344
- if (isEmpty(options)) { // if no options supplied, reset all
345
- Tempo.#global.config = {}; // remove previous config
346
- Tempo.#global.parse = {
347
- snippet: { ...Snippet },
348
- layout: { ...Layout },
349
- event: { ...Event },
350
- period: { ...Period },
351
- mdyLocales: Tempo.#mdyLocales(Default.mdyLocales),
352
- mdyLayouts: asArray(Default.mdyLayouts),
353
- }; // remove previous parsing rules
354
- for (const key of Object.keys(Token)) // purge user-allocated Tokens
355
- if (key.startsWith('usr.')) // only remove 'usr.' prefixed keys
356
- delete Token[key];
357
- Tempo.#usrCount = 0; // reset user-key counter
358
- const dateTime = Intl.DateTimeFormat().resolvedOptions();
359
- Object.assign(Tempo.#global.config, {
360
- calendar: dateTime.calendar,
361
- timeZone: dateTime.timeZone,
362
- locale: Tempo.#locale(), // get from browser, if possible
363
- });
364
- Tempo.#setConfig(Tempo.#global, { store: STORAGEKEY, scope: 'global' }, Default, // set Tempo defaults
365
- Tempo.readStore());
308
+ // helper to check for duplicate named capture-groups
309
+ function checker(source) {
310
+ names.clear(); // clear the set of names
311
+ return source.replace(Match.captures, (match, name) => {
312
+ if (names.has(name))
313
+ return `(\\k<${name}>)`; // replace with a back-reference to the {name}
314
+ names.add(name); // add {name} to the set of names
315
+ return match;
316
+ });
317
+ }
318
+ const names = new Set(); // track the regex named capture-groups
319
+ layout = matcher(layout); // initiate the layout-parse
320
+ layout = checker(layout); // check for named capture-groups
321
+ return new RegExp(`^(${layout})$`, 'i'); // translate the source into a regex
366
322
  }
367
- else {
368
- // if (options.store !== STORAGEKEY)
369
- // Tempo.#setConfig(Tempo.#global, Tempo.readStore(options.store)); // user-defined local storage
370
- Tempo.#setConfig(Tempo.#global, options); // overload with init() argument (options)
323
+ /**
324
+ * Compares two `Tempo` instances or date-time values.
325
+ * Useful for sorting or determining chronological order.
326
+ *
327
+ * @param tempo1 - The first value to compare.
328
+ * @param tempo2 - The second value to compare (defaults to 'now').
329
+ * @returns `-1` if `tempo1 < tempo2`, `0` if tempo1 == tempo2, `1` if `tempo1 > tempo2`.
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const sorted = [t1, t2].sort(Tempo.compare);
334
+ * ```
335
+ */
336
+ static compare(tempo1, tempo2) {
337
+ const one = new Tempo(tempo1), two = new Tempo(tempo2);
338
+ return Number((one.nano > two.nano) || -(one.nano < two.nano)) + 0;
371
339
  }
372
- if (Context.type === CONTEXT.Browser || options.debug === true)
373
- Tempo.#dbg.info(Tempo.config, 'Tempo:', Tempo.#global.config);
374
- return Tempo.#global.config;
375
- }
376
- /**
377
- * Reads `Tempo` options from persistent storage (e.g., localStorage).
378
- * @returns The stored configuration or an empty object.
379
- */
380
- static readStore(key = Tempo.#global.config.store) {
381
- return getStorage(key, {});
382
- }
383
- /**
384
- * Writes the provided configuration into persistent storage.
385
- * @param config - The options to save.
386
- */
387
- static writeStore(config, key = Tempo.#global.config.store) {
388
- return setStorage(key, config);
389
- }
390
- /**
391
- * looks-up or registers a new `Symbol` for a given key.
392
- * auto-maintains the `Token` registry for internal consistency.
393
- *
394
- * @param key - The description for which to retrieve/create a Symbol.
395
- */
396
- static getSymbol(key) {
397
- if (isUndefined(key)) {
398
- const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key
399
- return Token[usr] = Symbol(usr); // add to Symbol register
340
+ static from(tempo, options) { return new Tempo(tempo, options); }
341
+ /** Returns current time as epoch nanoseconds (BigInt). */
342
+ static now() { return Temporal.Now.instant().epochNanoseconds; }
343
+ /** static Tempo.terms getter */
344
+ static get terms() {
345
+ return secure(terms
346
+ .map(({ define, ...rest }) => rest)); // omit the 'define' method
400
347
  }
401
- if (isSymbol(key)) {
402
- const name = key.description ?? Cipher.randomKey(); // get Symbol description, else allocate random string
403
- return Token[name] ??= key;
348
+ /** static Tempo properties getter */
349
+ static get properties() {
350
+ return secure(getAccessors(Tempo)
351
+ .filter(acc => getType(acc) !== 'Symbol')); // omit any Symbol properties
404
352
  }
405
- if (isDefined(Token[key])) // already registered (internal)
406
- return Token[key]; // return existing Symbol
407
- const usr = `usr.${key}`;
408
- if (isDefined(Token[usr])) // already registered (user)
409
- return Token[usr]; // return existing Symbol
410
- const description = key
411
- .split(Match.separator)
412
- .filter(s => !isEmpty(s)).pop() || key;
413
- return Token[usr] = Symbol(description); // add to Symbol register
414
- }
415
- /**
416
- * translates a {layout} string into an anchored, case-insensitive regular expression.
417
- * supports placeholder expansion using the {snippet} registry (e.g., `{yy}`, `{mm}`).
418
- */
419
- static regexp(layout, snippet) {
420
- // helper function to replace {name} placeholders with their corresponding snippets
421
- function matcher(str) {
422
- let source = isRegExp(str) ? str.source : str;
423
- if (isRegExpLike(source)) // string that looks like a RegExp
424
- source = source.substring(1, source.length - 1); // remove the leading/trailing "/"
425
- if (source.startsWith('^') && source.endsWith('$'))
426
- source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $)
427
- return source.replace(Match.braces, (match, name) => {
428
- const token = Tempo.getSymbol(name); // get the symbol for this {name}
429
- let snip = snippet?.[token]?.source // get the snippet source (custom)
430
- ?? Snippet[token]?.source // else get the snippet source (global)
431
- ?? Layout[token]; // else get the layout source
432
- if (isNullish(snip) && name.includes('.')) { // if no definition found, try fallback
433
- const prefix = name.split('.')[0]; // get the base token name
434
- const pToken = Tempo.getSymbol(prefix);
435
- snip = snippet?.[pToken]?.source
436
- ?? Snippet[pToken]?.source
437
- ?? Layout[pToken];
438
- if (snip) {
439
- const safeName = name.replace(/\./g, '_'); // e.g. aaa.bbb -> aaa_bbb
440
- snip = `(?<${safeName}>${snip.replace(Match.captures, '(?:$2)')})`;
441
- }
442
- }
443
- return (isNullish(snip) || snip === match) // if no definition found,
444
- ? match // return the original match
445
- : matcher(snip); // else recurse to see if snippet contains embedded "{}" pairs
446
- });
353
+ /** Tempo global config settings */
354
+ static get config() {
355
+ return secure({ ...__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config });
447
356
  }
448
- // helper to check for duplicate named capture-groups
449
- function checker(source) {
450
- names.clear(); // clear the set of names
451
- return source.replace(Match.captures, (match, name) => {
452
- if (names.has(name))
453
- return `(\\k<${name}>)`; // replace with a back-reference to the {name}
454
- names.add(name); // add {name} to the set of names
455
- return match;
456
- });
357
+ /** Tempo initial default settings */
358
+ static get default() {
359
+ return secure(Default);
457
360
  }
458
- const names = new Set(); // track the regex named capture-groups
459
- layout = matcher(layout); // initiate the layout-parse
460
- layout = checker(layout); // check for named capture-groups
461
- return new RegExp(`^(${layout})$`, 'i'); // translate the source into a regex
462
- }
463
- /**
464
- * Compares two `Tempo` instances or date-time values.
465
- * Useful for sorting or determining chronological order.
466
- *
467
- * @param tempo1 - The first value to compare.
468
- * @param tempo2 - The second value to compare (defaults to 'now').
469
- * @returns `-1` if `tempo1 < tempo2`, `0` if tempo1 == tempo2, `1` if `tempo1 > tempo2`.
470
- *
471
- * @example
472
- * ```typescript
473
- * const sorted = [t1, t2].sort(Tempo.compare);
474
- * ```
475
- */
476
- static compare(tempo1, tempo2) {
477
- const one = new Tempo(tempo1), two = new Tempo(tempo2);
478
- return Number((one.nano > two.nano) || -(one.nano < two.nano)) + 0;
479
- }
480
- static from(tempo, options) { return new Tempo(tempo, options); }
481
- /** Returns current time as epoch nanoseconds (BigInt). */
482
- static now() { return Temporal.Now.instant().epochNanoseconds; }
483
- /** static Tempo.terms getter */
484
- static get terms() {
485
- return secure(terms
486
- .map(({ define, ...rest }) => rest)); // omit the 'define' method
487
- }
488
- /** static Tempo properties getter */
489
- static get properties() {
490
- return secure(getAccessors(Tempo)
491
- .filter(acc => getType(acc) !== 'Symbol')); // omit any Symbol properties
492
- }
493
- /** Tempo global config settings */
494
- static get config() {
495
- return secure({ ...Tempo.#global.config });
496
- }
497
- /** Tempo initial default settings */
498
- static get default() {
499
- return secure(Default);
500
- }
501
- /**
502
- * configuration governing the static 'rules' used when parsing Tempo.DateTime argument
503
- */
504
- static get parse() {
505
- const parse = Tempo.#global.parse;
506
- return secure({
507
- ...parse,
508
- snippet: { ...parse.snippet },
509
- layout: { ...parse.layout },
510
- event: { ...parse.event },
511
- period: { ...parse.period },
512
- // token: { ...parse.token }, // I don't believe the Token needs to be exposed
513
- });
514
- }
515
- /** iterate over Tempo properties */
516
- static [Symbol.iterator]() {
517
- return Tempo.properties[Symbol.iterator](); // static Iterator over array of 'getters'
518
- }
519
- // #endregion Static public methods
520
- // #region Instance symbols~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
521
- /** allow for auto-convert of Tempo to BigInt */
522
- [Symbol.toPrimitive](hint) {
523
- Tempo.#dbg.info(this.config, getType(this), '.hint: ', hint);
524
- return this.nano;
525
- }
526
- /** iterate over instance formats */
527
- [Symbol.iterator]() {
528
- return ownEntries(this.#fmt)[Symbol.iterator](); // instance Iterator over tuple of FormatType[]
529
- }
530
- get [Symbol.toStringTag]() {
531
- return 'Tempo'; // hard-coded to avoid minification mangling
532
- }
533
- // #endregion Instance symbols
534
- // #region Instance properties~~~~~~~~~~~~~~~~~~~~~~~~~~~~
535
- /** constructor tempo */ #tempo;
536
- /** constructor options */ #options = {};
537
- /** instantiation Temporal Instant */ #now;
538
- /** underlying Temporal ZonedDateTime */ #zdt;
539
- /** prebuilt formats, for convenience */ #fmt = {};
540
- /** instance term plugins */ #term = {};
541
- /** instance values to complement static values */ #local = {
542
- /** instance configuration */ config: {},
543
- /** instance parse rules (only populated when local overrides exist) */ parse: {}
544
- };
545
- constructor(tempo, options = {}) {
546
- this.#now = Temporal.Now.instant(); // stash current Instant
547
- // swap arguments around, if arg1=Options or Temporal-like
548
- [this.#tempo, this.#options] = (this.#isOptions(tempo) || this.#isZonedDateTimeLike(tempo))
549
- ? [tempo?.value, tempo]
550
- : [tempo, { ...options }];
551
- // parse the local options looking for overrides to Tempo.#global.config
552
- this.#setLocal(this.#options);
553
- // we now have all the info we need to instantiate a new Tempo
554
- try {
555
- this.#zdt = this.#parse(this.#tempo); // attempt to interpret the DateTime arg
556
- if (['iso8601', 'gregory'].includes(this.#local.config['calendar'])) {
557
- const formats = ownEntries(Tempo.FORMAT);
558
- for (const [key, val] of formats)
559
- this.#fmt[key] = this.format(val);
560
- }
561
- terms // add the plug-in getters for the pre-defined Terms to the instance
562
- .forEach(({ key, scope, define }) => {
563
- this.#setTerm(this, key, define, true); // add a getter which returns the key-field only
564
- this.#setTerm(this, scope, define, false); // add a getter which returns a range-object
361
+ /**
362
+ * configuration governing the static 'rules' used when parsing Tempo.DateTime argument
363
+ */
364
+ static get parse() {
365
+ const parse = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse;
366
+ return secure({
367
+ ...parse,
368
+ snippet: { ...parse.snippet },
369
+ layout: { ...parse.layout },
370
+ event: { ...parse.event },
371
+ period: { ...parse.period },
372
+ // token: { ...parse.token }, // I don't believe the Token needs to be exposed
565
373
  });
566
- if (isDefined(Tempo.#pending)) { // are we mutating with 'set()' ?
567
- this.#local.parse.result = Tempo.#pending; // stash collected parse-matches
568
- Tempo.#pending = void 0; // and reset mutating-flag
569
- }
570
- secure(this.#fmt); // prevent mutations
571
- secure(this.#local.config);
572
- secure(this.#local.parse);
573
374
  }
574
- catch (err) {
575
- Tempo.#dbg.catch(this.config, `Cannot create Tempo: ${err.message}\n${err.stack}`);
576
- return {}; // return empty Object
577
- }
578
- }
579
- // This function has be defined within the Tempo class (and not imported from another module) because it references a private-variable
580
- /** this will add the self-updating {getter} on the Tempo.term object */
581
- #setTerm(self, name, define, isKeyOnly) {
582
- if (isDefined(name) && isDefined(define)) {
583
- Object.defineProperty(self.#term, name, {
584
- configurable: false,
585
- enumerable: false,
586
- get: function () {
587
- const props = Object.getOwnPropertyDescriptors(self.#term);
588
- self.#term = {}; // wipe down the 'term'
589
- ownEntries(props)
590
- .forEach(([prop, desc]) => {
591
- if (prop !== name) // except the current one
592
- Object.defineProperty(self.#term, prop, desc);
375
+ /** iterate over Tempo properties */
376
+ static [(_Tempo_proto = function _Tempo_proto(obj) { return Object.getPrototypeOf(obj); }, _Tempo_hasOwn = function _Tempo_hasOwn(obj, key) { return Object.hasOwn(obj, key); }, _Tempo_isFrozen = function _Tempo_isFrozen(obj) { return isDefined(obj) && Object.isFrozen(obj); }, _Tempo_isLocal = function _Tempo_isLocal(shape) { return shape.config.scope === 'local'; }, _Tempo_create = function _Tempo_create(obj, name) { return Object.create(__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_proto).call(Tempo, obj)[name]); }, _Tempo_setEvents = function _Tempo_setEvents(shape) {
377
+ const events = ownEntries(shape.parse.event, true);
378
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'event') && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'isMonthDay'))
379
+ return; // no local change needed
380
+ const src = shape.config.scope.substring(0, 1); // 'g'lobal or 'l'ocal
381
+ const groups = events
382
+ .map(([pat, _], idx) => `(?<${src}evt${idx}>${pat})`) // assign a number to the pattern
383
+ .join('|'); // make an 'Or' pattern for the event-keys
384
+ if (groups) {
385
+ const protoEvt = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_proto).call(Tempo, shape.parse.snippet)[Token.evt]?.source;
386
+ if (!__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) || groups !== protoEvt) {
387
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'snippet'))
388
+ shape.parse.snippet = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_create).call(Tempo, shape.parse, 'snippet');
389
+ Object.defineProperty(shape.parse.snippet, Token.evt, {
390
+ value: new RegExp(groups),
391
+ enumerable: true,
392
+ writable: true,
393
+ configurable: true
593
394
  });
594
- const value = define.call(self, isKeyOnly); // evaluate the term range-lookup
595
- Object.defineProperty(self.#term, name, {
596
- value,
597
- configurable: false,
598
- writable: false,
395
+ }
396
+ }
397
+ if (shape.parse.isMonthDay) {
398
+ const protoDt = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_proto).call(Tempo, shape.parse.layout)[Token.dt];
399
+ const localDt = '{mm}{sep}?{dd}({sep}?{yy})?|{mod}?({evt})';
400
+ if (!__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) || localDt !== protoDt) {
401
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'layout'))
402
+ shape.parse.layout = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_create).call(Tempo, shape.parse, 'layout');
403
+ Object.defineProperty(shape.parse.layout, Token.dt, {
404
+ value: localDt,
599
405
  enumerable: true,
406
+ writable: true,
407
+ configurable: true
600
408
  });
601
- secure(self.#term);
602
- return secure(value);
409
+ }
410
+ }
411
+ }, _Tempo_setPeriods = function _Tempo_setPeriods(shape) {
412
+ const periods = ownEntries(shape.parse.period, true);
413
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'period'))
414
+ return; // no local change needed
415
+ const src = shape.config.scope.substring(0, 1); // 'g'lobal or 'l'ocal
416
+ const groups = periods
417
+ .map(([pat, _], idx) => `(?<${src}per${idx}>${pat})`) // {pattern} is the 1st element of the tuple
418
+ .join('|'); // make an 'or' pattern for the period-keys
419
+ if (groups) {
420
+ const protoPer = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_proto).call(Tempo, shape.parse.snippet)[Token.per]?.source;
421
+ if (!__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) || groups !== protoPer) {
422
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'snippet'))
423
+ shape.parse.snippet = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_create).call(Tempo, shape.parse, 'snippet');
424
+ Object.defineProperty(shape.parse.snippet, Token.per, {
425
+ value: new RegExp(groups),
426
+ enumerable: true,
427
+ writable: true,
428
+ configurable: true
429
+ });
430
+ }
431
+ }
432
+ }, _Tempo_isMonthDay = function _Tempo_isMonthDay(shape) {
433
+ const monthDay = [...asArray(__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse.mdyLocales)];
434
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'mdyLocales'))
435
+ monthDay.push(...shape.parse.mdyLocales); // append local mdyLocales (not overwrite global)
436
+ return monthDay.some(mdy => mdy.timeZones?.includes(shape.config.timeZone));
437
+ }, _Tempo_swapLayout = function _Tempo_swapLayout(shape) {
438
+ const layouts = ownEntries(shape.parse.layout); // get entries of Layout Record
439
+ const swap = shape.parse.mdyLayouts; // get the swap-tuple
440
+ let chg = false; // no need to rebuild, if no change
441
+ swap
442
+ .forEach(([dmy, mdy]) => {
443
+ const idx1 = layouts.findIndex(([key]) => key.description === dmy); // 1st swap element exists in {layouts}
444
+ const idx2 = layouts.findIndex(([key]) => key.description === mdy); // 2nd swap element exists in {layouts}
445
+ if (idx1 === -1 || idx2 === -1)
446
+ return; // no pair to swap
447
+ const swap1 = (idx1 < idx2) && shape.parse.isMonthDay; // we prefer {mdy} and the 1st tuple was found earlier than the 2nd
448
+ const swap2 = (idx1 > idx2) && !shape.parse.isMonthDay; // we dont prefer {mdy} and the 1st tuple was found later than the 2nd
449
+ if (swap1 || swap2) { // since {layouts} is an array, ok to swap by-reference
450
+ [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
451
+ chg = true;
603
452
  }
604
453
  });
605
- }
606
- }
607
- // #endregion Constructor
608
- // #region Instance public accessors~~~~~~~~~~~~~~~~~~~~~~
609
- /** 4-digit year (e.g., 2024) */ get yy() { return this.#zdt.year; }
610
- /** Month number: Jan=1, Dec=12 */ get mm() { return this.#zdt.month; }
611
- /** ISO week number of the year */ get ww() { return this.#zdt.weekOfYear; }
612
- /** Day of the month (1-31) */ get dd() { return this.#zdt.day; }
613
- /** Day of the month (alias for `dd`) */ get day() { return this.#zdt.day; }
614
- /** Hour of the day (0-23) */ get hh() { return this.#zdt.hour; }
615
- /** Minutes of the hour (0-59) */ get mi() { return this.#zdt.minute; }
616
- /** Seconds of the minute (0-59) */ get ss() { return this.#zdt.second; }
617
- /** Milliseconds of the second (0-999) */ get ms() { return this.#zdt.millisecond; }
618
- /** Microseconds of the millisecond (0-999) */ get us() { return this.#zdt.microsecond; }
619
- /** Nanoseconds of the microsecond (0-999) */ get ns() { return this.#zdt.nanosecond; }
620
- /** Fractional seconds (e.g., 0.123456789) */ get ff() { return +(`0.${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`); }
621
- /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return this.#zdt.timeZoneId; }
622
- /** Unix timestamp (defaults to milliseconds) */ get ts() { return this.epoch[this.#local.config['timeStamp']]; }
623
- /** Short month name (e.g., 'Jan') */ get mmm() { return Tempo.MONTH.keyOf(this.#zdt.month); }
624
- /** Full month name (e.g., 'January') */ get mon() { return Tempo.MONTHS.keyOf(this.#zdt.month); }
625
- /** Short weekday name (e.g., 'Mon') */ get www() { return Tempo.WEEKDAY.keyOf(this.#zdt.dayOfWeek); }
626
- /** Full weekday name (e.g., 'Monday') */ get wkd() { return Tempo.WEEKDAYS.keyOf(this.#zdt.dayOfWeek); }
627
- /** ISO weekday number: Mon=1, Sun=7 */ get dow() { return this.#zdt.dayOfWeek; }
628
- /** Nanoseconds since Unix epoch (BigInt) */ get nano() { return this.#zdt.epochNanoseconds; }
629
- /** Instance-specific configuration settings */ get config() { return this.#local.config; }
630
- /** Instance-specific parse rules (merged with global) */ get parse() { return this.#local.parse; }
631
- /** Object containing results from all term plugins */ get term() { return this.#term; }
632
- /** Formatted results for all pre-defined format codes */ get fmt() { return this.#fmt; }
633
- /** units since epoch */ get epoch() {
634
- return secure({
635
- /** seconds since epoch */ ss: Math.trunc(this.#zdt.epochMilliseconds / 1_000),
636
- /** milliseconds since epoch */ ms: this.#zdt.epochMilliseconds,
637
- /** microseconds since epoch */ us: Number(this.#zdt.epochNanoseconds / BigInt(1_000)),
638
- /** nanoseconds since epoch */ ns: this.#zdt.epochNanoseconds,
639
- });
640
- }
641
- /** time duration until another date-time */ until(optsOrDate, optsOrUntil) { return this.#until(optsOrDate, optsOrUntil); }
642
- /** time elapsed since another date-time */ since(optsOrDate, optsOrUntil) { return this.#since(optsOrDate, optsOrUntil); }
643
- /** applies a format to the current `Tempo` instance. */ format(fmt) { return this.#format(fmt); }
644
- /** returns a new `Tempo` with specific duration added. */ add(mutate) { return this.#add(mutate); }
645
- /** returns a new `Tempo` with specific offsets. */ set(offset) { return this.#set(offset); }
646
- /** `true` if the underlying date-time is valid. */ isValid() { return !isEmpty(this); }
647
- /** the underlying `Temporal.ZonedDateTime` object. */ toDateTime() { return this.#zdt; }
648
- /** the date-time as a `Temporal.Instant`. */ toInstant() { return this.#now; }
649
- /** the date-time as a standard `Date` object. */ toDate() { return new Date(this.#zdt.round({ smallestUnit: 'millisecond' }).epochMilliseconds); }
650
- /** the ISO8601 string representation of the date-time. */ toString() { return this.#zdt.toString(); }
651
- /** Custom JSON serialization for `JSON.stringify`. */ toJSON() { return { ...this.#local.config, value: this.toString() }; }
652
- // #endregion Instance public methods
653
- // #region Instance private methods~~~~~~~~~~~~~~~~~~~~~~~
654
- /**
655
- * setup local 'config' and 'parse' rules (with prototype set to global)
656
- * we copy down the current global config to the local instance, then apply any options provided.
657
- * in this way, we preserve immutability of this instance, in case the user later changes the global config.
658
- *
659
- * we do not copy down the current global parse rules, but instead create a new parse object
660
- * that prototypes the global parse object. this way, we can add new parse rules to the local
661
- * parse object without affecting the global parse object.
662
- */
663
- #setLocal(options) {
664
- // copy down current global config to local instance
665
- this.#local.config = Object.create(Tempo.#global.config); // set prototype-;ink to global config
666
- const { mdyLocales, mdyLayouts, ...config } = Tempo.#global.config;
667
- Object.assign(this.#local.config, config, { level: 'local' });
668
- // setup effective parse rules for this instance (prototype-link to global)
669
- this.#local.parse = Object.create(Tempo.#global.parse); // set prototype to global parse
670
- this.#local.parse.result = []; // start with empty result
671
- Tempo.#setConfig(this.#local, options); // set #local config
672
- }
673
- /** parse DateTime input */
674
- #parse(tempo, dateTime) {
675
- const timeZone = this.#local.config['timeZone'];
676
- const calendar = this.#local.config['calendar'];
677
- const today = dateTime ?? this.#now // use provided ZonedDateTime, else cast instantiation to current timeZone
678
- .toZonedDateTimeISO(timeZone);
679
- const { type, value } = this.#conform(tempo, today); // if String or Number, conform the input against known patterns
680
- if (isEmpty(this.#local.parse.result)) // #conform() didn't find any matches
681
- this.#local.parse.result = [{ type, value }];
682
- Tempo.#dbg.info(this.#local.config, 'parse', `{type: ${type}, value: ${value}}`); // show what we're parsing
683
- switch (type) {
684
- case 'Null':
685
- case 'Void':
686
- case 'Empty':
687
- case 'Undefined':
688
- return today;
689
- case 'String': // String which didn't conform to a Tempo.pattern
690
- case 'Temporal.ZonedDateTime':
691
- try {
692
- return Temporal.ZonedDateTime.from(value); // see if Temporal can parse value
454
+ if (chg)
455
+ shape.parse.layout = Object.fromEntries(layouts); // rebuild Layout in new parse order
456
+ }, _Tempo_setConfig = function _Tempo_setConfig(shape, ...options) {
457
+ /** helper to normalize snippet/layout Options into the target Config */
458
+ const collect = (target, value, convert) => {
459
+ const itm = asType(value);
460
+ target ??= {};
461
+ switch (itm.type) {
462
+ case 'Object':
463
+ ownEntries(itm.value)
464
+ .forEach(([k, v]) => target[Tempo.getSymbol(k)] = convert(v));
465
+ break;
466
+ case 'String':
467
+ case 'RegExp':
468
+ target[Tempo.getSymbol()] = convert(itm.value);
469
+ break;
470
+ case 'Array':
471
+ itm.value.forEach(elm => collect(target, elm, convert));
472
+ break;
693
473
  }
694
- catch { // else see if Date.parse can parse value
695
- const fallback = { match: 'Date.parse' };
696
- this.#result({ type, value }, fallback);
697
- Tempo.#dbg.warn(this.#local.config, 'Cannot detect DateTime; fallback to Date.parse');
698
- return Temporal.ZonedDateTime
699
- .from(`${new Date(value.toString()).toISOString()}[${timeZone}]`)
700
- .withCalendar(calendar);
474
+ };
475
+ const mergedOptions = Object.assign({}, ...options);
476
+ ownEntries(mergedOptions)
477
+ .forEach(([optKey, optVal]) => {
478
+ if (isUndefined(optVal))
479
+ return; // skip undefined values
480
+ const arg = asType(optVal);
481
+ switch (optKey) {
482
+ case 'snippet':
483
+ case 'layout':
484
+ case 'event':
485
+ case 'period':
486
+ // lazy-shadowing: only create local object if it doesn't already exist on local shape
487
+ if (!__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, optKey))
488
+ shape.parse[optKey] = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_create).call(Tempo, shape.parse, optKey);
489
+ const rule = shape.parse[optKey];
490
+ if (['snippet', 'layout'].includes(optKey)) {
491
+ collect(rule, arg.value, v => optKey === 'snippet'
492
+ ? isRegExp(v) ? v : new RegExp(v)
493
+ : isRegExp(v) ? v.source : v);
494
+ }
495
+ else {
496
+ asArray(arg.value)
497
+ .forEach(elm => ownEntries(elm).forEach(([key, val]) => rule[key] = val));
498
+ }
499
+ break;
500
+ case 'mdyLocales':
501
+ shape.parse[optKey] = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_mdyLocales).call(Tempo, arg.value);
502
+ break;
503
+ case 'mdyLayouts': // these are the 'layouts' that need to swap parse-order
504
+ shape.parse[optKey] = asArray(arg.value);
505
+ break;
506
+ case 'timeZone':
507
+ const zone = String(arg.value).toLowerCase();
508
+ shape.config.timeZone = TimeZone[zone] ?? arg.value;
509
+ break;
510
+ default: // else just add to config
511
+ Object.assign(shape.config, { [optKey]: optVal });
512
+ break;
701
513
  }
702
- case 'Temporal.PlainDate':
703
- case 'Temporal.PlainDateTime':
704
- return value
705
- .toZonedDateTime(timeZone)
706
- .withCalendar(calendar);
707
- case 'Temporal.PlainTime':
708
- return today.withPlainTime(value);
709
- case 'Temporal.PlainYearMonth': // assume current day, else end-of-month
710
- return value
711
- .toPlainDate({ day: Math.min(today.day, value.daysInMonth) })
712
- .toZonedDateTime(timeZone)
713
- .withCalendar(calendar);
714
- case 'Temporal.PlainMonthDay': // assume current year
715
- return value
716
- .toPlainDate({ year: today.year })
717
- .toZonedDateTime(timeZone)
718
- .withCalendar(calendar);
719
- case 'Temporal.Instant':
720
- return value
721
- .toZonedDateTimeISO(timeZone)
722
- .withCalendar(calendar);
723
- case 'Tempo':
724
- return value
725
- .toDateTime(); // clone provided Tempo
726
- case 'Date':
727
- return new Temporal.ZonedDateTime(BigInt(value.getTime() * 1_000_000), timeZone, calendar);
728
- case 'Number': // Number which didn't conform to a Tempo.pattern
729
- const [seconds = BigInt(0), suffix = BigInt(0)] = value.toString().split('.').map(BigInt);
730
- const nano = BigInt(suffix.toString().substring(0, 9).padEnd(9, '0'));
731
- return new Temporal.ZonedDateTime(seconds * BigInt(1_000_000_000) + nano, timeZone, calendar);
732
- case 'BigInt': // BigInt is not conformed against a Tempo.pattern
733
- return new Temporal.ZonedDateTime(value, timeZone, calendar);
734
- default:
735
- Tempo.#dbg.catch(this.#local.config, `Unexpected Tempo parameter type: ${type}, ${String(value)}`);
736
- return today;
737
- }
738
- }
739
- /** check if we've been given a Tempo Options object */
740
- #isOptions(arg) {
741
- return isObject(arg) && ownKeys(arg)
742
- .some(key => ['snippet', 'layout', 'event', 'period', 'mdyLocales', 'mdyLayouts', 'debug', 'catch', 'store', 'pivot'].includes(key));
743
- }
744
- /** check if we've been given a ZonedDateTimeLike object */
745
- #isZonedDateTimeLike(tempo) {
746
- if (!isObject(tempo) || isEmpty(tempo))
747
- return false;
748
- // if it contains any 'options' keys, it's not a ZonedDateTime
749
- const keys = ownKeys(tempo);
750
- if (keys.some(key => ['snippet', 'layout', 'event', 'period', 'mdyLocales', 'mdyLayouts', 'debug', 'catch', 'store', 'pivot'].includes(key)))
751
- return false;
752
- // we include {value} to allow for Tempo instances
753
- return keys
754
- .filter(isString)
755
- .every(key => ['value', 'timeZoneId', 'calendarId', 'year', 'month', 'monthCode', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'timeZone'].includes(key)); // if every key in tempo-object is included in this array
756
- }
757
- /** trace the initial instance pattern-match */
758
- #result(base, ...rest) {
759
- (Tempo.#pending ?? this.#local.parse.result)
760
- .push(Object.assign({}, base, ...rest));
761
- }
762
- /** evaluate 'string | number' input against known patterns */
763
- #conform(tempo, dateTime) {
764
- const arg = asType(tempo);
765
- if (this.#isZonedDateTimeLike(tempo)) { // tempo is ZonedDateTime-ish object (throw away 'value' property)
766
- const { timeZone, calendar, value, ...options } = tempo;
767
- let zdt = !isEmpty(options)
768
- ? dateTime.with({ ...options })
769
- : dateTime;
770
- if (timeZone)
771
- zdt = zdt.withTimeZone(timeZone); // optionally set timeZone
772
- if (calendar)
773
- zdt = zdt.withCalendar(calendar); // optionally set calendar
774
- this.#result({ type: 'Temporal.ZonedDateTimeLike', value: zdt, match: 'Temporal.ZonedDateTimeLike' });
775
- return Object.assign(arg, {
776
- type: 'Temporal.ZonedDateTime', // override {arg.type}
777
- value: zdt,
778
514
  });
515
+ const isMonthDay = __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isMonthDay).call(Tempo, shape);
516
+ if (isMonthDay !== __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_proto).call(Tempo, shape.parse).isMonthDay) // this will always set on 'global', conditionally on 'local'
517
+ shape.parse.isMonthDay = isMonthDay;
518
+ shape.config.sphere = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_setSphere).call(Tempo, shape, mergedOptions);
519
+ if (isDefined(shape.parse.mdyLayouts))
520
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_swapLayout).call(Tempo, shape);
521
+ if (isDefined(shape.parse.event))
522
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setEvents).call(Tempo, shape);
523
+ if (isDefined(shape.parse.period))
524
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setPeriods).call(Tempo, shape);
525
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setPatterns).call(Tempo, shape); // setup Regex DateTime patterns
526
+ }, _Tempo_mdyLocales = function _Tempo_mdyLocales(value) {
527
+ return asArray(value)
528
+ .map(mdy => new Intl.Locale(mdy))
529
+ .map(mdy => ({ locale: mdy.baseName, timeZones: mdy.getTimeZones?.() ?? [] }));
530
+ }, _Tempo_setPatterns = function _Tempo_setPatterns(shape) {
531
+ const snippet = shape.parse.snippet;
532
+ // if local and no snippet or layout overrides, we can just use the prototype's patterns
533
+ if (__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_isLocal).call(Tempo, shape) && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'snippet') && !__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'layout'))
534
+ return;
535
+ // ensure we have our own Map to mutate (shadow if local)
536
+ if (!__classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, shape.parse, 'pattern'))
537
+ shape.parse.pattern = new Map();
538
+ shape.parse.pattern.clear(); // reset {pattern} Map
539
+ for (const [sym, layout] of ownEntries(shape.parse.layout, true))
540
+ shape.parse.pattern.set(sym, Tempo.regexp(layout, snippet));
541
+ }, Symbol.iterator)]() {
542
+ return Tempo.properties[Symbol.iterator](); // static Iterator over array of 'getters'
543
+ }
544
+ // #endregion Static public methods
545
+ // #region Instance symbols~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
546
+ /** allow for auto-convert of Tempo to BigInt */
547
+ [Symbol.toPrimitive](hint) {
548
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).info(this.config, getType(this), '.hint: ', hint);
549
+ return this.nano;
550
+ }
551
+ /** iterate over instance formats */
552
+ [Symbol.iterator]() {
553
+ return ownEntries(this.#fmt)[Symbol.iterator](); // instance Iterator over tuple of FormatType[]
554
+ }
555
+ get [Symbol.toStringTag]() {
556
+ return 'Tempo'; // hard-coded to avoid minification mangling
779
557
  }
780
- if (!isType(arg.value, 'String', 'Number'))
781
- return arg; // only conform String or Number (not BigInt, etc) against known patterns
782
- const value = trimAll(arg.value, Match.strips); // cast as String, remove \( \) and control-characters
783
- if (isString(arg.value)) { // if original value is String
784
- if (isEmpty(value)) { // don't conform empty string
785
- this.#result(arg, { match: 'Empty' });
786
- return Object.assign(arg, { type: 'Empty' });
558
+ // #endregion Instance symbols
559
+ // #region Instance properties~~~~~~~~~~~~~~~~~~~~~~~~~~~~
560
+ /** constructor tempo */ #tempo;
561
+ /** constructor options */ #options = {};
562
+ /** instantiation Temporal Instant */ #now;
563
+ /** underlying Temporal ZonedDateTime */ #zdt;
564
+ /** prebuilt formats, for convenience */ #fmt = {};
565
+ /** instance term plugins */ #term = {};
566
+ /** instance values to complement static values */ #local = {
567
+ /** instance configuration */ config: {},
568
+ /** instance parse rules (only populated when local overrides exist) */ parse: {}
569
+ };
570
+ constructor(tempo, options = {}) {
571
+ this.#now = Temporal.Now.instant(); // stash current Instant
572
+ // swap arguments around, if arg1=Options or Temporal-like
573
+ [this.#tempo, this.#options] = (this.#isOptions(tempo) || this.#isZonedDateTimeLike(tempo))
574
+ ? [tempo?.value, tempo]
575
+ : [tempo, { ...options }];
576
+ // parse the local options looking for overrides to Tempo.#global.config
577
+ this.#setLocal(this.#options);
578
+ // we now have all the info we need to instantiate a new Tempo
579
+ try {
580
+ this.#zdt = this.#parse(this.#tempo); // attempt to interpret the DateTime arg
581
+ if (['iso8601', 'gregory'].includes(this.#local.config['calendar'])) {
582
+ const formats = ownEntries(Tempo.FORMAT);
583
+ for (const [key, val] of formats)
584
+ this.#fmt[key] = this.format(val);
585
+ }
586
+ terms // add the plug-in getters for the pre-defined Terms to the instance
587
+ .forEach(({ key, scope, define }) => {
588
+ this.#setTerm(this, key, define, true); // add a getter which returns the key-field only
589
+ this.#setTerm(this, scope, define, false); // add a getter which returns a range-object
590
+ });
591
+ if (isDefined(__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_pending))) { // are we mutating with 'set()' ?
592
+ this.#local.parse.result = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_pending); // stash collected parse-matches
593
+ __classPrivateFieldSet(Tempo, _classThis, void 0, "f", _Tempo_pending); // and reset mutating-flag
594
+ }
595
+ secure(this.#fmt); // prevent mutations
596
+ secure(this.#local.config);
597
+ secure(this.#local.parse);
787
598
  }
788
- if (isIntegerLike(value)) { // if string representation of BigInt literal
789
- this.#result(arg, { match: 'BigInt' });
790
- return Object.assign(arg, { type: 'BigInt', value: asInteger(value) });
599
+ catch (err) {
600
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.config, `Cannot create Tempo: ${err.message}\n${err.stack}`);
601
+ return {}; // return empty Object
791
602
  }
792
603
  }
793
- else { // else it is a Number
794
- if (value.length <= 7) { // cannot reliably interpret small numbers: might be {ss} or {yymmdd} or {dmmyyyy}
795
- Tempo.#dbg.catch(this.#local.config, 'Cannot safely interpret number with less than 8-digits: use string instead');
796
- return arg;
604
+ // This function has be defined within the Tempo class (and not imported from another module) because it references a private-variable
605
+ /** this will add the self-updating {getter} on the Tempo.term object */
606
+ #setTerm(self, name, define, isKeyOnly) {
607
+ if (isDefined(name) && isDefined(define)) {
608
+ Object.defineProperty(self.#term, name, {
609
+ configurable: false,
610
+ enumerable: false,
611
+ get: function () {
612
+ const props = Object.getOwnPropertyDescriptors(self.#term);
613
+ self.#term = {}; // wipe down the 'term'
614
+ ownEntries(props)
615
+ .forEach(([prop, desc]) => {
616
+ if (prop !== name) // except the current one
617
+ Object.defineProperty(self.#term, prop, desc);
618
+ });
619
+ const value = define.call(self, isKeyOnly); // evaluate the term range-lookup
620
+ Object.defineProperty(self.#term, name, {
621
+ value,
622
+ configurable: false,
623
+ writable: false,
624
+ enumerable: true,
625
+ });
626
+ secure(self.#term);
627
+ return secure(value);
628
+ }
629
+ });
797
630
  }
798
631
  }
799
- if (isUndefined(this.#zdt)) // if first pass
800
- dateTime = dateTime.withPlainTime('00:00:00'); // strip out all time-components
801
- const map = this.#local.parse.pattern;
802
- for (const [sym, pat] of map) {
803
- const groups = this.#parseMatch(pat, value); // determine pattern-match groups
804
- if (isEmpty(groups))
805
- continue; // no match, so skip this iteration
806
- this.#result(arg, { match: sym.description, groups: cleanify(groups) }); // stash the {key} of the pattern that was matched
807
- this.#parseGroups(groups); // mutate the {groups} object
808
- dateTime = this.#parseWeekday(groups, dateTime); // if {weekDay} pattern, calculate a calendar value
809
- dateTime = this.#parseDate(groups, dateTime); // if {calendar}|{event} pattern, translate to date value
810
- dateTime = this.#parseTime(groups, dateTime); // if {clock}|{period} pattern, translate to a time value
811
- /**
812
- * finished analyzing a matched pattern.
813
- * rebuild {arg.value} into a ZonedDateTime
814
- */
815
- Object.assign(arg, { type: 'Temporal.ZonedDateTime', value: dateTime });
816
- Tempo.#dbg.info(this.#local.config, 'groups', groups); // show the resolved date-time elements
817
- Tempo.#dbg.info(this.#local.config, 'pattern', sym.description); // show the pattern that was matched
818
- break; // stop checking patterns
632
+ // #endregion Constructor
633
+ // #region Instance public accessors~~~~~~~~~~~~~~~~~~~~~~
634
+ /** 4-digit year (e.g., 2024) */ get yy() { return this.#zdt.year; }
635
+ /** Month number: Jan=1, Dec=12 */ get mm() { return this.#zdt.month; }
636
+ /** ISO week number of the year */ get ww() { return this.#zdt.weekOfYear; }
637
+ /** Day of the month (1-31) */ get dd() { return this.#zdt.day; }
638
+ /** Day of the month (alias for `dd`) */ get day() { return this.#zdt.day; }
639
+ /** Hour of the day (0-23) */ get hh() { return this.#zdt.hour; }
640
+ /** Minutes of the hour (0-59) */ get mi() { return this.#zdt.minute; }
641
+ /** Seconds of the minute (0-59) */ get ss() { return this.#zdt.second; }
642
+ /** Milliseconds of the second (0-999) */ get ms() { return this.#zdt.millisecond; }
643
+ /** Microseconds of the millisecond (0-999) */ get us() { return this.#zdt.microsecond; }
644
+ /** Nanoseconds of the microsecond (0-999) */ get ns() { return this.#zdt.nanosecond; }
645
+ /** Fractional seconds (e.g., 0.123456789) */ get ff() { return +(`0.${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`); }
646
+ /** IANA Time Zone ID (e.g., 'Australia/Sydney') */ get tz() { return this.#zdt.timeZoneId; }
647
+ /** Unix timestamp (defaults to milliseconds) */ get ts() { return this.epoch[this.#local.config['timeStamp']]; }
648
+ /** Short month name (e.g., 'Jan') */ get mmm() { return Tempo.MONTH.keyOf(this.#zdt.month); }
649
+ /** Full month name (e.g., 'January') */ get mon() { return Tempo.MONTHS.keyOf(this.#zdt.month); }
650
+ /** Short weekday name (e.g., 'Mon') */ get www() { return Tempo.WEEKDAY.keyOf(this.#zdt.dayOfWeek); }
651
+ /** Full weekday name (e.g., 'Monday') */ get wkd() { return Tempo.WEEKDAYS.keyOf(this.#zdt.dayOfWeek); }
652
+ /** ISO weekday number: Mon=1, Sun=7 */ get dow() { return this.#zdt.dayOfWeek; }
653
+ /** Nanoseconds since Unix epoch (BigInt) */ get nano() { return this.#zdt.epochNanoseconds; }
654
+ /** Instance-specific configuration settings */ get config() { return this.#local.config; }
655
+ /** Instance-specific parse rules (merged with global) */ get parse() { return this.#local.parse; }
656
+ /** Object containing results from all term plugins */ get term() { return this.#term; }
657
+ /** Formatted results for all pre-defined format codes */ get fmt() { return this.#fmt; }
658
+ /** units since epoch */ get epoch() {
659
+ return secure({
660
+ /** seconds since epoch */ ss: Math.trunc(this.#zdt.epochMilliseconds / 1_000),
661
+ /** milliseconds since epoch */ ms: this.#zdt.epochMilliseconds,
662
+ /** microseconds since epoch */ us: Number(this.#zdt.epochNanoseconds / BigInt(1_000)),
663
+ /** nanoseconds since epoch */ ns: this.#zdt.epochNanoseconds,
664
+ });
819
665
  }
820
- return arg;
821
- }
822
- /** apply a regex-match against a value, and clean the result */
823
- #parseMatch(pat, value) {
824
- const groups = value.toString().match(pat)?.groups || {};
825
- ownEntries(groups) // remove undefined, NaN, null and empty values
826
- .forEach(([key, val]) => isEmpty(val) && delete groups[key]);
827
- return groups;
828
- }
829
- /**
830
- * resolve any {event} | {period} to their date | time values,
831
- * intercept any {month} string,
832
- * set default {nbr} if {mod} present,
833
- * Note: this will mutate the {groups} object
834
- */
835
- #parseGroups(groups) {
836
- // fix {event}
837
- const event = ownKeys(groups).find(key => key.match(Match.event));
838
- if (event) {
839
- const idx = +event.substring(4); // number index of the {event}
840
- const src = event.startsWith('g') ? Tempo.#global.parse.event : this.#local.parse.event;
841
- const [_key, evt] = ownEntries(src, true)[idx]; // fetch the indexed tuple's value
842
- Object.assign(groups, this.#parseEvent(evt)); // determine the date-values for the {event}
843
- delete groups[event];
844
- const { yy, mm, dd } = groups;
845
- if (isEmpty(yy) && isEmpty(mm) && isEmpty(dd))
846
- return Tempo.#dbg.catch(this.#local.config, `Cannot determine a {date} or {event} from "${evt}"`);
666
+ /** time duration until another date-time */ until(optsOrDate, optsOrUntil) { return this.#until(optsOrDate, optsOrUntil); }
667
+ /** time elapsed since another date-time */ since(optsOrDate, optsOrUntil) { return this.#since(optsOrDate, optsOrUntil); }
668
+ /** applies a format to the current `Tempo` instance. */ format(fmt) { return this.#format(fmt); }
669
+ /** returns a new `Tempo` with specific duration added. */ add(mutate) { return this.#add(mutate); }
670
+ /** returns a new `Tempo` with specific offsets. */ set(offset) { return this.#set(offset); }
671
+ /** `true` if the underlying date-time is valid. */ isValid() { return !isEmpty(this); }
672
+ /** the underlying `Temporal.ZonedDateTime` object. */ toDateTime() { return this.#zdt; }
673
+ /** the date-time as a `Temporal.Instant`. */ toInstant() { return this.#now; }
674
+ /** the date-time as a standard `Date` object. */ toDate() { return new Date(this.#zdt.round({ smallestUnit: 'millisecond' }).epochMilliseconds); }
675
+ /** the ISO8601 string representation of the date-time. */ toString() { return this.#zdt.toString(); }
676
+ /** Custom JSON serialization for `JSON.stringify`. */ toJSON() { return { ...this.#local.config, value: this.toString() }; }
677
+ // #endregion Instance public methods
678
+ // #region Instance private methods~~~~~~~~~~~~~~~~~~~~~~~
679
+ /**
680
+ * setup local 'config' and 'parse' rules (with prototype set to global)
681
+ * we copy down the current global config to the local instance, then apply any options provided.
682
+ * in this way, we preserve immutability of this instance, in case the user later changes the global config.
683
+ *
684
+ * we do not copy down the current global parse rules, but instead create a new parse object
685
+ * that prototypes the global parse object. this way, we can add new parse rules to the local
686
+ * parse object without affecting the global parse object.
687
+ */
688
+ #setLocal(options) {
689
+ // copy down current global config to local instance
690
+ this.#local.config = Object.create(__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config); // set prototype-;ink to global config
691
+ const { mdyLocales, mdyLayouts, ...config } = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).config;
692
+ Object.assign(this.#local.config, config, { level: 'local' });
693
+ // setup effective parse rules for this instance (prototype-link to global)
694
+ this.#local.parse = Object.create(__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse); // set prototype to global parse
695
+ this.#local.parse.result = []; // start with empty result
696
+ __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_setConfig).call(Tempo, this.#local, options); // set #local config
847
697
  }
848
- // fix {period}
849
- const period = ownKeys(groups).find(key => key.match(Match.period));
850
- if (period) {
851
- const idx = +period.substring(4); // number index of the {period}
852
- const src = period.startsWith('g') ? Tempo.#global.parse.period : this.#local.parse.period;
853
- const [_key, per] = ownEntries(src, true)[idx]; // fetch the indexed tuple's value
854
- Object.assign(groups, this.#parsePeriod(per)); // determine the time-values for the {period}
855
- delete groups[period];
856
- if (isEmpty(groups["hh"])) // must have at-least {hh} time-component
857
- return Tempo.#dbg.catch(this.#local.config, `Cannot determine a {time} or {period} from "${per}"`);
698
+ /** parse DateTime input */
699
+ #parse(tempo, dateTime) {
700
+ const timeZone = this.#local.config['timeZone'];
701
+ const calendar = this.#local.config['calendar'];
702
+ const today = dateTime ?? this.#now // use provided ZonedDateTime, else cast instantiation to current timeZone
703
+ .toZonedDateTimeISO(timeZone);
704
+ const { type, value } = this.#conform(tempo, today); // if String or Number, conform the input against known patterns
705
+ if (isEmpty(this.#local.parse.result)) // #conform() didn't find any matches
706
+ this.#local.parse.result = [{ type, value }];
707
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).info(this.#local.config, 'parse', `{type: ${type}, value: ${value}}`); // show what we're parsing
708
+ switch (type) {
709
+ case 'Null':
710
+ case 'Void':
711
+ case 'Empty':
712
+ case 'Undefined':
713
+ return today;
714
+ case 'String': // String which didn't conform to a Tempo.pattern
715
+ case 'Temporal.ZonedDateTime':
716
+ try {
717
+ return Temporal.ZonedDateTime.from(value); // see if Temporal can parse value
718
+ }
719
+ catch { // else see if Date.parse can parse value
720
+ const fallback = { match: 'Date.parse' };
721
+ this.#result({ type, value }, fallback);
722
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).warn(this.#local.config, 'Cannot detect DateTime; fallback to Date.parse');
723
+ return Temporal.ZonedDateTime
724
+ .from(`${new Date(value.toString()).toISOString()}[${timeZone}]`)
725
+ .withCalendar(calendar);
726
+ }
727
+ case 'Temporal.PlainDate':
728
+ case 'Temporal.PlainDateTime':
729
+ return value
730
+ .toZonedDateTime(timeZone)
731
+ .withCalendar(calendar);
732
+ case 'Temporal.PlainTime':
733
+ return today.withPlainTime(value);
734
+ case 'Temporal.PlainYearMonth': // assume current day, else end-of-month
735
+ return value
736
+ .toPlainDate({ day: Math.min(today.day, value.daysInMonth) })
737
+ .toZonedDateTime(timeZone)
738
+ .withCalendar(calendar);
739
+ case 'Temporal.PlainMonthDay': // assume current year
740
+ return value
741
+ .toPlainDate({ year: today.year })
742
+ .toZonedDateTime(timeZone)
743
+ .withCalendar(calendar);
744
+ case 'Temporal.Instant':
745
+ return value
746
+ .toZonedDateTimeISO(timeZone)
747
+ .withCalendar(calendar);
748
+ case 'Tempo':
749
+ return value
750
+ .toDateTime(); // clone provided Tempo
751
+ case 'Date':
752
+ return new Temporal.ZonedDateTime(BigInt(value.getTime() * 1_000_000), timeZone, calendar);
753
+ case 'Number': // Number which didn't conform to a Tempo.pattern
754
+ const [seconds = BigInt(0), suffix = BigInt(0)] = value.toString().split('.').map(BigInt);
755
+ const nano = BigInt(suffix.toString().substring(0, 9).padEnd(9, '0'));
756
+ return new Temporal.ZonedDateTime(seconds * BigInt(1_000_000_000) + nano, timeZone, calendar);
757
+ case 'BigInt': // BigInt is not conformed against a Tempo.pattern
758
+ return new Temporal.ZonedDateTime(value, timeZone, calendar);
759
+ default:
760
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Unexpected Tempo parameter type: ${type}, ${String(value)}`);
761
+ return today;
762
+ }
858
763
  }
859
- // fix {mm}
860
- if (isDefined(groups["mm"]) && !isNumeric(groups["mm"])) {
861
- const mm = Tempo.#prefix(groups["mm"]); // conform month-name
862
- groups["mm"] = Tempo.MONTH.keys()
863
- .findIndex(el => el === mm) // resolve month-name into a month-number
864
- .toString() // (some browsers do not allow month-names when parsing a Date)
865
- .padStart(2, '0');
764
+ /** check if we've been given a Tempo Options object */
765
+ #isOptions(arg) {
766
+ return isObject(arg) && ownKeys(arg)
767
+ .some(key => ['snippet', 'layout', 'event', 'period', 'mdyLocales', 'mdyLayouts', 'debug', 'catch', 'store', 'pivot'].includes(key));
866
768
  }
867
- // fix {rdt}
868
- if (isDefined(groups["rdt"])) {
869
- const idx = ['yesterday', 'tomorrow', 'today'].indexOf(groups["rdt"]);
870
- const val = [Event['yesterday'], Event['tomorrow'], Event['today']][idx];
871
- const zdt = val.bind(this)();
872
- Object.assign(groups, {
873
- yy: zdt.year.toString(),
874
- mm: zdt.month.toString().padStart(2, '0'),
875
- dd: zdt.day.toString().padStart(2, '0'),
876
- });
877
- delete groups["rdt"];
769
+ /** check if we've been given a ZonedDateTimeLike object */
770
+ #isZonedDateTimeLike(tempo) {
771
+ if (!isObject(tempo) || isEmpty(tempo))
772
+ return false;
773
+ // if it contains any 'options' keys, it's not a ZonedDateTime
774
+ const keys = ownKeys(tempo);
775
+ if (keys.some(key => ['snippet', 'layout', 'event', 'period', 'mdyLocales', 'mdyLayouts', 'debug', 'catch', 'store', 'pivot'].includes(key)))
776
+ return false;
777
+ // we include {value} to allow for Tempo instances
778
+ return keys
779
+ .filter(isString)
780
+ .every(key => ['value', 'timeZoneId', 'calendarId', 'year', 'month', 'monthCode', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'timeZone'].includes(key)); // if every key in tempo-object is included in this array
878
781
  }
879
- return groups;
880
- }
881
- /**
882
- * We expect similar offset-logic to apply to 'modifiers' when parsing a string DateTime.
883
- * returns {adjust} to make, based on {modifier}, {offset}, and {period}
884
- * - previous period
885
- * + next period
886
- * -3 three periods ago
887
- * < prior to base-date (asIs)
888
- * <= prior to base-date (plus one)
889
- */
890
- #parseModifier({ mod, adjust, offset, period }) {
891
- adjust = Math.abs(adjust);
892
- switch (mod) {
893
- case void 0: // no adjustment
894
- case '=':
895
- case 'this': // current period
896
- return 0;
897
- case '+': // next period
898
- case 'next':
899
- return adjust;
900
- case '-': // previous period
901
- case 'prev':
902
- case 'last':
903
- return -adjust;
904
- case '<': // period before base-date
905
- case 'ago':
906
- return (period <= offset)
907
- ? -adjust
908
- : -(adjust - 1);
909
- case '<=': // period before or including base-date
910
- return (period < offset)
911
- ? -adjust
912
- : -(adjust - 1);
913
- case '>': // period after base-date
914
- case 'hence':
915
- return (period > offset)
916
- ? adjust
917
- : (adjust - 1);
918
- case '>=': // period after or including base-date
919
- case '+=':
920
- return (period >= offset)
921
- ? adjust
922
- : (adjust - 1);
923
- default: // unexpected modifier
924
- return 0;
782
+ /** trace the initial instance pattern-match */
783
+ #result(base, ...rest) {
784
+ (__classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_pending) ?? this.#local.parse.result)
785
+ .push(Object.assign({}, base, ...rest));
786
+ }
787
+ /** evaluate 'string | number' input against known patterns */
788
+ #conform(tempo, dateTime) {
789
+ const arg = asType(tempo);
790
+ if (this.#isZonedDateTimeLike(tempo)) { // tempo is ZonedDateTime-ish object (throw away 'value' property)
791
+ const { timeZone, calendar, value, ...options } = tempo;
792
+ let zdt = !isEmpty(options)
793
+ ? dateTime.with({ ...options })
794
+ : dateTime;
795
+ if (timeZone)
796
+ zdt = zdt.withTimeZone(timeZone); // optionally set timeZone
797
+ if (calendar)
798
+ zdt = zdt.withCalendar(calendar); // optionally set calendar
799
+ this.#result({ type: 'Temporal.ZonedDateTimeLike', value: zdt, match: 'Temporal.ZonedDateTimeLike' });
800
+ return Object.assign(arg, {
801
+ type: 'Temporal.ZonedDateTime', // override {arg.type}
802
+ value: zdt,
803
+ });
804
+ }
805
+ if (!isType(arg.value, 'String', 'Number'))
806
+ return arg; // only conform String or Number (not BigInt, etc) against known patterns
807
+ const value = trimAll(arg.value, Match.strips); // cast as String, remove \( \) and control-characters
808
+ if (isString(arg.value)) { // if original value is String
809
+ if (isEmpty(value)) { // don't conform empty string
810
+ this.#result(arg, { match: 'Empty' });
811
+ return Object.assign(arg, { type: 'Empty' });
812
+ }
813
+ if (isIntegerLike(value)) { // if string representation of BigInt literal
814
+ this.#result(arg, { match: 'BigInt' });
815
+ return Object.assign(arg, { type: 'BigInt', value: asInteger(value) });
816
+ }
817
+ }
818
+ else { // else it is a Number
819
+ if (value.length <= 7) { // cannot reliably interpret small numbers: might be {ss} or {yymmdd} or {dmmyyyy}
820
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, 'Cannot safely interpret number with less than 8-digits: use string instead');
821
+ return arg;
822
+ }
823
+ }
824
+ if (isUndefined(this.#zdt)) // if first pass
825
+ dateTime = dateTime.withPlainTime('00:00:00'); // strip out all time-components
826
+ const map = this.#local.parse.pattern;
827
+ for (const [sym, pat] of map) {
828
+ const groups = this.#parseMatch(pat, value); // determine pattern-match groups
829
+ if (isEmpty(groups))
830
+ continue; // no match, so skip this iteration
831
+ this.#result(arg, { match: sym.description, groups: cleanify(groups) }); // stash the {key} of the pattern that was matched
832
+ this.#parseGroups(groups); // mutate the {groups} object
833
+ dateTime = this.#parseWeekday(groups, dateTime); // if {weekDay} pattern, calculate a calendar value
834
+ dateTime = this.#parseDate(groups, dateTime); // if {calendar}|{event} pattern, translate to date value
835
+ dateTime = this.#parseTime(groups, dateTime); // if {clock}|{period} pattern, translate to a time value
836
+ /**
837
+ * finished analyzing a matched pattern.
838
+ * rebuild {arg.value} into a ZonedDateTime
839
+ */
840
+ Object.assign(arg, { type: 'Temporal.ZonedDateTime', value: dateTime });
841
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).info(this.#local.config, 'groups', groups); // show the resolved date-time elements
842
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).info(this.#local.config, 'pattern', sym.description); // show the pattern that was matched
843
+ break; // stop checking patterns
844
+ }
845
+ return arg;
846
+ }
847
+ /** apply a regex-match against a value, and clean the result */
848
+ #parseMatch(pat, value) {
849
+ const groups = value.toString().match(pat)?.groups || {};
850
+ ownEntries(groups) // remove undefined, NaN, null and empty values
851
+ .forEach(([key, val]) => isEmpty(val) && delete groups[key]);
852
+ return groups;
925
853
  }
926
- }
927
- /**
928
- * if named-group 'wkd' detected (with optional 'mod', 'nbr', or time-units), then calc relative weekday offset
929
- * | Example | Result | Note |
930
- * | :--- | :---- | :---- |
931
- * | `Wed` | Wed this week | might be earlier or later or equal to current day |
932
- * | `-Wed` | Wed last week | same as new Tempo('Wed').add({ weeks: -1 }) |
933
- * | `+Wed` | Wed next week | same as new Tempo('Wed').add({ weeks: 1 }) |
934
- * | `-3Wed` | Wed three weeks ago | same as new Tempo('Wed').add({ weeks: -3 }) |
935
- * | `<Wed` | Wed prior to today | might be current or previous week |
936
- * | `<=Wed` | Wed prior to tomorrow | might be current or previous week |
937
- * | `Wed noon` | Wed this week at 12:00pm | even though time-periods may be present, ignore them in this method |
938
- *
939
- * @returns ZonedDateTime with computed date-offset
940
- */
941
- #parseWeekday(groups, dateTime) {
942
- const { wkd, mod, nbr = '1', sfx, ...rest } = groups;
943
- if (isUndefined(wkd)) // this is not a true {weekDay} pattern match
944
- return dateTime;
945
854
  /**
946
- * the {weekDay} pattern should only have keys of {wkd}, {mod}, {nbr}, {sfx} (and optionally time-units)
947
- * for example: {wkd: 'Wed', mod: '>', hh: '10', mer: 'pm'}
948
- * we early-exit if we find anything other than time-units
855
+ * resolve any {event} | {period} to their date | time values,
856
+ * intercept any {month} string,
857
+ * set default {nbr} if {mod} present,
858
+ * Note: this will mutate the {groups} object
949
859
  */
950
- const time = ['hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'mer'];
951
- if (!ownKeys(rest)
952
- .every(key => time.includes(key))) // non 'time-unit' keys detected
953
- return dateTime; // this is not a true {weekDay} pattern, so early-exit
954
- if (!isEmpty(mod) && !isEmpty(sfx)) {
955
- Tempo.#dbg.warn(`Cannot provide both a modifier '${mod}' and suffix '${sfx}'`);
956
- return dateTime; // cannot provide both 'modifier' and 'suffix'
860
+ #parseGroups(groups) {
861
+ // fix {event}
862
+ const event = ownKeys(groups).find(key => key.match(Match.event));
863
+ if (event) {
864
+ const idx = +event.substring(4); // number index of the {event}
865
+ const src = event.startsWith('g') ? __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse.event : this.#local.parse.event;
866
+ const [_key, evt] = ownEntries(src, true)[idx]; // fetch the indexed tuple's value
867
+ Object.assign(groups, this.#parseEvent(evt)); // determine the date-values for the {event}
868
+ delete groups[event];
869
+ const { yy, mm, dd } = groups;
870
+ if (isEmpty(yy) && isEmpty(mm) && isEmpty(dd))
871
+ return __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Cannot determine a {date} or {event} from "${evt}"`);
872
+ }
873
+ // fix {period}
874
+ const period = ownKeys(groups).find(key => key.match(Match.period));
875
+ if (period) {
876
+ const idx = +period.substring(4); // number index of the {period}
877
+ const src = period.startsWith('g') ? __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_global).parse.period : this.#local.parse.period;
878
+ const [_key, per] = ownEntries(src, true)[idx]; // fetch the indexed tuple's value
879
+ Object.assign(groups, this.#parsePeriod(per)); // determine the time-values for the {period}
880
+ delete groups[period];
881
+ if (isEmpty(groups["hh"])) // must have at-least {hh} time-component
882
+ return __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Cannot determine a {time} or {period} from "${per}"`);
883
+ }
884
+ // fix {mm}
885
+ if (isDefined(groups["mm"]) && !isNumeric(groups["mm"])) {
886
+ const mm = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_prefix).call(Tempo, groups["mm"]); // conform month-name
887
+ groups["mm"] = Tempo.MONTH.keys()
888
+ .findIndex(el => el === mm) // resolve month-name into a month-number
889
+ .toString() // (some browsers do not allow month-names when parsing a Date)
890
+ .padStart(2, '0');
891
+ }
892
+ // fix {rdt}
893
+ if (isDefined(groups["rdt"])) {
894
+ const idx = ['yesterday', 'tomorrow', 'today'].indexOf(groups["rdt"]);
895
+ const val = [Event['yesterday'], Event['tomorrow'], Event['today']][idx];
896
+ const zdt = val.bind(this)();
897
+ Object.assign(groups, {
898
+ yy: zdt.year.toString(),
899
+ mm: zdt.month.toString().padStart(2, '0'),
900
+ dd: zdt.day.toString().padStart(2, '0'),
901
+ });
902
+ delete groups["rdt"];
903
+ }
904
+ return groups;
957
905
  }
958
- const weekday = Tempo.#prefix(wkd); // conform weekday-name
959
- const adjust = +nbr; // how many weeks to adjust
960
- const offset = Tempo.WEEKDAY.keys() // how far weekday is from today
961
- .findIndex(el => el === weekday);
962
- const days = offset - dateTime.dayOfWeek // number of days to offset from dateTime
963
- + (this.#parseModifier({ mod: mod ?? sfx, adjust, offset, period: dateTime.dayOfWeek }) * dateTime.daysInWeek);
964
- delete groups["wkd"];
965
- delete groups["mod"];
966
- delete groups["nbr"];
967
- delete groups["sfx"];
968
- return dateTime
969
- .add({ days }); // set new {day}
970
- }
971
- /**
972
- * match input against date patterns
973
- * @returns adjusted ZonedDateTime with resolved time-components
974
- */
975
- #parseDate(groups, dateTime) {
976
- const { mod, nbr = '1', afx, unt, yy, mm, dd } = groups;
977
- if (isEmpty(yy) && isEmpty(mm) && isEmpty(dd) && isUndefined(unt))
978
- return dateTime; // return default
979
- if (!isEmpty(mod) && !isEmpty(afx)) {
980
- Tempo.#dbg.warn(`Cannot provide both a modifier '${mod}' and suffix '${afx}'`);
981
- return dateTime;
906
+ /**
907
+ * We expect similar offset-logic to apply to 'modifiers' when parsing a string DateTime.
908
+ * returns {adjust} to make, based on {modifier}, {offset}, and {period}
909
+ * - previous period
910
+ * + next period
911
+ * -3 three periods ago
912
+ * < prior to base-date (asIs)
913
+ * <= prior to base-date (plus one)
914
+ */
915
+ #parseModifier({ mod, adjust, offset, period }) {
916
+ adjust = Math.abs(adjust);
917
+ switch (mod) {
918
+ case void 0: // no adjustment
919
+ case '=':
920
+ case 'this': // current period
921
+ return 0;
922
+ case '+': // next period
923
+ case 'next':
924
+ return adjust;
925
+ case '-': // previous period
926
+ case 'prev':
927
+ case 'last':
928
+ return -adjust;
929
+ case '<': // period before base-date
930
+ case 'ago':
931
+ return (period <= offset)
932
+ ? -adjust
933
+ : -(adjust - 1);
934
+ case '<=': // period before or including base-date
935
+ return (period < offset)
936
+ ? -adjust
937
+ : -(adjust - 1);
938
+ case '>': // period after base-date
939
+ case 'hence':
940
+ return (period > offset)
941
+ ? adjust
942
+ : (adjust - 1);
943
+ case '>=': // period after or including base-date
944
+ case '+=':
945
+ return (period >= offset)
946
+ ? adjust
947
+ : (adjust - 1);
948
+ default: // unexpected modifier
949
+ return 0;
950
+ }
982
951
  }
983
- let { year, month, day } = this.#num({
984
- year: yy ?? dateTime.year, // supplied year, else current year
985
- month: mm ?? dateTime.month, // supplied month, else current month
986
- day: dd ?? dateTime.day, // supplied day, else current day
987
- });
988
- // handle {unt} relative offset (e.g. '2 days ago')
989
- if (unt) {
990
- const adjust = +nbr;
991
- const direction = (mod === '<' || mod === '-' || afx === 'ago') ? -1 : 1;
992
- const plural = singular(unt) + 's';
993
- dateTime = dateTime.add({ [plural]: adjust * direction });
994
- delete groups["unt"];
995
- delete groups["nbr"];
996
- delete groups["afx"];
952
+ /**
953
+ * if named-group 'wkd' detected (with optional 'mod', 'nbr', or time-units), then calc relative weekday offset
954
+ * | Example | Result | Note |
955
+ * | :--- | :---- | :---- |
956
+ * | `Wed` | Wed this week | might be earlier or later or equal to current day |
957
+ * | `-Wed` | Wed last week | same as new Tempo('Wed').add({ weeks: -1 }) |
958
+ * | `+Wed` | Wed next week | same as new Tempo('Wed').add({ weeks: 1 }) |
959
+ * | `-3Wed` | Wed three weeks ago | same as new Tempo('Wed').add({ weeks: -3 }) |
960
+ * | `<Wed` | Wed prior to today | might be current or previous week |
961
+ * | `<=Wed` | Wed prior to tomorrow | might be current or previous week |
962
+ * | `Wed noon` | Wed this week at 12:00pm | even though time-periods may be present, ignore them in this method |
963
+ *
964
+ * @returns ZonedDateTime with computed date-offset
965
+ */
966
+ #parseWeekday(groups, dateTime) {
967
+ const { wkd, mod, nbr = '1', sfx, ...rest } = groups;
968
+ if (isUndefined(wkd)) // this is not a true {weekDay} pattern match
969
+ return dateTime;
970
+ /**
971
+ * the {weekDay} pattern should only have keys of {wkd}, {mod}, {nbr}, {sfx} (and optionally time-units)
972
+ * for example: {wkd: 'Wed', mod: '>', hh: '10', mer: 'pm'}
973
+ * we early-exit if we find anything other than time-units
974
+ */
975
+ const time = ['hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'mer'];
976
+ if (!ownKeys(rest)
977
+ .every(key => time.includes(key))) // non 'time-unit' keys detected
978
+ return dateTime; // this is not a true {weekDay} pattern, so early-exit
979
+ if (!isEmpty(mod) && !isEmpty(sfx)) {
980
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).warn(`Cannot provide both a modifier '${mod}' and suffix '${sfx}'`);
981
+ return dateTime; // cannot provide both 'modifier' and 'suffix'
982
+ }
983
+ const weekday = __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_prefix).call(Tempo, wkd); // conform weekday-name
984
+ const adjust = +nbr; // how many weeks to adjust
985
+ const offset = Tempo.WEEKDAY.keys() // how far weekday is from today
986
+ .findIndex(el => el === weekday);
987
+ const days = offset - dateTime.dayOfWeek // number of days to offset from dateTime
988
+ + (this.#parseModifier({ mod: mod ?? sfx, adjust, offset, period: dateTime.dayOfWeek }) * dateTime.daysInWeek);
989
+ delete groups["wkd"];
997
990
  delete groups["mod"];
998
- return dateTime;
991
+ delete groups["nbr"];
992
+ delete groups["sfx"];
993
+ return dateTime
994
+ .add({ days }); // set new {day}
999
995
  }
1000
996
  /**
1001
- * change two-digit year into four-digits using 'pivot-year' (defaulted to '75' years) to determine century
1002
- * pivot = (currYear - Tempo.pivot) % 100 // for example: Rem((2024 - 75) / 100) => 49
1003
- * century = Int(currYear / 100) // for example: Int(2024 / 100) => 20
1004
- * 22 => 2022 // 22 is less than pivot, so use {century}
1005
- * 57 => 1957 // 57 is more than pivot, so use {century - 1}
997
+ * match input against date patterns
998
+ * @returns adjusted ZonedDateTime with resolved time-components
1006
999
  */
1007
- if (year.toString().match(Match.twoDigit)) { // if {year} match just-two digits
1008
- const pivot = dateTime
1009
- .subtract({ years: this.#local.config['pivot'] }) // pivot cutoff to determine century
1010
- .year % 100; // remainder
1011
- const century = Math.trunc(dateTime.year / 100); // current century
1012
- year += (century - Number(year >= pivot)) * 100; // now a four-digit year
1013
- }
1014
- // adjust the {year} if a Modifier is present
1015
- const adjust = +nbr; // how many years to adjust
1016
- const offset = Number(pad(month) + '.' + pad(day)); // the event month.day
1017
- const period = Number(pad(dateTime.month) + '.' + pad(dateTime.day + 1));
1018
- year += this.#parseModifier({ mod: mod ?? afx, adjust, offset, period });
1019
- Object.assign(groups, { yy: year, mm: month, dd: day });
1020
- delete groups["mod"];
1021
- delete groups["nbr"];
1022
- delete groups["afx"];
1023
- // all date-components are now set; check for overflow in case past end-of-month
1024
- return Temporal.PlainDate.from({ year, month, day }, { overflow: 'constrain' })
1025
- .toZonedDateTime(dateTime.timeZoneId) // adjust to constrained date
1026
- .withPlainTime(dateTime.toPlainTime()); // restore the time
1027
- }
1028
- /**
1029
- * match input against 'tm' pattern.
1030
- * {groups} is expected to contain time-components (like {hh:'3', mi:'30', mer:'pm'}).
1031
- * returns an adjusted ZonedDateTime
1032
- */
1033
- #parseTime(groups = {}, dateTime) {
1034
- if (isUndefined(groups["hh"])) // must contain 'time' with at least {hh}
1035
- return dateTime;
1036
- let { hh = 0, mi = 0, ss = 0, ms = 0, us = 0, ns = 0 } = this.#num(groups);
1037
- if (hh >= 24) {
1038
- dateTime = dateTime.add({ days: Math.trunc(hh / 24) }); // move the date forward number of days to offset
1039
- hh %= 24; // midnight is '00:00' on the next-day
1040
- }
1041
- if (isDefined(groups["ff"])) { // {ff} is fractional seconds and overrides {ms|us|ns}
1042
- const ff = groups["ff"].substring(0, 9).padEnd(9, '0');
1043
- ms = +ff.substring(0, 3);
1044
- us = +ff.substring(3, 6);
1045
- ns = +ff.substring(6, 9);
1000
+ #parseDate(groups, dateTime) {
1001
+ const { mod, nbr = '1', afx, unt, yy, mm, dd } = groups;
1002
+ if (isEmpty(yy) && isEmpty(mm) && isEmpty(dd) && isUndefined(unt))
1003
+ return dateTime; // return default
1004
+ if (!isEmpty(mod) && !isEmpty(afx)) {
1005
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).warn(`Cannot provide both a modifier '${mod}' and suffix '${afx}'`);
1006
+ return dateTime;
1007
+ }
1008
+ let { year, month, day } = this.#num({
1009
+ year: yy ?? dateTime.year, // supplied year, else current year
1010
+ month: mm ?? dateTime.month, // supplied month, else current month
1011
+ day: dd ?? dateTime.day, // supplied day, else current day
1012
+ });
1013
+ // handle {unt} relative offset (e.g. '2 days ago')
1014
+ if (unt) {
1015
+ const adjust = +nbr;
1016
+ const direction = (mod === '<' || mod === '-' || afx === 'ago') ? -1 : 1;
1017
+ const plural = singular(unt) + 's';
1018
+ dateTime = dateTime.add({ [plural]: adjust * direction });
1019
+ delete groups["unt"];
1020
+ delete groups["nbr"];
1021
+ delete groups["afx"];
1022
+ delete groups["mod"];
1023
+ return dateTime;
1024
+ }
1025
+ /**
1026
+ * change two-digit year into four-digits using 'pivot-year' (defaulted to '75' years) to determine century
1027
+ * pivot = (currYear - Tempo.pivot) % 100 // for example: Rem((2024 - 75) / 100) => 49
1028
+ * century = Int(currYear / 100) // for example: Int(2024 / 100) => 20
1029
+ * 22 => 2022 // 22 is less than pivot, so use {century}
1030
+ * 57 => 1957 // 57 is more than pivot, so use {century - 1}
1031
+ */
1032
+ if (year.toString().match(Match.twoDigit)) { // if {year} match just-two digits
1033
+ const pivot = dateTime
1034
+ .subtract({ years: this.#local.config['pivot'] }) // pivot cutoff to determine century
1035
+ .year % 100; // remainder
1036
+ const century = Math.trunc(dateTime.year / 100); // current century
1037
+ year += (century - Number(year >= pivot)) * 100; // now a four-digit year
1038
+ }
1039
+ // adjust the {year} if a Modifier is present
1040
+ const adjust = +nbr; // how many years to adjust
1041
+ const offset = Number(pad(month) + '.' + pad(day)); // the event month.day
1042
+ const period = Number(pad(dateTime.month) + '.' + pad(dateTime.day + 1));
1043
+ year += this.#parseModifier({ mod: mod ?? afx, adjust, offset, period });
1044
+ Object.assign(groups, { yy: year, mm: month, dd: day });
1045
+ delete groups["mod"];
1046
+ delete groups["nbr"];
1047
+ delete groups["afx"];
1048
+ // all date-components are now set; check for overflow in case past end-of-month
1049
+ return Temporal.PlainDate.from({ year, month, day }, { overflow: 'constrain' })
1050
+ .toZonedDateTime(dateTime.timeZoneId) // adjust to constrained date
1051
+ .withPlainTime(dateTime.toPlainTime()); // restore the time
1046
1052
  }
1047
- if (groups["mer"]?.toLowerCase() === 'pm' && hh < 12 && (hh + mi + ss + ms + us + ns) > 0)
1048
- hh += 12; // anything after midnight and before midday
1049
- if (groups["mer"]?.toLowerCase() === 'am' && hh >= 12)
1050
- hh -= 12; // anything after midday
1051
- return dateTime // return the computed time-values
1052
- .withPlainTime({ hour: hh, minute: mi, second: ss, millisecond: ms, microsecond: us, nanosecond: ns });
1053
- }
1054
- /**
1055
- * match an {event} string against a date pattern
1056
- * if {evt} is a function, it is bound to the current instance (to allow access to properties/methods)
1057
- */
1058
- #parseEvent(evt) {
1059
- const groups = {};
1060
- const val = isFunction(evt)
1061
- ? evt.bind(this)().toString()
1062
- : evt;
1063
- const pats = this.#local.parse.isMonthDay // first find out if we have a US-format timeZone
1064
- ? ['mdy', 'dmy', 'ymd'] // try {mdy} before {dmy} if US-format
1065
- : ['dmy', 'mdy', 'ymd']; // else try {dmy} before {mdy}
1066
- for (const pat of pats) {
1067
- const reg = this.#local.parse.pattern.get(Tempo.getSymbol(pat)); // get the RegExp for the date-pattern
1068
- if (isUndefined(reg)) {
1069
- Tempo.#dbg.catch(this.#local.config, `Cannot find pattern: "${pat}"`);
1053
+ /**
1054
+ * match input against 'tm' pattern.
1055
+ * {groups} is expected to contain time-components (like {hh:'3', mi:'30', mer:'pm'}).
1056
+ * returns an adjusted ZonedDateTime
1057
+ */
1058
+ #parseTime(groups = {}, dateTime) {
1059
+ if (isUndefined(groups["hh"])) // must contain 'time' with at least {hh}
1060
+ return dateTime;
1061
+ let { hh = 0, mi = 0, ss = 0, ms = 0, us = 0, ns = 0 } = this.#num(groups);
1062
+ if (hh >= 24) {
1063
+ dateTime = dateTime.add({ days: Math.trunc(hh / 24) }); // move the date forward number of days to offset
1064
+ hh %= 24; // midnight is '00:00' on the next-day
1070
1065
  }
1071
- else {
1072
- const match = this.#parseMatch(reg, val);
1073
- if (!isEmpty(match))
1074
- this.#result({ type: 'Event', value: val, match: pat, groups: cleanify(match) });
1075
- Object.assign(groups, match);
1066
+ if (isDefined(groups["ff"])) { // {ff} is fractional seconds and overrides {ms|us|ns}
1067
+ const ff = groups["ff"].substring(0, 9).padEnd(9, '0');
1068
+ ms = +ff.substring(0, 3);
1069
+ us = +ff.substring(3, 6);
1070
+ ns = +ff.substring(6, 9);
1076
1071
  }
1077
- if (!isEmpty(groups))
1078
- break; // return on the first matched pattern
1072
+ if (groups["mer"]?.toLowerCase() === 'pm' && hh < 12 && (hh + mi + ss + ms + us + ns) > 0)
1073
+ hh += 12; // anything after midnight and before midday
1074
+ if (groups["mer"]?.toLowerCase() === 'am' && hh >= 12)
1075
+ hh -= 12; // anything after midday
1076
+ return dateTime // return the computed time-values
1077
+ .withPlainTime({ hour: hh, minute: mi, second: ss, millisecond: ms, microsecond: us, nanosecond: ns });
1079
1078
  }
1080
- return groups; // overlay the match date-components
1081
- }
1082
- /**
1083
- * match a {period} string against a time pattern
1084
- * if {per} is a function, it is bound to the current instance (to allow access to properties/methods)
1085
- */
1086
- #parsePeriod(per) {
1087
- const groups = {};
1088
- const tm = this.#local.parse.pattern.get(Tempo.getSymbol('tm')); // get the RegExp for the time-pattern
1089
- if (isUndefined(tm)) {
1090
- Tempo.#dbg.catch(this.#local.config, `Cannot find pattern "tm"`);
1091
- return;
1079
+ /**
1080
+ * match an {event} string against a date pattern
1081
+ * if {evt} is a function, it is bound to the current instance (to allow access to properties/methods)
1082
+ */
1083
+ #parseEvent(evt) {
1084
+ const groups = {};
1085
+ const val = isFunction(evt)
1086
+ ? evt.bind(this)().toString()
1087
+ : evt;
1088
+ const pats = this.#local.parse.isMonthDay // first find out if we have a US-format timeZone
1089
+ ? ['mdy', 'dmy', 'ymd'] // try {mdy} before {dmy} if US-format
1090
+ : ['dmy', 'mdy', 'ymd']; // else try {dmy} before {mdy}
1091
+ for (const pat of pats) {
1092
+ const reg = this.#local.parse.pattern.get(Tempo.getSymbol(pat)); // get the RegExp for the date-pattern
1093
+ if (isUndefined(reg)) {
1094
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Cannot find pattern: "${pat}"`);
1095
+ }
1096
+ else {
1097
+ const match = this.#parseMatch(reg, val);
1098
+ if (!isEmpty(match))
1099
+ this.#result({ type: 'Event', value: val, match: pat, groups: cleanify(match) });
1100
+ Object.assign(groups, match);
1101
+ }
1102
+ if (!isEmpty(groups))
1103
+ break; // return on the first matched pattern
1104
+ }
1105
+ return groups; // overlay the match date-components
1092
1106
  }
1093
- const val = isFunction(per)
1094
- ? per.bind(this)().toString()
1095
- : per;
1096
- const match = this.#parseMatch(tm, val);
1097
- if (!isEmpty(match))
1098
- this.#result({ type: 'Period', value: val, match: 'tm', groups: cleanify(match) });
1099
- Object.assign(groups, match);
1100
- return groups;
1101
- }
1102
- /** return a new object, with only numeric values */
1103
- #num = (groups) => {
1104
- return ownEntries(groups)
1105
- .reduce((acc, [key, val]) => {
1106
- if (isNumeric(val))
1107
- acc[key] = ifNumeric(val);
1108
- return acc;
1109
- }, {});
1110
- };
1111
- /** create new Tempo with {offset} property */
1112
- #add = (arg) => {
1113
- Tempo.#pending ??= [...this.#local.parse.result]; // collected parse-results so-far
1114
- const mutate = 'add';
1115
- const zdt = ownEntries(arg) // loop through each mutation
1116
- .reduce((zdt, [unit, offset]) => {
1117
- const single = singular(unit);
1118
- const plural = single + 's';
1119
- switch (`${mutate}.${single}`) {
1120
- case 'add.year':
1121
- case 'add.month':
1122
- case 'add.week':
1123
- case 'add.day':
1124
- case 'add.hour':
1125
- case 'add.minute':
1126
- case 'add.second':
1127
- case 'add.millisecond':
1128
- case 'add.microsecond':
1129
- case 'add.nanosecond':
1130
- return zdt
1131
- .add({ [plural]: offset });
1132
- default:
1133
- Tempo.#dbg.catch(this.#local.config, `Unexpected method(${mutate}), unit(${unit}) and offset(${offset})`);
1134
- return zdt;
1107
+ /**
1108
+ * match a {period} string against a time pattern
1109
+ * if {per} is a function, it is bound to the current instance (to allow access to properties/methods)
1110
+ */
1111
+ #parsePeriod(per) {
1112
+ const groups = {};
1113
+ const tm = this.#local.parse.pattern.get(Tempo.getSymbol('tm')); // get the RegExp for the time-pattern
1114
+ if (isUndefined(tm)) {
1115
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Cannot find pattern "tm"`);
1116
+ return;
1135
1117
  }
1136
- }, this.#zdt);
1137
- return new Tempo(zdt, this.#options);
1138
- };
1139
- /** create a new Tempo with {adjust} property */
1140
- #set = (args) => {
1141
- Tempo.#pending ??= [...this.#local.parse.result]; // collected parse-results so-far
1142
- const zdt = ownEntries(args) // loop through each mutation
1143
- .reduce((zdt, [key, adjust]) => {
1144
- const { mutate, offset, single } = ((key) => {
1145
- switch (key) {
1146
- case 'start':
1147
- case 'mid':
1148
- case 'end':
1149
- return { mutate: key, offset: 0, single: singular(adjust?.toString() ?? '') };
1118
+ const val = isFunction(per)
1119
+ ? per.bind(this)().toString()
1120
+ : per;
1121
+ const match = this.#parseMatch(tm, val);
1122
+ if (!isEmpty(match))
1123
+ this.#result({ type: 'Period', value: val, match: 'tm', groups: cleanify(match) });
1124
+ Object.assign(groups, match);
1125
+ return groups;
1126
+ }
1127
+ /** return a new object, with only numeric values */
1128
+ #num = (groups) => {
1129
+ return ownEntries(groups)
1130
+ .reduce((acc, [key, val]) => {
1131
+ if (isNumeric(val))
1132
+ acc[key] = ifNumeric(val);
1133
+ return acc;
1134
+ }, {});
1135
+ };
1136
+ /** create new Tempo with {offset} property */
1137
+ #add = (arg) => {
1138
+ var _b;
1139
+ __classPrivateFieldSet(_b = Tempo, _classThis, __classPrivateFieldGet(_b, _classThis, "f", _Tempo_pending) ?? [...this.#local.parse.result], "f", _Tempo_pending); // collected parse-results so-far
1140
+ const mutate = 'add';
1141
+ const zdt = ownEntries(arg) // loop through each mutation
1142
+ .reduce((zdt, [unit, offset]) => {
1143
+ const single = singular(unit);
1144
+ const plural = single + 's';
1145
+ switch (`${mutate}.${single}`) {
1146
+ case 'add.year':
1147
+ case 'add.month':
1148
+ case 'add.week':
1149
+ case 'add.day':
1150
+ case 'add.hour':
1151
+ case 'add.minute':
1152
+ case 'add.second':
1153
+ case 'add.millisecond':
1154
+ case 'add.microsecond':
1155
+ case 'add.nanosecond':
1156
+ return zdt
1157
+ .add({ [plural]: offset });
1150
1158
  default:
1151
- return { mutate: 'set', offset: adjust, single: singular(key) };
1159
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Unexpected method(${mutate}), unit(${unit}) and offset(${offset})`);
1160
+ return zdt;
1152
1161
  }
1153
- })(key); // IIFE to analyze arguments
1154
- switch (`${mutate}.${single}`) {
1155
- case 'set.period':
1156
- case 'set.time':
1157
- case 'set.date':
1158
- case 'set.event':
1159
- case 'set.dow': // set day-of-week by number
1160
- case 'set.wkd': // set day-of-week by name
1161
- return this.#parse(offset, zdt);
1162
- case 'set.year':
1163
- case 'set.month':
1164
- // case 'set.week': // not defined
1165
- case 'set.day':
1166
- case 'set.hour':
1167
- case 'set.minute':
1168
- case 'set.second':
1169
- case 'set.millisecond':
1170
- case 'set.microsecond':
1171
- case 'set.nanosecond':
1172
- return zdt
1173
- .with({ [single]: offset });
1174
- case 'set.yy':
1175
- case 'set.mm':
1176
- // case 'set.ww': // not defined
1177
- case 'set.dd':
1178
- case 'set.hh':
1179
- case 'set.mi':
1180
- case 'set.ss':
1181
- case 'set.ms':
1182
- case 'set.us':
1183
- case 'set.ns':
1184
- const value = Tempo.ELEMENT[single];
1185
- return zdt
1186
- .with({ [value]: offset });
1187
- case 'start.year':
1188
- return zdt
1189
- .with({ month: Tempo.MONTH.Jan, day: 1 })
1190
- .startOfDay();
1191
- case 'start.term': // TODO
1192
- return zdt;
1193
- case 'start.month':
1194
- return zdt
1195
- .with({ day: 1 })
1196
- .startOfDay();
1197
- case 'start.week':
1198
- return zdt
1199
- .add({ days: -(this.dow - Tempo.WEEKDAY.Mon) })
1200
- .startOfDay();
1201
- case 'start.day':
1202
- return zdt
1203
- .startOfDay();
1204
- case 'start.hour':
1205
- case 'start.minute':
1206
- case 'start.second':
1207
- return zdt
1208
- .round({ smallestUnit: offset, roundingMode: 'trunc' });
1209
- // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1210
- case 'mid.year':
1211
- return zdt
1212
- .with({ month: Tempo.MONTH.Jul, day: 1 })
1213
- .startOfDay();
1214
- case 'mid.term': // TODO: relevant?
1215
- return zdt;
1216
- case 'mid.month':
1217
- return zdt
1218
- .with({ day: Math.trunc(zdt.daysInMonth / 2) })
1219
- .startOfDay();
1220
- case 'mid.week':
1221
- return zdt
1222
- .add({ days: -(this.dow - Tempo.WEEKDAY.Thu) })
1223
- .startOfDay();
1224
- case 'mid.day':
1225
- return zdt
1226
- .round({ smallestUnit: 'day', roundingMode: 'trunc' })
1227
- .add({ hours: 12 });
1228
- case 'mid.hour':
1229
- return zdt
1230
- .round({ smallestUnit: 'hour', roundingMode: 'trunc' })
1231
- .add({ minutes: 30 });
1232
- case 'mid.minute':
1233
- return zdt
1234
- .round({ smallestUnit: 'minute', roundingMode: 'trunc' })
1235
- .add({ seconds: 30 });
1236
- case 'mid.second':
1237
- return zdt
1238
- .round({ smallestUnit: 'second', roundingMode: 'trunc' })
1239
- .add({ milliseconds: 500 });
1240
- // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1241
- case 'end.year':
1242
- return zdt
1243
- .add({ years: 1 })
1244
- .with({ month: Tempo.MONTH.Jan, day: 1 })
1245
- .startOfDay()
1246
- .subtract({ nanoseconds: 1 });
1247
- case 'end.term': // TODO
1248
- return zdt
1249
- .subtract({ nanoseconds: 1 });
1250
- case 'end.month':
1251
- return zdt
1252
- .add({ months: 1 })
1253
- .with({ day: 1 })
1254
- .startOfDay()
1255
- .subtract({ nanoseconds: 1 });
1256
- case 'end.week':
1257
- return zdt
1258
- .add({ days: (Tempo.WEEKDAY.Sun - this.dow) + 1 })
1259
- .startOfDay()
1260
- .subtract({ nanoseconds: 1 });
1261
- case 'end.day':
1262
- case 'end.hour':
1263
- case 'end.minute':
1264
- case 'end.second':
1265
- return zdt
1266
- .round({ smallestUnit: offset, roundingMode: 'ceil' })
1267
- .subtract({ nanoseconds: 1 });
1162
+ }, this.#zdt);
1163
+ return new Tempo(zdt, this.#options);
1164
+ };
1165
+ /** create a new Tempo with {adjust} property */
1166
+ #set = (args) => {
1167
+ var _b;
1168
+ __classPrivateFieldSet(_b = Tempo, _classThis, __classPrivateFieldGet(_b, _classThis, "f", _Tempo_pending) ?? [...this.#local.parse.result], "f", _Tempo_pending); // collected parse-results so-far
1169
+ const zdt = ownEntries(args) // loop through each mutation
1170
+ .reduce((zdt, [key, adjust]) => {
1171
+ const { mutate, offset, single } = ((key) => {
1172
+ switch (key) {
1173
+ case 'start':
1174
+ case 'mid':
1175
+ case 'end':
1176
+ return { mutate: key, offset: 0, single: singular(adjust?.toString() ?? '') };
1177
+ default:
1178
+ return { mutate: 'set', offset: adjust, single: singular(key) };
1179
+ }
1180
+ })(key); // IIFE to analyze arguments
1181
+ switch (`${mutate}.${single}`) {
1182
+ case 'set.period':
1183
+ case 'set.time':
1184
+ case 'set.date':
1185
+ case 'set.event':
1186
+ case 'set.dow': // set day-of-week by number
1187
+ case 'set.wkd': // set day-of-week by name
1188
+ return this.#parse(offset, zdt);
1189
+ case 'set.year':
1190
+ case 'set.month':
1191
+ // case 'set.week': // not defined
1192
+ case 'set.day':
1193
+ case 'set.hour':
1194
+ case 'set.minute':
1195
+ case 'set.second':
1196
+ case 'set.millisecond':
1197
+ case 'set.microsecond':
1198
+ case 'set.nanosecond':
1199
+ return zdt
1200
+ .with({ [single]: offset });
1201
+ case 'set.yy':
1202
+ case 'set.mm':
1203
+ // case 'set.ww': // not defined
1204
+ case 'set.dd':
1205
+ case 'set.hh':
1206
+ case 'set.mi':
1207
+ case 'set.ss':
1208
+ case 'set.ms':
1209
+ case 'set.us':
1210
+ case 'set.ns':
1211
+ const value = Tempo.ELEMENT[single];
1212
+ return zdt
1213
+ .with({ [value]: offset });
1214
+ case 'start.year':
1215
+ return zdt
1216
+ .with({ month: Tempo.MONTH.Jan, day: 1 })
1217
+ .startOfDay();
1218
+ case 'start.term': // TODO
1219
+ return zdt;
1220
+ case 'start.month':
1221
+ return zdt
1222
+ .with({ day: 1 })
1223
+ .startOfDay();
1224
+ case 'start.week':
1225
+ return zdt
1226
+ .add({ days: -(this.dow - Tempo.WEEKDAY.Mon) })
1227
+ .startOfDay();
1228
+ case 'start.day':
1229
+ return zdt
1230
+ .startOfDay();
1231
+ case 'start.hour':
1232
+ case 'start.minute':
1233
+ case 'start.second':
1234
+ return zdt
1235
+ .round({ smallestUnit: offset, roundingMode: 'trunc' });
1236
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1237
+ case 'mid.year':
1238
+ return zdt
1239
+ .with({ month: Tempo.MONTH.Jul, day: 1 })
1240
+ .startOfDay();
1241
+ case 'mid.term': // TODO: relevant?
1242
+ return zdt;
1243
+ case 'mid.month':
1244
+ return zdt
1245
+ .with({ day: Math.trunc(zdt.daysInMonth / 2) })
1246
+ .startOfDay();
1247
+ case 'mid.week':
1248
+ return zdt
1249
+ .add({ days: -(this.dow - Tempo.WEEKDAY.Thu) })
1250
+ .startOfDay();
1251
+ case 'mid.day':
1252
+ return zdt
1253
+ .round({ smallestUnit: 'day', roundingMode: 'trunc' })
1254
+ .add({ hours: 12 });
1255
+ case 'mid.hour':
1256
+ return zdt
1257
+ .round({ smallestUnit: 'hour', roundingMode: 'trunc' })
1258
+ .add({ minutes: 30 });
1259
+ case 'mid.minute':
1260
+ return zdt
1261
+ .round({ smallestUnit: 'minute', roundingMode: 'trunc' })
1262
+ .add({ seconds: 30 });
1263
+ case 'mid.second':
1264
+ return zdt
1265
+ .round({ smallestUnit: 'second', roundingMode: 'trunc' })
1266
+ .add({ milliseconds: 500 });
1267
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1268
+ case 'end.year':
1269
+ return zdt
1270
+ .add({ years: 1 })
1271
+ .with({ month: Tempo.MONTH.Jan, day: 1 })
1272
+ .startOfDay()
1273
+ .subtract({ nanoseconds: 1 });
1274
+ case 'end.term': // TODO
1275
+ return zdt
1276
+ .subtract({ nanoseconds: 1 });
1277
+ case 'end.month':
1278
+ return zdt
1279
+ .add({ months: 1 })
1280
+ .with({ day: 1 })
1281
+ .startOfDay()
1282
+ .subtract({ nanoseconds: 1 });
1283
+ case 'end.week':
1284
+ return zdt
1285
+ .add({ days: (Tempo.WEEKDAY.Sun - this.dow) + 1 })
1286
+ .startOfDay()
1287
+ .subtract({ nanoseconds: 1 });
1288
+ case 'end.day':
1289
+ case 'end.hour':
1290
+ case 'end.minute':
1291
+ case 'end.second':
1292
+ return zdt
1293
+ .round({ smallestUnit: offset, roundingMode: 'ceil' })
1294
+ .subtract({ nanoseconds: 1 });
1295
+ default:
1296
+ __classPrivateFieldGet(Tempo, _classThis, "f", _Tempo_dbg).catch(this.#local.config, `Unexpected method(${mutate}), unit(${adjust}) and offset(${single})`);
1297
+ return zdt;
1298
+ }
1299
+ }, this.#zdt); // start reduce with the Tempo zonedDateTime
1300
+ return new Tempo(zdt, this.#options);
1301
+ };
1302
+ #format = (fmt) => {
1303
+ if (isNull(this.#tempo))
1304
+ return void 0; // don't format <null> dates
1305
+ const obj = Tempo.FORMAT;
1306
+ const template = isString(fmt) && __classPrivateFieldGet(Tempo, _classThis, "m", _Tempo_hasOwn).call(Tempo, obj, fmt)
1307
+ ? obj[fmt]
1308
+ : fmt;
1309
+ const sTemplate = String(template);
1310
+ switch (sTemplate) {
1311
+ case obj.yearWeek:
1312
+ const offset = this.ww === 1 && this.mm === Tempo.MONTH.Dec; // if late-Dec, add 1 to yy
1313
+ return +`${this.yy + +offset}${pad(this.ww)}`;
1314
+ case obj.yearMonth:
1315
+ return +`${this.yy}${pad(this.mm)}`;
1316
+ case obj.yearMonthDay:
1317
+ return +`${this.yy}${pad(this.mm)}${pad(this.dd)}`;
1268
1318
  default:
1269
- Tempo.#dbg.catch(this.#local.config, `Unexpected method(${mutate}), unit(${adjust}) and offset(${single})`);
1270
- return zdt;
1271
- }
1272
- }, this.#zdt); // start reduce with the Tempo zonedDateTime
1273
- return new Tempo(zdt, this.#options);
1274
- };
1275
- #format = (fmt) => {
1276
- if (isNull(this.#tempo))
1277
- return void 0; // don't format <null> dates
1278
- const obj = Tempo.FORMAT;
1279
- const template = isString(fmt) && Tempo.#hasOwn(obj, fmt)
1280
- ? obj[fmt]
1281
- : fmt;
1282
- const sTemplate = String(template);
1283
- switch (sTemplate) {
1284
- case obj.yearWeek:
1285
- const offset = this.ww === 1 && this.mm === Tempo.MONTH.Dec; // if late-Dec, add 1 to yy
1286
- return +`${this.yy + +offset}${pad(this.ww)}`;
1287
- case obj.yearMonth:
1288
- return +`${this.yy}${pad(this.mm)}`;
1289
- case obj.yearMonthDay:
1290
- return +`${this.yy}${pad(this.mm)}${pad(this.dd)}`;
1291
- default:
1292
- return sTemplate.replace(Match.braces, (_match, token) => {
1293
- switch (token) {
1294
- case 'yyyy': return pad(this.yy, 4);
1295
- case 'yy': return pad(this.yy % 100);
1296
- case 'mon': return this.mon;
1297
- case 'mmm': return this.mmm;
1298
- case 'mm': return pad(this.mm);
1299
- case 'dd': return pad(this.dd);
1300
- case 'day': return this.day.toString();
1301
- case 'dow': return this.dow.toString();
1302
- case 'wkd': return this.wkd;
1303
- case 'www': return this.www;
1304
- case 'ww': return pad(this.ww);
1305
- case 'hh': return pad(this.hh);
1306
- case 'HH': return pad(this.hh > 12 ? this.hh % 12 : this.hh || 12);
1307
- case 'mer': return this.hh >= 12 ? 'pm' : 'am';
1308
- case 'MER': return this.hh >= 12 ? 'PM' : 'AM';
1309
- case 'mi': return pad(this.mi);
1310
- case 'ss': return pad(this.ss);
1311
- case 'ms': return pad(this.ms, 3);
1312
- case 'us': return pad(this.us, 3);
1313
- case 'ns': return pad(this.ns, 3);
1314
- case 'ff': return `${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`;
1315
- case 'hhmiss': return pad(this.hh) + pad(this.mi) + pad(this.ss);
1316
- case 'ts': return this.ts.toString();
1317
- case 'nano': return this.nano.toString();
1318
- case 'tz': return this.tz;
1319
- default: {
1320
- return token.startsWith('term.')
1321
- ? stringify(this.term[token.slice(5)])
1322
- : `{${token}}`; // unknown format-code, return as-is
1319
+ return sTemplate.replace(Match.braces, (_match, token) => {
1320
+ switch (token) {
1321
+ case 'yyyy': return pad(this.yy, 4);
1322
+ case 'yy': return pad(this.yy % 100);
1323
+ case 'mon': return this.mon;
1324
+ case 'mmm': return this.mmm;
1325
+ case 'mm': return pad(this.mm);
1326
+ case 'dd': return pad(this.dd);
1327
+ case 'day': return this.day.toString();
1328
+ case 'dow': return this.dow.toString();
1329
+ case 'wkd': return this.wkd;
1330
+ case 'www': return this.www;
1331
+ case 'ww': return pad(this.ww);
1332
+ case 'hh': return pad(this.hh);
1333
+ case 'HH': return pad(this.hh > 12 ? this.hh % 12 : this.hh || 12);
1334
+ case 'mer': return this.hh >= 12 ? 'pm' : 'am';
1335
+ case 'MER': return this.hh >= 12 ? 'PM' : 'AM';
1336
+ case 'mi': return pad(this.mi);
1337
+ case 'ss': return pad(this.ss);
1338
+ case 'ms': return pad(this.ms, 3);
1339
+ case 'us': return pad(this.us, 3);
1340
+ case 'ns': return pad(this.ns, 3);
1341
+ case 'ff': return `${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`;
1342
+ case 'hhmiss': return pad(this.hh) + pad(this.mi) + pad(this.ss);
1343
+ case 'ts': return this.ts.toString();
1344
+ case 'nano': return this.nano.toString();
1345
+ case 'tz': return this.tz;
1346
+ default: {
1347
+ return token.startsWith('term.')
1348
+ ? stringify(this.term[token.slice(5)])
1349
+ : `{${token}}`; // unknown format-code, return as-is
1350
+ }
1323
1351
  }
1324
- }
1325
- });
1352
+ });
1353
+ }
1354
+ };
1355
+ #until(arg, until = {}, since = false) {
1356
+ let value, opts = {}, unit = void 0;
1357
+ switch (true) {
1358
+ case isString(arg) && Tempo.ELEMENT.includes(singular(arg)):
1359
+ unit = arg; // e.g. tempo.until('hours')
1360
+ ({ value, ...opts } = until);
1361
+ break;
1362
+ case isString(arg): // assume 'arg' is a dateTime string
1363
+ value = arg; // e.g. tempo.until('20-May-1957', {unit: 'years'})
1364
+ if (isObject(until))
1365
+ ({ unit, ...opts } = until);
1366
+ else
1367
+ unit = until; // assume the 'until' arg is a 'unit' string
1368
+ break;
1369
+ case isObject(arg) && isString(until): // assume 'until' is a Unit
1370
+ unit = until; // e.g. tempo.until({value:'20-May-1957}, 'years'})
1371
+ ({ value, ...opts } = arg);
1372
+ break;
1373
+ case isObject(arg) && isObject(until): // assume combination of Tempo.Options and Tempo.Until
1374
+ ({ value, unit, ...opts } = Object.assign({}, arg, until));
1375
+ break;
1376
+ case isString(until):
1377
+ unit = until;
1378
+ value = arg;
1379
+ break;
1380
+ case isObject(until):
1381
+ unit = until.unit;
1382
+ value = arg;
1383
+ break;
1384
+ default:
1385
+ value = arg; // assume 'arg' is a DateTime
1386
+ }
1387
+ const offset = new Tempo(value, opts); // create the offset Tempo
1388
+ const diffZone = this.#zdt.timeZoneId !== offset.#zdt.timeZoneId;
1389
+ const duration = this.#zdt.until(offset.#zdt, { largestUnit: diffZone ? 'hours' : (unit ?? 'years') });
1390
+ if (isDefined(unit))
1391
+ unit = `${singular(unit)}s`; // coerce to plural
1392
+ return (isUndefined(unit) || since) // if no 'unit' provided, or if called via #since()
1393
+ ? getAccessors(duration) // return an Object with all the duration accessors
1394
+ .reduce((acc, dur) => Object.assign(acc, { [dur]: duration[dur] }), ifDefined({ iso: duration.toString(), unit }))
1395
+ : duration.total({ relativeTo: this.#zdt, unit }); // sum-up the duration components
1326
1396
  }
1327
- };
1328
- #until(arg, until = {}, since = false) {
1329
- let value, opts = {}, unit = void 0;
1330
- switch (true) {
1331
- case isString(arg) && Tempo.ELEMENT.includes(singular(arg)):
1332
- unit = arg; // e.g. tempo.until('hours')
1333
- ({ value, ...opts } = until);
1334
- break;
1335
- case isString(arg): // assume 'arg' is a dateTime string
1336
- value = arg; // e.g. tempo.until('20-May-1957', {unit: 'years'})
1337
- if (isObject(until))
1338
- ({ unit, ...opts } = until);
1339
- else
1340
- unit = until; // assume the 'until' arg is a 'unit' string
1341
- break;
1342
- case isObject(arg) && isString(until): // assume 'until' is a Unit
1343
- unit = until; // e.g. tempo.until({value:'20-May-1957}, 'years'})
1344
- ({ value, ...opts } = arg);
1345
- break;
1346
- case isObject(arg) && isObject(until): // assume combination of Tempo.Options and Tempo.Until
1347
- ({ value, unit, ...opts } = Object.assign({}, arg, until));
1348
- break;
1349
- case isString(until):
1350
- unit = until;
1351
- value = arg;
1352
- break;
1353
- case isObject(until):
1354
- unit = until.unit;
1355
- value = arg;
1356
- break;
1357
- default:
1358
- value = arg; // assume 'arg' is a DateTime
1397
+ /** format the elapsed time between two Tempos (to nanosecond) */
1398
+ #since(arg, until = {}) {
1399
+ const dur = this.#until(arg, until, true); // get a Tempo.Duration object
1400
+ const date = [dur.years, dur.months, dur.days];
1401
+ const time = [dur.hours, dur.minutes, dur.seconds];
1402
+ const fraction = [dur.milliseconds, dur.microseconds, dur.nanoseconds]
1403
+ .map(nbr => nbr.toString().padStart(3, '0'))
1404
+ .join('');
1405
+ const rtf = new Intl.RelativeTimeFormat(this.#local.config['locale'], { style: 'narrow' });
1406
+ switch (dur.unit) {
1407
+ case void 0:
1408
+ return `${date.join('.')}T${time.join(':')}.${fraction}`;
1409
+ case 'years':
1410
+ return rtf.format(date[0], 'years');
1411
+ case 'months':
1412
+ return rtf.format(date[1], 'months');
1413
+ case 'weeks':
1414
+ return rtf.format(date[1], 'weeks');
1415
+ case 'days':
1416
+ return rtf.format(date[2], 'days');
1417
+ case 'hours':
1418
+ return rtf.format(time[0], 'hours');
1419
+ case 'minutes':
1420
+ return rtf.format(time[1], 'minutes');
1421
+ case 'seconds':
1422
+ return rtf.format(time[2], 'seconds');
1423
+ case 'milliseconds':
1424
+ case 'microseconds':
1425
+ case 'nanoseconds':
1426
+ return `${fraction}`;
1427
+ default:
1428
+ return dur.iso;
1429
+ }
1359
1430
  }
1360
- const offset = new Tempo(value, opts); // create the offset Tempo
1361
- const diffZone = this.#zdt.timeZoneId !== offset.#zdt.timeZoneId;
1362
- const duration = this.#zdt.until(offset.#zdt, { largestUnit: diffZone ? 'hours' : (unit ?? 'years') });
1363
- if (isDefined(unit))
1364
- unit = `${singular(unit)}s`; // coerce to plural
1365
- return (isUndefined(unit) || since) // if no 'unit' provided, or if called via #since()
1366
- ? getAccessors(duration) // return an Object with all the duration accessors
1367
- .reduce((acc, dur) => Object.assign(acc, { [dur]: duration[dur] }), ifDefined({ iso: duration.toString(), unit }))
1368
- : duration.total({ relativeTo: this.#zdt, unit }); // sum-up the duration components
1369
- }
1370
- /** format the elapsed time between two Tempos (to nanosecond) */
1371
- #since(arg, until = {}) {
1372
- const dur = this.#until(arg, until, true); // get a Tempo.Duration object
1373
- const date = [dur.years, dur.months, dur.days];
1374
- const time = [dur.hours, dur.minutes, dur.seconds];
1375
- const fraction = [dur.milliseconds, dur.microseconds, dur.nanoseconds]
1376
- .map(nbr => nbr.toString().padStart(3, '0'))
1377
- .join('');
1378
- const rtf = new Intl.RelativeTimeFormat(this.#local.config['locale'], { style: 'narrow' });
1379
- switch (dur.unit) {
1380
- case void 0:
1381
- return `${date.join('.')}T${time.join(':')}.${fraction}`;
1382
- case 'years':
1383
- return rtf.format(date[0], 'years');
1384
- case 'months':
1385
- return rtf.format(date[1], 'months');
1386
- case 'weeks':
1387
- return rtf.format(date[1], 'weeks');
1388
- case 'days':
1389
- return rtf.format(date[2], 'days');
1390
- case 'hours':
1391
- return rtf.format(time[0], 'hours');
1392
- case 'minutes':
1393
- return rtf.format(time[1], 'minutes');
1394
- case 'seconds':
1395
- return rtf.format(time[2], 'seconds');
1396
- case 'milliseconds':
1397
- case 'microseconds':
1398
- case 'nanoseconds':
1399
- return `${fraction}`;
1400
- default:
1401
- return dur.iso;
1431
+ static {
1432
+ __runInitializers(_classThis, _classExtraInitializers);
1402
1433
  }
1403
- }
1404
- }
1434
+ };
1435
+ return Tempo = _classThis;
1436
+ })();
1437
+ export { Tempo };
1405
1438
  // #endregion Namespace
1406
1439
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1407
1440
  Tempo.init(); // initialize default global configuration