@ix-xs/node-comfort 2.0.0 → 2.1.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/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Time.js
CHANGED
|
@@ -74,6 +74,21 @@ const { types } = require("node:util");
|
|
|
74
74
|
* Week settings.
|
|
75
75
|
* @typedef {object} WeekOptions
|
|
76
76
|
* @property {0 | 1 | 2 | 3 | 4 | 5 | 6} [weekStartsOn] First day of the week: `0` for Sunday, `1` for Monday. Defaults to `1`.
|
|
77
|
+
* @property {string} [timeZone] Zone whose calendar is used. Defaults to the global zone, then the system's.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Options for `parse()`.
|
|
82
|
+
* @typedef {object} ParseDateOptions
|
|
83
|
+
* @property {string} [locale] Language of month names in the text. Defaults to the global locale.
|
|
84
|
+
* @property {string} [timeZone] Zone the text is written in, unless it carries its own offset. Defaults to the global zone, then the system's.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Options for `range()`.
|
|
89
|
+
* @typedef {object} DateRangeOptions
|
|
90
|
+
* @property {number} [step] How many units between two dates. Defaults to `1`.
|
|
91
|
+
* @property {string} [timeZone] Zone whose calendar is used for days, weeks, months and years.
|
|
77
92
|
*/
|
|
78
93
|
|
|
79
94
|
/**
|
|
@@ -97,6 +112,7 @@ const { types } = require("node:util");
|
|
|
97
112
|
* @property {DateInput} [from] `nextRun()` only: search after this date. Defaults to now.
|
|
98
113
|
* @property {boolean} [unref] Let the process exit even if the schedule is active.
|
|
99
114
|
* @property {boolean} [overlap] Start a run even if the previous one hasn't finished. By default it's skipped.
|
|
115
|
+
* @property {boolean} [immediate] `cron()` only: also run once right away.
|
|
100
116
|
* @property {(error: unknown) => void} [onError] Called when the task fails.
|
|
101
117
|
*/
|
|
102
118
|
|
|
@@ -119,22 +135,11 @@ const { types } = require("node:util");
|
|
|
119
135
|
* @property {() => void} reset Starts over.
|
|
120
136
|
*/
|
|
121
137
|
|
|
122
|
-
const
|
|
138
|
+
const shared = require("./internal/shared.js");
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
const UNIT_ALIASES = {
|
|
126
|
-
ms: "ms", msec: "ms", milli: "ms", millis: "ms", millisecond: "ms", milliseconds: "ms",
|
|
127
|
-
s: "s", sec: "s", secs: "s", second: "s", seconds: "s",
|
|
128
|
-
m: "m", min: "m", mins: "m", minute: "m", minutes: "m",
|
|
129
|
-
h: "h", hr: "h", hrs: "h", hour: "h", hours: "h",
|
|
130
|
-
d: "d", day: "d", days: "d",
|
|
131
|
-
w: "w", wk: "w", wks: "w", week: "w", weeks: "w",
|
|
132
|
-
M: "M", mo: "M", month: "M", months: "M",
|
|
133
|
-
y: "y", yr: "y", yrs: "y", year: "y", years: "y",
|
|
134
|
-
};
|
|
140
|
+
const { MS, intl, unitOf: _unit, setLongTimeout: _longTimeout } = shared;
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
const _config = { locale: "en", timeZone: /** @type {string | undefined} */ (undefined) };
|
|
142
|
+
const _config = shared.timeConfig;
|
|
138
143
|
|
|
139
144
|
/**
|
|
140
145
|
* @param {DateInput | undefined} value
|
|
@@ -148,11 +153,6 @@ const _toDate = (value) => (types.isDate(value) ? new Date(value.getTime()) : va
|
|
|
148
153
|
*/
|
|
149
154
|
const _pad = (n, len = 2) => String(Math.abs(n)).padStart(len, "0");
|
|
150
155
|
|
|
151
|
-
/**
|
|
152
|
-
* @param {string} unit
|
|
153
|
-
* @returns {keyof typeof MS | "M" | "y" | undefined}
|
|
154
|
-
*/
|
|
155
|
-
const _unit = (unit) => UNIT_ALIASES[unit] ?? UNIT_ALIASES[unit.toLowerCase()];
|
|
156
156
|
|
|
157
157
|
/** @type {Map<string, Intl.DateTimeFormat>} */
|
|
158
158
|
const _dtfCache = new Map();
|
|
@@ -227,42 +227,156 @@ const _fromWall = (year, month, day, hour, minute, second, timeZone) => {
|
|
|
227
227
|
* @param {number} year
|
|
228
228
|
* @param {number} month 1-12
|
|
229
229
|
*/
|
|
230
|
-
const _daysIn = (year, month) =>
|
|
230
|
+
const _daysIn = (year, month) => (month === 2 ? ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28) : month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31);
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Wall-clock time in a zone to an instant. A time that happens twice (clocks
|
|
234
|
+
* going back) keeps its first occurrence; one that never happens (clocks
|
|
235
|
+
* going forward) moves past the gap.
|
|
236
|
+
* @param {number} year
|
|
237
|
+
* @param {number} month 1-12
|
|
238
|
+
* @param {number} day
|
|
239
|
+
* @param {number} hour
|
|
240
|
+
* @param {number} minute
|
|
241
|
+
* @param {number} second
|
|
242
|
+
* @param {number} ms
|
|
243
|
+
* @param {string} [timeZone]
|
|
244
|
+
* @returns {Date}
|
|
245
|
+
*/
|
|
246
|
+
const _instantOf = (year, month, day, hour, minute, second, ms, timeZone) => {
|
|
247
|
+
if (!timeZone) return new Date(year, month - 1, day, hour, minute, second, ms);
|
|
248
|
+
const guess = Date.UTC(year, month - 1, day, hour, minute, second, ms);
|
|
249
|
+
// The offsets a day before and a day after frame any clock change.
|
|
250
|
+
const early = guess - _offset(new Date(guess - MS.d), timeZone) * 60_000;
|
|
251
|
+
const late = guess - _offset(new Date(guess + MS.d), timeZone) * 60_000;
|
|
252
|
+
if (early === late) return new Date(early);
|
|
253
|
+
const first = Math.min(early, late);
|
|
254
|
+
const at = _parts(new Date(first), timeZone);
|
|
255
|
+
if (at.day === day && at.hour === hour && at.minute === minute) return new Date(first);
|
|
256
|
+
return new Date(Math.max(early, late));
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The first moment of a unit, as wall-clock fields.
|
|
261
|
+
* @param {{ year: number, month: number, day: number, hour: number, minute: number, second: number, weekday: number }} at
|
|
262
|
+
* @param {string} unit
|
|
263
|
+
* @param {number} [weekStartsOn]
|
|
264
|
+
* @returns {[number, number, number, number, number, number]}
|
|
265
|
+
* @throws {TypeError} If the unit isn't one.
|
|
266
|
+
*/
|
|
267
|
+
const _startWall = (at, unit, weekStartsOn) => {
|
|
268
|
+
switch (unit) {
|
|
269
|
+
case "year": return [at.year, 1, 1, 0, 0, 0];
|
|
270
|
+
case "quarter": return [at.year, Math.floor((at.month - 1) / 3) * 3 + 1, 1, 0, 0, 0];
|
|
271
|
+
case "month": return [at.year, at.month, 1, 0, 0, 0];
|
|
272
|
+
case "week":
|
|
273
|
+
case "isoWeek": {
|
|
274
|
+
const first = unit === "isoWeek" ? 1 : weekStartsOn ?? 1;
|
|
275
|
+
const monday = new Date(Date.UTC(at.year, at.month - 1, at.day - ((at.weekday - first + 7) % 7)));
|
|
276
|
+
return [monday.getUTCFullYear(), monday.getUTCMonth() + 1, monday.getUTCDate(), 0, 0, 0];
|
|
277
|
+
}
|
|
278
|
+
case "day": return [at.year, at.month, at.day, 0, 0, 0];
|
|
279
|
+
case "hour": return [at.year, at.month, at.day, at.hour, 0, 0];
|
|
280
|
+
case "minute": return [at.year, at.month, at.day, at.hour, at.minute, 0];
|
|
281
|
+
case "second": return [at.year, at.month, at.day, at.hour, at.minute, at.second];
|
|
282
|
+
default: throw new TypeError(`Unknown unit: ${JSON.stringify(unit)}`);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Calendar days from `b` to `a` in a zone: 1 means "the day after".
|
|
288
|
+
* @param {Date} a
|
|
289
|
+
* @param {Date} b
|
|
290
|
+
* @param {string} [timeZone]
|
|
291
|
+
* @returns {number}
|
|
292
|
+
*/
|
|
293
|
+
const _dayDelta = (a, b, timeZone) => {
|
|
294
|
+
const x = _parts(a, timeZone);
|
|
295
|
+
const y = _parts(b, timeZone);
|
|
296
|
+
return Math.round((Date.UTC(x.year, x.month - 1, x.day) - Date.UTC(y.year, y.month - 1, y.day)) / MS.d);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Adds calendar days: the same wall-clock time, that many days later.
|
|
301
|
+
* @param {Date} d
|
|
302
|
+
* @param {number} days
|
|
303
|
+
* @param {string} [timeZone]
|
|
304
|
+
* @returns {Date}
|
|
305
|
+
*/
|
|
306
|
+
const _addDays = (d, days, timeZone) => {
|
|
307
|
+
if (timeZone === "UTC") return new Date(d.getTime() + days * MS.d);
|
|
308
|
+
const at = _parts(d, timeZone);
|
|
309
|
+
// Date.UTC carries a day past the end of the month into the next one.
|
|
310
|
+
const day = new Date(Date.UTC(at.year, at.month - 1, at.day + days));
|
|
311
|
+
return _instantOf(day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate(), at.hour, at.minute, at.second, at.ms, timeZone);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The short name of a unit, or an error that names what was given.
|
|
316
|
+
* @param {string} unit
|
|
317
|
+
* @returns {"ms" | "s" | "m" | "h" | "d" | "w" | "M" | "y"}
|
|
318
|
+
*/
|
|
319
|
+
const _knownUnit = (unit) => {
|
|
320
|
+
const known = _unit(unit);
|
|
321
|
+
if (!known) throw new TypeError(`Unknown unit: ${JSON.stringify(unit)}`);
|
|
322
|
+
return known;
|
|
323
|
+
};
|
|
231
324
|
|
|
232
325
|
/**
|
|
233
326
|
* Adds calendar months, keeping the day in range (Jan 31 + 1 month = end of Feb).
|
|
234
327
|
* @param {Date} d
|
|
235
328
|
* @param {number} months
|
|
329
|
+
* @param {string} [timeZone]
|
|
236
330
|
* @returns {Date}
|
|
237
331
|
*/
|
|
238
|
-
const _addMonths = (d, months) => {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return out;
|
|
332
|
+
const _addMonths = (d, months, timeZone) => {
|
|
333
|
+
const at = _parts(d, timeZone);
|
|
334
|
+
const index = at.year * 12 + at.month - 1 + Math.trunc(months);
|
|
335
|
+
const year = Math.floor(index / 12);
|
|
336
|
+
const month = index - year * 12 + 1;
|
|
337
|
+
return _instantOf(year, month, Math.min(at.day, _daysIn(year, month)), at.hour, at.minute, at.second, at.ms, timeZone);
|
|
245
338
|
};
|
|
246
339
|
|
|
247
340
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* @param {
|
|
251
|
-
* @param {
|
|
252
|
-
* @
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
341
|
+
* Adds a duration written as text: a fixed one (`"1h30m"`) or one with
|
|
342
|
+
* calendar units (`"1 month"`, `"2y 3d"`).
|
|
343
|
+
* @param {Date} d
|
|
344
|
+
* @param {string} text
|
|
345
|
+
* @param {1 | -1} sign
|
|
346
|
+
* @param {string} [timeZone]
|
|
347
|
+
* @returns {Date}
|
|
348
|
+
*/
|
|
349
|
+
const _addText = (d, text, sign, timeZone) => {
|
|
350
|
+
const fixed = shared.parseDuration(text);
|
|
351
|
+
// Under a day there is no calendar to follow.
|
|
352
|
+
if (fixed !== null && Math.abs(fixed) < MS.d) return new Date(d.getTime() + sign * fixed);
|
|
353
|
+
const regex = /([-+]?\d*\.?\d+)\s*([a-zA-Z]+)/g;
|
|
354
|
+
let months = 0;
|
|
355
|
+
let days = 0;
|
|
356
|
+
let ms = 0;
|
|
357
|
+
let consumed = "";
|
|
358
|
+
let match;
|
|
359
|
+
while ((match = regex.exec(text)) !== null) {
|
|
360
|
+
const unit = _unit(match[2]);
|
|
361
|
+
if (!unit) break;
|
|
362
|
+
const amount = Number(match[1]);
|
|
363
|
+
if (unit === "M") months += amount;
|
|
364
|
+
else if (unit === "y") months += amount * 12;
|
|
365
|
+
else if ((unit === "d" || unit === "w") && Number.isInteger(amount)) days += unit === "w" ? amount * 7 : amount;
|
|
366
|
+
else ms += amount * MS[unit];
|
|
367
|
+
consumed += match[0];
|
|
368
|
+
}
|
|
369
|
+
if (!consumed || consumed.replace(/\s+/g, "") !== text.replace(/[\s,]+|and/g, "")) {
|
|
370
|
+
// A bare number of milliseconds, as text.
|
|
371
|
+
if (fixed !== null) return new Date(d.getTime() + sign * fixed);
|
|
372
|
+
throw new TypeError(`Invalid duration: ${JSON.stringify(text)}`);
|
|
373
|
+
}
|
|
374
|
+
let moved = months ? _addMonths(d, sign * months, timeZone) : d;
|
|
375
|
+
if (days) moved = _addDays(moved, sign * days, timeZone);
|
|
376
|
+
return new Date(moved.getTime() + sign * ms);
|
|
264
377
|
};
|
|
265
378
|
|
|
379
|
+
|
|
266
380
|
/**
|
|
267
381
|
* @param {() => unknown} task
|
|
268
382
|
* @param {((error: unknown) => void) | undefined} onError
|
|
@@ -364,39 +478,7 @@ function offset(timeZone, date) {
|
|
|
364
478
|
* @returns {number | null} `null` when it isn't a duration.
|
|
365
479
|
*/
|
|
366
480
|
function parseDuration(input) {
|
|
367
|
-
|
|
368
|
-
if (typeof input !== "string") return null;
|
|
369
|
-
const text = input.trim();
|
|
370
|
-
if (!text) return null;
|
|
371
|
-
if (/^[-+]?\d+(?:\.\d+)?$/.test(text)) return Number(text);
|
|
372
|
-
|
|
373
|
-
const iso = /^([-+])?P(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/i.exec(text);
|
|
374
|
-
if (iso && /\d/.test(text)) {
|
|
375
|
-
const [, sign, w, d, h, m, s] = iso;
|
|
376
|
-
const n = (/** @type {string | undefined} */ v) => (v ? Number(v.replace(",", ".")) : 0);
|
|
377
|
-
const total = n(w) * MS.w + n(d) * MS.d + n(h) * MS.h + n(m) * MS.m + n(s) * MS.s;
|
|
378
|
-
return sign === "-" ? -total : total;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const clock = /^([-+])?(?:(\d+):)?(\d+):(\d{1,2})(?:\.(\d{1,3}))?$/.exec(text);
|
|
382
|
-
if (clock) {
|
|
383
|
-
const [, sign, h, m, s, ms] = clock;
|
|
384
|
-
const total = Number(h ?? 0) * MS.h + Number(m) * MS.m + Number(s) * MS.s + (ms ? Number(ms.padEnd(3, "0")) : 0);
|
|
385
|
-
return sign === "-" ? -total : total;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const regex = /(-?\d*\.?\d+)\s*([a-zA-Z]+)/g;
|
|
389
|
-
let total = 0;
|
|
390
|
-
let consumed = "";
|
|
391
|
-
let match;
|
|
392
|
-
while ((match = regex.exec(text)) !== null) {
|
|
393
|
-
const unit = _unit(match[2]);
|
|
394
|
-
if (!unit || unit === "M" || unit === "y") return null;
|
|
395
|
-
total += Number(match[1]) * MS[unit];
|
|
396
|
-
consumed += match[0];
|
|
397
|
-
}
|
|
398
|
-
if (!consumed || consumed.replace(/\s+/g, "") !== text.replace(/[\s,]+|and/g, "")) return null;
|
|
399
|
-
return total;
|
|
481
|
+
return shared.parseDuration(input);
|
|
400
482
|
}
|
|
401
483
|
|
|
402
484
|
/**
|
|
@@ -408,13 +490,15 @@ function parseDuration(input) {
|
|
|
408
490
|
* time.formatDuration(90_000, { long: true, locale: "fr" }); // "1 minute 30 secondes"
|
|
409
491
|
* time.formatDuration(93_784_000, { units: 2 }); // "1d 2h"
|
|
410
492
|
* time.formatDuration(5_405_000, { clock: true }); // "01:30:05"
|
|
493
|
+
* time.formatDuration("90m"); // "1h 30m"
|
|
411
494
|
*
|
|
412
|
-
* @param {number}
|
|
495
|
+
* @param {number | string} duration Milliseconds, or text like `"90m"`.
|
|
413
496
|
* @param {FormatDurationOptions} [options]
|
|
414
497
|
* @returns {string}
|
|
415
498
|
*/
|
|
416
|
-
function formatDuration(
|
|
417
|
-
|
|
499
|
+
function formatDuration(duration, options = {}) {
|
|
500
|
+
const ms = typeof duration === "string" ? shared.parseDuration(duration) ?? NaN : duration;
|
|
501
|
+
if (!Number.isFinite(ms)) return String(duration);
|
|
418
502
|
const sign = ms < 0 ? "-" : "";
|
|
419
503
|
let rest = Math.abs(Math.trunc(ms));
|
|
420
504
|
|
|
@@ -428,20 +512,21 @@ function formatDuration(ms, options = {}) {
|
|
|
428
512
|
|
|
429
513
|
const locale = options.locale ?? _config.locale;
|
|
430
514
|
const showMs = options.milliseconds ?? true;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
515
|
+
const limit = options.units ?? Infinity;
|
|
516
|
+
/** @param {string} unit */
|
|
517
|
+
const long = (unit) => intl.number(locale, { style: "unit", unit, unitDisplay: "long" });
|
|
518
|
+
let out = "";
|
|
519
|
+
let shown = 0;
|
|
520
|
+
for (let i = 0; i < _DURATION_UNITS.length && shown < limit; i++) {
|
|
521
|
+
const [size, short, unit] = _DURATION_UNITS[i];
|
|
522
|
+
const value = Math.floor(rest / size);
|
|
523
|
+
rest -= value * size;
|
|
524
|
+
if (value === 0 || (unit === "millisecond" && !showMs)) continue;
|
|
525
|
+
out += `${shown ? " " : ""}${options.long ? long(unit).format(value) : `${value}${short}`}`;
|
|
526
|
+
shown++;
|
|
437
527
|
}
|
|
438
|
-
if (!
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
return sign + parts
|
|
442
|
-
.slice(0, options.units ?? Infinity)
|
|
443
|
-
.map((p) => (options.long ? new Intl.NumberFormat(locale, { style: "unit", unit: p.unit, unitDisplay: "long" }).format(p.v) : `${p.v}${p.short}`))
|
|
444
|
-
.join(" ");
|
|
528
|
+
if (!shown) return options.long ? long("second").format(0) : "0s";
|
|
529
|
+
return sign + out;
|
|
445
530
|
}
|
|
446
531
|
|
|
447
532
|
/**
|
|
@@ -460,16 +545,13 @@ function formatDuration(ms, options = {}) {
|
|
|
460
545
|
*/
|
|
461
546
|
function relative(date, from, options = {}) {
|
|
462
547
|
const locale = options.locale ?? _config.locale;
|
|
548
|
+
const style = options.style ?? "long";
|
|
463
549
|
const diffMs = _toDate(date).getTime() - _toDate(from).getTime();
|
|
464
550
|
const abs = Math.abs(diffMs);
|
|
465
|
-
if (abs < MS.s) return
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
[MS.d * 30, MS.w, "week"], [MS.d * 365, MS.d * 30, "month"], [Infinity, MS.d * 365, "year"],
|
|
470
|
-
];
|
|
471
|
-
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: options.numeric ?? "always", style: options.style ?? "long" });
|
|
472
|
-
for (const [limit, size, unit] of scale) {
|
|
551
|
+
if (abs < MS.s) return intl.relativeTime(locale, { numeric: "auto", style }).format(0, "second");
|
|
552
|
+
const rtf = intl.relativeTime(locale, { numeric: options.numeric ?? "always", style });
|
|
553
|
+
for (let i = 0; i < _RELATIVE_SCALE.length; i++) {
|
|
554
|
+
const [limit, size, unit] = _RELATIVE_SCALE[i];
|
|
473
555
|
if (abs < limit) {
|
|
474
556
|
const value = Math.floor(abs / size);
|
|
475
557
|
return rtf.format(diffMs < 0 ? -value : value, unit);
|
|
@@ -495,24 +577,14 @@ function calendar(date, options = {}) {
|
|
|
495
577
|
const locale = options.locale ?? _config.locale;
|
|
496
578
|
const timeZone = options.timeZone ?? _config.timeZone;
|
|
497
579
|
const d = _toDate(date);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
const literal = joined.find((part, i) => part.type === "literal" && joined[i + 1]?.type === "hour")?.value ?? " ";
|
|
507
|
-
return `${label.charAt(0).toUpperCase()}${label.slice(1)}${literal}${hhmm}`;
|
|
508
|
-
};
|
|
509
|
-
if (Math.abs(delta) <= 1) {
|
|
510
|
-
return at(new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(delta, "day"));
|
|
511
|
-
}
|
|
512
|
-
if (delta > 1 && delta < 7) {
|
|
513
|
-
return at(new Intl.DateTimeFormat(locale, { weekday: "long", timeZone }).format(d));
|
|
514
|
-
}
|
|
515
|
-
return new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone }).format(d);
|
|
580
|
+
const delta = _dayDelta(d, _toDate(options.now), timeZone);
|
|
581
|
+
if (delta < -1 || delta >= 7) return intl.dateTime(locale, { dateStyle: "long", timeZone }).format(d);
|
|
582
|
+
const hhmm = intl.dateTime(locale, { timeStyle: "short", timeZone }).format(d);
|
|
583
|
+
const label = delta <= 1 ? intl.relativeTime(locale, { numeric: "auto" }).format(delta, "day") : intl.dateTime(locale, { weekday: "long", timeZone }).format(d);
|
|
584
|
+
// The word that joins a date and a time in this language ("at", "à", "um").
|
|
585
|
+
const joined = intl.dateTime(locale, { dateStyle: "full", timeStyle: "short", timeZone }).formatToParts(d);
|
|
586
|
+
const literal = joined.find((part, i) => part.type === "literal" && joined[i + 1]?.type === "hour")?.value ?? " ";
|
|
587
|
+
return `${label.charAt(0).toUpperCase()}${label.slice(1)}${literal}${hhmm}`;
|
|
516
588
|
}
|
|
517
589
|
|
|
518
590
|
/**
|
|
@@ -528,6 +600,7 @@ function calendar(date, options = {}) {
|
|
|
528
600
|
* | `dddd` / `ddd` | Monday / Mon | | `A` / `a` | PM / pm |
|
|
529
601
|
* | `d` | weekday, 0 is Sunday | | `Z` / `ZZ` | +02:00 / +0200 |
|
|
530
602
|
* | `W` / `WW` | ISO week | | `X` / `x` | Unix seconds / ms |
|
|
603
|
+
* | `Do` | 5th | | `z` | zone name, like EST or GMT+1 |
|
|
531
604
|
*
|
|
532
605
|
* Text in brackets stays as is: `"[Today is] dddd"`.
|
|
533
606
|
*
|
|
@@ -546,53 +619,246 @@ function format(date, pattern = "YYYY-MM-DD HH:mm:ss", options = {}) {
|
|
|
546
619
|
if (Number.isNaN(d.getTime())) return "Invalid Date";
|
|
547
620
|
const locale = options.locale ?? _config.locale;
|
|
548
621
|
const timeZone = options.timeZone ?? _config.timeZone;
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
622
|
+
const at = _parts(d, timeZone);
|
|
623
|
+
let compiled = _patterns.get(pattern);
|
|
624
|
+
if (compiled === undefined) {
|
|
625
|
+
compiled = _compile(pattern);
|
|
626
|
+
if (_patterns.size >= 200) _patterns.clear();
|
|
627
|
+
_patterns.set(pattern, compiled);
|
|
628
|
+
}
|
|
629
|
+
// Literal text and tokens alternate, starting with text.
|
|
630
|
+
let out = compiled[0];
|
|
631
|
+
for (let i = 1; i < compiled.length; i += 2) out += _token(compiled[i], d, at, locale, timeZone) + compiled[i + 1];
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const _TOKENS = /\[([^\]]*)\]|YYYY|MMMM|dddd|SSS|MMM|ddd|Do|YY|MM|DD|HH|hh|mm|ss|ZZ|WW|M|D|d|H|h|m|s|A|a|Z|z|Q|X|x|W/g;
|
|
636
|
+
|
|
637
|
+
/** @type {Map<string, string[]>} */
|
|
638
|
+
const _patterns = new Map();
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Splits a pattern into literal text and tokens, alternating.
|
|
642
|
+
* @param {string} pattern
|
|
643
|
+
* @returns {string[]}
|
|
644
|
+
*/
|
|
645
|
+
const _compile = (pattern) => {
|
|
646
|
+
/** @type {string[]} */
|
|
647
|
+
const out = [""];
|
|
648
|
+
let last = 0;
|
|
649
|
+
for (const match of pattern.matchAll(_TOKENS)) {
|
|
650
|
+
const literal = pattern.slice(last, match.index);
|
|
651
|
+
last = match.index + match[0].length;
|
|
652
|
+
if (match[1] !== undefined) {
|
|
653
|
+
out[out.length - 1] += literal + match[1];
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
out[out.length - 1] += literal;
|
|
657
|
+
out.push(match[0], "");
|
|
658
|
+
}
|
|
659
|
+
out[out.length - 1] += pattern.slice(last);
|
|
660
|
+
return out;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* @param {Date} d
|
|
665
|
+
* @param {string | undefined} timeZone
|
|
666
|
+
* @param {string} separator
|
|
667
|
+
*/
|
|
668
|
+
const _offsetText = (d, timeZone, separator) => {
|
|
669
|
+
const offset = _offset(d, timeZone);
|
|
670
|
+
const abs = Math.abs(offset);
|
|
671
|
+
return `${offset < 0 ? "-" : "+"}${_pad(Math.floor(abs / 60))}${separator}${_pad(abs % 60)}`;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* @param {string} token
|
|
676
|
+
* @param {Date} d
|
|
677
|
+
* @param {{ year: number, month: number, day: number, hour: number, minute: number, second: number, ms: number, weekday: number }} at
|
|
678
|
+
* @param {string} locale
|
|
679
|
+
* @param {string} [timeZone]
|
|
680
|
+
* @returns {string}
|
|
681
|
+
*/
|
|
682
|
+
const _token = (token, d, at, locale, timeZone) => {
|
|
683
|
+
switch (token) {
|
|
684
|
+
case "YYYY": return String(at.year);
|
|
685
|
+
case "YY": return String(at.year).slice(-2);
|
|
686
|
+
case "Q": return String(Math.ceil(at.month / 3));
|
|
687
|
+
case "MMMM": return intl.dateTime(locale, { month: "long", timeZone }).format(d);
|
|
688
|
+
case "MMM": return intl.dateTime(locale, { month: "short", timeZone }).format(d);
|
|
689
|
+
case "MM": return _pad(at.month);
|
|
690
|
+
case "M": return String(at.month);
|
|
691
|
+
case "DD": return _pad(at.day);
|
|
692
|
+
case "D": return String(at.day);
|
|
693
|
+
case "Do": return require("./Num.js").ordinal(at.day, { locale });
|
|
694
|
+
case "dddd": return intl.dateTime(locale, { weekday: "long", timeZone }).format(d);
|
|
695
|
+
case "ddd": return intl.dateTime(locale, { weekday: "short", timeZone }).format(d);
|
|
696
|
+
case "d": return String(at.weekday);
|
|
697
|
+
case "HH": return _pad(at.hour);
|
|
698
|
+
case "H": return String(at.hour);
|
|
699
|
+
case "hh": return _pad(at.hour % 12 === 0 ? 12 : at.hour % 12);
|
|
700
|
+
case "h": return String(at.hour % 12 === 0 ? 12 : at.hour % 12);
|
|
701
|
+
case "mm": return _pad(at.minute);
|
|
702
|
+
case "m": return String(at.minute);
|
|
703
|
+
case "ss": return _pad(at.second);
|
|
704
|
+
case "s": return String(at.second);
|
|
705
|
+
case "SSS": return _pad(at.ms, 3);
|
|
706
|
+
case "A": return at.hour < 12 ? "AM" : "PM";
|
|
707
|
+
case "a": return at.hour < 12 ? "am" : "pm";
|
|
708
|
+
case "Z": return _offsetText(d, timeZone, ":");
|
|
709
|
+
case "ZZ": return _offsetText(d, timeZone, "");
|
|
710
|
+
case "z": return intl.dateTime(locale, { timeZoneName: "short", timeZone }).formatToParts(d).find((part) => part.type === "timeZoneName")?.value ?? "";
|
|
711
|
+
case "X": return String(Math.floor(d.getTime() / 1000));
|
|
712
|
+
case "x": return String(d.getTime());
|
|
713
|
+
case "W": return String(_isoWeek(at.year, at.month, at.day));
|
|
714
|
+
case "WW": return _pad(_isoWeek(at.year, at.month, at.day));
|
|
715
|
+
default: return token;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* ISO week number of a calendar date.
|
|
721
|
+
* @param {number} year
|
|
722
|
+
* @param {number} month 1-12
|
|
723
|
+
* @param {number} day
|
|
724
|
+
* @returns {number}
|
|
725
|
+
*/
|
|
726
|
+
const _isoWeek = (year, month, day) => {
|
|
727
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
728
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - (utc.getUTCDay() || 7));
|
|
729
|
+
return Math.ceil(((utc.getTime() - Date.UTC(utc.getUTCFullYear(), 0, 1)) / MS.d + 1) / 7);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
/** @type {Array<[number, string, string]>} */
|
|
733
|
+
const _DURATION_UNITS = [[MS.d, "d", "day"], [MS.h, "h", "hour"], [MS.m, "m", "minute"], [MS.s, "s", "second"], [1, "ms", "millisecond"]];
|
|
734
|
+
|
|
735
|
+
/** @type {Array<[number, number, Intl.RelativeTimeFormatUnit]>} */
|
|
736
|
+
const _RELATIVE_SCALE = [
|
|
737
|
+
[MS.m, MS.s, "second"], [MS.h, MS.m, "minute"], [MS.d, MS.h, "hour"], [MS.w, MS.d, "day"],
|
|
738
|
+
[MS.d * 30, MS.w, "week"], [MS.d * 365, MS.d * 30, "month"], [Infinity, MS.d * 365, "year"],
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
/** @type {Map<string, { regex: RegExp, tokens: string[] }>} */
|
|
742
|
+
const _parsers = new Map();
|
|
743
|
+
|
|
744
|
+
/** @type {Record<string, string>} */
|
|
745
|
+
const _PARSE_TOKENS = {
|
|
746
|
+
YYYY: "(\\d{4})", YY: "(\\d{2})", MM: "(\\d{2})", M: "(\\d{1,2})", DD: "(\\d{2})", D: "(\\d{1,2})", Do: "(\\d{1,2})\\D{0,4}?",
|
|
747
|
+
HH: "(\\d{2})", H: "(\\d{1,2})", hh: "(\\d{2})", h: "(\\d{1,2})", mm: "(\\d{2})", m: "(\\d{1,2})", ss: "(\\d{2})", s: "(\\d{1,2})",
|
|
748
|
+
SSS: "(\\d{3})", A: "([AaPp][Mm])", a: "([AaPp][Mm])", Z: "(Z|[+-]\\d{2}:\\d{2})", ZZ: "(Z|[+-]\\d{4})", X: "(-?\\d+)", x: "(-?\\d+)",
|
|
749
|
+
MMMM: "([^\\d\\s,./-]+\\.?)", MMM: "([^\\d\\s,./-]+\\.?)", dddd: "[^\\d\\s,]+\\.?", ddd: "[^\\d\\s,]+\\.?",
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/** @type {Map<string, Map<string, number>>} */
|
|
753
|
+
const _monthNames = new Map();
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Month names of a language, long and short, mapped to 1-12.
|
|
757
|
+
* @param {string} locale
|
|
758
|
+
* @returns {Map<string, number>}
|
|
759
|
+
*/
|
|
760
|
+
const _monthsOf = (locale) => {
|
|
761
|
+
let names = _monthNames.get(locale);
|
|
762
|
+
if (!names) {
|
|
763
|
+
names = new Map();
|
|
764
|
+
for (let month = 1; month <= 12; month++) {
|
|
765
|
+
const sample = new Date(Date.UTC(2024, month - 1, 15));
|
|
766
|
+
for (const style of /** @type {const} */ (["long", "short"])) {
|
|
767
|
+
names.set(intl.dateTime(locale, { month: style, timeZone: "UTC" }).format(sample).toLowerCase().replace(/\.$/, ""), month);
|
|
593
768
|
}
|
|
594
|
-
}
|
|
595
|
-
|
|
769
|
+
}
|
|
770
|
+
_monthNames.set(locale, names);
|
|
771
|
+
}
|
|
772
|
+
return names;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Reads a date written in a known format: the opposite of `format()`, with
|
|
777
|
+
* the same tokens. Parts the pattern doesn't have default to today's date
|
|
778
|
+
* and to midnight.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* time.parse("15/01/2024 14:30", "DD/MM/YYYY HH:mm");
|
|
782
|
+
* time.parse("15 janvier 2024", "D MMMM YYYY", { locale: "fr" });
|
|
783
|
+
* time.parse("2024-01-15 09:00", "YYYY-MM-DD HH:mm", { timeZone: "Asia/Tokyo" });
|
|
784
|
+
* time.parse("31/02/2024", "DD/MM/YYYY"); // undefined, no such day
|
|
785
|
+
*
|
|
786
|
+
* @param {string} text
|
|
787
|
+
* @param {string} pattern Tokens like `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`, `A`, `Z`. Text in brackets must match as is.
|
|
788
|
+
* @param {ParseDateOptions} [options]
|
|
789
|
+
* @returns {Date | undefined} `undefined` when the text doesn't match or isn't a real date.
|
|
790
|
+
*/
|
|
791
|
+
function parse(text, pattern, options = {}) {
|
|
792
|
+
let parser = _parsers.get(pattern);
|
|
793
|
+
if (!parser) {
|
|
794
|
+
const compiled = _compile(pattern);
|
|
795
|
+
const escape = (/** @type {string} */ literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
796
|
+
let source = escape(compiled[0]);
|
|
797
|
+
/** @type {string[]} */
|
|
798
|
+
const tokens = [];
|
|
799
|
+
for (let i = 1; i < compiled.length; i += 2) {
|
|
800
|
+
const expression = _PARSE_TOKENS[compiled[i]];
|
|
801
|
+
if (!expression) throw new TypeError(`parse() does not support the "${compiled[i]}" token`);
|
|
802
|
+
if (expression.startsWith("(")) tokens.push(compiled[i]);
|
|
803
|
+
source += expression + escape(compiled[i + 1]);
|
|
804
|
+
}
|
|
805
|
+
parser = { regex: new RegExp(`^\\s*${source}\\s*$`), tokens };
|
|
806
|
+
if (_parsers.size >= 200) _parsers.clear();
|
|
807
|
+
_parsers.set(pattern, parser);
|
|
808
|
+
}
|
|
809
|
+
const match = parser.regex.exec(String(text));
|
|
810
|
+
if (!match) return undefined;
|
|
811
|
+
|
|
812
|
+
const locale = options.locale ?? _config.locale;
|
|
813
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
814
|
+
/** @type {Record<string, number | undefined>} */
|
|
815
|
+
const found = {};
|
|
816
|
+
/** @type {string | undefined} */
|
|
817
|
+
let meridiem;
|
|
818
|
+
/** @type {number | undefined} */
|
|
819
|
+
let offset;
|
|
820
|
+
for (let i = 0; i < parser.tokens.length; i++) {
|
|
821
|
+
const token = parser.tokens[i];
|
|
822
|
+
const value = match[i + 1];
|
|
823
|
+
switch (token) {
|
|
824
|
+
case "YYYY": found.year = Number(value); break;
|
|
825
|
+
case "YY": found.year = 2000 + Number(value); break;
|
|
826
|
+
case "MM": case "M": found.month = Number(value); break;
|
|
827
|
+
case "MMMM": case "MMM": {
|
|
828
|
+
const month = _monthsOf(locale).get(value.toLowerCase().replace(/\.$/, ""));
|
|
829
|
+
if (month === undefined) return undefined;
|
|
830
|
+
found.month = month;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case "DD": case "D": case "Do": found.day = Number(value); break;
|
|
834
|
+
case "HH": case "H": case "hh": case "h": found.hour = Number(value); break;
|
|
835
|
+
case "mm": case "m": found.minute = Number(value); break;
|
|
836
|
+
case "ss": case "s": found.second = Number(value); break;
|
|
837
|
+
case "SSS": found.ms = Number(value); break;
|
|
838
|
+
case "A": case "a": meridiem = value.toLowerCase(); break;
|
|
839
|
+
case "Z": case "ZZ": {
|
|
840
|
+
const digits = value.replace(":", "");
|
|
841
|
+
offset = value === "Z" ? 0 : (digits[0] === "-" ? -1 : 1) * (Number(digits.slice(1, 3)) * 60 + Number(digits.slice(3, 5)));
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
case "X": return new Date(Number(value) * 1000);
|
|
845
|
+
case "x": return new Date(Number(value));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const today = _parts(new Date(), offset === undefined ? timeZone : "UTC");
|
|
849
|
+
const year = found.year ?? today.year;
|
|
850
|
+
const month = found.month ?? (found.year === undefined ? today.month : 1);
|
|
851
|
+
const day = found.day ?? (found.year === undefined && found.month === undefined ? today.day : 1);
|
|
852
|
+
let hour = found.hour ?? 0;
|
|
853
|
+
if (meridiem !== undefined) {
|
|
854
|
+
if (hour < 1 || hour > 12) return undefined;
|
|
855
|
+
hour = (hour % 12) + (meridiem === "pm" ? 12 : 0);
|
|
856
|
+
}
|
|
857
|
+
const minute = found.minute ?? 0;
|
|
858
|
+
const second = found.second ?? 0;
|
|
859
|
+
if (month < 1 || month > 12 || day < 1 || day > _daysIn(year, month) || hour > 23 || minute > 59 || second > 59) return undefined;
|
|
860
|
+
if (offset !== undefined) return new Date(Date.UTC(year, month - 1, day, hour, minute, second, found.ms ?? 0) - offset * 60_000);
|
|
861
|
+
return _instantOf(year, month, day, hour, minute, second, found.ms ?? 0, timeZone);
|
|
596
862
|
}
|
|
597
863
|
|
|
598
864
|
/**
|
|
@@ -611,25 +877,34 @@ function toISODate(date, options = {}) {
|
|
|
611
877
|
}
|
|
612
878
|
|
|
613
879
|
/**
|
|
614
|
-
* Adds time to a date and returns a new `Date`.
|
|
615
|
-
*
|
|
880
|
+
* Adds time to a date and returns a new `Date`. Whole days, weeks, months
|
|
881
|
+
* and years follow the calendar: one day later is the same time tomorrow,
|
|
882
|
+
* even when the clocks change in between, and January 31 plus one month is
|
|
883
|
+
* the end of February. Hours and smaller units are exact.
|
|
616
884
|
*
|
|
617
885
|
* @example
|
|
618
886
|
* time.add(new Date(), "2h30m");
|
|
619
887
|
* time.add(new Date(), 3, "days");
|
|
888
|
+
* time.add(new Date(), "1 month");
|
|
620
889
|
* time.add(new Date(2024, 0, 31), 1, "month"); // 2024-02-29
|
|
890
|
+
* time.add(date, "1 day", { timeZone: "Europe/Paris" });
|
|
621
891
|
*
|
|
622
892
|
* @param {DateInput} date
|
|
623
|
-
* @param {number | string} amount A number with `unit`, or a duration like `"1h30m"`.
|
|
624
|
-
* @param {CalendarUnit} [unit="ms"]
|
|
893
|
+
* @param {number | string} amount A number with `unit`, or a duration like `"1h30m"` or `"1 month"`.
|
|
894
|
+
* @param {CalendarUnit | TimeZoneOptions} [unit="ms"] The unit of a number. With a text amount there is none to give: the options can come here.
|
|
895
|
+
* @param {TimeZoneOptions} [options] The zone whose calendar is followed.
|
|
625
896
|
* @returns {Date}
|
|
897
|
+
* @throws {TypeError} If a text amount isn't a duration, or the unit isn't one.
|
|
626
898
|
*/
|
|
627
|
-
function add(date, amount, unit = "ms") {
|
|
899
|
+
function add(date, amount, unit = "ms", options = {}) {
|
|
628
900
|
const d = _toDate(date);
|
|
629
|
-
if (typeof
|
|
630
|
-
const
|
|
631
|
-
if (
|
|
632
|
-
|
|
901
|
+
if (typeof unit !== "string") return _addText(d, String(amount), 1, unit.timeZone ?? _config.timeZone);
|
|
902
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
903
|
+
if (typeof amount === "string") return _addText(d, amount, 1, timeZone);
|
|
904
|
+
const u = _knownUnit(unit);
|
|
905
|
+
if (u === "M") return _addMonths(d, amount, timeZone);
|
|
906
|
+
if (u === "y") return _addMonths(d, amount * 12, timeZone);
|
|
907
|
+
if ((u === "d" || u === "w") && Number.isInteger(amount)) return _addDays(d, u === "w" ? amount * 7 : amount, timeZone);
|
|
633
908
|
return new Date(d.getTime() + amount * MS[u]);
|
|
634
909
|
}
|
|
635
910
|
|
|
@@ -642,12 +917,15 @@ function add(date, amount, unit = "ms") {
|
|
|
642
917
|
*
|
|
643
918
|
* @param {DateInput} date
|
|
644
919
|
* @param {number | string} amount
|
|
645
|
-
* @param {CalendarUnit} [unit="ms"]
|
|
920
|
+
* @param {CalendarUnit | TimeZoneOptions} [unit="ms"] The unit of a number, or the options when the amount is text.
|
|
921
|
+
* @param {TimeZoneOptions} [options]
|
|
646
922
|
* @returns {Date}
|
|
923
|
+
* @throws {TypeError} If a text amount isn't a duration, or the unit isn't one.
|
|
647
924
|
*/
|
|
648
|
-
function subtract(date, amount, unit = "ms") {
|
|
649
|
-
|
|
650
|
-
return
|
|
925
|
+
function subtract(date, amount, unit = "ms", options = {}) {
|
|
926
|
+
const given = typeof unit === "string" ? options : unit;
|
|
927
|
+
if (typeof amount === "string" || typeof unit !== "string") return _addText(_toDate(date), String(amount), -1, given.timeZone ?? _config.timeZone);
|
|
928
|
+
return add(date, -amount, unit, options);
|
|
651
929
|
}
|
|
652
930
|
|
|
653
931
|
/**
|
|
@@ -661,29 +939,36 @@ function subtract(date, amount, unit = "ms") {
|
|
|
661
939
|
* @param {DateInput} a
|
|
662
940
|
* @param {DateInput} b
|
|
663
941
|
* @param {CalendarUnit} [unit="ms"]
|
|
942
|
+
* @param {TimeZoneOptions} [options] The zone whose calendar months and years follow.
|
|
664
943
|
* @returns {number}
|
|
944
|
+
* @throws {TypeError} If the unit isn't one.
|
|
665
945
|
*/
|
|
666
|
-
function diff(a, b, unit = "ms") {
|
|
946
|
+
function diff(a, b, unit = "ms", options = {}) {
|
|
667
947
|
const da = _toDate(a);
|
|
668
948
|
const db = _toDate(b);
|
|
669
|
-
const u =
|
|
949
|
+
const u = _knownUnit(unit);
|
|
670
950
|
if (u === "M" || u === "y") {
|
|
951
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
671
952
|
const sign = da >= db ? 1 : -1;
|
|
672
953
|
const [late, early] = sign > 0 ? [da, db] : [db, da];
|
|
673
|
-
|
|
674
|
-
|
|
954
|
+
const x = _parts(late, timeZone);
|
|
955
|
+
const y = _parts(early, timeZone);
|
|
956
|
+
let months = (x.year - y.year) * 12 + x.month - y.month;
|
|
957
|
+
if (_addMonths(early, months, timeZone) > late) months--;
|
|
675
958
|
return sign * (u === "y" ? Math.trunc(months / 12) : months) || 0;
|
|
676
959
|
}
|
|
677
960
|
return (da.getTime() - db.getTime()) / MS[u];
|
|
678
961
|
}
|
|
679
962
|
|
|
680
963
|
/**
|
|
681
|
-
* A new `Date` at the start of the day, week, month... in
|
|
964
|
+
* A new `Date` at the start of the day, week, month... in the global time
|
|
965
|
+
* zone, or the one you name.
|
|
682
966
|
*
|
|
683
967
|
* @example
|
|
684
|
-
* time.startOf(new Date(), "day");
|
|
685
|
-
* time.startOf(new Date(), "week");
|
|
686
|
-
* time.startOf(new Date(), "week", { weekStartsOn: 0 });
|
|
968
|
+
* time.startOf(new Date(), "day"); // today at 00:00
|
|
969
|
+
* time.startOf(new Date(), "week"); // Monday at 00:00
|
|
970
|
+
* time.startOf(new Date(), "week", { weekStartsOn: 0 }); // Sunday at 00:00
|
|
971
|
+
* time.startOf(new Date(), "day", { timeZone: "Asia/Tokyo" }); // midnight in Tokyo
|
|
687
972
|
*
|
|
688
973
|
* @param {DateInput} date
|
|
689
974
|
* @param {StartOfUnit} unit
|
|
@@ -692,40 +977,9 @@ function diff(a, b, unit = "ms") {
|
|
|
692
977
|
*/
|
|
693
978
|
function startOf(date, unit, options = {}) {
|
|
694
979
|
const d = _toDate(date);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
d.setHours(0, 0, 0, 0);
|
|
699
|
-
break;
|
|
700
|
-
case "quarter":
|
|
701
|
-
d.setMonth(Math.floor(d.getMonth() / 3) * 3, 1);
|
|
702
|
-
d.setHours(0, 0, 0, 0);
|
|
703
|
-
break;
|
|
704
|
-
case "month":
|
|
705
|
-
d.setDate(1);
|
|
706
|
-
d.setHours(0, 0, 0, 0);
|
|
707
|
-
break;
|
|
708
|
-
case "week":
|
|
709
|
-
case "isoWeek": {
|
|
710
|
-
const start = unit === "isoWeek" ? 1 : options.weekStartsOn ?? 1;
|
|
711
|
-
d.setDate(d.getDate() - ((d.getDay() - start + 7) % 7));
|
|
712
|
-
d.setHours(0, 0, 0, 0);
|
|
713
|
-
break;
|
|
714
|
-
}
|
|
715
|
-
case "day":
|
|
716
|
-
d.setHours(0, 0, 0, 0);
|
|
717
|
-
break;
|
|
718
|
-
case "hour":
|
|
719
|
-
d.setMinutes(0, 0, 0);
|
|
720
|
-
break;
|
|
721
|
-
case "minute":
|
|
722
|
-
d.setSeconds(0, 0);
|
|
723
|
-
break;
|
|
724
|
-
case "second":
|
|
725
|
-
d.setMilliseconds(0);
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
return d;
|
|
980
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
981
|
+
const wall = _startWall(_parts(d, timeZone), unit, options.weekStartsOn);
|
|
982
|
+
return _instantOf(wall[0], wall[1], wall[2], wall[3], wall[4], wall[5], 0, timeZone);
|
|
729
983
|
}
|
|
730
984
|
|
|
731
985
|
/**
|
|
@@ -740,21 +994,31 @@ function startOf(date, unit, options = {}) {
|
|
|
740
994
|
* @returns {Date}
|
|
741
995
|
*/
|
|
742
996
|
function endOf(date, unit, options = {}) {
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
997
|
+
const d = _toDate(date);
|
|
998
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
999
|
+
const wall = _startWall(_parts(d, timeZone), unit, options.weekStartsOn);
|
|
1000
|
+
// The start of the next unit, minus one millisecond.
|
|
1001
|
+
const step = unit === "year" ? 0 : unit === "quarter" || unit === "month" ? 1 : unit === "week" || unit === "isoWeek" || unit === "day" ? 2 : unit === "hour" ? 3 : unit === "minute" ? 4 : 5;
|
|
1002
|
+
wall[step] += unit === "quarter" ? 3 : unit === "week" || unit === "isoWeek" ? 7 : 1;
|
|
1003
|
+
const next = new Date(Date.UTC(wall[0], wall[1] - 1, wall[2], wall[3], wall[4], wall[5]));
|
|
1004
|
+
return new Date(_instantOf(next.getUTCFullYear(), next.getUTCMonth() + 1, next.getUTCDate(), next.getUTCHours(), next.getUTCMinutes(), next.getUTCSeconds(), 0, timeZone).getTime() - 1);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Are both dates in the same year, month, week, day, hour...?
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* time.isSame(a, b, "month");
|
|
1012
|
+
* time.isSame(a, b, "day", { timeZone: "America/New_York" });
|
|
1013
|
+
*
|
|
1014
|
+
* @param {DateInput} a
|
|
1015
|
+
* @param {DateInput} b
|
|
1016
|
+
* @param {StartOfUnit} unit
|
|
1017
|
+
* @param {WeekOptions} [options]
|
|
1018
|
+
* @returns {boolean}
|
|
1019
|
+
*/
|
|
1020
|
+
function isSame(a, b, unit, options = {}) {
|
|
1021
|
+
return startOf(a, unit, options).getTime() === startOf(b, unit, options).getTime();
|
|
758
1022
|
}
|
|
759
1023
|
|
|
760
1024
|
/**
|
|
@@ -833,7 +1097,7 @@ function isToday(date, options = {}) {
|
|
|
833
1097
|
* @returns {boolean}
|
|
834
1098
|
*/
|
|
835
1099
|
function isYesterday(date, options = {}) {
|
|
836
|
-
return
|
|
1100
|
+
return _dayDelta(_toDate(date), new Date(), options.timeZone ?? _config.timeZone) === -1;
|
|
837
1101
|
}
|
|
838
1102
|
|
|
839
1103
|
/**
|
|
@@ -844,7 +1108,7 @@ function isYesterday(date, options = {}) {
|
|
|
844
1108
|
* @returns {boolean}
|
|
845
1109
|
*/
|
|
846
1110
|
function isTomorrow(date, options = {}) {
|
|
847
|
-
return
|
|
1111
|
+
return _dayDelta(_toDate(date), new Date(), options.timeZone ?? _config.timeZone) === 1;
|
|
848
1112
|
}
|
|
849
1113
|
|
|
850
1114
|
/**
|
|
@@ -915,11 +1179,12 @@ function daysInMonth(dateOrYear, month) {
|
|
|
915
1179
|
* Day of the year, from 1 to 366.
|
|
916
1180
|
*
|
|
917
1181
|
* @param {DateInput} [date] Defaults to now.
|
|
1182
|
+
* @param {TimeZoneOptions} [options]
|
|
918
1183
|
* @returns {number}
|
|
919
1184
|
*/
|
|
920
|
-
function dayOfYear(date) {
|
|
921
|
-
const
|
|
922
|
-
return Math.round((Date.UTC(
|
|
1185
|
+
function dayOfYear(date, options = {}) {
|
|
1186
|
+
const at = _parts(_toDate(date), options.timeZone ?? _config.timeZone);
|
|
1187
|
+
return Math.round((Date.UTC(at.year, at.month - 1, at.day) - Date.UTC(at.year, 0, 1)) / MS.d) + 1;
|
|
923
1188
|
}
|
|
924
1189
|
|
|
925
1190
|
/**
|
|
@@ -930,18 +1195,12 @@ function dayOfYear(date) {
|
|
|
930
1195
|
* time.weekOfYear(new Date(2021, 0, 3)); // 53, the last week of 2020
|
|
931
1196
|
*
|
|
932
1197
|
* @param {DateInput} [date] Defaults to now.
|
|
933
|
-
* @param {{ utc?: boolean }} [options]
|
|
1198
|
+
* @param {{ utc?: boolean, timeZone?: string }} [options] `utc` reads the date in UTC, `timeZone` in another zone.
|
|
934
1199
|
* @returns {number}
|
|
935
1200
|
*/
|
|
936
1201
|
function weekOfYear(date, options = {}) {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
? new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
|
|
940
|
-
: new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
941
|
-
const day = utc.getUTCDay() || 7;
|
|
942
|
-
utc.setUTCDate(utc.getUTCDate() + 4 - day);
|
|
943
|
-
const yearStart = Date.UTC(utc.getUTCFullYear(), 0, 1);
|
|
944
|
-
return Math.ceil(((utc.getTime() - yearStart) / MS.d + 1) / 7);
|
|
1202
|
+
const at = _parts(_toDate(date), options.utc ? "UTC" : options.timeZone ?? _config.timeZone);
|
|
1203
|
+
return _isoWeek(at.year, at.month, at.day);
|
|
945
1204
|
}
|
|
946
1205
|
|
|
947
1206
|
/**
|
|
@@ -985,6 +1244,52 @@ function unix() {
|
|
|
985
1244
|
return Math.floor(Date.now() / 1000);
|
|
986
1245
|
}
|
|
987
1246
|
|
|
1247
|
+
/**
|
|
1248
|
+
* A `Date` from a Unix timestamp in seconds, as found in JWTs and many APIs.
|
|
1249
|
+
*
|
|
1250
|
+
* @example
|
|
1251
|
+
* time.fromUnix(1705329000); // 2024-01-15T14:30:00.000Z
|
|
1252
|
+
*
|
|
1253
|
+
* @param {number} seconds
|
|
1254
|
+
* @returns {Date}
|
|
1255
|
+
*/
|
|
1256
|
+
function fromUnix(seconds) {
|
|
1257
|
+
return new Date(seconds * 1000);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Every date from `start` to `end`, both included, one unit apart. Handy
|
|
1262
|
+
* for charts, calendars and reports.
|
|
1263
|
+
*
|
|
1264
|
+
* @example
|
|
1265
|
+
* time.range("2024-01-01", "2024-01-07", "day"); // 7 dates
|
|
1266
|
+
* time.range(start, end, "month"); // the same day each month
|
|
1267
|
+
* time.range(start, end, "minute", { step: 15 }); // every quarter of an hour
|
|
1268
|
+
*
|
|
1269
|
+
* @param {DateInput} start
|
|
1270
|
+
* @param {DateInput} end
|
|
1271
|
+
* @param {CalendarUnit} [unit="day"]
|
|
1272
|
+
* @param {DateRangeOptions} [options]
|
|
1273
|
+
* @returns {Date[]}
|
|
1274
|
+
* @throws {RangeError} If the step isn't positive, or the range would hold more than a million dates.
|
|
1275
|
+
*/
|
|
1276
|
+
function range(start, end, unit = "day", options = {}) {
|
|
1277
|
+
const from = _toDate(start);
|
|
1278
|
+
const to = _toDate(end).getTime();
|
|
1279
|
+
const step = options.step ?? 1;
|
|
1280
|
+
if (!(step > 0)) throw new RangeError("range() needs a positive step");
|
|
1281
|
+
/** @type {Date[]} */
|
|
1282
|
+
const out = [];
|
|
1283
|
+
// Each date is computed from the start, so a month-end start never drifts.
|
|
1284
|
+
for (let i = 0; ; i++) {
|
|
1285
|
+
const next = add(from, i * step, unit, options);
|
|
1286
|
+
if (!(next.getTime() <= to)) break;
|
|
1287
|
+
if (out.length >= 1_000_000) throw new RangeError("range() would return more than a million dates");
|
|
1288
|
+
out.push(next);
|
|
1289
|
+
}
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
988
1293
|
/**
|
|
989
1294
|
* Starts a precise stopwatch.
|
|
990
1295
|
*
|
|
@@ -1057,7 +1362,7 @@ async function measure(fn) {
|
|
|
1057
1362
|
* @throws {RangeError} If the interval isn't a positive duration.
|
|
1058
1363
|
*/
|
|
1059
1364
|
function every(interval, task, options = {}) {
|
|
1060
|
-
const ms = parseDuration(interval);
|
|
1365
|
+
const ms = shared.parseDuration(interval);
|
|
1061
1366
|
if (ms === null || ms <= 0) throw new RangeError(`Invalid interval: ${String(interval)}`);
|
|
1062
1367
|
let stopped = false;
|
|
1063
1368
|
let running = false;
|
|
@@ -1091,72 +1396,10 @@ function every(interval, task, options = {}) {
|
|
|
1091
1396
|
};
|
|
1092
1397
|
}
|
|
1093
1398
|
|
|
1094
|
-
|
|
1095
|
-
* Parsed cron fields.
|
|
1096
|
-
* @typedef {{ seconds: Set<number>, minutes: Set<number>, hours: Set<number>, days: Set<number>, months: Set<number>, weekdays: Set<number>, anyDay: boolean, anyWeekday: boolean }} CronSpec
|
|
1097
|
-
*/
|
|
1098
|
-
|
|
1099
|
-
/** @type {Record<string, string>} */
|
|
1100
|
-
const _CRON_MACROS = {
|
|
1101
|
-
"@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *", "@monthly": "0 0 1 * *", "@weekly": "0 0 * * 0",
|
|
1102
|
-
"@daily": "0 0 * * *", "@midnight": "0 0 * * *", "@hourly": "0 * * * *",
|
|
1103
|
-
};
|
|
1104
|
-
const _MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
1105
|
-
const _DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
1399
|
+
const _parseCron = shared.parseCron;
|
|
1106
1400
|
|
|
1107
1401
|
/**
|
|
1108
|
-
* @param {
|
|
1109
|
-
* @returns {CronSpec}
|
|
1110
|
-
*/
|
|
1111
|
-
const _parseCron = (expression) => {
|
|
1112
|
-
const text = _CRON_MACROS[expression.trim().toLowerCase()] ?? expression.trim();
|
|
1113
|
-
const fields = text.split(/\s+/);
|
|
1114
|
-
if (fields.length !== 5 && fields.length !== 6) throw new SyntaxError(`Invalid cron expression "${expression}": expected 5 or 6 fields`);
|
|
1115
|
-
if (fields.length === 5) fields.unshift("0");
|
|
1116
|
-
/** @type {(field: string, min: number, max: number, names?: string[]) => Set<number>} */
|
|
1117
|
-
const parse = (field, min, max, names) => {
|
|
1118
|
-
const values = new Set();
|
|
1119
|
-
for (const part of field.split(",")) {
|
|
1120
|
-
const [rangePart, stepPart] = part.split("/");
|
|
1121
|
-
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
1122
|
-
if (!Number.isInteger(step) || step < 1) throw new SyntaxError(`Invalid step "${part}" in cron expression "${expression}"`);
|
|
1123
|
-
const toNumber = (/** @type {string} */ token) => {
|
|
1124
|
-
const index = names ? names.indexOf(token.toLowerCase()) : -1;
|
|
1125
|
-
const n = index >= 0 ? index + (min === 1 ? 1 : 0) : Number(token);
|
|
1126
|
-
if (!Number.isInteger(n)) throw new SyntaxError(`Invalid value "${token}" in cron expression "${expression}"`);
|
|
1127
|
-
return n;
|
|
1128
|
-
};
|
|
1129
|
-
let from = min;
|
|
1130
|
-
let to = max;
|
|
1131
|
-
if (rangePart !== "*" && rangePart !== "?") {
|
|
1132
|
-
const [a, b] = rangePart.split("-");
|
|
1133
|
-
from = toNumber(a);
|
|
1134
|
-
to = b === undefined ? (stepPart === undefined ? from : max) : toNumber(b);
|
|
1135
|
-
}
|
|
1136
|
-
if (from < min || to > max || from > to) throw new SyntaxError(`Value out of range in "${part}" (allowed ${min}-${max}) in cron expression "${expression}"`);
|
|
1137
|
-
for (let v = from; v <= to; v += step) values.add(v);
|
|
1138
|
-
}
|
|
1139
|
-
return values;
|
|
1140
|
-
};
|
|
1141
|
-
const weekdays = parse(fields[5], 0, 7, _DAY_NAMES);
|
|
1142
|
-
if (weekdays.has(7)) {
|
|
1143
|
-
weekdays.delete(7);
|
|
1144
|
-
weekdays.add(0);
|
|
1145
|
-
}
|
|
1146
|
-
return {
|
|
1147
|
-
seconds: parse(fields[0], 0, 59),
|
|
1148
|
-
minutes: parse(fields[1], 0, 59),
|
|
1149
|
-
hours: parse(fields[2], 0, 23),
|
|
1150
|
-
days: parse(fields[3], 1, 31),
|
|
1151
|
-
months: parse(fields[4], 1, 12, _MONTH_NAMES),
|
|
1152
|
-
weekdays,
|
|
1153
|
-
anyDay: fields[3] === "*" || fields[3] === "?",
|
|
1154
|
-
anyWeekday: fields[5] === "*" || fields[5] === "?",
|
|
1155
|
-
};
|
|
1156
|
-
};
|
|
1157
|
-
|
|
1158
|
-
/**
|
|
1159
|
-
* @param {CronSpec} spec
|
|
1402
|
+
* @param {import("./internal/shared").CronSpec} spec
|
|
1160
1403
|
* @param {Date} from
|
|
1161
1404
|
* @param {string | undefined} timeZone
|
|
1162
1405
|
* @returns {Date | undefined}
|
|
@@ -1167,7 +1410,7 @@ const _nextCron = (spec, from, timeZone) => {
|
|
|
1167
1410
|
const wall = new Date(Date.UTC(start.year, start.month - 1, start.day, start.hour, start.minute, start.second));
|
|
1168
1411
|
const limit = wall.getTime() + 8 * 366 * MS.d;
|
|
1169
1412
|
const dayMatches = () => {
|
|
1170
|
-
const dom = spec.days.has(wall.getUTCDate());
|
|
1413
|
+
const dom = spec.days.has(wall.getUTCDate()) || (spec.lastDay && wall.getUTCDate() === _daysIn(wall.getUTCFullYear(), wall.getUTCMonth() + 1));
|
|
1171
1414
|
const dow = spec.weekdays.has(wall.getUTCDay());
|
|
1172
1415
|
if (spec.anyDay && spec.anyWeekday) return true;
|
|
1173
1416
|
if (spec.anyDay) return dow;
|
|
@@ -1209,9 +1452,10 @@ const _nextCron = (spec, from, timeZone) => {
|
|
|
1209
1452
|
*
|
|
1210
1453
|
* Expressions have 5 fields (`minute hour day month weekday`), or 6 with
|
|
1211
1454
|
* seconds first. You can use `*`, lists (`1,15`), ranges (`1-5`), steps
|
|
1212
|
-
* (`*\/15`), names (`jan`, `mon-fri`)
|
|
1213
|
-
* `@monthly`, `@yearly`. As in classic
|
|
1214
|
-
* weekday are set, either one matching is
|
|
1455
|
+
* (`*\/15`), names (`jan`, `mon-fri`), `L` for the last day of the month,
|
|
1456
|
+
* and `@daily`, `@hourly`, `@weekly`, `@monthly`, `@yearly`. As in classic
|
|
1457
|
+
* cron, when both the day and the weekday are set, either one matching is
|
|
1458
|
+
* enough.
|
|
1215
1459
|
*
|
|
1216
1460
|
* @example
|
|
1217
1461
|
* time.nextRun("0 9 * * mon-fri"); // next weekday at 09:00
|
|
@@ -1269,6 +1513,13 @@ function cron(expression, task, options = {}) {
|
|
|
1269
1513
|
}, target.getTime() - Date.now(), options.unref ?? false);
|
|
1270
1514
|
};
|
|
1271
1515
|
plan(new Date());
|
|
1516
|
+
if (options.immediate) {
|
|
1517
|
+
running = true;
|
|
1518
|
+
runs++;
|
|
1519
|
+
void _run(task, options.onError).finally(() => {
|
|
1520
|
+
running = false;
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1272
1523
|
|
|
1273
1524
|
return {
|
|
1274
1525
|
stop: () => {
|
|
@@ -1293,6 +1544,7 @@ module.exports = {
|
|
|
1293
1544
|
relative,
|
|
1294
1545
|
calendar,
|
|
1295
1546
|
format,
|
|
1547
|
+
parse,
|
|
1296
1548
|
toISODate,
|
|
1297
1549
|
add,
|
|
1298
1550
|
subtract,
|
|
@@ -1302,6 +1554,7 @@ module.exports = {
|
|
|
1302
1554
|
isBefore,
|
|
1303
1555
|
isAfter,
|
|
1304
1556
|
isBetween,
|
|
1557
|
+
isSame,
|
|
1305
1558
|
isSameDay,
|
|
1306
1559
|
isToday,
|
|
1307
1560
|
isYesterday,
|
|
@@ -1315,6 +1568,8 @@ module.exports = {
|
|
|
1315
1568
|
min,
|
|
1316
1569
|
max,
|
|
1317
1570
|
unix,
|
|
1571
|
+
fromUnix,
|
|
1572
|
+
range,
|
|
1318
1573
|
stopwatch,
|
|
1319
1574
|
measure,
|
|
1320
1575
|
every,
|