@ix-xs/node-comfort 1.2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Time.js CHANGED
@@ -1,485 +1,1578 @@
1
+ "use strict";
2
+
1
3
  /**
2
- * Date & time utilities.
3
- *
4
- * Parsing durations, human-friendly relative time ("3 hours ago"), duration
5
- * formatting, add/diff arithmetic and a tiny date formatter • with no
6
- * dependency on heavyweight date libraries.
7
- *
8
- * ### Internationalization & time zones
9
- *
10
- * `relative`, `formatDuration` and `format` are locale-aware and use the
11
- * platform's built-in `Intl` (full ICU ships with Node), so **every language
12
- * is supported out of the box** • no locale files, no dependencies.
13
- *
14
- * The default locale is `"en"` (for deterministic output). Change it globally
15
- * with {@link module:Time.setLocale}, or per call via an `options.locale`. A
16
- * time zone can be set globally with {@link module:Time.setTimezone} or passed
17
- * per call via `options.timeZone` (IANA name, e.g. `"Europe/Paris"`).
4
+ * Dates and durations: formatting, relative time, calendar math, time zones
5
+ * and scheduling, in any language `Intl` knows. The default language is
6
+ * English so results are the same on every machine; change it with
7
+ * `setLocale()` or per call with `{ locale }`. Time zones work the same way
8
+ * with `setTimezone()` or `{ timeZone }`.
18
9
  *
19
10
  * @example
20
- * nodeComfort.time.setLocale("fr").setTimezone("Europe/Paris");
21
- * nodeComfort.time.relative(Date.now() - 3600e3); // "il y a 1 heure"
22
- * nodeComfort.time.format(Date.now(), "dddd D MMMM YYYY"); // "lundi 15 janvier 2024"
11
+ * time.setLocale("fr").setTimezone("Europe/Paris");
12
+ * time.relative(Date.now() - 3600e3); // "il y a 1 heure"
13
+ * time.format(new Date(), "dddd D MMMM YYYY"); // "lundi 15 janvier 2024"
14
+ * time.cron("0 9 * * 1-5", sendReport); // weekdays at 9:00
23
15
  */
24
16
 
17
+ const { types } = require("node:util");
18
+
25
19
  /**
26
- * The time module itself • used as the return type of the chainable setters so
27
- * `setLocale().setTimezone()...` stays fully typed without inlining the type.
28
- * @typedef {typeof import("./Time")} TimeModule
20
+ * A date: a `Date`, a timestamp in ms, or a string `new Date()` understands
21
+ * (ISO 8601 is safest).
22
+ * @typedef {Date | number | string} DateInput
29
23
  */
30
24
 
31
- const MS = {
32
- ms: 1,
33
- s: 1000,
34
- m: 60_000,
35
- h: 3_600_000,
36
- d: 86_400_000,
37
- w: 604_800_000,
38
- };
25
+ /**
26
+ * A unit with a fixed length.
27
+ * @typedef {"ms" | "millisecond" | "milliseconds" | "s" | "sec" | "second" | "seconds" | "m" | "min" | "minute" | "minutes" | "h" | "hour" | "hours" | "d" | "day" | "days" | "w" | "week" | "weeks"} DurationUnit
28
+ */
39
29
 
40
- const UNIT_ALIASES = {
41
- ms: "ms", milli: "ms", millisecond: "ms", milliseconds: "ms",
42
- s: "s", sec: "s", secs: "s", second: "s", seconds: "s",
43
- m: "m", min: "m", mins: "m", minute: "m", minutes: "m",
44
- h: "h", hr: "h", hrs: "h", hour: "h", hours: "h",
45
- d: "d", day: "d", days: "d",
46
- w: "w", week: "w", weeks: "w",
47
- };
30
+ /**
31
+ * A fixed unit, or months and years, whose length varies.
32
+ * @typedef {DurationUnit | "M" | "month" | "months" | "y" | "year" | "years"} CalendarUnit
33
+ */
34
+
35
+ /**
36
+ * A unit for `startOf()` and `endOf()`.
37
+ * @typedef {"year" | "quarter" | "month" | "week" | "isoWeek" | "day" | "hour" | "minute" | "second"} StartOfUnit
38
+ */
39
+
40
+ /**
41
+ * Options for `formatDuration()`.
42
+ * @typedef {object} FormatDurationOptions
43
+ * @property {boolean} [long] Full, translated words: `"1 hour 30 minutes"` instead of `"1h 30m"`.
44
+ * @property {number} [units] Show at most this many units, largest first: `units: 2` turns `"1d 3h 5m"` into `"1d 3h"`.
45
+ * @property {boolean} [clock] Clock style, like `"01:30:05"`.
46
+ * @property {boolean} [milliseconds] Show milliseconds. Defaults to `true`.
47
+ * @property {string} [locale] Language for `long`. Defaults to the global locale.
48
+ */
49
+
50
+ /**
51
+ * Options for `relative()`.
52
+ * @typedef {object} RelativeOptions
53
+ * @property {string} [locale] Defaults to the global locale.
54
+ * @property {"always" | "auto"} [numeric] `"auto"` says `"yesterday"` instead of `"1 day ago"`. Defaults to `"always"`.
55
+ * @property {"long" | "short" | "narrow"} [style] `"short"` gives `"in 3 mo."`. Defaults to `"long"`.
56
+ */
57
+
58
+ /**
59
+ * Options for `format()`.
60
+ * @typedef {object} FormatDateOptions
61
+ * @property {string} [locale] Language of month and day names. Defaults to the global locale.
62
+ * @property {string} [timeZone] Show the date in this zone, like `"Asia/Tokyo"`. Defaults to the global zone, then the system's.
63
+ */
64
+
65
+ /**
66
+ * Options for `calendar()`.
67
+ * @typedef {object} CalendarOptions
68
+ * @property {string} [locale] Defaults to the global locale.
69
+ * @property {string} [timeZone] Defaults to the global zone, then the system's.
70
+ * @property {DateInput} [now] What "now" is. Defaults to the current time.
71
+ */
48
72
 
49
73
  /**
50
- * Global defaults for locale-aware formatting. `locale` defaults to `"en"` so
51
- * output is deterministic regardless of the host machine; `timeZone`
52
- * `undefined` means the system local zone.
53
- * @private
54
- * @type {{ locale: string, timeZone: string|undefined }}
74
+ * Week settings.
75
+ * @typedef {object} WeekOptions
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.
55
78
  */
56
- const _config = { locale: "en", timeZone: undefined };
57
79
 
58
80
  /**
59
- * Coerces a value into a Date.
60
- * @private
61
- * @param {Date|number|string} [value]
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.
92
+ */
93
+
94
+ /**
95
+ * Time zone setting.
96
+ * @typedef {object} TimeZoneOptions
97
+ * @property {string} [timeZone] Zone used to decide which day it is. Defaults to the global zone, then the system's.
98
+ */
99
+
100
+ /**
101
+ * Options for `every()`.
102
+ * @typedef {object} EveryOptions
103
+ * @property {boolean} [immediate] Also run once right away.
104
+ * @property {boolean} [unref] Let the process exit even if the schedule is active.
105
+ * @property {(error: unknown) => void} [onError] Called when the task fails. Without it, errors surface as unhandled errors.
106
+ */
107
+
108
+ /**
109
+ * Options for `cron()` and `nextRun()`.
110
+ * @typedef {object} CronOptions
111
+ * @property {string} [timeZone] Zone in which the expression is read, like `"Europe/Paris"`. Defaults to the global zone, then the system's.
112
+ * @property {DateInput} [from] `nextRun()` only: search after this date. Defaults to now.
113
+ * @property {boolean} [unref] Let the process exit even if the schedule is active.
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.
116
+ * @property {(error: unknown) => void} [onError] Called when the task fails.
117
+ */
118
+
119
+ /**
120
+ * A schedule returned by `every()` and `cron()`.
121
+ * @typedef {object} ScheduledTask
122
+ * @property {() => void} stop Stops the schedule. A run in progress finishes.
123
+ * @property {() => Date | undefined} next When the next run is planned.
124
+ * @property {() => boolean} isRunning Whether a run is in progress.
125
+ * @property {() => number} runs How many runs have started.
126
+ */
127
+
128
+ /**
129
+ * Returned by `stopwatch()`.
130
+ * @typedef {object} Stopwatch
131
+ * @property {() => number} elapsed Milliseconds since the start.
132
+ * @property {(format?: boolean) => number | string} stop The elapsed time; `stop(true)` formats it, like `"1.5s"`.
133
+ * @property {(label?: string) => number} lap Records a lap and returns the ms since the previous one.
134
+ * @property {() => Array<{ label: string, ms: number, total: number }>} laps Every lap so far.
135
+ * @property {() => void} reset Starts over.
136
+ */
137
+
138
+ const shared = require("./internal/shared.js");
139
+
140
+ const { MS, intl, unitOf: _unit, setLongTimeout: _longTimeout } = shared;
141
+
142
+ const _config = shared.timeConfig;
143
+
144
+ /**
145
+ * @param {DateInput | undefined} value
62
146
  * @returns {Date}
63
147
  */
64
- const _toDate = (value) => (value instanceof Date ? new Date(value.getTime()) : value === undefined ? new Date() : new Date(value));
148
+ const _toDate = (value) => (types.isDate(value) ? new Date(value.getTime()) : value === undefined ? new Date() : new Date(value));
65
149
 
150
+ /**
151
+ * @param {number} n
152
+ * @param {number} [len=2]
153
+ */
66
154
  const _pad = (n, len = 2) => String(Math.abs(n)).padStart(len, "0");
67
155
 
156
+
157
+ /** @type {Map<string, Intl.DateTimeFormat>} */
158
+ const _dtfCache = new Map();
159
+
68
160
  /**
69
- * Extracts numeric calendar parts of a date, optionally in a target time zone.
70
- * When no zone is given, the host's local time is used (fast path).
71
- * @private
72
- * @param {Date} d - Date to read.
73
- * @param {string} [timeZone] - IANA time-zone name.
74
- * @returns {{ year: number, month: number, day: number, hour: number, minute: number, second: number, ms: number }}
161
+ * Calendar parts of a date in a time zone.
162
+ * @param {Date} d
163
+ * @param {string} [timeZone]
164
+ * @returns {{ year: number, month: number, day: number, hour: number, minute: number, second: number, ms: number, weekday: number }}
75
165
  */
76
- const _numericParts = (d, timeZone) => {
166
+ const _parts = (d, timeZone) => {
77
167
  if (!timeZone) {
78
168
  return {
79
- year: d.getFullYear(),
80
- month: d.getMonth() + 1,
81
- day: d.getDate(),
82
- hour: d.getHours(),
83
- minute: d.getMinutes(),
84
- second: d.getSeconds(),
85
- ms: d.getMilliseconds(),
169
+ year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate(), hour: d.getHours(),
170
+ minute: d.getMinutes(), second: d.getSeconds(), ms: d.getMilliseconds(), weekday: d.getDay(),
86
171
  };
87
172
  }
88
- const dtf = new Intl.DateTimeFormat("en-US", {
89
- timeZone,
90
- year: "numeric",
91
- month: "2-digit",
92
- day: "2-digit",
93
- hour: "2-digit",
94
- minute: "2-digit",
95
- second: "2-digit",
96
- hourCycle: "h23",
97
- });
98
- const parts = {};
99
- for (const p of dtf.formatToParts(d)) parts[p.type] = p.value;
173
+ let dtf = _dtfCache.get(timeZone);
174
+ if (!dtf) {
175
+ dtf = new Intl.DateTimeFormat("en-US", {
176
+ timeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit",
177
+ minute: "2-digit", second: "2-digit", hourCycle: "h23", weekday: "short",
178
+ });
179
+ _dtfCache.set(timeZone, dtf);
180
+ }
181
+ /** @type {Record<string, string>} */
182
+ const p = {};
183
+ for (const part of dtf.formatToParts(d)) p[part.type] = part.value;
100
184
  return {
101
- year: +parts.year,
102
- month: +parts.month,
103
- day: +parts.day,
104
- hour: +parts.hour,
105
- minute: +parts.minute,
106
- second: +parts.second,
107
- ms: d.getMilliseconds(),
185
+ year: Number(p.year), month: Number(p.month), day: Number(p.day), hour: Number(p.hour) % 24,
186
+ minute: Number(p.minute), second: Number(p.second), ms: d.getMilliseconds(),
187
+ weekday: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(p.weekday),
108
188
  };
109
189
  };
110
190
 
111
191
  /**
112
- * Formats a single localized component (month/weekday name) via `Intl`.
113
- * @private
114
- * @param {Date} d - Date to format.
115
- * @param {Intl.DateTimeFormatOptions} opt - Component options.
116
- * @param {string} locale - BCP-47 locale.
117
- * @param {string} [timeZone] - IANA time-zone name.
192
+ * UTC offset of a zone at an instant, in minutes.
193
+ * @param {Date} d
194
+ * @param {string} [timeZone]
195
+ * @returns {number}
196
+ */
197
+ const _offset = (d, timeZone) => {
198
+ if (!timeZone) return -d.getTimezoneOffset();
199
+ const p = _parts(d, timeZone);
200
+ const asUTC = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second, p.ms);
201
+ return Math.round((asUTC - d.getTime()) / 60_000);
202
+ };
203
+
204
+ /**
205
+ * Wall-clock time in a zone to an instant. `undefined` when that time is
206
+ * skipped by a daylight-saving change.
207
+ * @param {number} year
208
+ * @param {number} month 1-12
209
+ * @param {number} day
210
+ * @param {number} hour
211
+ * @param {number} minute
212
+ * @param {number} second
213
+ * @param {string} [timeZone]
214
+ * @returns {Date | undefined}
215
+ */
216
+ const _fromWall = (year, month, day, hour, minute, second, timeZone) => {
217
+ const guess = Date.UTC(year, month - 1, day, hour, minute, second);
218
+ let instant = guess - _offset(new Date(guess), timeZone) * 60_000;
219
+ const second2 = guess - _offset(new Date(instant), timeZone) * 60_000;
220
+ if (second2 !== instant) instant = second2;
221
+ const p = _parts(new Date(instant), timeZone);
222
+ if (p.year !== year || p.month !== month || p.day !== day || p.hour !== hour || p.minute !== minute) return undefined;
223
+ return new Date(instant);
224
+ };
225
+
226
+ /**
227
+ * @param {number} year
228
+ * @param {number} month 1-12
229
+ */
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
+ };
324
+
325
+ /**
326
+ * Adds calendar months, keeping the day in range (Jan 31 + 1 month = end of Feb).
327
+ * @param {Date} d
328
+ * @param {number} months
329
+ * @param {string} [timeZone]
330
+ * @returns {Date}
331
+ */
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);
338
+ };
339
+
340
+ /**
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);
377
+ };
378
+
379
+
380
+ /**
381
+ * @param {() => unknown} task
382
+ * @param {((error: unknown) => void) | undefined} onError
383
+ * @returns {Promise<void>}
384
+ */
385
+ const _run = async (task, onError) => {
386
+ try {
387
+ await task();
388
+ } catch (error) {
389
+ if (onError) onError(error);
390
+ else setImmediate(() => { throw error; });
391
+ }
392
+ };
393
+
394
+ /**
395
+ * Sets the default language for `relative`, `calendar`, `formatDuration`
396
+ * and `format`.
397
+ *
398
+ * @example
399
+ * time.setLocale("fr");
400
+ * time.relative(Date.now() - 60e3); // "il y a 1 minute"
401
+ *
402
+ * @param {string} locale Like `"fr"`, `"en-GB"`, `"pt-BR"` or `"ja"`.
403
+ * @returns {typeof import("./Time")} The module, so calls chain.
404
+ */
405
+ function setLocale(locale) {
406
+ _config.locale = locale;
407
+ return module.exports;
408
+ }
409
+
410
+ /**
411
+ * Sets the default time zone for `format`, `calendar`, `isSameDay`,
412
+ * `cron`... Pass `undefined` to go back to the system zone.
413
+ *
414
+ * @example
415
+ * time.setTimezone("Europe/Paris");
416
+ *
417
+ * @param {string | undefined} timeZone Like `"America/New_York"`.
418
+ * @returns {typeof import("./Time")} The module, so calls chain.
419
+ * @throws {RangeError} If the zone doesn't exist.
420
+ */
421
+ function setTimezone(timeZone) {
422
+ if (timeZone !== undefined) new Intl.DateTimeFormat("en-US", { timeZone });
423
+ _config.timeZone = timeZone;
424
+ return module.exports;
425
+ }
426
+
427
+ /**
428
+ * The current defaults.
429
+ *
430
+ * @example
431
+ * time.getConfig(); // { locale: "en", timeZone: undefined }
432
+ *
433
+ * @returns {{ locale: string, timeZone: string | undefined }}
434
+ */
435
+ function getConfig() {
436
+ return { ..._config };
437
+ }
438
+
439
+ /**
440
+ * Every time zone Node knows, sorted.
441
+ *
442
+ * @example
443
+ * time.timezones().includes("Europe/Paris"); // true
444
+ *
445
+ * @returns {string[]}
446
+ */
447
+ function timezones() {
448
+ return Intl.supportedValuesOf("timeZone");
449
+ }
450
+
451
+ /**
452
+ * A zone's UTC offset at a given date, in minutes. Daylight saving included.
453
+ *
454
+ * @example
455
+ * time.offset("Europe/Paris", "2024-07-01"); // 120
456
+ * time.offset("Europe/Paris", "2024-01-01"); // 60
457
+ *
458
+ * @param {string} [timeZone] Defaults to the global zone, then the system's.
459
+ * @param {DateInput} [date] Defaults to now.
460
+ * @returns {number}
461
+ */
462
+ function offset(timeZone, date) {
463
+ return _offset(_toDate(date), timeZone ?? _config.timeZone);
464
+ }
465
+
466
+ /**
467
+ * Turns a duration into milliseconds. Understands `"1h30m"`, `"2 days"`,
468
+ * `"500ms"`, clock times like `"01:30:00"`, ISO durations like `"PT1H30M"`,
469
+ * and plain numbers. Months and years are refused because their length
470
+ * varies; use `add()` for those.
471
+ *
472
+ * @example
473
+ * time.parseDuration("1h30m"); // 5400000
474
+ * time.parseDuration("01:30:00"); // 5400000
475
+ * time.parseDuration("banana"); // null
476
+ *
477
+ * @param {string | number} input
478
+ * @returns {number | null} `null` when it isn't a duration.
479
+ */
480
+ function parseDuration(input) {
481
+ return shared.parseDuration(input);
482
+ }
483
+
484
+ /**
485
+ * Writes a duration for humans.
486
+ *
487
+ * @example
488
+ * time.formatDuration(5_400_000); // "1h 30m"
489
+ * time.formatDuration(90_000, { long: true }); // "1 minute 30 seconds"
490
+ * time.formatDuration(90_000, { long: true, locale: "fr" }); // "1 minute 30 secondes"
491
+ * time.formatDuration(93_784_000, { units: 2 }); // "1d 2h"
492
+ * time.formatDuration(5_405_000, { clock: true }); // "01:30:05"
493
+ * time.formatDuration("90m"); // "1h 30m"
494
+ *
495
+ * @param {number | string} duration Milliseconds, or text like `"90m"`.
496
+ * @param {FormatDurationOptions} [options]
118
497
  * @returns {string}
119
498
  */
120
- const _named = (d, opt, locale, timeZone) =>
121
- new Intl.DateTimeFormat(locale, { ...opt, timeZone }).format(d);
499
+ function formatDuration(duration, options = {}) {
500
+ const ms = typeof duration === "string" ? shared.parseDuration(duration) ?? NaN : duration;
501
+ if (!Number.isFinite(ms)) return String(duration);
502
+ const sign = ms < 0 ? "-" : "";
503
+ let rest = Math.abs(Math.trunc(ms));
122
504
 
123
- module.exports = {
124
- /**
125
- * Sets the global default locale used by `relative`, `formatDuration` and
126
- * `format` (named tokens). Chainable.
127
- *
128
- * @example
129
- * nodeComfort.time.setLocale("fr");
130
- *
131
- * @param {string} locale - A BCP-47 locale tag (e.g. `"fr"`, `"en-GB"`, `"ja"`).
132
- * @returns {TimeModule} The time module (for chaining).
133
- */
134
- setLocale(locale) {
135
- _config.locale = locale;
136
- return this;
137
- },
138
-
139
- /**
140
- * Sets the global default time zone used by `format` and `isSameDay`.
141
- * Chainable. Pass `undefined` to fall back to the system local zone.
142
- *
143
- * @example
144
- * nodeComfort.time.setTimezone("Europe/Paris");
145
- *
146
- * @param {string|undefined} timeZone - An IANA time-zone name (e.g. `"America/New_York"`).
147
- * @returns {TimeModule} The time module (for chaining).
148
- */
149
- setTimezone(timeZone) {
150
- _config.timeZone = timeZone;
151
- return this;
152
- },
153
-
154
- /**
155
- * Returns the current global `{ locale, timeZone }` defaults.
156
- *
157
- * @returns {{ locale: string, timeZone: string|undefined }} A copy of the config.
158
- */
159
- getConfig() {
160
- return { ..._config };
161
- },
162
-
163
- /**
164
- * Parses a human duration string into milliseconds.
165
- *
166
- * Accepts things like `"1h"`, `"30m"`, `"1h30m"`, `"2 days"`, `"500ms"`.
167
- * A plain number is treated as milliseconds.
168
- *
169
- * @example
170
- * nodeComfort.time.parseDuration("1h30m"); // 5400000
171
- * nodeComfort.time.parseDuration("2 days"); // 172800000
172
- *
173
- * @param {string|number} input - Duration string or millisecond count.
174
- * @returns {number|null} Milliseconds, or null if it can't be parsed.
175
- */
176
- parseDuration(input) {
177
- if (typeof input === "number") return input;
178
- if (typeof input !== "string") return null;
179
-
180
- const regex = /(-?\d*\.?\d+)\s*([a-z]+)/gi;
181
- let total = 0;
182
- let matched = false;
183
- let match;
184
- while ((match = regex.exec(input)) !== null) {
185
- const value = parseFloat(match[1]);
186
- const unit = UNIT_ALIASES[match[2].toLowerCase()];
187
- if (!unit) return null;
188
- total += value * MS[unit];
189
- matched = true;
505
+ if (options.clock) {
506
+ const d = Math.floor(rest / MS.d);
507
+ const h = Math.floor((rest % MS.d) / MS.h);
508
+ const m = Math.floor((rest % MS.h) / MS.m);
509
+ const s = Math.floor((rest % MS.m) / MS.s);
510
+ return `${sign}${d ? `${d}:${_pad(h)}` : _pad(h)}:${_pad(m)}:${_pad(s)}`;
511
+ }
512
+
513
+ const locale = options.locale ?? _config.locale;
514
+ const showMs = options.milliseconds ?? true;
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++;
527
+ }
528
+ if (!shown) return options.long ? long("second").format(0) : "0s";
529
+ return sign + out;
530
+ }
531
+
532
+ /**
533
+ * Describes a date relative to now: `"3 hours ago"`, `"in 2 days"`, in
534
+ * any language.
535
+ *
536
+ * @example
537
+ * time.relative(Date.now() - 3_600_000); // "1 hour ago"
538
+ * time.relative(Date.now() - 86_400_000, undefined, { numeric: "auto" }); // "yesterday"
539
+ * time.relative(Date.now() - 3_600_000, undefined, { locale: "fr" }); // "il y a 1 heure"
540
+ *
541
+ * @param {DateInput} date
542
+ * @param {DateInput} [from] Compare with this date instead of now.
543
+ * @param {RelativeOptions} [options]
544
+ * @returns {string}
545
+ */
546
+ function relative(date, from, options = {}) {
547
+ const locale = options.locale ?? _config.locale;
548
+ const style = options.style ?? "long";
549
+ const diffMs = _toDate(date).getTime() - _toDate(from).getTime();
550
+ const abs = Math.abs(diffMs);
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];
555
+ if (abs < limit) {
556
+ const value = Math.floor(abs / size);
557
+ return rtf.format(diffMs < 0 ? -value : value, unit);
558
+ }
559
+ }
560
+ return rtf.format(0, "second");
561
+ }
562
+
563
+ /**
564
+ * Describes a date like a chat app does: `"Today at 2:30 PM"`,
565
+ * `"Yesterday at 9:05 AM"`, a weekday for the coming days, and the full date
566
+ * beyond that.
567
+ *
568
+ * @example
569
+ * time.calendar(new Date()); // "Today at 2:30 PM"
570
+ * time.calendar(yesterday, { locale: "fr" }); // "Hier à 09:05"
571
+ *
572
+ * @param {DateInput} date
573
+ * @param {CalendarOptions} [options]
574
+ * @returns {string}
575
+ */
576
+ function calendar(date, options = {}) {
577
+ const locale = options.locale ?? _config.locale;
578
+ const timeZone = options.timeZone ?? _config.timeZone;
579
+ const d = _toDate(date);
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}`;
588
+ }
589
+
590
+ /**
591
+ * Formats a date with tokens, in any language and time zone.
592
+ *
593
+ * | Token | Output | | Token | Output |
594
+ * | --- | --- | --- | --- | --- |
595
+ * | `YYYY` / `YY` | 2024 / 24 | | `HH` / `H` | 09 / 9 (24h) |
596
+ * | `Q` | quarter, 1-4 | | `hh` / `h` | 09 / 9 (12h) |
597
+ * | `MMMM` / `MMM` | January / Jan | | `mm` / `m` | 05 / 5 |
598
+ * | `MM` / `M` | 01 / 1 | | `ss` / `s` | 07 / 7 |
599
+ * | `DD` / `D` | 05 / 5 | | `SSS` | 042 (ms) |
600
+ * | `dddd` / `ddd` | Monday / Mon | | `A` / `a` | PM / pm |
601
+ * | `d` | weekday, 0 is Sunday | | `Z` / `ZZ` | +02:00 / +0200 |
602
+ * | `W` / `WW` | ISO week | | `X` / `x` | Unix seconds / ms |
603
+ * | `Do` | 5th | | `z` | zone name, like EST or GMT+1 |
604
+ *
605
+ * Text in brackets stays as is: `"[Today is] dddd"`.
606
+ *
607
+ * @example
608
+ * time.format(new Date(), "YYYY-MM-DD HH:mm:ss"); // "2024-01-15 14:30:05"
609
+ * time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" }); // "lundi 15 janvier 2024"
610
+ * time.format(Date.now(), "HH:mm Z", { timeZone: "Asia/Tokyo" }); // "22:30 +09:00"
611
+ *
612
+ * @param {DateInput} date
613
+ * @param {string} [pattern="YYYY-MM-DD HH:mm:ss"]
614
+ * @param {FormatDateOptions} [options]
615
+ * @returns {string} Or `"Invalid Date"`.
616
+ */
617
+ function format(date, pattern = "YYYY-MM-DD HH:mm:ss", options = {}) {
618
+ const d = _toDate(date);
619
+ if (Number.isNaN(d.getTime())) return "Invalid Date";
620
+ const locale = options.locale ?? _config.locale;
621
+ const timeZone = options.timeZone ?? _config.timeZone;
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;
190
655
  }
191
- return matched ? total : null;
192
- },
193
-
194
- /**
195
- * Formats a millisecond duration into a human string.
196
- *
197
- * The compact form (default) is language-neutral (`"1h 30m"`). The `long`
198
- * form is fully localized through `Intl` • pass `options.locale` or set a
199
- * global locale with {@link module:Time.setLocale}.
200
- *
201
- * @example
202
- * nodeComfort.time.formatDuration(5400000); // "1h 30m"
203
- * nodeComfort.time.formatDuration(90000, { long: true }); // "1 minute 30 seconds"
204
- * nodeComfort.time.formatDuration(90000, { long: true, locale: "fr" }); // "1 minute 30 secondes"
205
- *
206
- * @param {number} ms - Duration in milliseconds.
207
- * @param {{ long?: boolean, units?: number, locale?: string }} [options] - Formatting options (`units` caps how many parts show).
208
- * @returns {string} The formatted duration.
209
- */
210
- formatDuration(ms, options = {}) {
211
- const long = options.long ?? false;
212
- const maxUnits = options.units ?? Infinity;
213
- const locale = options.locale ?? _config.locale;
214
- if (!Number.isFinite(ms)) return String(ms);
215
- const sign = ms < 0 ? "-" : "";
216
- ms = Math.abs(Math.trunc(ms));
217
-
218
- const parts = [
219
- { v: Math.floor(ms / MS.d), short: "d", unit: "day" },
220
- { v: Math.floor((ms % MS.d) / MS.h), short: "h", unit: "hour" },
221
- { v: Math.floor((ms % MS.h) / MS.m), short: "m", unit: "minute" },
222
- { v: Math.floor((ms % MS.m) / MS.s), short: "s", unit: "second" },
223
- { v: ms % MS.s, short: "ms", unit: "millisecond" },
224
- ].filter((p) => p.v > 0);
225
-
226
- if (!parts.length) {
227
- return long
228
- ? new Intl.NumberFormat(locale, { style: "unit", unit: "second", unitDisplay: "long" }).format(0)
229
- : "0s";
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);
768
+ }
230
769
  }
770
+ _monthNames.set(locale, names);
771
+ }
772
+ return names;
773
+ };
231
774
 
232
- return sign + parts
233
- .slice(0, maxUnits)
234
- .map((p) =>
235
- long
236
- ? new Intl.NumberFormat(locale, { style: "unit", unit: p.unit, unitDisplay: "long" }).format(p.v)
237
- : `${p.v}${p.short}`,
238
- )
239
- .join(" ");
240
- },
241
-
242
- /**
243
- * Returns a human-friendly, localized relative time string.
244
- *
245
- * Built on `Intl.RelativeTimeFormat`, so it speaks every language. Set
246
- * `options.locale` (or a global locale) to translate; `options.numeric`
247
- * controls wording (`"always"` → "1 day ago", `"auto"` → "yesterday").
248
- *
249
- * @example
250
- * nodeComfort.time.relative(Date.now() - 3600e3); // "1 hour ago"
251
- * nodeComfort.time.relative(Date.now() - 3600e3, undefined, { locale: "fr" }); // "il y a 1 heure"
252
- * nodeComfort.time.relative(Date.now() - 86400e3, undefined, { numeric: "auto" }); // "yesterday"
253
- *
254
- * @param {Date|number|string} date - Target date.
255
- * @param {Date|number|string} [from] - Reference date (defaults to now).
256
- * @param {{ locale?: string, numeric?: "always"|"auto" }} [options] - Formatting options.
257
- * @returns {string} The relative time string.
258
- */
259
- relative(date, from, options = {}) {
260
- const locale = options.locale ?? _config.locale;
261
- const numeric = options.numeric ?? "always";
262
- const target = _toDate(date).getTime();
263
- const base = _toDate(from).getTime();
264
- const diff = target - base;
265
- const abs = Math.abs(diff);
266
-
267
- if (abs < MS.s) {
268
- return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(0, "second");
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]);
269
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;
270
811
 
271
- const units = [
272
- { limit: MS.m, div: MS.s, name: "second" },
273
- { limit: MS.h, div: MS.m, name: "minute" },
274
- { limit: MS.d, div: MS.h, name: "hour" },
275
- { limit: MS.w, div: MS.d, name: "day" },
276
- { limit: MS.d * 30, div: MS.w, name: "week" },
277
- { limit: MS.d * 365, div: MS.d * 30, name: "month" },
278
- { limit: Infinity, div: MS.d * 365, name: "year" },
279
- ];
280
-
281
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric });
282
- for (const unit of units) {
283
- if (abs < unit.limit) {
284
- const value = Math.floor(abs / unit.div);
285
- return rtf.format(diff < 0 ? -value : value, /** @type {Intl.RelativeTimeFormatUnit} */ (unit.name));
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;
286
843
  }
844
+ case "X": return new Date(Number(value) * 1000);
845
+ case "x": return new Date(Number(value));
287
846
  }
288
- return rtf.format(0, "second");
289
- },
290
-
291
- /**
292
- * Formats a date using a small token syntax, with optional locale & time zone.
293
- *
294
- * **Numeric tokens:** `YYYY` `YY` `MM` `M` `DD` `D` `HH` `H` (24h) `hh` `h`
295
- * (12h) `mm` `m` `ss` `s` `SSS`.
296
- * **Localized tokens:** `MMMM` (month name) `MMM` (short month) `dddd` (weekday)
297
- * `ddd` (short weekday) `A` (AM/PM) `a` (am/pm).
298
- *
299
- * Wrap literal text in square brackets to keep it verbatim: `"[Today is] dddd"`.
300
- *
301
- * @example
302
- * nodeComfort.time.format(new Date(), "YYYY-MM-DD HH:mm:ss");
303
- * nodeComfort.time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" });
304
- * nodeComfort.time.format(Date.now(), "HH:mm", { timeZone: "Asia/Tokyo" });
305
- *
306
- * @param {Date|number|string} date - Date to format.
307
- * @param {string} [pattern="YYYY-MM-DD HH:mm:ss"] - Format pattern.
308
- * @param {{ locale?: string, timeZone?: string }} [options] - Locale / time-zone overrides.
309
- * @returns {string} The formatted date.
310
- */
311
- format(date, pattern = "YYYY-MM-DD HH:mm:ss", options = {}) {
312
- const d = _toDate(date);
313
- if (Number.isNaN(d.getTime())) return "Invalid Date";
314
-
315
- const locale = options.locale ?? _config.locale;
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);
862
+ }
863
+
864
+ /**
865
+ * The date as `YYYY-MM-DD`, the format `<input type="date">` and most APIs
866
+ * expect.
867
+ *
868
+ * @example
869
+ * time.toISODate(new Date(2024, 0, 5)); // "2024-01-05"
870
+ *
871
+ * @param {DateInput} [date] Defaults to now.
872
+ * @param {TimeZoneOptions} [options]
873
+ * @returns {string}
874
+ */
875
+ function toISODate(date, options = {}) {
876
+ return format(date ?? new Date(), "YYYY-MM-DD", { timeZone: options.timeZone ?? _config.timeZone });
877
+ }
878
+
879
+ /**
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.
884
+ *
885
+ * @example
886
+ * time.add(new Date(), "2h30m");
887
+ * time.add(new Date(), 3, "days");
888
+ * time.add(new Date(), "1 month");
889
+ * time.add(new Date(2024, 0, 31), 1, "month"); // 2024-02-29
890
+ * time.add(date, "1 day", { timeZone: "Europe/Paris" });
891
+ *
892
+ * @param {DateInput} date
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.
896
+ * @returns {Date}
897
+ * @throws {TypeError} If a text amount isn't a duration, or the unit isn't one.
898
+ */
899
+ function add(date, amount, unit = "ms", options = {}) {
900
+ const d = _toDate(date);
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);
908
+ return new Date(d.getTime() + amount * MS[u]);
909
+ }
910
+
911
+ /**
912
+ * Subtracts time from a date and returns a new `Date`.
913
+ *
914
+ * @example
915
+ * time.subtract(new Date(), 7, "days");
916
+ * time.subtract(new Date(), "90m");
917
+ *
918
+ * @param {DateInput} date
919
+ * @param {number | string} amount
920
+ * @param {CalendarUnit | TimeZoneOptions} [unit="ms"] The unit of a number, or the options when the amount is text.
921
+ * @param {TimeZoneOptions} [options]
922
+ * @returns {Date}
923
+ * @throws {TypeError} If a text amount isn't a duration, or the unit isn't one.
924
+ */
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);
929
+ }
930
+
931
+ /**
932
+ * `a - b` in a unit. Fixed units can give fractions; months and years are
933
+ * whole calendar months and years.
934
+ *
935
+ * @example
936
+ * time.diff("2024-01-03", "2024-01-01", "days"); // 2
937
+ * time.diff("2024-03-15", "2024-01-20", "months"); // 1
938
+ *
939
+ * @param {DateInput} a
940
+ * @param {DateInput} b
941
+ * @param {CalendarUnit} [unit="ms"]
942
+ * @param {TimeZoneOptions} [options] The zone whose calendar months and years follow.
943
+ * @returns {number}
944
+ * @throws {TypeError} If the unit isn't one.
945
+ */
946
+ function diff(a, b, unit = "ms", options = {}) {
947
+ const da = _toDate(a);
948
+ const db = _toDate(b);
949
+ const u = _knownUnit(unit);
950
+ if (u === "M" || u === "y") {
316
951
  const timeZone = options.timeZone ?? _config.timeZone;
317
- const p = _numericParts(d, timeZone);
318
-
319
- const replacer = (token, escaped) => {
320
- if (escaped !== undefined) return escaped;
321
- switch (token) {
322
- case "YYYY": return String(p.year);
323
- case "YY": return String(p.year).slice(-2);
324
- case "MMMM": return _named(d, { month: "long" }, locale, timeZone);
325
- case "MMM": return _named(d, { month: "short" }, locale, timeZone);
326
- case "MM": return _pad(p.month);
327
- case "M": return String(p.month);
328
- case "DD": return _pad(p.day);
329
- case "D": return String(p.day);
330
- case "dddd": return _named(d, { weekday: "long" }, locale, timeZone);
331
- case "ddd": return _named(d, { weekday: "short" }, locale, timeZone);
332
- case "HH": return _pad(p.hour);
333
- case "H": return String(p.hour);
334
- case "hh": return _pad(p.hour % 12 === 0 ? 12 : p.hour % 12);
335
- case "h": return String(p.hour % 12 === 0 ? 12 : p.hour % 12);
336
- case "mm": return _pad(p.minute);
337
- case "m": return String(p.minute);
338
- case "ss": return _pad(p.second);
339
- case "s": return String(p.second);
340
- case "SSS": return _pad(p.ms, 3);
341
- case "A": return p.hour < 12 ? "AM" : "PM";
342
- case "a": return p.hour < 12 ? "am" : "pm";
343
- default: return token;
344
- }
345
- };
952
+ const sign = da >= db ? 1 : -1;
953
+ const [late, early] = sign > 0 ? [da, db] : [db, da];
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--;
958
+ return sign * (u === "y" ? Math.trunc(months / 12) : months) || 0;
959
+ }
960
+ return (da.getTime() - db.getTime()) / MS[u];
961
+ }
962
+
963
+ /**
964
+ * A new `Date` at the start of the day, week, month... in the global time
965
+ * zone, or the one you name.
966
+ *
967
+ * @example
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
972
+ *
973
+ * @param {DateInput} date
974
+ * @param {StartOfUnit} unit
975
+ * @param {WeekOptions} [options]
976
+ * @returns {Date}
977
+ */
978
+ function startOf(date, unit, options = {}) {
979
+ const d = _toDate(date);
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);
983
+ }
984
+
985
+ /**
986
+ * A new `Date` at the last millisecond of the day, week, month...
987
+ *
988
+ * @example
989
+ * time.endOf(new Date(), "month"); // last day of the month, 23:59:59.999
990
+ *
991
+ * @param {DateInput} date
992
+ * @param {StartOfUnit} unit
993
+ * @param {WeekOptions} [options]
994
+ * @returns {Date}
995
+ */
996
+ function endOf(date, unit, options = {}) {
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();
1022
+ }
1023
+
1024
+ /**
1025
+ * Is `a` before `b`?
1026
+ *
1027
+ * @param {DateInput} a
1028
+ * @param {DateInput} b
1029
+ * @returns {boolean}
1030
+ */
1031
+ function isBefore(a, b) {
1032
+ return _toDate(a).getTime() < _toDate(b).getTime();
1033
+ }
1034
+
1035
+ /**
1036
+ * Is `a` after `b`?
1037
+ *
1038
+ * @example
1039
+ * if (time.isAfter(expiresAt, Date.now())) grantAccess();
1040
+ *
1041
+ * @param {DateInput} a
1042
+ * @param {DateInput} b
1043
+ * @returns {boolean}
1044
+ */
1045
+ function isAfter(a, b) {
1046
+ return _toDate(a).getTime() > _toDate(b).getTime();
1047
+ }
1048
+
1049
+ /**
1050
+ * Is the date between `start` and `end`, both included?
1051
+ *
1052
+ * @param {DateInput} date
1053
+ * @param {DateInput} start
1054
+ * @param {DateInput} end
1055
+ * @returns {boolean}
1056
+ */
1057
+ function isBetween(date, start, end) {
1058
+ const t = _toDate(date).getTime();
1059
+ return t >= _toDate(start).getTime() && t <= _toDate(end).getTime();
1060
+ }
1061
+
1062
+ /**
1063
+ * Are both dates on the same calendar day?
1064
+ *
1065
+ * @example
1066
+ * time.isSameDay(a, b); // local time
1067
+ * time.isSameDay(a, b, { timeZone: "Asia/Tokyo" }); // as seen in Tokyo
1068
+ *
1069
+ * @param {DateInput} a
1070
+ * @param {DateInput} b
1071
+ * @param {TimeZoneOptions} [options]
1072
+ * @returns {boolean}
1073
+ */
1074
+ function isSameDay(a, b, options = {}) {
1075
+ const timeZone = options.timeZone ?? _config.timeZone;
1076
+ const pa = _parts(_toDate(a), timeZone);
1077
+ const pb = _parts(_toDate(b), timeZone);
1078
+ return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
1079
+ }
1080
+
1081
+ /**
1082
+ * Is the date today?
1083
+ *
1084
+ * @param {DateInput} date
1085
+ * @param {TimeZoneOptions} [options]
1086
+ * @returns {boolean}
1087
+ */
1088
+ function isToday(date, options = {}) {
1089
+ return isSameDay(date, new Date(), options);
1090
+ }
1091
+
1092
+ /**
1093
+ * Was the date yesterday?
1094
+ *
1095
+ * @param {DateInput} date
1096
+ * @param {TimeZoneOptions} [options]
1097
+ * @returns {boolean}
1098
+ */
1099
+ function isYesterday(date, options = {}) {
1100
+ return _dayDelta(_toDate(date), new Date(), options.timeZone ?? _config.timeZone) === -1;
1101
+ }
1102
+
1103
+ /**
1104
+ * Is the date tomorrow?
1105
+ *
1106
+ * @param {DateInput} date
1107
+ * @param {TimeZoneOptions} [options]
1108
+ * @returns {boolean}
1109
+ */
1110
+ function isTomorrow(date, options = {}) {
1111
+ return _dayDelta(_toDate(date), new Date(), options.timeZone ?? _config.timeZone) === 1;
1112
+ }
1113
+
1114
+ /**
1115
+ * Is it a Saturday or a Sunday?
1116
+ *
1117
+ * @param {DateInput} date
1118
+ * @param {TimeZoneOptions} [options]
1119
+ * @returns {boolean}
1120
+ */
1121
+ function isWeekend(date, options = {}) {
1122
+ const day = _parts(_toDate(date), options.timeZone ?? _config.timeZone).weekday;
1123
+ return day === 0 || day === 6;
1124
+ }
1125
+
1126
+ /**
1127
+ * Is it a leap year?
1128
+ *
1129
+ * @example
1130
+ * time.isLeapYear(2024); // true
1131
+ * time.isLeapYear(1900); // false
1132
+ *
1133
+ * @param {number | DateInput} yearOrDate A year (1000 to 9999) or a date.
1134
+ * @returns {boolean}
1135
+ */
1136
+ function isLeapYear(yearOrDate) {
1137
+ const year = typeof yearOrDate === "number" && yearOrDate >= 1000 && yearOrDate <= 9999 ? yearOrDate : _toDate(yearOrDate).getFullYear();
1138
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
1139
+ }
1140
+
1141
+ /**
1142
+ * Is it a real date, or something that parses to one?
1143
+ *
1144
+ * @example
1145
+ * time.isValid("2024-02-29"); // true
1146
+ * time.isValid("2024-02-30"); // false
1147
+ *
1148
+ * @param {unknown} value
1149
+ * @returns {boolean}
1150
+ */
1151
+ function isValid(value) {
1152
+ if (types.isDate(value)) return !Number.isNaN(value.getTime());
1153
+ if (typeof value === "string") {
1154
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
1155
+ if (m && Number(m[3]) > _daysIn(Number(m[1]), Number(m[2]))) return false;
1156
+ }
1157
+ if (typeof value !== "string" && typeof value !== "number") return false;
1158
+ return !Number.isNaN(new Date(value).getTime());
1159
+ }
1160
+
1161
+ /**
1162
+ * Number of days in a month.
1163
+ *
1164
+ * @example
1165
+ * time.daysInMonth(new Date(2024, 1)); // 29
1166
+ * time.daysInMonth(2023, 2); // 28
1167
+ *
1168
+ * @param {DateInput | number} dateOrYear A date, or a year when you pass `month`.
1169
+ * @param {number} [month] 1 to 12.
1170
+ * @returns {number}
1171
+ */
1172
+ function daysInMonth(dateOrYear, month) {
1173
+ if (typeof dateOrYear === "number" && month !== undefined) return _daysIn(dateOrYear, month);
1174
+ const d = _toDate(dateOrYear);
1175
+ return _daysIn(d.getFullYear(), d.getMonth() + 1);
1176
+ }
346
1177
 
347
- return pattern.replace(
348
- /\[([^\]]*)\]|YYYY|MMMM|dddd|SSS|MMM|ddd|YY|MM|DD|HH|hh|mm|ss|M|D|H|h|m|s|A|a/g,
349
- replacer,
350
- );
351
- },
352
-
353
- /**
354
- * Adds a duration to a date and returns a new Date.
355
- *
356
- * The duration may be a millisecond number or a string like `"1h30m"`.
357
- *
358
- * @example
359
- * nodeComfort.time.add(new Date(), "2h");
360
- * nodeComfort.time.add(new Date(), -1, "days");
361
- *
362
- * @param {Date|number|string} date - Base date.
363
- * @param {number|string} amount - Amount (ms/string) or a number paired with `unit`.
364
- * @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit] - Unit when `amount` is a plain number.
365
- * @returns {Date} The resulting date.
366
- */
367
- add(date, amount, unit) {
368
- const d = _toDate(date);
369
- let ms;
370
- if (typeof amount === "number") {
371
- ms = unit ? amount * (MS[UNIT_ALIASES[unit] ?? unit] ?? 1) : amount;
372
- } else {
373
- ms = this.parseDuration(amount) ?? 0;
1178
+ /**
1179
+ * Day of the year, from 1 to 366.
1180
+ *
1181
+ * @param {DateInput} [date] Defaults to now.
1182
+ * @param {TimeZoneOptions} [options]
1183
+ * @returns {number}
1184
+ */
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;
1188
+ }
1189
+
1190
+ /**
1191
+ * ISO week number, from 1 to 53. Weeks start on Monday, and week 1 holds
1192
+ * the year's first Thursday.
1193
+ *
1194
+ * @example
1195
+ * time.weekOfYear(new Date(2021, 0, 3)); // 53, the last week of 2020
1196
+ *
1197
+ * @param {DateInput} [date] Defaults to now.
1198
+ * @param {{ utc?: boolean, timeZone?: string }} [options] `utc` reads the date in UTC, `timeZone` in another zone.
1199
+ * @returns {number}
1200
+ */
1201
+ function weekOfYear(date, options = {}) {
1202
+ const at = _parts(_toDate(date), options.utc ? "UTC" : options.timeZone ?? _config.timeZone);
1203
+ return _isoWeek(at.year, at.month, at.day);
1204
+ }
1205
+
1206
+ /**
1207
+ * The earliest date.
1208
+ *
1209
+ * @param {Iterable<DateInput>} dates
1210
+ * @returns {Date | undefined}
1211
+ */
1212
+ function min(dates) {
1213
+ /** @type {Date | undefined} */
1214
+ let best;
1215
+ for (const value of dates) {
1216
+ const d = _toDate(value);
1217
+ if (!best || d < best) best = d;
1218
+ }
1219
+ return best;
1220
+ }
1221
+
1222
+ /**
1223
+ * The latest date.
1224
+ *
1225
+ * @param {Iterable<DateInput>} dates
1226
+ * @returns {Date | undefined}
1227
+ */
1228
+ function max(dates) {
1229
+ /** @type {Date | undefined} */
1230
+ let best;
1231
+ for (const value of dates) {
1232
+ const d = _toDate(value);
1233
+ if (!best || d > best) best = d;
1234
+ }
1235
+ return best;
1236
+ }
1237
+
1238
+ /**
1239
+ * The current Unix timestamp, in seconds.
1240
+ *
1241
+ * @returns {number}
1242
+ */
1243
+ function unix() {
1244
+ return Math.floor(Date.now() / 1000);
1245
+ }
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
+
1293
+ /**
1294
+ * Starts a precise stopwatch.
1295
+ *
1296
+ * @example
1297
+ * const sw = time.stopwatch();
1298
+ * await loadConfig(); sw.lap("config");
1299
+ * await connectDb(); sw.lap("db");
1300
+ * console.log(sw.stop(true)); // "182.4ms"
1301
+ *
1302
+ * @returns {Stopwatch}
1303
+ */
1304
+ function stopwatch() {
1305
+ let start = process.hrtime.bigint();
1306
+ let lastLap = start;
1307
+ /** @type {Array<{ label: string, ms: number, total: number }>} */
1308
+ let laps = [];
1309
+ const since = (/** @type {bigint} */ t) => Number(process.hrtime.bigint() - t) / 1e6;
1310
+ const pretty = (/** @type {number} */ ms) =>
1311
+ ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : ms < MS.m ? `${Math.round(ms / 10) / 100}s` : formatDuration(ms, { units: 2, milliseconds: false });
1312
+ return {
1313
+ elapsed: () => since(start),
1314
+ stop: (fmt = false) => (fmt ? pretty(since(start)) : since(start)),
1315
+ lap: (label) => {
1316
+ const now = process.hrtime.bigint();
1317
+ const ms = Number(now - lastLap) / 1e6;
1318
+ lastLap = now;
1319
+ laps.push({ label: label ?? `lap ${laps.length + 1}`, ms, total: Number(now - start) / 1e6 });
1320
+ return ms;
1321
+ },
1322
+ laps: () => laps.map((l) => ({ ...l })),
1323
+ reset: () => {
1324
+ start = process.hrtime.bigint();
1325
+ lastLap = start;
1326
+ laps = [];
1327
+ },
1328
+ };
1329
+ }
1330
+
1331
+ /**
1332
+ * Runs a function, sync or async, and tells you how long it took.
1333
+ *
1334
+ * @example
1335
+ * const { result, duration } = await time.measure(() => db.query(sql));
1336
+ * nc.info(`Query took ${duration.toFixed(1)}ms`);
1337
+ *
1338
+ * @template T
1339
+ * @param {() => T} fn
1340
+ * @returns {Promise<{ result: Awaited<T>, duration: number }>} `duration` is in milliseconds.
1341
+ */
1342
+ async function measure(fn) {
1343
+ const start = process.hrtime.bigint();
1344
+ const result = await fn();
1345
+ return { result, duration: Number(process.hrtime.bigint() - start) / 1e6 };
1346
+ }
1347
+
1348
+ /**
1349
+ * Runs a task at a regular interval. Unlike `setInterval`, a slow run
1350
+ * delays the next one instead of piling up, timing doesn't drift, and
1351
+ * errors are caught.
1352
+ *
1353
+ * @example
1354
+ * const job = time.every("5m", syncInventory, { immediate: true, onError: nc.error });
1355
+ * job.next(); // when it runs next
1356
+ * job.stop();
1357
+ *
1358
+ * @param {number | string} interval In ms, or a duration like `"30s"`.
1359
+ * @param {() => unknown} task Can be async.
1360
+ * @param {EveryOptions} [options]
1361
+ * @returns {ScheduledTask}
1362
+ * @throws {RangeError} If the interval isn't a positive duration.
1363
+ */
1364
+ function every(interval, task, options = {}) {
1365
+ const ms = shared.parseDuration(interval);
1366
+ if (ms === null || ms <= 0) throw new RangeError(`Invalid interval: ${String(interval)}`);
1367
+ let stopped = false;
1368
+ let running = false;
1369
+ let runs = 0;
1370
+ /** @type {() => void} */
1371
+ let cancel = () => {};
1372
+ let nextAt = Date.now() + (options.immediate ? 0 : ms);
1373
+
1374
+ const tick = async () => {
1375
+ if (stopped) return;
1376
+ running = true;
1377
+ runs++;
1378
+ await _run(task, options.onError);
1379
+ running = false;
1380
+ if (stopped) return;
1381
+ const now = Date.now();
1382
+ nextAt += ms;
1383
+ if (nextAt <= now) nextAt = now + ms - ((now - nextAt) % ms);
1384
+ cancel = _longTimeout(tick, nextAt - Date.now(), options.unref ?? false);
1385
+ };
1386
+ cancel = _longTimeout(tick, nextAt - Date.now(), options.unref ?? false);
1387
+
1388
+ return {
1389
+ stop: () => {
1390
+ stopped = true;
1391
+ cancel();
1392
+ },
1393
+ next: () => (stopped ? undefined : new Date(nextAt)),
1394
+ isRunning: () => running,
1395
+ runs: () => runs,
1396
+ };
1397
+ }
1398
+
1399
+ const _parseCron = shared.parseCron;
1400
+
1401
+ /**
1402
+ * @param {import("./internal/shared").CronSpec} spec
1403
+ * @param {Date} from
1404
+ * @param {string | undefined} timeZone
1405
+ * @returns {Date | undefined}
1406
+ */
1407
+ const _nextCron = (spec, from, timeZone) => {
1408
+ const start = _parts(new Date(from.getTime() + 1000), timeZone);
1409
+ // Walk wall-clock time (stored in a UTC Date) and convert matches to instants.
1410
+ const wall = new Date(Date.UTC(start.year, start.month - 1, start.day, start.hour, start.minute, start.second));
1411
+ const limit = wall.getTime() + 8 * 366 * MS.d;
1412
+ const dayMatches = () => {
1413
+ const dom = spec.days.has(wall.getUTCDate()) || (spec.lastDay && wall.getUTCDate() === _daysIn(wall.getUTCFullYear(), wall.getUTCMonth() + 1));
1414
+ const dow = spec.weekdays.has(wall.getUTCDay());
1415
+ if (spec.anyDay && spec.anyWeekday) return true;
1416
+ if (spec.anyDay) return dow;
1417
+ if (spec.anyWeekday) return dom;
1418
+ return dom || dow;
1419
+ };
1420
+ while (wall.getTime() <= limit) {
1421
+ if (!spec.months.has(wall.getUTCMonth() + 1)) {
1422
+ wall.setUTCMonth(wall.getUTCMonth() + 1, 1);
1423
+ wall.setUTCHours(0, 0, 0, 0);
1424
+ continue;
374
1425
  }
375
- return new Date(d.getTime() + ms);
376
- },
377
-
378
- /**
379
- * Subtracts a duration from a date and returns a new Date.
380
- *
381
- * @param {Date|number|string} date - Base date.
382
- * @param {number|string} amount - Amount (ms/string) or a number paired with `unit`.
383
- * @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit] - Unit when `amount` is a plain number.
384
- * @returns {Date} The resulting date.
385
- */
386
- subtract(date, amount, unit) {
387
- if (typeof amount === "number") return this.add(date, -amount, unit);
388
- const ms = this.parseDuration(amount) ?? 0;
389
- return this.add(date, -ms);
390
- },
391
-
392
- /**
393
- * Returns the difference between two dates in the requested unit.
394
- *
395
- * @example
396
- * nodeComfort.time.diff("2024-01-02", "2024-01-01", "d"); // 1
397
- *
398
- * @param {Date|number|string} a - First date.
399
- * @param {Date|number|string} b - Second date.
400
- * @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit="ms"] - Result unit.
401
- * @returns {number} The signed difference (`a - b`) in the given unit.
402
- */
403
- diff(a, b, unit = "ms") {
404
- const ms = _toDate(a).getTime() - _toDate(b).getTime();
405
- return ms / (MS[UNIT_ALIASES[unit] ?? unit] ?? 1);
406
- },
407
-
408
- /**
409
- * Checks whether two dates fall on the same calendar day.
410
- *
411
- * Uses local time by default; pass `options.timeZone` (or set a global zone)
412
- * to compare calendar days in a specific zone.
413
- *
414
- * @param {Date|number|string} a - First date.
415
- * @param {Date|number|string} b - Second date.
416
- * @param {{ timeZone?: string }} [options] - Time-zone override.
417
- * @returns {boolean} True if both are the same day.
418
- */
419
- isSameDay(a, b, options = {}) {
420
- const timeZone = options.timeZone ?? _config.timeZone;
421
- const pa = _numericParts(_toDate(a), timeZone);
422
- const pb = _numericParts(_toDate(b), timeZone);
423
- return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
424
- },
425
-
426
- /**
427
- * Returns a new Date at the start of the given unit (day/hour/etc).
428
- *
429
- * @example
430
- * nodeComfort.time.startOf(new Date(), "day"); // midnight today
431
- *
432
- * @param {Date|number|string} date - Base date.
433
- * @param {"year"|"month"|"day"|"hour"|"minute"|"second"} unit - Unit to reset to.
434
- * @returns {Date} The start-of-unit date.
435
- */
436
- startOf(date, unit) {
437
- const d = _toDate(date);
438
- switch (unit) {
439
- case "year": d.setMonth(0);
440
- // falls through
441
- case "month": d.setDate(1);
442
- // falls through
443
- case "day": d.setHours(0);
444
- // falls through
445
- case "hour": d.setMinutes(0);
446
- // falls through
447
- case "minute": d.setSeconds(0);
448
- // falls through
449
- case "second": d.setMilliseconds(0);
1426
+ if (!dayMatches()) {
1427
+ wall.setUTCDate(wall.getUTCDate() + 1);
1428
+ wall.setUTCHours(0, 0, 0, 0);
1429
+ continue;
450
1430
  }
451
- return d;
452
- },
453
-
454
- /**
455
- * Returns the current Unix timestamp in seconds.
456
- *
457
- * @returns {number} Seconds since the Unix epoch.
458
- */
459
- unix() {
460
- return Math.floor(Date.now() / 1000);
461
- },
462
-
463
- /**
464
- * Returns a high-resolution stopwatch you can `.stop()` to get elapsed ms.
465
- *
466
- * @example
467
- * const timer = nodeComfort.time.stopwatch();
468
- * doWork();
469
- * console.log(timer.stop()); // e.g. 12.34 (ms, sub-ms precise)
470
- * console.log(timer.stop(true)); // "12.34ms"
471
- *
472
- * @returns {{ elapsed: () => number, stop: (format?: boolean) => number|string }} A stopwatch handle.
473
- */
474
- stopwatch() {
475
- const start = process.hrtime.bigint();
476
- const elapsed = () => Number(process.hrtime.bigint() - start) / 1e6;
477
- return {
478
- elapsed,
479
- stop: (format = false) => {
480
- const ms = elapsed();
481
- return format ? `${Math.round(ms * 100) / 100}ms` : ms;
482
- },
483
- };
484
- },
1431
+ if (!spec.hours.has(wall.getUTCHours())) {
1432
+ wall.setUTCHours(wall.getUTCHours() + 1, 0, 0, 0);
1433
+ continue;
1434
+ }
1435
+ if (!spec.minutes.has(wall.getUTCMinutes())) {
1436
+ wall.setUTCMinutes(wall.getUTCMinutes() + 1, 0, 0);
1437
+ continue;
1438
+ }
1439
+ if (!spec.seconds.has(wall.getUTCSeconds())) {
1440
+ wall.setUTCSeconds(wall.getUTCSeconds() + 1, 0);
1441
+ continue;
1442
+ }
1443
+ const instant = _fromWall(wall.getUTCFullYear(), wall.getUTCMonth() + 1, wall.getUTCDate(), wall.getUTCHours(), wall.getUTCMinutes(), wall.getUTCSeconds(), timeZone);
1444
+ if (instant && instant.getTime() > from.getTime()) return instant;
1445
+ wall.setUTCSeconds(wall.getUTCSeconds() + 1, 0);
1446
+ }
1447
+ return undefined;
1448
+ };
1449
+
1450
+ /**
1451
+ * The next date matching a cron expression, without scheduling anything.
1452
+ *
1453
+ * Expressions have 5 fields (`minute hour day month weekday`), or 6 with
1454
+ * seconds first. You can use `*`, lists (`1,15`), ranges (`1-5`), steps
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.
1459
+ *
1460
+ * @example
1461
+ * time.nextRun("0 9 * * mon-fri"); // next weekday at 09:00
1462
+ * time.nextRun("0 0 1 * *", { timeZone: "America/New_York" }); // next 1st of the month
1463
+ *
1464
+ * @param {string} expression
1465
+ * @param {CronOptions} [options]
1466
+ * @returns {Date | undefined} `undefined` if nothing matches within 8 years.
1467
+ * @throws {SyntaxError} If the expression is invalid.
1468
+ */
1469
+ function nextRun(expression, options = {}) {
1470
+ return _nextCron(_parseCron(expression), _toDate(options.from), options.timeZone ?? _config.timeZone);
1471
+ }
1472
+
1473
+ /**
1474
+ * Runs a task on a cron schedule, in any time zone. Daylight-saving changes
1475
+ * are handled: a skipped time is skipped, a repeated one runs once. See
1476
+ * `nextRun()` for the syntax.
1477
+ *
1478
+ * @example
1479
+ * const job = time.cron("0 9 * * 1-5", sendDailyReport, { timeZone: "Europe/Paris" });
1480
+ * time.cron("*\/15 * * * *", refreshCache, { onError: nc.error });
1481
+ * job.stop();
1482
+ *
1483
+ * @param {string} expression
1484
+ * @param {() => unknown} task Can be async.
1485
+ * @param {CronOptions} [options]
1486
+ * @returns {ScheduledTask}
1487
+ * @throws {SyntaxError} If the expression is invalid.
1488
+ */
1489
+ function cron(expression, task, options = {}) {
1490
+ const spec = _parseCron(expression);
1491
+ const timeZone = options.timeZone ?? _config.timeZone;
1492
+ let stopped = false;
1493
+ let running = false;
1494
+ let runs = 0;
1495
+ /** @type {Date | undefined} */
1496
+ let nextDate;
1497
+ /** @type {() => void} */
1498
+ let cancel = () => {};
1499
+
1500
+ const plan = (/** @type {Date} */ after) => {
1501
+ nextDate = _nextCron(spec, after, timeZone);
1502
+ if (!nextDate || stopped) return;
1503
+ const target = nextDate;
1504
+ cancel = _longTimeout(() => {
1505
+ if (stopped) return;
1506
+ plan(target);
1507
+ if (running && !options.overlap) return;
1508
+ running = true;
1509
+ runs++;
1510
+ _run(task, options.onError).finally(() => {
1511
+ running = false;
1512
+ });
1513
+ }, target.getTime() - Date.now(), options.unref ?? false);
1514
+ };
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
+ }
1523
+
1524
+ return {
1525
+ stop: () => {
1526
+ stopped = true;
1527
+ nextDate = undefined;
1528
+ cancel();
1529
+ },
1530
+ next: () => (stopped ? undefined : nextDate),
1531
+ isRunning: () => running,
1532
+ runs: () => runs,
1533
+ };
1534
+ }
1535
+
1536
+ module.exports = {
1537
+ setLocale,
1538
+ setTimezone,
1539
+ getConfig,
1540
+ timezones,
1541
+ offset,
1542
+ parseDuration,
1543
+ formatDuration,
1544
+ relative,
1545
+ calendar,
1546
+ format,
1547
+ parse,
1548
+ toISODate,
1549
+ add,
1550
+ subtract,
1551
+ diff,
1552
+ startOf,
1553
+ endOf,
1554
+ isBefore,
1555
+ isAfter,
1556
+ isBetween,
1557
+ isSame,
1558
+ isSameDay,
1559
+ isToday,
1560
+ isYesterday,
1561
+ isTomorrow,
1562
+ isWeekend,
1563
+ isLeapYear,
1564
+ isValid,
1565
+ daysInMonth,
1566
+ dayOfYear,
1567
+ weekOfYear,
1568
+ min,
1569
+ max,
1570
+ unix,
1571
+ fromUnix,
1572
+ range,
1573
+ stopwatch,
1574
+ measure,
1575
+ every,
1576
+ nextRun,
1577
+ cron,
485
1578
  };