@magmacomputing/tempo 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/array.library.d.ts +25 -0
- package/dist/array.library.js +78 -0
- package/dist/buffer.library.d.ts +6 -0
- package/dist/buffer.library.js +192 -0
- package/dist/cipher.class.d.ts +18 -0
- package/dist/cipher.class.js +65 -0
- package/dist/class.library.d.ts +10 -0
- package/dist/class.library.js +57 -0
- package/dist/coercion.library.d.ts +14 -0
- package/dist/coercion.library.js +71 -0
- package/dist/enumerate.library.d.ts +64 -0
- package/dist/enumerate.library.js +54 -0
- package/dist/function.library.d.ts +9 -0
- package/dist/function.library.js +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/logify.class.d.ts +33 -0
- package/dist/logify.class.js +53 -0
- package/dist/number.library.d.ts +16 -0
- package/dist/number.library.js +36 -0
- package/dist/object.library.d.ts +37 -0
- package/dist/object.library.js +94 -0
- package/dist/pledge.class.d.ts +54 -0
- package/dist/pledge.class.js +131 -0
- package/dist/prototype.library.d.ts +33 -0
- package/dist/prototype.library.js +51 -0
- package/dist/reflection.library.d.ts +74 -0
- package/dist/reflection.library.js +97 -0
- package/dist/serialize.library.d.ts +25 -0
- package/dist/serialize.library.js +266 -0
- package/dist/storage.library.d.ts +8 -0
- package/dist/storage.library.js +57 -0
- package/dist/string.library.d.ts +37 -0
- package/dist/string.library.js +93 -0
- package/dist/tempo.class.d.ts +556 -0
- package/dist/tempo.class.js +1412 -0
- package/dist/tempo.config/plugins/term.import.d.ts +42 -0
- package/dist/tempo.config/plugins/term.import.js +44 -0
- package/dist/tempo.config/plugins/term.quarter.d.ts +7 -0
- package/dist/tempo.config/plugins/term.quarter.js +28 -0
- package/dist/tempo.config/plugins/term.season.d.ts +7 -0
- package/dist/tempo.config/plugins/term.season.js +36 -0
- package/dist/tempo.config/plugins/term.timeline.d.ts +7 -0
- package/dist/tempo.config/plugins/term.timeline.js +19 -0
- package/dist/tempo.config/plugins/term.utils.d.ts +17 -0
- package/dist/tempo.config/plugins/term.utils.js +38 -0
- package/dist/tempo.config/plugins/term.zodiac.d.ts +7 -0
- package/dist/tempo.config/plugins/term.zodiac.js +62 -0
- package/dist/tempo.config/tempo.default.d.ts +169 -0
- package/dist/tempo.config/tempo.default.js +158 -0
- package/dist/tempo.config/tempo.enum.d.ts +99 -0
- package/dist/tempo.config/tempo.enum.js +78 -0
- package/dist/temporal.polyfill.d.ts +9 -0
- package/dist/temporal.polyfill.js +18 -0
- package/dist/type.library.d.ts +296 -0
- package/dist/type.library.js +80 -0
- package/dist/utility.library.d.ts +32 -0
- package/dist/utility.library.js +54 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
// #region library modules~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
2
|
+
import { Logify } from './logify.class.js';
|
|
3
|
+
import { ifDefined } from './object.library.js';
|
|
4
|
+
import { secure } from './utility.library.js';
|
|
5
|
+
import { Immutable, Serializable } from './class.library.js';
|
|
6
|
+
import { Cipher } from './cipher.class.js';
|
|
7
|
+
import { asArray } from './array.library.js';
|
|
8
|
+
import { cleanify, stringify } from './serialize.library.js';
|
|
9
|
+
import { getStorage, setStorage } from './storage.library.js';
|
|
10
|
+
import { ownKeys, ownEntries, getAccessors } from './reflection.library.js';
|
|
11
|
+
import { getContext, CONTEXT } from './utility.library.js';
|
|
12
|
+
import { asInteger, isNumeric, ifNumeric } from './number.library.js';
|
|
13
|
+
import { pad, singular, toProperCase, trimAll } from './string.library.js';
|
|
14
|
+
import { getType, asType, isType, isEmpty, isNull, isNullish, isDefined, isUndefined, isString, isObject, isRegExp, isRegExpLike, isIntegerLike, isSymbol, isFunction } from './type.library.js';
|
|
15
|
+
import * as enums from './tempo.config/tempo.enum.js';
|
|
16
|
+
import terms from './tempo.config/plugins/term.import.js';
|
|
17
|
+
import { Match, Token, Snippet, Layout, Event, Period, Default, TimeZone } from './tempo.config/tempo.default.js';
|
|
18
|
+
import './prototype.library.js'; // patch prototypes
|
|
19
|
+
// #endregion
|
|
20
|
+
const STORAGEKEY = '$Tempo'; // for stash in persistent storage
|
|
21
|
+
const Context = getContext(); // get current execution context
|
|
22
|
+
// #endregion Const variables
|
|
23
|
+
/**
|
|
24
|
+
* # Tempo
|
|
25
|
+
* **Tempo** is a powerful wrapper around `Temporal.ZonedDateTime` designed for flexible parsing and intuitive manipulation of date-time objects.
|
|
26
|
+
*
|
|
27
|
+
* It bridges the gap between raw string/number inputs and the strict requirements of the ECMAScript Temporal API.
|
|
28
|
+
*
|
|
29
|
+
* ### Key Features
|
|
30
|
+
* - **Flexible Parsing**: Interprets strings, numbers, BigInts, and various Temporal objects.
|
|
31
|
+
* - **Static Utility**: Access to common enums like `WEEKDAY`, `MONTH` and `SEASON`.
|
|
32
|
+
* - **Fluent API**: Methods for adding, setting, formatting, and comparing date-times.
|
|
33
|
+
* - **Alias Parsing**: Define custom `events` (e.g., "xmas" → "25 Dec") and `periods` (e.g., "noon" → "12:00") for intuitive parsing.
|
|
34
|
+
* - **Plugin System**: Extensible via "terms" to provide contextual date calculations (e.g., quarters, seasons, zodiac signs, etc.).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Standard parsing
|
|
39
|
+
* const t1 = new Tempo('2024-05-20');
|
|
40
|
+
*
|
|
41
|
+
* // Using an event alias (pre-defined or custom)
|
|
42
|
+
* const t2 = new Tempo('christmas'); // Dec 25th
|
|
43
|
+
*
|
|
44
|
+
* // Using a period alias
|
|
45
|
+
* const t3 = new Tempo('2024-05-20 midnight'); // 2024-05-20T00:00:00
|
|
46
|
+
*
|
|
47
|
+
* // Custom events and periods for this instance
|
|
48
|
+
* const t4 = new Tempo('birthday', {
|
|
49
|
+
* event: [['birthday', '20 May']],
|
|
50
|
+
* period: [['tea-time', '15:30']]
|
|
51
|
+
* })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
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
|
+
}
|
|
111
|
+
}
|
|
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
|
+
}
|
|
125
|
+
}
|
|
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
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
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
|
|
171
|
+
}
|
|
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];
|
|
211
|
+
}
|
|
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;
|
|
254
|
+
}
|
|
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));
|
|
279
|
+
}
|
|
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;
|
|
294
|
+
}
|
|
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());
|
|
366
|
+
}
|
|
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)
|
|
371
|
+
}
|
|
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
|
|
400
|
+
}
|
|
401
|
+
if (isSymbol(key)) {
|
|
402
|
+
const name = key.description ?? Cipher.randomKey(); // get Symbol description, else allocate random string
|
|
403
|
+
return Token[name] ??= key;
|
|
404
|
+
}
|
|
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
|
+
});
|
|
447
|
+
}
|
|
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
|
+
});
|
|
457
|
+
}
|
|
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
|
|
565
|
+
});
|
|
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
|
+
}
|
|
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);
|
|
593
|
+
});
|
|
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,
|
|
599
|
+
enumerable: true,
|
|
600
|
+
});
|
|
601
|
+
secure(self.#term);
|
|
602
|
+
return secure(value);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
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
|
|
693
|
+
}
|
|
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);
|
|
701
|
+
}
|
|
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
|
+
});
|
|
779
|
+
}
|
|
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' });
|
|
787
|
+
}
|
|
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) });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
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;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
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
|
|
819
|
+
}
|
|
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}"`);
|
|
847
|
+
}
|
|
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}"`);
|
|
858
|
+
}
|
|
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');
|
|
866
|
+
}
|
|
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"];
|
|
878
|
+
}
|
|
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;
|
|
925
|
+
}
|
|
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
|
+
/**
|
|
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
|
|
949
|
+
*/
|
|
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'
|
|
957
|
+
}
|
|
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;
|
|
982
|
+
}
|
|
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"];
|
|
997
|
+
delete groups["mod"];
|
|
998
|
+
return dateTime;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
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}
|
|
1006
|
+
*/
|
|
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);
|
|
1046
|
+
}
|
|
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}"`);
|
|
1070
|
+
}
|
|
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);
|
|
1076
|
+
}
|
|
1077
|
+
if (!isEmpty(groups))
|
|
1078
|
+
break; // return on the first matched pattern
|
|
1079
|
+
}
|
|
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;
|
|
1092
|
+
}
|
|
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;
|
|
1135
|
+
}
|
|
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() ?? '') };
|
|
1150
|
+
default:
|
|
1151
|
+
return { mutate: 'set', offset: adjust, single: singular(key) };
|
|
1152
|
+
}
|
|
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 });
|
|
1268
|
+
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
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
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
|
|
1359
|
+
}
|
|
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;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// #endregion Namespace
|
|
1406
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1407
|
+
Tempo.init(); // initialize default global configuration
|
|
1408
|
+
// shortcut functions to common Tempo properties / methods
|
|
1409
|
+
/** check valid Tempo */ export const isTempo = (tempo) => isType(tempo, 'Tempo');
|
|
1410
|
+
/** current timestamp (ts) */ export const getStamp = ((tempo, options) => new Tempo(tempo, options).ts);
|
|
1411
|
+
/** create new Tempo */ export const getTempo = ((tempo, options) => new Tempo(tempo, options));
|
|
1412
|
+
/** format a Tempo */ export const fmtTempo = ((fmt, tempo, options) => new Tempo(tempo, options).format(fmt));
|