datetick 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2422 @@
1
+ /**
2
+ * Returns the number of days in a given month (month is 0-based).
3
+ */
4
+ const getDaysInMonth = (year, month) => {
5
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
6
+ };
7
+ /**
8
+ * Reads the wall-clock components of an instant as seen in a given timezone.
9
+ */
10
+ const partsOf = (date, timezone) => {
11
+ // Guard invalid dates: `Intl.DateTimeFormat` throws a RangeError on an invalid Date, which would
12
+ // otherwise make every getter (`year()`, `month()`, …) throw instead of surfacing NaN.
13
+ if (Number.isNaN(date.getTime())) {
14
+ return {
15
+ year: Number.NaN,
16
+ month: Number.NaN,
17
+ day: Number.NaN,
18
+ hour: Number.NaN,
19
+ minute: Number.NaN,
20
+ second: Number.NaN,
21
+ millisecond: Number.NaN,
22
+ weekday: Number.NaN,
23
+ };
24
+ }
25
+ const dtf = new Intl.DateTimeFormat('en-US', {
26
+ timeZone: timezone,
27
+ hourCycle: 'h23',
28
+ year: 'numeric',
29
+ month: '2-digit',
30
+ day: '2-digit',
31
+ hour: '2-digit',
32
+ minute: '2-digit',
33
+ second: '2-digit',
34
+ });
35
+ const map = {};
36
+ for (const part of dtf.formatToParts(date)) {
37
+ if (part.type !== 'literal')
38
+ map[part.type] = Number(part.value);
39
+ }
40
+ // Day-of-week is derived from the zoned Y/M/D treated as a UTC date (a stable, locale-free lookup).
41
+ const weekday = new Date(Date.UTC(map.year, map.month - 1, map.day)).getUTCDay();
42
+ return {
43
+ year: map.year,
44
+ month: map.month - 1,
45
+ day: map.day,
46
+ hour: map.hour,
47
+ minute: map.minute,
48
+ second: map.second,
49
+ millisecond: date.getMilliseconds(),
50
+ weekday,
51
+ };
52
+ };
53
+ /**
54
+ * The timezone's offset from UTC (in ms) at a given instant (positive when ahead of UTC).
55
+ */
56
+ const offsetMs = (instant, timezone) => {
57
+ const p = partsOf(instant, timezone);
58
+ const asUtc = Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second, p.millisecond);
59
+ return asUtc - instant.getTime();
60
+ };
61
+ /**
62
+ * Converts wall-clock components in a timezone back to an absolute instant.
63
+ * Runs a two-pass correction so it stays correct across DST transitions.
64
+ */
65
+ const instantFromParts = (parts, timezone) => {
66
+ const utcGuess = Date.UTC(parts.year, parts.month, parts.day, parts.hour, parts.minute, parts.second, parts.millisecond);
67
+ const offset1 = offsetMs(new Date(utcGuess), timezone);
68
+ let instant = utcGuess - offset1;
69
+ const offset2 = offsetMs(new Date(instant), timezone);
70
+ if (offset2 !== offset1)
71
+ instant = utcGuess - offset2;
72
+ return new Date(instant);
73
+ };
74
+ // Localized month/weekday names are stable per (locale, style); creating a fresh `Intl.DateTimeFormat`
75
+ // on every call is costly, so resolved names are memoized (e.g. when rendering a calendar grid).
76
+ const monthNamesCache = new Map();
77
+ const weekdayNamesCache = new Map();
78
+ /**
79
+ * Returns the localized month names for a locale (index 0 = January).
80
+ */
81
+ const localeMonthNames = (locale, style) => {
82
+ const key = `${locale} ${style}`;
83
+ let names = monthNamesCache.get(key);
84
+ if (!names) {
85
+ const fmt = new Intl.DateTimeFormat(locale, { month: style, timeZone: 'UTC' });
86
+ names = Array.from({ length: 12 }, (_, i) => fmt.format(new Date(Date.UTC(2021, i, 15))));
87
+ monthNamesCache.set(key, names);
88
+ }
89
+ return names;
90
+ };
91
+ /**
92
+ * Returns the localized weekday names for a locale, Sunday-first (index 0 = Sunday).
93
+ */
94
+ const localeWeekdayNames = (locale, style) => {
95
+ const key = `${locale} ${style}`;
96
+ let names = weekdayNamesCache.get(key);
97
+ if (!names) {
98
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: style, timeZone: 'UTC' });
99
+ // 2020-06-07 is a Sunday, so +i walks Sunday..Saturday.
100
+ names = Array.from({ length: 7 }, (_, i) => fmt.format(new Date(Date.UTC(2020, 5, 7 + i))));
101
+ weekdayNamesCache.set(key, names);
102
+ }
103
+ return names;
104
+ };
105
+ const meridiemCache = new Map();
106
+ /**
107
+ * Returns a locale's AM/PM markers (e.g. en -> `{ am: 'AM', pm: 'PM' }`, zh -> morning/afternoon
108
+ * ideographs), falling back to English when a locale exposes none.
109
+ */
110
+ const localeMeridiems = (locale) => {
111
+ const cached = meridiemCache.get(locale);
112
+ if (cached)
113
+ return cached;
114
+ const dtf = new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true, timeZone: 'UTC' });
115
+ const at = (hour) => dtf.formatToParts(new Date(Date.UTC(2020, 0, 1, hour))).find((p) => p.type === 'dayPeriod')?.value ?? '';
116
+ const markers = { am: at(6) || 'AM', pm: at(18) || 'PM' };
117
+ meridiemCache.set(locale, markers);
118
+ return markers;
119
+ };
120
+ /**
121
+ * Builds a case-insensitive regex alternation matching a locale's AM/PM markers, for use when parsing
122
+ * the `A`/`a` token (e.g. `PM|pm|AM|am`).
123
+ */
124
+ const meridiemRegexGroup = (locale) => {
125
+ const { am, pm } = localeMeridiems(locale);
126
+ const variants = (s) => [s.toLocaleUpperCase(locale), s.toLocaleLowerCase(locale)];
127
+ return Array.from(new Set([...variants(pm), ...variants(am)]))
128
+ .filter(Boolean)
129
+ .sort((a, b) => b.length - a.length)
130
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
131
+ .join('|');
132
+ };
133
+ /**
134
+ * Returns the English ordinal suffix ('st', 'nd', 'rd', 'th') for a number.
135
+ */
136
+ const ordinalSuffix = (n) => {
137
+ const v = n % 100;
138
+ if (v >= 11 && v <= 13)
139
+ return 'th';
140
+ switch (n % 10) {
141
+ case 1:
142
+ return 'st';
143
+ case 2:
144
+ return 'nd';
145
+ case 3:
146
+ return 'rd';
147
+ default:
148
+ return 'th';
149
+ }
150
+ };
151
+ // dayjs-style localized tokens, described as the (date-part, time-part) Intl option pair each one
152
+ // derives from. Date and time are requested as separate `Intl` calls and joined with a space so that
153
+ // modern ICU never injects an " at " connector between them.
154
+ const TIME_LT = { hour: 'numeric', minute: '2-digit' };
155
+ const TIME_LTS = { hour: 'numeric', minute: '2-digit', second: '2-digit' };
156
+ const LOCALIZED_PARTS = {
157
+ LT: { time: TIME_LT },
158
+ LTS: { time: TIME_LTS },
159
+ L: { date: { year: 'numeric', month: '2-digit', day: '2-digit' } },
160
+ l: { date: { year: 'numeric', month: 'numeric', day: 'numeric' } },
161
+ LL: { date: { year: 'numeric', month: 'long', day: 'numeric' } },
162
+ ll: { date: { year: 'numeric', month: 'short', day: 'numeric' } },
163
+ LLL: { date: { year: 'numeric', month: 'long', day: 'numeric' }, time: TIME_LT },
164
+ lll: { date: { year: 'numeric', month: 'short', day: 'numeric' }, time: TIME_LT },
165
+ LLLL: { date: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }, time: TIME_LT },
166
+ llll: { date: { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }, time: TIME_LT },
167
+ };
168
+ // Derived localized patterns are cached per (locale, token): `formatToParts` is comparatively costly.
169
+ const localizedCache = new Map();
170
+ // Base token for each field, keyed by the `Intl` option value requested for it. Picks the token whose
171
+ // width matches the option so a round-trip through `formatPattern`/`parse` stays faithful.
172
+ const MONTH_TOKENS = { numeric: 'M', '2-digit': 'MM', short: 'MMM', long: 'MMMM' };
173
+ const WEEKDAY_TOKENS = { narrow: 'dd', short: 'ddd', long: 'dddd' };
174
+ // Resolves a single `Intl` part into a base token; missing entries are separators/literals.
175
+ const PART_TOKEN = {
176
+ year: (o) => (o.year === '2-digit' ? 'YY' : 'YYYY'),
177
+ month: (o) => MONTH_TOKENS[o.month ?? 'numeric'] ?? 'MMMM',
178
+ day: (o) => (o.day === '2-digit' ? 'DD' : 'D'),
179
+ weekday: (o) => WEEKDAY_TOKENS[o.weekday ?? 'long'] ?? 'dddd',
180
+ hour: (_o, hour12) => (hour12 ? 'h' : 'HH'),
181
+ minute: () => 'mm',
182
+ second: () => 'ss',
183
+ dayPeriod: () => 'A',
184
+ };
185
+ /**
186
+ * Translates one `Intl` skeleton into DateTick base tokens for the given locale, preserving that locale's
187
+ * field order and separators. Separators come back as `[escaped]` literals (with exotic spaces
188
+ * normalized), while each field maps to the base token whose width matches the requested option, so
189
+ * the surrounding `formatPattern`/`parse` still resolves values through the configured timezone.
190
+ */
191
+ const tokensFromIntl = (locale, options) => {
192
+ const dtf = new Intl.DateTimeFormat(locale, { timeZone: 'UTC', ...options });
193
+ const hour12 = dtf.resolvedOptions().hour12 ?? false;
194
+ // A reference instant with every field distinct (Thursday, 2 Jan 2020 03:04:05); only part *types*
195
+ // are read, so the concrete values never leak into the produced pattern.
196
+ const ref = new Date(Date.UTC(2020, 0, 2, 3, 4, 5));
197
+ return dtf.formatToParts(ref).map((part) => {
198
+ const resolve = PART_TOKEN[part.type];
199
+ // Normalize no-break / narrow-no-break spaces so output stays stable across ICU versions.
200
+ return resolve ? resolve(options, hour12) : `[${part.value.replace(/[\u00a0\u202f]/g, ' ')}]`;
201
+ });
202
+ };
203
+ /**
204
+ * Expands a localized token (`L`/`LL`/`LT`/etc.) into locale-aware base tokens.
205
+ */
206
+ const localizedTokens = (token, locale) => {
207
+ const key = `${locale} ${token}`;
208
+ const cached = localizedCache.get(key);
209
+ if (cached)
210
+ return cached;
211
+ const spec = LOCALIZED_PARTS[token];
212
+ const tokens = [];
213
+ if (spec.date)
214
+ tokens.push(...tokensFromIntl(locale, spec.date));
215
+ if (spec.date && spec.time)
216
+ tokens.push('[ ]');
217
+ if (spec.time)
218
+ tokens.push(...tokensFromIntl(locale, spec.time));
219
+ localizedCache.set(key, tokens);
220
+ return tokens;
221
+ };
222
+ // Fallback base-token patterns for localized tokens when no locale is supplied (US-English layout).
223
+ const LOCALIZED_FALLBACK = {
224
+ LTS: 'h:mm:ss A',
225
+ LT: 'h:mm A',
226
+ LLLL: 'dddd, MMMM D, YYYY h:mm A',
227
+ LLL: 'MMMM D, YYYY h:mm A',
228
+ LL: 'MMMM D, YYYY',
229
+ L: 'MM/DD/YYYY',
230
+ llll: 'ddd, MMM D, YYYY h:mm A',
231
+ lll: 'MMM D, YYYY h:mm A',
232
+ ll: 'MMM D, YYYY',
233
+ l: 'M/D/YYYY',
234
+ };
235
+ // Recognized tokens, ordered by descending length so the matcher is greedy (e.g. `LLLL` before `LL`).
236
+ const TOKENS = [
237
+ 'YYYY',
238
+ 'MMMM',
239
+ 'dddd',
240
+ 'LLLL',
241
+ 'llll',
242
+ 'MMM',
243
+ 'ddd',
244
+ 'SSS',
245
+ 'LLL',
246
+ 'LTS',
247
+ 'lll',
248
+ 'YY',
249
+ 'MM',
250
+ 'DD',
251
+ 'dd',
252
+ 'HH',
253
+ 'hh',
254
+ 'mm',
255
+ 'ss',
256
+ 'ZZ',
257
+ 'Do',
258
+ 'kk',
259
+ 'ww',
260
+ 'wo',
261
+ 'WW',
262
+ 'Wo',
263
+ 'LL',
264
+ 'LT',
265
+ 'll',
266
+ 'M',
267
+ 'D',
268
+ 'd',
269
+ 'H',
270
+ 'h',
271
+ 'm',
272
+ 's',
273
+ 'A',
274
+ 'a',
275
+ 'Z',
276
+ 'Q',
277
+ 'k',
278
+ 'X',
279
+ 'x',
280
+ 'w',
281
+ 'W',
282
+ 'L',
283
+ 'l',
284
+ ];
285
+ // Expands a matched token: localized tokens resolve to locale-aware base tokens (or the US fallback
286
+ // when no locale is given); everything else passes through unchanged.
287
+ const expandToken = (token, locale) => {
288
+ if (!(token in LOCALIZED_FALLBACK))
289
+ return [token];
290
+ if (locale)
291
+ return localizedTokens(token, locale);
292
+ return splitTokens(LOCALIZED_FALLBACK[token]);
293
+ };
294
+ /**
295
+ * Greedy pattern tokenizer shared by date and duration formatting. Splits a pattern into an ordered
296
+ * list of recognized tokens (matched longest-first from `tokens`), `[escaped]` literals (brackets
297
+ * retained), and single passthrough characters. An unclosed `[` is emitted as a literal character.
298
+ */
299
+ const tokenize = (pattern, tokens) => {
300
+ const out = [];
301
+ let i = 0;
302
+ while (i < pattern.length) {
303
+ const end = pattern[i] === '[' ? pattern.indexOf(']', i) : -1;
304
+ if (end !== -1) {
305
+ out.push(pattern.slice(i, end + 1));
306
+ i = end + 1;
307
+ continue;
308
+ }
309
+ const matched = tokens.find((t) => pattern.startsWith(t, i));
310
+ if (matched) {
311
+ out.push(matched);
312
+ i += matched.length;
313
+ }
314
+ else {
315
+ out.push(pattern[i]);
316
+ i += 1;
317
+ }
318
+ }
319
+ return out;
320
+ };
321
+ /**
322
+ * Splits a token pattern into an ordered list of tokens, literal characters and `[escaped]` literals.
323
+ * Longer tokens are matched before their prefixes (e.g. `MMMM` before `MM`), and localized tokens
324
+ * (`L`/`LL`/`LT`/etc.) are expanded in place into their underlying base tokens. When a `locale` is
325
+ * given, localized tokens follow that locale's field order and separators; otherwise they fall back to
326
+ * the US-English layout. Literals and non-localized tokens pass through `expandToken` unchanged.
327
+ */
328
+ const splitTokens = (pattern, locale) => {
329
+ return tokenize(pattern, TOKENS).flatMap((segment) => expandToken(segment, locale));
330
+ };
331
+
332
+ /**
333
+ * Fixed unit ratios in milliseconds (1 month = 30 days, 1 year = 365 days), the same approximation
334
+ * used by `DateTick.timeAgo`.
335
+ */
336
+ const RATIOS = {
337
+ millisecond: 1,
338
+ second: 1000,
339
+ minute: 60_000,
340
+ hour: 3_600_000,
341
+ date: 86_400_000,
342
+ day: 86_400_000,
343
+ week: 604_800_000,
344
+ month: 2_592_000_000, // 30 days
345
+ quarter: 7_776_000_000, // 90 days
346
+ year: 31_536_000_000, // 365 days
347
+ };
348
+ const ratioFor = (unit) => {
349
+ const ratio = RATIOS[unit];
350
+ if (ratio == null)
351
+ throw new Error(`Invalid duration unit: ${unit}`);
352
+ return ratio;
353
+ };
354
+ // Duration format tokens, ordered longest-first so the greedy matcher prefers e.g. `YYYY` over `YY`.
355
+ const FORMAT_TOKENS = ['YYYY', 'SSS', 'YY', 'MM', 'DD', 'HH', 'mm', 'ss', 'Y', 'M', 'D', 'H', 'm', 's'];
356
+ // ISO 8601 duration: an optional sign, `P`, the date part (`Y`/`M`/`W`/`D`) and an optional `T` time
357
+ // part (`H`/`M`/`S`). Each amount may be fractional with `.` or `,`.
358
+ const ISO_DURATION = /^([+-])?P(?:(\d+(?:[.,]\d+)?)Y)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/;
359
+ /**
360
+ * Parses an ISO 8601 duration string (e.g. 'P1Y2M10DT2H30M') into milliseconds, using the same fixed
361
+ * unit ratios as the rest of the class (1 month = 30 days, 1 year = 365 days).
362
+ */
363
+ const msFromISODuration = (input) => {
364
+ const m = ISO_DURATION.exec(input.trim());
365
+ // Reject `P`/`PT` with no components (m[1] is the sign; m[2..8] are the amounts).
366
+ if (!m || !m.slice(2).some(Boolean))
367
+ throw new Error(`Invalid ISO 8601 duration: "${input}"`);
368
+ const num = (value) => (value ? Number.parseFloat(value.replace(',', '.')) : 0);
369
+ const ms = num(m[2]) * RATIOS.year +
370
+ num(m[3]) * RATIOS.month +
371
+ num(m[4]) * RATIOS.week +
372
+ num(m[5]) * RATIOS.day +
373
+ num(m[6]) * RATIOS.hour +
374
+ num(m[7]) * RATIOS.minute +
375
+ num(m[8]) * RATIOS.second;
376
+ return m[1] === '-' ? -ms : ms;
377
+ };
378
+ /**
379
+ * An immutable length of time, independent of any particular instant.
380
+ *
381
+ * Conversions use fixed ratios (1 month = 30 days, 1 year = 365 days). `humanize()` is locale-aware via `Intl`.
382
+ *
383
+ * @class Duration
384
+ */
385
+ class Duration {
386
+ _locale;
387
+ _ms;
388
+ /**
389
+ * Creates a Duration.
390
+ *
391
+ * @param {number | string | DurationInput} value - A millisecond amount, an amount paired with `unit`,
392
+ * an ISO 8601 duration string (e.g. 'P1Y2M10DT2H30M'), or a components object
393
+ * @param {DateUnit} [unit] - The unit when `value` is a number (defaults to milliseconds)
394
+ * @param {string} [locale='en'] - The locale used by `humanize()`
395
+ * @throws {Error} If `value` is a string that is not a valid ISO 8601 duration
396
+ * @memberof Duration
397
+ */
398
+ constructor(value, unit, locale = 'en') {
399
+ this._locale = locale;
400
+ if (typeof value === 'string') {
401
+ this._ms = msFromISODuration(value);
402
+ }
403
+ else if (typeof value === 'number') {
404
+ this._ms = value * (unit ? ratioFor(unit) : 1);
405
+ }
406
+ else if (value instanceof Duration) {
407
+ this._ms = value.valueOf();
408
+ }
409
+ else if (value && typeof value === 'object') {
410
+ this._ms =
411
+ (value.years ?? 0) * RATIOS.year +
412
+ (value.months ?? 0) * RATIOS.month +
413
+ (value.weeks ?? 0) * RATIOS.week +
414
+ (value.days ?? 0) * RATIOS.day +
415
+ (value.hours ?? 0) * RATIOS.hour +
416
+ (value.minutes ?? 0) * RATIOS.minute +
417
+ (value.seconds ?? 0) * RATIOS.second +
418
+ (value.milliseconds ?? 0);
419
+ }
420
+ else {
421
+ this._ms = 0;
422
+ }
423
+ }
424
+ /**
425
+ * Checks whether a value is a Duration instance.
426
+ *
427
+ * @param {unknown} value - The value to check
428
+ * @returns {boolean} True if the value is a Duration
429
+ * @memberof Duration
430
+ */
431
+ static isDuration(value) {
432
+ return value instanceof Duration;
433
+ }
434
+ /**
435
+ * Returns a new Duration with the given amount added
436
+ *
437
+ * @param {number} value - The amount to add
438
+ * @param {DateUnit} [unit='millisecond'] - The unit of the amount
439
+ * @returns {Duration} The new Duration
440
+ * @memberof Duration
441
+ */
442
+ add(value, unit = 'millisecond') {
443
+ return new Duration(this._ms + value * ratioFor(unit), undefined, this._locale);
444
+ }
445
+ /**
446
+ * The whole duration expressed in a chosen unit.
447
+ *
448
+ * @param {DateUnit} unit - The target unit
449
+ * @returns {number} The duration in that unit
450
+ * @memberof Duration
451
+ */
452
+ as(unit) {
453
+ return this._ms / ratioFor(unit);
454
+ }
455
+ /**
456
+ * The whole duration expressed in days (fractional)
457
+ *
458
+ * @returns {number} The duration in days
459
+ * @memberof Duration
460
+ */
461
+ asDays() {
462
+ return this._ms / RATIOS.day;
463
+ }
464
+ /**
465
+ * The whole duration expressed in hours (fractional)
466
+ *
467
+ * @returns {number} The duration in hours
468
+ * @memberof Duration
469
+ */
470
+ asHours() {
471
+ return this._ms / RATIOS.hour;
472
+ }
473
+ /**
474
+ * The whole duration expressed in milliseconds
475
+ *
476
+ * @returns {number} The duration in milliseconds
477
+ * @memberof Duration
478
+ */
479
+ asMilliseconds() {
480
+ return this._ms;
481
+ }
482
+ /**
483
+ * The whole duration expressed in minutes (fractional)
484
+ *
485
+ * @returns {number} The duration in minutes
486
+ * @memberof Duration
487
+ */
488
+ asMinutes() {
489
+ return this._ms / RATIOS.minute;
490
+ }
491
+ /**
492
+ * The whole duration expressed in months (fractional, 1 month = 30 days)
493
+ *
494
+ * @returns {number} The duration in months
495
+ * @memberof Duration
496
+ */
497
+ asMonths() {
498
+ return this._ms / RATIOS.month;
499
+ }
500
+ /**
501
+ * The whole duration expressed in seconds (fractional)
502
+ *
503
+ * @returns {number} The duration in seconds
504
+ * @memberof Duration
505
+ */
506
+ asSeconds() {
507
+ return this._ms / RATIOS.second;
508
+ }
509
+ /**
510
+ * The whole duration expressed in weeks (fractional)
511
+ *
512
+ * @returns {number} The duration in weeks
513
+ * @memberof Duration
514
+ */
515
+ asWeeks() {
516
+ return this._ms / RATIOS.week;
517
+ }
518
+ /**
519
+ * The whole duration expressed in years (fractional, 1 year = 365 days)
520
+ *
521
+ * @returns {number} The duration in years
522
+ * @memberof Duration
523
+ */
524
+ asYears() {
525
+ return this._ms / RATIOS.year;
526
+ }
527
+ /**
528
+ * The days component after extracting whole years and months
529
+ *
530
+ * @returns {number} The days component
531
+ * @memberof Duration
532
+ */
533
+ days() {
534
+ return this.components().days;
535
+ }
536
+ /**
537
+ * Formats the duration with Day.js-style duration tokens.
538
+ *
539
+ * Supported tokens: `Y` `YY` `YYYY` `M` `MM` `D` `DD` `H` `HH` `m` `mm` `s` `ss` `SSS`.
540
+ * Wrap literal text in `[square brackets]`.
541
+ *
542
+ * @param {string} [pattern='HH:mm:ss'] - The token pattern
543
+ * @returns {string} The formatted duration
544
+ * @memberof Duration
545
+ */
546
+ format(pattern = 'HH:mm:ss') {
547
+ const sign = this._ms < 0 ? '-' : '';
548
+ const c = this.absComponents();
549
+ const two = (n) => String(n).padStart(2, '0');
550
+ const map = {
551
+ YYYY: String(c.years).padStart(4, '0'),
552
+ YY: two(c.years % 100),
553
+ Y: String(c.years),
554
+ MM: two(c.months),
555
+ M: String(c.months),
556
+ DD: two(c.days),
557
+ D: String(c.days),
558
+ HH: two(c.hours),
559
+ H: String(c.hours),
560
+ mm: two(c.minutes),
561
+ m: String(c.minutes),
562
+ ss: two(c.seconds),
563
+ s: String(c.seconds),
564
+ SSS: String(c.milliseconds).padStart(3, '0'),
565
+ };
566
+ const out = tokenize(pattern, FORMAT_TOKENS)
567
+ .map((segment) => {
568
+ if (segment.startsWith('[') && segment.endsWith(']'))
569
+ return segment.slice(1, -1);
570
+ return segment in map ? map[segment] : segment;
571
+ })
572
+ .join('');
573
+ return sign + out;
574
+ }
575
+ /**
576
+ * Returns the duration component for a unit.
577
+ *
578
+ * @param {DateUnit} unit - The component unit
579
+ * @returns {number} The component value
580
+ * @memberof Duration
581
+ */
582
+ get(unit) {
583
+ const c = this.components();
584
+ switch (unit) {
585
+ case 'year':
586
+ return c.years;
587
+ case 'quarter':
588
+ return Math.trunc(c.months / 3);
589
+ case 'month':
590
+ return c.months;
591
+ case 'week':
592
+ return Math.trunc(c.days / 7);
593
+ case 'date':
594
+ case 'day':
595
+ return c.days;
596
+ case 'hour':
597
+ return c.hours;
598
+ case 'minute':
599
+ return c.minutes;
600
+ case 'second':
601
+ return c.seconds;
602
+ case 'millisecond':
603
+ return c.milliseconds;
604
+ default:
605
+ throw new Error(`Invalid duration unit: ${unit}`);
606
+ }
607
+ }
608
+ /**
609
+ * The hours component
610
+ *
611
+ * @returns {number} The hours component
612
+ * @memberof Duration
613
+ */
614
+ hours() {
615
+ return this.components().hours;
616
+ }
617
+ /**
618
+ * Returns a human-readable, locale-aware string for the duration (e.g. '2 days', 'in 2 days')
619
+ *
620
+ * @param {boolean} [withSuffix=false] - Include a relative suffix (past/future) based on the sign
621
+ * @returns {string} The humanized string
622
+ * @memberof Duration
623
+ */
624
+ humanize(withSuffix = false) {
625
+ const abs = Math.abs(this._ms);
626
+ const seconds = abs / 1000;
627
+ const minutes = seconds / 60;
628
+ const hours = minutes / 60;
629
+ const days = hours / 24;
630
+ let unit;
631
+ let value;
632
+ if (seconds < 45) {
633
+ unit = 'second';
634
+ value = Math.round(seconds);
635
+ }
636
+ else if (minutes < 45) {
637
+ unit = 'minute';
638
+ value = Math.round(minutes);
639
+ }
640
+ else if (hours < 22) {
641
+ unit = 'hour';
642
+ value = Math.round(hours);
643
+ }
644
+ else if (days < 26) {
645
+ unit = 'day';
646
+ value = Math.round(days);
647
+ }
648
+ else if (days / 30 < 11) {
649
+ unit = 'month';
650
+ value = Math.round(days / 30);
651
+ }
652
+ else {
653
+ unit = 'year';
654
+ value = Math.round(days / 365);
655
+ }
656
+ if (withSuffix) {
657
+ const signed = this._ms < 0 ? -value : value;
658
+ return new Intl.RelativeTimeFormat(this._locale, { numeric: 'auto' }).format(signed, unit);
659
+ }
660
+ return new Intl.NumberFormat(this._locale, { style: 'unit', unit, unitDisplay: 'long' }).format(value);
661
+ }
662
+ /**
663
+ * The milliseconds component
664
+ *
665
+ * @returns {number} The milliseconds component
666
+ * @memberof Duration
667
+ */
668
+ milliseconds() {
669
+ return this.components().milliseconds;
670
+ }
671
+ /**
672
+ * The minutes component
673
+ *
674
+ * @returns {number} The minutes component
675
+ * @memberof Duration
676
+ */
677
+ minutes() {
678
+ return this.components().minutes;
679
+ }
680
+ /**
681
+ * The months component after extracting whole years
682
+ *
683
+ * @returns {number} The months component
684
+ * @memberof Duration
685
+ */
686
+ months() {
687
+ return this.components().months;
688
+ }
689
+ /**
690
+ * The seconds component
691
+ *
692
+ * @returns {number} The seconds component
693
+ * @memberof Duration
694
+ */
695
+ seconds() {
696
+ return this.components().seconds;
697
+ }
698
+ /**
699
+ * Returns a new Duration with the given amount subtracted
700
+ *
701
+ * @param {number} value - The amount to subtract
702
+ * @param {DateUnit} [unit='millisecond'] - The unit of the amount
703
+ * @returns {Duration} The new Duration
704
+ * @memberof Duration
705
+ */
706
+ subtract(value, unit = 'millisecond') {
707
+ return this.add(-value, unit);
708
+ }
709
+ /**
710
+ * Returns the ISO 8601 duration representation (e.g. 'P1Y2M3DT4H5M6S')
711
+ *
712
+ * @returns {string} The ISO 8601 duration string
713
+ * @memberof Duration
714
+ */
715
+ toISOString() {
716
+ const c = this.components();
717
+ const sign = this._ms < 0 ? '-' : '';
718
+ const part = (value, suffix) => (value ? `${Math.abs(value)}${suffix}` : '');
719
+ const date = `${part(c.years, 'Y')}${part(c.months, 'M')}${part(c.days, 'D')}`;
720
+ const seconds = Math.abs(c.seconds) + Math.abs(c.milliseconds) / 1000;
721
+ const time = `${part(c.hours, 'H')}${part(c.minutes, 'M')}${seconds ? `${seconds}S` : ''}`;
722
+ if (!date && !time)
723
+ return 'P0D';
724
+ return `${sign}P${date}${time ? `T${time}` : ''}`;
725
+ }
726
+ /**
727
+ * Serializes the duration as its ISO 8601 string (so `JSON.stringify` works)
728
+ *
729
+ * @returns {string} The ISO 8601 duration string
730
+ * @memberof Duration
731
+ */
732
+ toJSON() {
733
+ return this.toISOString();
734
+ }
735
+ /**
736
+ * The total duration in milliseconds (enables numeric coercion, e.g. `+duration`)
737
+ *
738
+ * @returns {number} The duration in milliseconds
739
+ * @memberof Duration
740
+ */
741
+ valueOf() {
742
+ return this._ms;
743
+ }
744
+ /**
745
+ * The years component
746
+ *
747
+ * @returns {number} The years component
748
+ * @memberof Duration
749
+ */
750
+ years() {
751
+ return this.components().years;
752
+ }
753
+ absComponents() {
754
+ const c = this.components();
755
+ return {
756
+ years: Math.abs(c.years),
757
+ months: Math.abs(c.months),
758
+ weeks: Math.abs(c.weeks),
759
+ days: Math.abs(c.days),
760
+ hours: Math.abs(c.hours),
761
+ minutes: Math.abs(c.minutes),
762
+ seconds: Math.abs(c.seconds),
763
+ milliseconds: Math.abs(c.milliseconds),
764
+ };
765
+ }
766
+ /**
767
+ * Decomposes the absolute duration into signed calendar components (years → milliseconds).
768
+ */
769
+ components() {
770
+ const sign = this._ms < 0 ? -1 : 1;
771
+ let rem = Math.abs(this._ms);
772
+ const years = Math.floor(rem / RATIOS.year);
773
+ rem -= years * RATIOS.year;
774
+ const months = Math.floor(rem / RATIOS.month);
775
+ rem -= months * RATIOS.month;
776
+ const days = Math.floor(rem / RATIOS.day);
777
+ rem -= days * RATIOS.day;
778
+ const hours = Math.floor(rem / RATIOS.hour);
779
+ rem -= hours * RATIOS.hour;
780
+ const minutes = Math.floor(rem / RATIOS.minute);
781
+ rem -= minutes * RATIOS.minute;
782
+ const seconds = Math.floor(rem / RATIOS.second);
783
+ rem -= seconds * RATIOS.second;
784
+ return {
785
+ years: years * sign,
786
+ months: months * sign,
787
+ weeks: 0,
788
+ days: days * sign,
789
+ hours: hours * sign,
790
+ minutes: minutes * sign,
791
+ seconds: seconds * sign,
792
+ milliseconds: rem * sign,
793
+ };
794
+ }
795
+ }
796
+
797
+ /**
798
+ * Framework-agnostic, fully timezone-aware date utility class.
799
+ *
800
+ * Instances are immutable: every getter/setter-style method returns a brand new
801
+ * `DateTick` rather than mutating the current one, so it is safe to share instances
802
+ * across Angular components, React renders, Vue computed properties, or plain scripts.
803
+ *
804
+ * Every calendar operation — reading components, setting them, `add`/`subtract`,
805
+ * `startOf`/`endOf`, week/quarter math and formatting — is resolved through the configured
806
+ * IANA `timezone`, so a single absolute instant is consistently interpreted in one zone.
807
+ *
808
+ * @class DateTick
809
+ * @author Andreas Nicolaou <anicolaou66@gmail.com>
810
+ */
811
+ class DateTick {
812
+ _date;
813
+ _locale;
814
+ _ordinal;
815
+ _timezone;
816
+ _weekStartsOn;
817
+ /**
818
+ * Creates a new DateTick instance.
819
+ *
820
+ * @param {string} [locale='en'] - The BCP 47 locale used for formatting
821
+ * @param {string} [timezone] - The IANA timezone all calendar operations resolve through (defaults to the runtime's timezone)
822
+ * @param {DateInput} [date] - The wrapped instant or zoned wall-clock parts (defaults to now). Date instances are cloned defensively.
823
+ * @param {number} [weekStartsOn=0] - First day of the week (0 = Sunday … 6 = Saturday), used by `week()` and `startOf`/`endOf('week')`
824
+ * @param {OrdinalFn} [ordinal] - Overrides the English ordinal used by `ordinal()` and the `Do`/`wo`/`Wo` tokens
825
+ * @memberof DateTick
826
+ */
827
+ constructor(locale = 'en', timezone = DateTick.guessTimezone(), date, weekStartsOn = 0, ordinal) {
828
+ this._locale = locale;
829
+ this._timezone = timezone;
830
+ this._weekStartsOn = ((weekStartsOn % 7) + 7) % 7;
831
+ this._ordinal = ordinal;
832
+ if (date == null) {
833
+ this._date = new Date();
834
+ }
835
+ else {
836
+ this._date = DateTick.resolveInput(date, timezone);
837
+ }
838
+ }
839
+ /**
840
+ * Creates a {@link Duration} representing a length of time.
841
+ *
842
+ * @param {number | string | DurationInput} value - A millisecond amount, an amount paired with `unit`,
843
+ * an ISO 8601 duration string (e.g. 'P1Y2M10DT2H30M'), or a components object
844
+ * @param {DateUnit} [unit] - The unit when `value` is a number (defaults to milliseconds)
845
+ * @param {string} [locale='en'] - The locale used by `humanize()`
846
+ * @returns {Duration} The duration
847
+ * @example DateTick.duration(2, 'hour').humanize() // '2 hours'
848
+ * @memberof DateTick
849
+ */
850
+ static duration(value, unit, locale = 'en') {
851
+ return new Duration(value, unit, locale);
852
+ }
853
+ /**
854
+ * Returns the runtime's best-guess IANA timezone.
855
+ *
856
+ * @returns {string} The guessed IANA timezone
857
+ * @memberof DateTick
858
+ */
859
+ static guessTimezone() {
860
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
861
+ }
862
+ /**
863
+ * Checks whether a value is a DateTick instance.
864
+ *
865
+ * @param {unknown} value - The value to check
866
+ * @returns {boolean} True if the value is a DateTick
867
+ * @memberof DateTick
868
+ */
869
+ static isDateTick(value) {
870
+ return value instanceof DateTick;
871
+ }
872
+ /**
873
+ * Checks if a date value is valid
874
+ *
875
+ * @param {DateInput} date - The date to validate
876
+ * @returns {boolean} True if valid, false otherwise
877
+ * @memberof DateTick
878
+ */
879
+ static isValid(date) {
880
+ const d = DateTick.resolveInput(date);
881
+ return !Number.isNaN(d.getTime());
882
+ }
883
+ /**
884
+ * Returns the maximum (latest) date from a list
885
+ *
886
+ * @param {...DateInput[]} dates - Dates to compare
887
+ * @returns {Date} The latest date
888
+ * @memberof DateTick
889
+ */
890
+ static max(...dates) {
891
+ const maxTime = Math.max(...dates.map((d) => DateTick.resolveInput(d).getTime()));
892
+ return new Date(maxTime);
893
+ }
894
+ /**
895
+ * Returns the minimum (earliest) date from a list
896
+ *
897
+ * @param {...DateInput[]} dates - Dates to compare
898
+ * @returns {Date} The earliest date
899
+ * @memberof DateTick
900
+ */
901
+ static min(...dates) {
902
+ const minTime = Math.min(...dates.map((d) => DateTick.resolveInput(d).getTime()));
903
+ return new Date(minTime);
904
+ }
905
+ /**
906
+ * Parses a string into a DateTick using an explicit token pattern, interpreting the wall-clock
907
+ * values in the given timezone. Supported tokens mirror {@link DateTick.formatPattern}; anything
908
+ * else (and text wrapped in `[square brackets]`) is treated as a literal.
909
+ *
910
+ * @param {string} input - The string to parse (e.g. '25/06/2026 22:23')
911
+ * @param {string} pattern - The token pattern (e.g. 'DD/MM/YYYY HH:mm')
912
+ * @param {string} [locale='en'] - The BCP 47 locale (also used to resolve month names)
913
+ * @param {string} [timezone] - The IANA timezone the wall-clock values belong to
914
+ * @returns {DateTick} The parsed DateTick
915
+ * @throws {Error} If the input does not match the pattern
916
+ * @memberof DateTick
917
+ */
918
+ static parse(input, pattern, locale = 'en', timezone = DateTick.guessTimezone()) {
919
+ const acc = { year: 1970, month: 0, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 };
920
+ let pm = false;
921
+ let hasMeridiem = false;
922
+ let epoch = null;
923
+ let regex = '^';
924
+ const handlers = [];
925
+ const digits = (token, wide, narrow) => (token === wide ? '(\\d{2})' : narrow);
926
+ const unableToParse = () => new Error(`Unable to parse "${input}" with pattern "${pattern}"`);
927
+ const assertRange = (value, min, max) => {
928
+ if (!Number.isInteger(value) || value < min || value > max)
929
+ throw unableToParse();
930
+ };
931
+ for (const token of splitTokens(pattern, locale)) {
932
+ switch (token) {
933
+ case 'YYYY':
934
+ regex += '(\\d{4})';
935
+ handlers.push((v) => (acc.year = Number(v)));
936
+ break;
937
+ case 'YY':
938
+ regex += '(\\d{2})';
939
+ handlers.push((v) => (acc.year = 2000 + Number(v)));
940
+ break;
941
+ case 'MMMM':
942
+ case 'MMM': {
943
+ const names = localeMonthNames(locale, token === 'MMMM' ? 'long' : 'short');
944
+ // Match the actual month names (longest-first so a name that prefixes another wins) rather
945
+ // than a greedy `.{1,max}` wildcard, which backtracks into adjacent tokens when there is no
946
+ // separator between them (e.g. `MMMMDo` parsing `May25th`).
947
+ const alternation = [...names]
948
+ .sort((a, b) => b.length - a.length)
949
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
950
+ .join('|');
951
+ regex += `(${alternation})`;
952
+ handlers.push((v) => {
953
+ const normalized = v.toLocaleLowerCase(locale);
954
+ const idx = names.findIndex((n) => n.toLocaleLowerCase(locale) === normalized);
955
+ if (idx < 0)
956
+ throw unableToParse();
957
+ acc.month = idx;
958
+ });
959
+ break;
960
+ }
961
+ case 'MM':
962
+ case 'M':
963
+ regex += digits(token, 'MM', '(\\d{1,2})');
964
+ handlers.push((v) => {
965
+ const month = Number(v);
966
+ assertRange(month, 1, 12);
967
+ acc.month = month - 1;
968
+ });
969
+ break;
970
+ case 'DD':
971
+ case 'D':
972
+ regex += digits(token, 'DD', '(\\d{1,2})');
973
+ handlers.push((v) => {
974
+ const day = Number(v);
975
+ assertRange(day, 1, 31);
976
+ acc.day = day;
977
+ });
978
+ break;
979
+ case 'HH':
980
+ case 'H':
981
+ regex += digits(token, 'HH', '(\\d{1,2})');
982
+ handlers.push((v) => {
983
+ const hour = Number(v);
984
+ assertRange(hour, 0, 23);
985
+ acc.hour = hour;
986
+ });
987
+ break;
988
+ case 'hh':
989
+ case 'h':
990
+ regex += digits(token, 'hh', '(\\d{1,2})');
991
+ handlers.push((v) => {
992
+ const hour = Number(v);
993
+ assertRange(hour, 1, 12);
994
+ acc.hour = hour;
995
+ });
996
+ break;
997
+ case 'mm':
998
+ case 'm':
999
+ regex += digits(token, 'mm', '(\\d{1,2})');
1000
+ handlers.push((v) => {
1001
+ const minute = Number(v);
1002
+ assertRange(minute, 0, 59);
1003
+ acc.minute = minute;
1004
+ });
1005
+ break;
1006
+ case 'ss':
1007
+ case 's':
1008
+ regex += digits(token, 'ss', '(\\d{1,2})');
1009
+ handlers.push((v) => {
1010
+ const second = Number(v);
1011
+ assertRange(second, 0, 59);
1012
+ acc.second = second;
1013
+ });
1014
+ break;
1015
+ case 'SSS':
1016
+ regex += '(\\d{3})';
1017
+ handlers.push((v) => (acc.millisecond = Number(v)));
1018
+ break;
1019
+ case 'kk':
1020
+ case 'k':
1021
+ regex += digits(token, 'kk', '(\\d{1,2})');
1022
+ handlers.push((v) => {
1023
+ const hour = Number(v);
1024
+ assertRange(hour, 1, 24);
1025
+ acc.hour = hour % 24;
1026
+ });
1027
+ break;
1028
+ case 'Do':
1029
+ regex += '(\\d{1,2})(?:st|nd|rd|th)';
1030
+ handlers.push((v) => {
1031
+ const day = Number(v);
1032
+ assertRange(day, 1, 31);
1033
+ acc.day = day;
1034
+ });
1035
+ break;
1036
+ case 'A':
1037
+ case 'a':
1038
+ regex += `(${meridiemRegexGroup(locale)})`;
1039
+ handlers.push((v) => {
1040
+ hasMeridiem = true;
1041
+ pm = v.toLocaleLowerCase(locale) === localeMeridiems(locale).pm.toLocaleLowerCase(locale);
1042
+ });
1043
+ break;
1044
+ case 'X':
1045
+ regex += '(-?\\d{1,16})';
1046
+ handlers.push((v) => (epoch = Number(v) * 1000));
1047
+ break;
1048
+ case 'x':
1049
+ regex += '(-?\\d{1,16})';
1050
+ handlers.push((v) => (epoch = Number(v)));
1051
+ break;
1052
+ // Derived values that cannot reconstruct an instant on their own: consume but ignore.
1053
+ case 'Q':
1054
+ case 'w':
1055
+ case 'ww':
1056
+ case 'wo':
1057
+ case 'W':
1058
+ case 'WW':
1059
+ case 'Wo':
1060
+ regex += '(\\d{1,2})(?:st|nd|rd|th)?';
1061
+ handlers.push(() => undefined);
1062
+ break;
1063
+ default: {
1064
+ const literal = token.startsWith('[') && token.endsWith(']') ? token.slice(1, -1) : token;
1065
+ regex += literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1066
+ }
1067
+ }
1068
+ }
1069
+ regex += '$';
1070
+ const match = new RegExp(regex, 'i').exec(input.trim());
1071
+ if (!match)
1072
+ throw unableToParse();
1073
+ handlers.forEach((handler, i) => handler(match[i + 1]));
1074
+ if (epoch !== null)
1075
+ return new DateTick(locale, timezone, new Date(epoch));
1076
+ if (hasMeridiem) {
1077
+ if (pm && acc.hour < 12)
1078
+ acc.hour += 12;
1079
+ if (!pm && acc.hour === 12)
1080
+ acc.hour = 0;
1081
+ }
1082
+ if (acc.day > getDaysInMonth(acc.year, acc.month))
1083
+ throw unableToParse();
1084
+ return new DateTick(locale, timezone, instantFromParts(acc, timezone));
1085
+ }
1086
+ static inputParts(input) {
1087
+ if (Array.isArray(input)) {
1088
+ return {
1089
+ year: input[0],
1090
+ month: input[1],
1091
+ day: input[2],
1092
+ hour: input[3] ?? 0,
1093
+ minute: input[4] ?? 0,
1094
+ second: input[5] ?? 0,
1095
+ millisecond: input[6] ?? 0,
1096
+ };
1097
+ }
1098
+ return {
1099
+ year: input.year,
1100
+ month: input.month,
1101
+ day: input.date,
1102
+ hour: input.hour ?? 0,
1103
+ minute: input.minute ?? 0,
1104
+ second: input.second ?? 0,
1105
+ millisecond: input.millisecond ?? 0,
1106
+ };
1107
+ }
1108
+ static resolveInput(input, timezone = DateTick.guessTimezone()) {
1109
+ if (input instanceof DateTick)
1110
+ return input.toDate();
1111
+ if (input instanceof Date)
1112
+ return new Date(input.getTime());
1113
+ if (typeof input === 'string')
1114
+ return new Date(input);
1115
+ if (typeof input === 'number')
1116
+ return new Date(input);
1117
+ return instantFromParts(DateTick.inputParts(input), timezone);
1118
+ }
1119
+ /**
1120
+ * Adds a specified amount of time to the wrapped date (chainable, immutable).
1121
+ *
1122
+ * Calendar units (`date`/`day`, `week`, `month`, `quarter`, `year`) preserve the wall-clock
1123
+ * time in the configured timezone (so they survive DST). Fixed-duration units
1124
+ * (`millisecond`/`second`/`minute`/`hour`) add absolute time. Month/year additions clamp the day
1125
+ * to the last day of the resulting month instead of overflowing (Jan 31 + 1 month -> Feb 28/29).
1126
+ *
1127
+ * @param {number} amount - The amount to add (negative values subtract)
1128
+ * @param {DateUnit} unit - The unit to add (e.g. 'day', 'month')
1129
+ * @returns {DateTick} A new DateTick with the result
1130
+ * @memberof DateTick
1131
+ */
1132
+ add(amount, unit) {
1133
+ switch (unit) {
1134
+ case 'millisecond':
1135
+ return this.withDate(new Date(this._date.getTime() + amount));
1136
+ case 'second':
1137
+ return this.withDate(new Date(this._date.getTime() + amount * 1000));
1138
+ case 'minute':
1139
+ return this.withDate(new Date(this._date.getTime() + amount * 60_000));
1140
+ case 'hour':
1141
+ return this.withDate(new Date(this._date.getTime() + amount * 3_600_000));
1142
+ case 'date':
1143
+ case 'day':
1144
+ return this.rebuild({ day: this.parts().day + amount });
1145
+ case 'week':
1146
+ return this.rebuild({ day: this.parts().day + amount * 7 });
1147
+ case 'month':
1148
+ return this.addMonths(amount);
1149
+ case 'quarter':
1150
+ return this.addMonths(amount * 3);
1151
+ case 'year':
1152
+ return this.addMonths(amount * 12);
1153
+ default:
1154
+ return this.clone();
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Formats the date as a calendar-style relative label such as "Today at 7:23 PM".
1159
+ *
1160
+ * @param {DateInput} [reference] - The date to compare against (defaults to now)
1161
+ * @param {CalendarDisplayFormats} [formats] - Token patterns or callbacks for each calendar bucket
1162
+ * @returns {string} The calendar-style label
1163
+ * @memberof DateTick
1164
+ */
1165
+ calendar(reference = new Date(), formats = {}) {
1166
+ const diff = this.diffCalendar(reference, 'day');
1167
+ const key = diff < -6
1168
+ ? 'sameElse'
1169
+ : diff < -1
1170
+ ? 'lastWeek'
1171
+ : diff === -1
1172
+ ? 'lastDay'
1173
+ : diff === 0
1174
+ ? 'sameDay'
1175
+ : diff === 1
1176
+ ? 'nextDay'
1177
+ : diff < 7
1178
+ ? 'nextWeek'
1179
+ : 'sameElse';
1180
+ const defaults = {
1181
+ sameDay: '[Today at] LT',
1182
+ nextDay: '[Tomorrow at] LT',
1183
+ nextWeek: 'dddd [at] LT',
1184
+ lastDay: '[Yesterday at] LT',
1185
+ lastWeek: '[Last] dddd [at] LT',
1186
+ sameElse: 'L',
1187
+ };
1188
+ const format = formats[key] ?? defaults[key];
1189
+ return typeof format === 'function' ? format(this) : this.formatPattern(format);
1190
+ }
1191
+ /**
1192
+ * Returns localized month-grid data for rendering a calendar UI.
1193
+ *
1194
+ * The grid is padded with adjacent-month dates so every week has seven cells,
1195
+ * and the weekday order follows the configured `weekStartsOn`.
1196
+ *
1197
+ * @returns {CalendarMonth} Calendar data for the wrapped date's month
1198
+ * @memberof DateTick
1199
+ */
1200
+ calendarMonth(options = {}) {
1201
+ const selected = options.selected === false
1202
+ ? null
1203
+ : partsOf(options.selected === undefined ? this._date : this.toComparable(options.selected), this._timezone);
1204
+ const firstOfMonth = this.rebuild({ day: 1 }).startOf('day');
1205
+ const first = firstOfMonth.parts();
1206
+ const leadingDays = (first.weekday - this._weekStartsOn + 7) % 7;
1207
+ const daysInMonth = getDaysInMonth(first.year, first.month);
1208
+ const cellCount = Math.ceil((leadingDays + daysInMonth) / 7) * 7;
1209
+ const start = firstOfMonth.subtract(leadingDays, 'day');
1210
+ const today = new DateTick(this._locale, this._timezone, new Date(), this._weekStartsOn).parts();
1211
+ const monthLabel = new Intl.DateTimeFormat(this._locale, {
1212
+ month: 'long',
1213
+ year: 'numeric',
1214
+ timeZone: this._timezone,
1215
+ }).format(firstOfMonth.toDate());
1216
+ const formatDate = (date) => {
1217
+ if (options.dateFormat == null) {
1218
+ return date.format({ year: 'numeric', month: '2-digit', day: '2-digit' });
1219
+ }
1220
+ if (typeof options.dateFormat === 'function')
1221
+ return options.dateFormat(date);
1222
+ if (typeof options.dateFormat === 'string')
1223
+ return date.formatPattern(options.dateFormat);
1224
+ return date.format(options.dateFormat);
1225
+ };
1226
+ const weekdays = Array.from({ length: 7 }, (_, i) => {
1227
+ const day = (this._weekStartsOn + i) % 7;
1228
+ const sample = new Date(Date.UTC(2020, 5, 7 + day));
1229
+ return {
1230
+ day,
1231
+ isoDay: day === 0 ? 7 : day,
1232
+ long: new Intl.DateTimeFormat(this._locale, { weekday: 'long', timeZone: 'UTC' }).format(sample),
1233
+ short: new Intl.DateTimeFormat(this._locale, { weekday: 'short', timeZone: 'UTC' }).format(sample),
1234
+ narrow: new Intl.DateTimeFormat(this._locale, { weekday: 'narrow', timeZone: 'UTC' }).format(sample),
1235
+ };
1236
+ });
1237
+ const days = Array.from({ length: cellCount }, (_, i) => {
1238
+ const value = start.add(i, 'day');
1239
+ const p = value.parts();
1240
+ return {
1241
+ value,
1242
+ isoDate: value.formatPattern('YYYY-MM-DD'),
1243
+ formatted: formatDate(value),
1244
+ year: p.year,
1245
+ month: p.month,
1246
+ date: p.day,
1247
+ day: p.weekday,
1248
+ isoDay: p.weekday === 0 ? 7 : p.weekday,
1249
+ isCurrentMonth: p.year === first.year && p.month === first.month,
1250
+ isToday: p.year === today.year && p.month === today.month && p.day === today.day,
1251
+ isSelected: Boolean(selected && p.year === selected.year && p.month === selected.month && p.day === selected.day),
1252
+ };
1253
+ });
1254
+ return {
1255
+ year: first.year,
1256
+ month: first.month,
1257
+ label: monthLabel,
1258
+ weekdays,
1259
+ days,
1260
+ weeks: Array.from({ length: days.length / 7 }, (_, i) => days.slice(i * 7, i * 7 + 7)),
1261
+ };
1262
+ }
1263
+ /**
1264
+ * Returns a new DateTick with the exact same instant, locale and timezone
1265
+ *
1266
+ * @returns {DateTick} The cloned instance
1267
+ * @memberof DateTick
1268
+ */
1269
+ clone() {
1270
+ return this.withDate(this._date);
1271
+ }
1272
+ date(value) {
1273
+ if (value == null)
1274
+ return this.parts().day;
1275
+ return this.rebuild({ day: value });
1276
+ }
1277
+ dates(value) {
1278
+ return value == null ? this.date() : this.date(value);
1279
+ }
1280
+ day(value) {
1281
+ const p = this.parts();
1282
+ if (value == null)
1283
+ return p.weekday;
1284
+ return this.rebuild({ day: p.day - p.weekday + value });
1285
+ }
1286
+ dayOfYear(value) {
1287
+ if (value == null) {
1288
+ const p = this.parts();
1289
+ const startOfYear = Date.UTC(p.year, 0, 1);
1290
+ const current = Date.UTC(p.year, p.month, p.day);
1291
+ return Math.round((current - startOfYear) / 86_400_000) + 1;
1292
+ }
1293
+ return this.rebuild({ month: 0, day: value });
1294
+ }
1295
+ days(value) {
1296
+ return value == null ? this.day() : this.day(value);
1297
+ }
1298
+ /**
1299
+ * Gets the number of days in the wrapped date's month (in the configured timezone)
1300
+ *
1301
+ * @returns {number} The number of days in the month
1302
+ * @memberof DateTick
1303
+ */
1304
+ daysInMonth() {
1305
+ const p = this.parts();
1306
+ return getDaysInMonth(p.year, p.month);
1307
+ }
1308
+ /**
1309
+ * Gets the difference between the wrapped date and another date.
1310
+ *
1311
+ * @param {DateInput} date - The date to compare against
1312
+ * @param {DateUnit} [unit='millisecond'] - The unit of measurement
1313
+ * @param {boolean} [precise=false] - When true, returns a floating-point value instead of truncating.
1314
+ * For `month`/`quarter`/`year` the fractional part reflects progress through the current calendar unit.
1315
+ * @returns {number} The difference (positive when the wrapped date is later)
1316
+ * @memberof DateTick
1317
+ */
1318
+ diff(date, unit = 'millisecond', precise = false) {
1319
+ const other = this.toComparable(date);
1320
+ const diffMs = this._date.getTime() - other.getTime();
1321
+ const scale = (ms) => (precise ? ms : Math.trunc(ms));
1322
+ switch (unit) {
1323
+ case 'millisecond':
1324
+ return diffMs;
1325
+ case 'second':
1326
+ return scale(diffMs / 1000);
1327
+ case 'minute':
1328
+ return scale(diffMs / 60_000);
1329
+ case 'hour':
1330
+ return scale(diffMs / 3_600_000);
1331
+ case 'date':
1332
+ case 'day':
1333
+ return scale(diffMs / 86_400_000);
1334
+ case 'week':
1335
+ return scale(diffMs / 604_800_000);
1336
+ case 'month':
1337
+ return precise ? this.preciseCalendarDiff(other, 1) : this.wholeMonthDiff(other, 1);
1338
+ case 'quarter':
1339
+ return precise ? this.preciseCalendarDiff(other, 3) : this.wholeMonthDiff(other, 3);
1340
+ case 'year':
1341
+ return precise ? this.preciseCalendarDiff(other, 12) : this.wholeMonthDiff(other, 12);
1342
+ default:
1343
+ return diffMs;
1344
+ }
1345
+ }
1346
+ /**
1347
+ * Calendar-boundary difference in the configured timezone.
1348
+ *
1349
+ * Unlike {@link DateTick.diff}, this counts calendar units rather than elapsed duration.
1350
+ * For example, Friday 23:00 to Saturday 01:00 is one calendar day apart even though
1351
+ * only two hours elapsed.
1352
+ *
1353
+ * @param {DateInput} date - The date to compare against
1354
+ * @param {CalendarDiffUnit} [unit='day'] - Calendar unit to compare
1355
+ * @returns {number} The calendar difference, positive when this instance is later
1356
+ * @memberof DateTick
1357
+ */
1358
+ diffCalendar(date, unit = 'day') {
1359
+ const other = this.withDate(this.toComparable(date));
1360
+ switch (unit) {
1361
+ case 'date':
1362
+ case 'day':
1363
+ return this.calendarDayNumber() - other.calendarDayNumber();
1364
+ case 'week':
1365
+ return Math.trunc((this.startOf('week').calendarDayNumber() - other.startOf('week').calendarDayNumber()) / 7);
1366
+ case 'month': {
1367
+ const a = this.parts();
1368
+ const b = other.parts();
1369
+ return (a.year - b.year) * 12 + (a.month - b.month);
1370
+ }
1371
+ case 'quarter': {
1372
+ const a = this.parts();
1373
+ const b = other.parts();
1374
+ return (a.year - b.year) * 4 + (Math.floor(a.month / 3) - Math.floor(b.month / 3));
1375
+ }
1376
+ case 'year':
1377
+ return this.parts().year - other.parts().year;
1378
+ default:
1379
+ return Number.NaN;
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Returns a new DateTick set to the end of the given unit (one millisecond before the next unit starts)
1384
+ *
1385
+ * @param {DateUnit} unit - The unit to snap to (e.g. 'month', 'day')
1386
+ * @returns {DateTick} A new DateTick at the end of the unit
1387
+ * @memberof DateTick
1388
+ */
1389
+ endOf(unit) {
1390
+ if (unit === 'millisecond')
1391
+ return this.clone();
1392
+ const next = this.startOf(unit).add(1, unit);
1393
+ return this.withDate(new Date(next.valueOf() - 1));
1394
+ }
1395
+ /**
1396
+ * Formats a date using Intl.DateTimeFormat or a preset (in the configured timezone)
1397
+ *
1398
+ * @param {Intl.DateTimeFormatOptions | DateFormatPreset} [format] - Format options or preset
1399
+ * @returns {string} The formatted date string
1400
+ * @memberof DateTick
1401
+ */
1402
+ format(format) {
1403
+ const options = typeof format === 'string' ? this.getPreset(format) : format;
1404
+ return new Intl.DateTimeFormat(this._locale, {
1405
+ timeZone: this._timezone,
1406
+ ...options,
1407
+ }).format(this._date);
1408
+ }
1409
+ /**
1410
+ * Formats the date using a token pattern, resolved in the configured timezone.
1411
+ *
1412
+ * Core tokens: `YYYY` `YY` `MMMM` `MMM` `MM` `M` `DD` `D` `dddd` `ddd` `dd` `d` `HH` `H` `hh` `h`
1413
+ * `mm` `m` `ss` `s` `SSS` `A` `a` `Z` `ZZ`. Advanced: `Do` `Q` `k` `kk` `X` `x` `w` `ww` `wo` `W` `WW` `Wo`.
1414
+ * Localized (resolved to the configured locale's field order and separators): `L` `LL` `LLL` `LLLL`
1415
+ * `l` `ll` `lll` `llll` `LT` `LTS`. Wrap literal text in `[square brackets]`.
1416
+ *
1417
+ * @param {string} pattern - The token pattern (e.g. 'DD/MM/YYYY HH:mm')
1418
+ * @returns {string} The formatted string
1419
+ * @example date.formatPattern('DD/MM/YYYY') // '25/06/2026'
1420
+ * @memberof DateTick
1421
+ */
1422
+ formatPattern(pattern) {
1423
+ const p = this.parts();
1424
+ const two = (n) => String(n).padStart(2, '0');
1425
+ const intl = (opts) => new Intl.DateTimeFormat(this._locale, { timeZone: this._timezone, ...opts }).format(this._date);
1426
+ const hour12 = p.hour % 12 === 0 ? 12 : p.hour % 12;
1427
+ const hour24From1 = p.hour === 0 ? 24 : p.hour;
1428
+ const meridiem = localeMeridiems(this._locale);
1429
+ const dayPeriod = p.hour < 12 ? meridiem.am : meridiem.pm;
1430
+ const map = {
1431
+ YYYY: String(p.year).padStart(4, '0'),
1432
+ YY: two(p.year % 100),
1433
+ MMMM: intl({ month: 'long' }),
1434
+ MMM: intl({ month: 'short' }),
1435
+ MM: two(p.month + 1),
1436
+ M: String(p.month + 1),
1437
+ Do: this.ordinal(),
1438
+ DD: two(p.day),
1439
+ D: String(p.day),
1440
+ dddd: intl({ weekday: 'long' }),
1441
+ ddd: intl({ weekday: 'short' }),
1442
+ dd: intl({ weekday: 'narrow' }),
1443
+ d: String(p.weekday),
1444
+ HH: two(p.hour),
1445
+ H: String(p.hour),
1446
+ hh: two(hour12),
1447
+ h: String(hour12),
1448
+ kk: two(hour24From1),
1449
+ k: String(hour24From1),
1450
+ mm: two(p.minute),
1451
+ m: String(p.minute),
1452
+ SSS: String(p.millisecond).padStart(3, '0'),
1453
+ ss: two(p.second),
1454
+ s: String(p.second),
1455
+ A: dayPeriod,
1456
+ a: dayPeriod.toLocaleLowerCase(this._locale),
1457
+ Q: String(this.quarter()),
1458
+ ww: two(this.week()),
1459
+ w: String(this.week()),
1460
+ wo: this.ordinalFor(this.week()),
1461
+ WW: two(this.isoWeek()),
1462
+ W: String(this.isoWeek()),
1463
+ Wo: this.ordinalFor(this.isoWeek()),
1464
+ X: String(this.unix()),
1465
+ x: String(this.valueOf()),
1466
+ Z: this.offsetString(true),
1467
+ ZZ: this.offsetString(false),
1468
+ };
1469
+ return splitTokens(pattern, this._locale)
1470
+ .map((token) => {
1471
+ if (token.startsWith('[') && token.endsWith(']'))
1472
+ return token.slice(1, -1);
1473
+ return token in map ? map[token] : token;
1474
+ })
1475
+ .join('');
1476
+ }
1477
+ /**
1478
+ * Formats the date relative to another date, Day.js-style.
1479
+ *
1480
+ * @param {DateInput} date - The date to compare from
1481
+ * @param {boolean} [withoutSuffix=false] - When true, omit "ago" / "in"
1482
+ * @returns {string} The relative-time label
1483
+ * @memberof DateTick
1484
+ */
1485
+ from(date, withoutSuffix = false) {
1486
+ const diff = this._date.getTime() - this.toComparable(date).getTime();
1487
+ const duration = DateTick.duration(diff, 'millisecond', this._locale);
1488
+ return withoutSuffix ? duration.humanize() : duration.humanize(true);
1489
+ }
1490
+ /**
1491
+ * Formats the date relative to now, Day.js-style.
1492
+ *
1493
+ * @param {boolean} [withoutSuffix=false] - When true, omit "ago" / "in"
1494
+ * @returns {string} The relative-time label
1495
+ * @memberof DateTick
1496
+ */
1497
+ fromNow(withoutSuffix = false) {
1498
+ return this.from(new Date(), withoutSuffix);
1499
+ }
1500
+ /**
1501
+ * Gets a specific unit (or derived value) from the wrapped date
1502
+ *
1503
+ * @param {DateGettableUnit} unit - The unit to get (e.g. 'year', 'isoWeek')
1504
+ * @returns {number} The value of the unit
1505
+ * @memberof DateTick
1506
+ */
1507
+ get(unit) {
1508
+ switch (unit) {
1509
+ case 'millisecond':
1510
+ return this.millisecond();
1511
+ case 'second':
1512
+ return this.second();
1513
+ case 'minute':
1514
+ return this.minute();
1515
+ case 'hour':
1516
+ return this.hour();
1517
+ case 'date':
1518
+ return this.date();
1519
+ case 'day':
1520
+ return this.day();
1521
+ case 'week':
1522
+ return this.week();
1523
+ case 'month':
1524
+ return this.month();
1525
+ case 'quarter':
1526
+ return this.quarter();
1527
+ case 'year':
1528
+ return this.year();
1529
+ case 'dayOfYear':
1530
+ return this.dayOfYear();
1531
+ case 'isoDay':
1532
+ return this.isoDay();
1533
+ case 'isoWeek':
1534
+ return this.isoWeek();
1535
+ case 'isoWeekYear':
1536
+ return this.isoWeekYear();
1537
+ case 'isoWeeksInYear':
1538
+ return this.isoWeeksInYear();
1539
+ case 'weekYear':
1540
+ return this.weekYear();
1541
+ case 'weekday':
1542
+ return this.weekday();
1543
+ case 'weeksInYear':
1544
+ return this.weeksInYear();
1545
+ case 'daysInMonth':
1546
+ return this.daysInMonth();
1547
+ default:
1548
+ return Number.NaN;
1549
+ }
1550
+ }
1551
+ hour(value) {
1552
+ if (value == null)
1553
+ return this.parts().hour;
1554
+ return this.rebuild({ hour: value });
1555
+ }
1556
+ hours(value) {
1557
+ return value == null ? this.hour() : this.hour(value);
1558
+ }
1559
+ /**
1560
+ * Checks if the wrapped date is strictly after another date
1561
+ *
1562
+ * @param {DateInput} date - The date to compare against
1563
+ * @returns {boolean} True if the wrapped date is later
1564
+ * @memberof DateTick
1565
+ */
1566
+ isAfter(date) {
1567
+ return this._date.getTime() > this.toComparable(date).getTime();
1568
+ }
1569
+ /**
1570
+ * Checks if the wrapped date is strictly before another date
1571
+ *
1572
+ * @param {DateInput} date - The date to compare against
1573
+ * @returns {boolean} True if the wrapped date is earlier
1574
+ * @memberof DateTick
1575
+ */
1576
+ isBefore(date) {
1577
+ return this._date.getTime() < this.toComparable(date).getTime();
1578
+ }
1579
+ /**
1580
+ * Checks if the wrapped date falls between two dates.
1581
+ *
1582
+ * @param {DateInput} start - The lower bound
1583
+ * @param {DateInput} end - The upper bound
1584
+ * @param {boolean | Inclusivity} [inclusivity=false] - `false`/`'()'` excludes both bounds,
1585
+ * `true`/`'[]'` includes both, or use `'(]'`/`'[)'` to control each side independently
1586
+ * @returns {boolean} True if the wrapped date is between start and end
1587
+ * @memberof DateTick
1588
+ */
1589
+ isBetween(start, end, inclusivity = false) {
1590
+ const time = this._date.getTime();
1591
+ const startTime = this.toComparable(start).getTime();
1592
+ const endTime = this.toComparable(end).getTime();
1593
+ const mode = typeof inclusivity === 'boolean' ? (inclusivity ? '[]' : '()') : inclusivity;
1594
+ const afterStart = mode[0] === '[' ? time >= startTime : time > startTime;
1595
+ const beforeEnd = mode[1] === ']' ? time <= endTime : time < endTime;
1596
+ return afterStart && beforeEnd;
1597
+ }
1598
+ /**
1599
+ * Checks if the wrapped date's year is a leap year (in the configured timezone)
1600
+ *
1601
+ * @returns {boolean} True if the year is a leap year
1602
+ * @memberof DateTick
1603
+ */
1604
+ isLeapYear() {
1605
+ const year = this.parts().year;
1606
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
1607
+ }
1608
+ /**
1609
+ * Checks if the wrapped date is the same as another date, optionally truncated to a unit
1610
+ *
1611
+ * @param {DateInput} date - The date to compare against
1612
+ * @param {DateUnit} [unit] - When provided, compares after snapping both dates to the start of this unit
1613
+ * @returns {boolean} True if the dates are the same
1614
+ * @memberof DateTick
1615
+ */
1616
+ isSame(date, unit) {
1617
+ const other = this.toComparable(date);
1618
+ if (!unit)
1619
+ return this._date.getTime() === other.getTime();
1620
+ return this.startOf(unit).valueOf() === this.withDate(other).startOf(unit).valueOf();
1621
+ }
1622
+ /**
1623
+ * Checks if the wrapped date is the same as or after another date, optionally truncated to a unit
1624
+ *
1625
+ * @param {DateInput} date - The date to compare against
1626
+ * @param {DateUnit} [unit] - When provided, compares after snapping both dates to the start of this unit
1627
+ * @returns {boolean} True if the wrapped date is the same as or later than the other
1628
+ * @memberof DateTick
1629
+ */
1630
+ isSameOrAfter(date, unit) {
1631
+ return this.isSame(date, unit) || this.isAfter(date);
1632
+ }
1633
+ /**
1634
+ * Checks if the wrapped date is the same as or before another date, optionally truncated to a unit
1635
+ *
1636
+ * @param {DateInput} date - The date to compare against
1637
+ * @param {DateUnit} [unit] - When provided, compares after snapping both dates to the start of this unit
1638
+ * @returns {boolean} True if the wrapped date is the same as or earlier than the other
1639
+ * @memberof DateTick
1640
+ */
1641
+ isSameOrBefore(date, unit) {
1642
+ return this.isSame(date, unit) || this.isBefore(date);
1643
+ }
1644
+ /**
1645
+ * Checks if the wrapped date is today in the configured timezone.
1646
+ *
1647
+ * @returns {boolean} True if the date is today
1648
+ * @memberof DateTick
1649
+ */
1650
+ isToday() {
1651
+ return this.diffCalendar(new Date(), 'day') === 0;
1652
+ }
1653
+ /**
1654
+ * Checks if the wrapped date is tomorrow in the configured timezone.
1655
+ *
1656
+ * @returns {boolean} True if the date is tomorrow
1657
+ * @memberof DateTick
1658
+ */
1659
+ isTomorrow() {
1660
+ return this.diffCalendar(new Date(), 'day') === 1;
1661
+ }
1662
+ /**
1663
+ * Checks whether the wrapped date itself is a valid date
1664
+ *
1665
+ * @returns {boolean} True if valid, false otherwise
1666
+ * @memberof DateTick
1667
+ */
1668
+ isValid() {
1669
+ return DateTick.isValid(this._date);
1670
+ }
1671
+ /**
1672
+ * Checks if the wrapped date is yesterday in the configured timezone.
1673
+ *
1674
+ * @returns {boolean} True if the date is yesterday
1675
+ * @memberof DateTick
1676
+ */
1677
+ isYesterday() {
1678
+ return this.diffCalendar(new Date(), 'day') === -1;
1679
+ }
1680
+ /**
1681
+ * Gets the ISO day of the week (1-7, Monday-Sunday, in the configured timezone)
1682
+ *
1683
+ * @returns {number} The ISO day
1684
+ * @memberof DateTick
1685
+ */
1686
+ isoDay() {
1687
+ const weekday = this.parts().weekday;
1688
+ return weekday === 0 ? 7 : weekday;
1689
+ }
1690
+ /**
1691
+ * Gets the ISO week number (in the configured timezone)
1692
+ *
1693
+ * @returns {number} The ISO week number
1694
+ * @memberof DateTick
1695
+ */
1696
+ isoWeek() {
1697
+ const target = this.isoThursday();
1698
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
1699
+ return Math.ceil(((target.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);
1700
+ }
1701
+ /**
1702
+ * Gets the ISO week year (in the configured timezone)
1703
+ *
1704
+ * @returns {number} The ISO week year
1705
+ * @memberof DateTick
1706
+ */
1707
+ isoWeekYear() {
1708
+ return this.weekYear();
1709
+ }
1710
+ /**
1711
+ * Gets the number of ISO weeks in the year (in the configured timezone)
1712
+ *
1713
+ * @returns {number} The number of ISO weeks
1714
+ * @memberof DateTick
1715
+ */
1716
+ isoWeeksInYear() {
1717
+ return this.weeksInYearBy((t) => t.isoWeek());
1718
+ }
1719
+ /**
1720
+ * Returns a new DateTick in the runtime's local timezone, keeping the same instant.
1721
+ *
1722
+ * @returns {DateTick} The local-timezone instance
1723
+ * @memberof DateTick
1724
+ */
1725
+ local() {
1726
+ return this.withTimezone(DateTick.guessTimezone());
1727
+ }
1728
+ /**
1729
+ * Gets the configured locale
1730
+ *
1731
+ * @returns {string} The BCP 47 locale
1732
+ * @memberof DateTick
1733
+ */
1734
+ locale() {
1735
+ return this._locale;
1736
+ }
1737
+ /**
1738
+ * Returns locale metadata (month/weekday names, first day of week, meridiems, ordinal), resolved
1739
+ * through `Intl` — the Day.js `localeData()` equivalent.
1740
+ *
1741
+ * @returns {LocaleData} The locale metadata
1742
+ * @example datetick('2026-06-25', { locale: 'fr' }).localeData().months()[0] // 'janvier'
1743
+ * @memberof DateTick
1744
+ */
1745
+ localeData() {
1746
+ const locale = this._locale;
1747
+ const meridiems = localeMeridiems(locale);
1748
+ return {
1749
+ months: () => localeMonthNames(locale, 'long'),
1750
+ monthsShort: () => localeMonthNames(locale, 'short'),
1751
+ weekdays: () => localeWeekdayNames(locale, 'long'),
1752
+ weekdaysShort: () => localeWeekdayNames(locale, 'short'),
1753
+ weekdaysMin: () => localeWeekdayNames(locale, 'narrow'),
1754
+ firstDayOfWeek: () => this._weekStartsOn,
1755
+ meridiems: () => ({ ...meridiems }),
1756
+ meridiem: (hour, isLowercase = false) => {
1757
+ const marker = hour < 12 ? meridiems.am : meridiems.pm;
1758
+ return isLowercase ? marker.toLocaleLowerCase(locale) : marker;
1759
+ },
1760
+ ordinal: (n) => this.ordinalFor(n),
1761
+ };
1762
+ }
1763
+ millisecond(value) {
1764
+ if (value == null)
1765
+ return this.parts().millisecond;
1766
+ return this.rebuild({ millisecond: value });
1767
+ }
1768
+ milliseconds(value) {
1769
+ return value == null ? this.millisecond() : this.millisecond(value);
1770
+ }
1771
+ minute(value) {
1772
+ if (value == null)
1773
+ return this.parts().minute;
1774
+ return this.rebuild({ minute: value });
1775
+ }
1776
+ minutes(value) {
1777
+ return value == null ? this.minute() : this.minute(value);
1778
+ }
1779
+ month(value) {
1780
+ if (value == null)
1781
+ return this.parts().month;
1782
+ return this.rebuild({ month: value });
1783
+ }
1784
+ months(value) {
1785
+ return value == null ? this.month() : this.month(value);
1786
+ }
1787
+ /**
1788
+ * Gets the day of the month with its ordinal suffix (e.g. '1st', '22nd', '31st'). English by default,
1789
+ * or the factory's `ordinal` override when one is configured.
1790
+ *
1791
+ * @returns {string} The ordinal day-of-month
1792
+ * @memberof DateTick
1793
+ */
1794
+ ordinal() {
1795
+ return this.ordinalFor(this.parts().day);
1796
+ }
1797
+ quarter(value) {
1798
+ const month = this.parts().month;
1799
+ if (value == null)
1800
+ return Math.floor(month / 3) + 1;
1801
+ return this.month((month % 3) + 3 * (value - 1));
1802
+ }
1803
+ second(value) {
1804
+ if (value == null)
1805
+ return this.parts().second;
1806
+ return this.rebuild({ second: value });
1807
+ }
1808
+ seconds(value) {
1809
+ return value == null ? this.second() : this.second(value);
1810
+ }
1811
+ /**
1812
+ * Sets a specific unit on the wrapped date
1813
+ *
1814
+ * @param {DateUnit} unit - The unit to set (e.g. 'year', 'month')
1815
+ * @param {number} value - The value to set
1816
+ * @returns {DateTick} A new DateTick with the result
1817
+ * @memberof DateTick
1818
+ */
1819
+ set(unit, value) {
1820
+ switch (unit) {
1821
+ case 'millisecond':
1822
+ return this.millisecond(value);
1823
+ case 'second':
1824
+ return this.second(value);
1825
+ case 'minute':
1826
+ return this.minute(value);
1827
+ case 'hour':
1828
+ return this.hour(value);
1829
+ case 'date':
1830
+ return this.date(value);
1831
+ case 'day':
1832
+ return this.day(value);
1833
+ case 'month':
1834
+ return this.month(value);
1835
+ case 'quarter':
1836
+ return this.quarter(value);
1837
+ case 'year':
1838
+ return this.year(value);
1839
+ default:
1840
+ throw new Error(`Invalid unit for set(): ${unit}`);
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Returns a new DateTick set to the start of the given unit (in the configured timezone)
1845
+ *
1846
+ * @param {DateUnit} unit - The unit to snap to (e.g. 'month', 'day')
1847
+ * @returns {DateTick} A new DateTick at the start of the unit
1848
+ * @memberof DateTick
1849
+ */
1850
+ startOf(unit) {
1851
+ const p = this.parts();
1852
+ switch (unit) {
1853
+ case 'year':
1854
+ return this.rebuild({ month: 0, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
1855
+ case 'quarter':
1856
+ return this.rebuild({
1857
+ month: Math.floor(p.month / 3) * 3,
1858
+ day: 1,
1859
+ hour: 0,
1860
+ minute: 0,
1861
+ second: 0,
1862
+ millisecond: 0,
1863
+ });
1864
+ case 'month':
1865
+ return this.rebuild({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
1866
+ case 'week': {
1867
+ const offset = (p.weekday - this._weekStartsOn + 7) % 7;
1868
+ return this.rebuild({ day: p.day - offset, hour: 0, minute: 0, second: 0, millisecond: 0 });
1869
+ }
1870
+ case 'date':
1871
+ case 'day':
1872
+ return this.rebuild({ hour: 0, minute: 0, second: 0, millisecond: 0 });
1873
+ case 'hour':
1874
+ return this.rebuild({ minute: 0, second: 0, millisecond: 0 });
1875
+ case 'minute':
1876
+ return this.rebuild({ second: 0, millisecond: 0 });
1877
+ case 'second':
1878
+ return this.rebuild({ millisecond: 0 });
1879
+ default:
1880
+ return this.clone();
1881
+ }
1882
+ }
1883
+ /**
1884
+ * Subtracts a specified amount of time from the wrapped date (chainable, immutable)
1885
+ *
1886
+ * @param {number} amount - The amount to subtract
1887
+ * @param {DateUnit} unit - The unit to subtract (e.g. 'day', 'month')
1888
+ * @returns {DateTick} A new DateTick with the result
1889
+ * @memberof DateTick
1890
+ */
1891
+ subtract(amount, unit) {
1892
+ return this.add(-amount, unit);
1893
+ }
1894
+ /**
1895
+ * Returns a human-readable relative time string (e.g. '3 minutes ago')
1896
+ *
1897
+ * @param {Intl.RelativeTimeFormatOptions} [options] - Intl options
1898
+ * @param {RelativeTimeThresholds} [thresholds] - Override the unit step-up cutoffs
1899
+ * @returns {string} The relative time string
1900
+ * @memberof DateTick
1901
+ */
1902
+ timeAgo(options, thresholds = {}) {
1903
+ const t = {
1904
+ second: thresholds.second ?? 60,
1905
+ minute: thresholds.minute ?? 60,
1906
+ hour: thresholds.hour ?? 24,
1907
+ day: thresholds.day ?? 30,
1908
+ month: thresholds.month ?? 12,
1909
+ };
1910
+ const diff = this._date.getTime() - Date.now();
1911
+ const rtf = new Intl.RelativeTimeFormat(this._locale, { numeric: 'auto', ...options });
1912
+ const seconds = diff / 1000;
1913
+ const minutes = seconds / 60;
1914
+ const hours = minutes / 60;
1915
+ const days = hours / 24;
1916
+ const months = days / 30;
1917
+ const years = days / 365;
1918
+ if (Math.abs(seconds) < t.second)
1919
+ return rtf.format(Math.round(seconds), 'second');
1920
+ if (Math.abs(minutes) < t.minute)
1921
+ return rtf.format(Math.round(minutes), 'minute');
1922
+ if (Math.abs(hours) < t.hour)
1923
+ return rtf.format(Math.round(hours), 'hour');
1924
+ if (Math.abs(days) < t.day)
1925
+ return rtf.format(Math.round(days), 'day');
1926
+ if (Math.abs(months) < t.month)
1927
+ return rtf.format(Math.round(months), 'month');
1928
+ return rtf.format(Math.round(years), 'year');
1929
+ }
1930
+ /**
1931
+ * Returns a dependency-free live relative-time stream.
1932
+ *
1933
+ * Subscribers receive the current `timeAgo()` label immediately, then again whenever it is
1934
+ * expected to change. The refresh rate adapts from every second to every hour as the target
1935
+ * time moves farther away.
1936
+ *
1937
+ * @param {Intl.RelativeTimeFormatOptions} [options] - Intl options
1938
+ * @param {RelativeTimeThresholds} [thresholds] - Override the unit step-up cutoffs
1939
+ * @returns {TimeAgoLive} A live relative-time subscription handle
1940
+ * @memberof DateTick
1941
+ */
1942
+ timeAgoLive(options, thresholds) {
1943
+ return this.liveRelativeTime(() => this.timeAgo(options, thresholds));
1944
+ }
1945
+ /**
1946
+ * Subscribes to live relative-time labels and returns an unsubscribe function.
1947
+ *
1948
+ * @param {TimeAgoListener} listener - Receives each formatted relative-time label
1949
+ * @param {Intl.RelativeTimeFormatOptions} [options] - Intl options
1950
+ * @param {RelativeTimeThresholds} [thresholds] - Override the unit step-up cutoffs
1951
+ * @returns {() => void} Function that stops the live updates
1952
+ * @memberof DateTick
1953
+ */
1954
+ timeAgoSubscribe(listener, options, thresholds) {
1955
+ return this.timeAgoLive(options, thresholds).subscribe(listener);
1956
+ }
1957
+ /**
1958
+ * Gets the configured IANA timezone
1959
+ *
1960
+ * @returns {string} The timezone
1961
+ * @memberof DateTick
1962
+ */
1963
+ timezone() {
1964
+ return this._timezone;
1965
+ }
1966
+ /**
1967
+ * Formats another date relative to this one, Day.js-style.
1968
+ *
1969
+ * @param {DateInput} date - The target date to compare to
1970
+ * @param {boolean} [withoutSuffix=false] - When true, omit "ago" / "in"
1971
+ * @returns {string} The relative-time label
1972
+ * @memberof DateTick
1973
+ */
1974
+ to(date, withoutSuffix = false) {
1975
+ return this.withDate(date).from(this, withoutSuffix);
1976
+ }
1977
+ /**
1978
+ * Converts the wrapped date to an array of zoned calendar parts.
1979
+ *
1980
+ * @returns {DateArray} [year, month, date, hour, minute, second, millisecond]
1981
+ * @memberof DateTick
1982
+ */
1983
+ toArray() {
1984
+ const p = this.parts();
1985
+ return [p.year, p.month, p.day, p.hour, p.minute, p.second, p.millisecond];
1986
+ }
1987
+ /**
1988
+ * Converts the wrapped date to a plain Date object (a fresh clone of the absolute instant)
1989
+ *
1990
+ * @returns {Date} The Date object
1991
+ * @memberof DateTick
1992
+ */
1993
+ toDate() {
1994
+ return new Date(this._date);
1995
+ }
1996
+ /**
1997
+ * Converts the wrapped date to an ISO 8601 string (UTC)
1998
+ *
1999
+ * @returns {string} The ISO 8601 string
2000
+ * @memberof DateTick
2001
+ */
2002
+ toISOString() {
2003
+ return this._date.toISOString();
2004
+ }
2005
+ /**
2006
+ * Enables `JSON.stringify` to serialize a DateTick as an ISO 8601 string
2007
+ *
2008
+ * @returns {string} The ISO 8601 string
2009
+ * @memberof DateTick
2010
+ */
2011
+ toJSON() {
2012
+ return this.toISOString();
2013
+ }
2014
+ /**
2015
+ * Formats now relative to this date, Day.js-style.
2016
+ *
2017
+ * @param {boolean} [withoutSuffix=false] - When true, omit "ago" / "in"
2018
+ * @returns {string} The relative-time label
2019
+ * @memberof DateTick
2020
+ */
2021
+ toNow(withoutSuffix = false) {
2022
+ return this.to(new Date(), withoutSuffix);
2023
+ }
2024
+ /**
2025
+ * Converts the wrapped date to an object of zoned calendar parts.
2026
+ *
2027
+ * @returns {DatePartsObject} Zoned calendar parts
2028
+ * @memberof DateTick
2029
+ */
2030
+ toObject() {
2031
+ const p = this.parts();
2032
+ return {
2033
+ year: p.year,
2034
+ month: p.month,
2035
+ date: p.day,
2036
+ hour: p.hour,
2037
+ minute: p.minute,
2038
+ second: p.second,
2039
+ millisecond: p.millisecond,
2040
+ day: p.weekday,
2041
+ };
2042
+ }
2043
+ /**
2044
+ * Converts the wrapped date to its default string representation
2045
+ *
2046
+ * @returns {string} The string representation
2047
+ * @memberof DateTick
2048
+ */
2049
+ toString() {
2050
+ return this._date.toString();
2051
+ }
2052
+ /**
2053
+ * Gets the Unix timestamp (seconds since epoch)
2054
+ *
2055
+ * @returns {number} The Unix timestamp in seconds
2056
+ * @memberof DateTick
2057
+ */
2058
+ unix() {
2059
+ return Math.floor(this._date.getTime() / 1000);
2060
+ }
2061
+ /**
2062
+ * Returns a new DateTick in UTC, keeping the same instant.
2063
+ *
2064
+ * @returns {DateTick} The UTC instance
2065
+ * @memberof DateTick
2066
+ */
2067
+ utc() {
2068
+ return this.withTimezone('UTC');
2069
+ }
2070
+ /**
2071
+ * Gets the configured timezone's UTC offset for this instant, in minutes.
2072
+ *
2073
+ * @returns {number} Offset minutes, positive east of UTC
2074
+ * @memberof DateTick
2075
+ */
2076
+ utcOffset() {
2077
+ return offsetMs(this._date, this._timezone) / 60_000;
2078
+ }
2079
+ /**
2080
+ * Gets the timestamp in milliseconds since epoch. Enables direct numeric comparisons (e.g. `+datetick`)
2081
+ *
2082
+ * @returns {number} The timestamp in milliseconds
2083
+ * @memberof DateTick
2084
+ */
2085
+ valueOf() {
2086
+ return this._date.getTime();
2087
+ }
2088
+ /**
2089
+ * Gets the week number of the year (in the configured timezone)
2090
+ *
2091
+ * @returns {number} The week number
2092
+ * @memberof DateTick
2093
+ */
2094
+ week() {
2095
+ const p = this.parts();
2096
+ const jan1Weekday = new Date(Date.UTC(p.year, 0, 1)).getUTCDay();
2097
+ const offset = (jan1Weekday - this._weekStartsOn + 7) % 7;
2098
+ return Math.ceil((this.dayOfYear() + offset) / 7);
2099
+ }
2100
+ /**
2101
+ * Gets the configured first day of the week (0 = Sunday … 6 = Saturday)
2102
+ *
2103
+ * @returns {number} The first day of the week
2104
+ * @memberof DateTick
2105
+ */
2106
+ weekStart() {
2107
+ return this._weekStartsOn;
2108
+ }
2109
+ /**
2110
+ * Gets the week year, for ISO week calculations (in the configured timezone)
2111
+ *
2112
+ * @returns {number} The week year
2113
+ * @memberof DateTick
2114
+ */
2115
+ weekYear() {
2116
+ return this.isoThursday().getUTCFullYear();
2117
+ }
2118
+ weekday(value) {
2119
+ const p = this.parts();
2120
+ const relative = (p.weekday - this._weekStartsOn + 7) % 7;
2121
+ if (value == null)
2122
+ return relative;
2123
+ return this.rebuild({ day: p.day - relative + value });
2124
+ }
2125
+ /** Day.js-style plural getter alias of {@link DateTick.week} (weeks have no setter). */
2126
+ weeks() {
2127
+ return this.week();
2128
+ }
2129
+ /**
2130
+ * Gets the number of weeks in the year (in the configured timezone)
2131
+ *
2132
+ * @returns {number} The number of weeks
2133
+ * @memberof DateTick
2134
+ */
2135
+ weeksInYear() {
2136
+ return this.weeksInYearBy((t) => t.week());
2137
+ }
2138
+ /**
2139
+ * Returns a new DateTick instance with the given date, keeping the current locale and timezone
2140
+ *
2141
+ * @param {Date | string} date - The date to wrap
2142
+ * @returns {DateTick} The new DateTick
2143
+ * @memberof DateTick
2144
+ */
2145
+ withDate(date) {
2146
+ return new DateTick(this._locale, this._timezone, date, this._weekStartsOn, this._ordinal);
2147
+ }
2148
+ /**
2149
+ * Returns a new DateTick instance with the given locale, keeping the current instant and timezone
2150
+ *
2151
+ * @param {string} locale - The BCP 47 locale
2152
+ * @returns {DateTick} The new DateTick
2153
+ * @memberof DateTick
2154
+ */
2155
+ withLocale(locale) {
2156
+ return new DateTick(locale, this._timezone, this._date, this._weekStartsOn, this._ordinal);
2157
+ }
2158
+ /**
2159
+ * Returns a new DateTick instance with the given timezone, keeping the same absolute instant
2160
+ * (the wall-clock components will be re-interpreted in the new zone)
2161
+ *
2162
+ * @param {string} timezone - The IANA timezone
2163
+ * @returns {DateTick} The new DateTick
2164
+ * @memberof DateTick
2165
+ */
2166
+ withTimezone(timezone) {
2167
+ return new DateTick(this._locale, timezone, this._date, this._weekStartsOn, this._ordinal);
2168
+ }
2169
+ /**
2170
+ * Returns a new DateTick instance with the given first-day-of-week, keeping everything else
2171
+ *
2172
+ * @param {number} weekStartsOn - First day of the week (0 = Sunday … 6 = Saturday)
2173
+ * @returns {DateTick} The new DateTick
2174
+ * @memberof DateTick
2175
+ */
2176
+ withWeekStart(weekStartsOn) {
2177
+ return new DateTick(this._locale, this._timezone, this._date, weekStartsOn, this._ordinal);
2178
+ }
2179
+ year(value) {
2180
+ if (value == null)
2181
+ return this.parts().year;
2182
+ return this.rebuild({ year: value });
2183
+ }
2184
+ years(value) {
2185
+ return value == null ? this.year() : this.year(value);
2186
+ }
2187
+ /**
2188
+ * Adds calendar months (used for month/quarter/year), clamping the day to the last valid day.
2189
+ */
2190
+ addMonths(months) {
2191
+ const p = this.parts();
2192
+ const totalMonths = p.year * 12 + p.month + months;
2193
+ const targetYear = Math.floor(totalMonths / 12);
2194
+ const targetMonth = ((totalMonths % 12) + 12) % 12;
2195
+ const targetDay = Math.min(p.day, getDaysInMonth(targetYear, targetMonth));
2196
+ return this.rebuild({ year: targetYear, month: targetMonth, day: targetDay });
2197
+ }
2198
+ calendarDayNumber() {
2199
+ const p = this.parts();
2200
+ return Math.floor(Date.UTC(p.year, p.month, p.day) / 86_400_000);
2201
+ }
2202
+ /**
2203
+ * Returns a preset Intl.DateTimeFormatOptions for a given format string.
2204
+ */
2205
+ getPreset(format) {
2206
+ switch (format) {
2207
+ case 'short':
2208
+ return { dateStyle: 'short', timeStyle: 'short' };
2209
+ case 'medium':
2210
+ return { dateStyle: 'medium', timeStyle: 'medium' };
2211
+ case 'long':
2212
+ return { dateStyle: 'long', timeStyle: 'long' };
2213
+ case 'full':
2214
+ return { dateStyle: 'full', timeStyle: 'full' };
2215
+ case 'dateOnly':
2216
+ return { year: 'numeric', month: 'long', day: 'numeric' };
2217
+ case 'timeOnly':
2218
+ return { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false };
2219
+ case 'weekdayTime':
2220
+ return { weekday: 'long', hour: 'numeric', minute: 'numeric' };
2221
+ case 'isoStyle12h':
2222
+ return {
2223
+ year: 'numeric',
2224
+ month: '2-digit',
2225
+ day: '2-digit',
2226
+ hour: '2-digit',
2227
+ minute: '2-digit',
2228
+ second: '2-digit',
2229
+ hour12: true,
2230
+ };
2231
+ case 'isoStyle24h':
2232
+ return {
2233
+ year: 'numeric',
2234
+ month: '2-digit',
2235
+ day: '2-digit',
2236
+ hour: '2-digit',
2237
+ minute: '2-digit',
2238
+ second: '2-digit',
2239
+ hour12: false,
2240
+ };
2241
+ default:
2242
+ return {};
2243
+ }
2244
+ }
2245
+ /**
2246
+ * The Thursday of this instant's ISO week, as a UTC date built from the zoned calendar date so the
2247
+ * math is offset-free. Shared by {@link DateTick.isoWeek} and {@link DateTick.weekYear}.
2248
+ */
2249
+ isoThursday() {
2250
+ const p = this.parts();
2251
+ const target = new Date(Date.UTC(p.year, p.month, p.day));
2252
+ target.setUTCDate(target.getUTCDate() + 4 - (target.getUTCDay() || 7));
2253
+ return target;
2254
+ }
2255
+ /**
2256
+ * Creates an adaptive live relative-time stream.
2257
+ */
2258
+ liveRelativeTime(formatter) {
2259
+ const targetTime = this._date.getTime();
2260
+ const getRefreshInterval = (secondsDiff) => {
2261
+ if (secondsDiff < 60)
2262
+ return 1000;
2263
+ if (secondsDiff < 3600)
2264
+ return 30_000;
2265
+ if (secondsDiff < 86400)
2266
+ return 1_800_000;
2267
+ return 3_600_000;
2268
+ };
2269
+ let timeoutId;
2270
+ const listeners = new Set();
2271
+ const clearTimer = () => {
2272
+ if (timeoutId)
2273
+ clearTimeout(timeoutId);
2274
+ timeoutId = undefined;
2275
+ };
2276
+ const tick = () => {
2277
+ const value = formatter();
2278
+ listeners.forEach((listener) => listener(value));
2279
+ // A listener may unsubscribe during notification (common when a component unmounts). If that
2280
+ // drained the set, stop here instead of scheduling a timer that would never be cleared.
2281
+ if (listeners.size === 0)
2282
+ return;
2283
+ const secondsDiff = Math.abs((Date.now() - targetTime) / 1000);
2284
+ timeoutId = setTimeout(tick, getRefreshInterval(secondsDiff));
2285
+ };
2286
+ return {
2287
+ subscribe: (listener) => {
2288
+ listeners.add(listener);
2289
+ if (listeners.size === 1) {
2290
+ tick();
2291
+ }
2292
+ else {
2293
+ listener(formatter());
2294
+ }
2295
+ return () => {
2296
+ listeners.delete(listener);
2297
+ if (listeners.size === 0)
2298
+ clearTimer();
2299
+ };
2300
+ },
2301
+ unsubscribe: () => {
2302
+ listeners.clear();
2303
+ clearTimer();
2304
+ },
2305
+ };
2306
+ }
2307
+ /**
2308
+ * Formats the timezone's current offset from UTC as ±HH:mm (with colon) or ±HHmm (without).
2309
+ */
2310
+ offsetString(colon) {
2311
+ const offsetMinutes = offsetMs(this._date, this._timezone) / 60_000;
2312
+ const sign = offsetMinutes >= 0 ? '+' : '-';
2313
+ const abs = Math.abs(offsetMinutes);
2314
+ const hh = String(Math.floor(abs / 60)).padStart(2, '0');
2315
+ const mm = String(Math.round(abs % 60)).padStart(2, '0');
2316
+ return colon ? `${sign}${hh}:${mm}` : `${sign}${hh}${mm}`;
2317
+ }
2318
+ /**
2319
+ * Applies the configured ordinal override, falling back to the English suffix.
2320
+ */
2321
+ ordinalFor(n) {
2322
+ return this._ordinal ? this._ordinal(n) : `${n}${ordinalSuffix(n)}`;
2323
+ }
2324
+ /**
2325
+ * Reads the wrapped instant's wall-clock components in the configured timezone.
2326
+ */
2327
+ parts() {
2328
+ return partsOf(this._date, this._timezone);
2329
+ }
2330
+ /**
2331
+ * Floating-point calendar-unit difference (used by `diff` when `precise` is true).
2332
+ *
2333
+ * Computes the whole-unit count, then interpolates the fractional remainder between the
2334
+ * instant `whole` units away from `other` and the next unit boundary in the direction of `this`.
2335
+ */
2336
+ preciseCalendarDiff(other, unitMonths) {
2337
+ const whole = this.wholeMonthDiff(other, unitMonths);
2338
+ const otherDateTick = this.withDate(other);
2339
+ const anchor = otherDateTick.add(whole * unitMonths, 'month');
2340
+ const step = this._date.getTime() >= anchor.valueOf() ? 1 : -1;
2341
+ const nextAnchor = otherDateTick.add((whole + step) * unitMonths, 'month');
2342
+ const span = nextAnchor.valueOf() - anchor.valueOf();
2343
+ const progress = span === 0 ? 0 : (this._date.getTime() - anchor.valueOf()) / span;
2344
+ return whole + progress * step;
2345
+ }
2346
+ /**
2347
+ * Returns a new DateTick built from the current zoned components with the given overrides applied,
2348
+ * then converted back to an absolute instant in the configured timezone.
2349
+ */
2350
+ rebuild(overrides) {
2351
+ if (!this.isValid())
2352
+ throw new Error(`Invalid date: ${this._date}`);
2353
+ const merged = { ...this.parts(), ...overrides };
2354
+ return this.withDate(instantFromParts(merged, this._timezone));
2355
+ }
2356
+ /**
2357
+ * Normalizes a DateInput (Date, string, or DateTick) to a plain Date
2358
+ */
2359
+ toComparable(value) {
2360
+ return value instanceof DateTick ? value.toDate() : this.validateDate(value);
2361
+ }
2362
+ /**
2363
+ * Validates a date and throws an error if invalid
2364
+ */
2365
+ validateDate(date) {
2366
+ if (!DateTick.isValid(date)) {
2367
+ throw new Error(`Invalid date: ${date}`);
2368
+ }
2369
+ return DateTick.resolveInput(date, this._timezone);
2370
+ }
2371
+ /**
2372
+ * Number of weeks in this instant's year, per the given week-numbering function: Dec 31 usually
2373
+ * carries the highest week number, unless it rolls into week 1 of the next year (then Dec 24 does).
2374
+ * Shared by {@link DateTick.weeksInYear} and {@link DateTick.isoWeeksInYear}.
2375
+ */
2376
+ weeksInYearBy(weekOf) {
2377
+ const week = weekOf(this.rebuild({ month: 11, day: 31 }));
2378
+ return week === 1 ? weekOf(this.rebuild({ month: 11, day: 31 - 7 })) : week;
2379
+ }
2380
+ /**
2381
+ * Whole calendar-month difference (in units of `unitMonths` months), truncated toward zero.
2382
+ */
2383
+ wholeMonthDiff(other, unitMonths) {
2384
+ const a = this.parts();
2385
+ const b = partsOf(other, this._timezone);
2386
+ return Math.trunc(((a.year - b.year) * 12 + (a.month - b.month)) / unitMonths);
2387
+ }
2388
+ }
2389
+
2390
+ const createDateTickFactory = (defaults) => {
2391
+ const factory = (date, options = {}) => {
2392
+ const merged = { ...defaults, ...options };
2393
+ const input = date instanceof DateTick ? date.toDate() : date;
2394
+ return new DateTick(merged.locale ?? 'en', merged.timezone, input, merged.weekStartsOn ?? 0, merged.ordinal);
2395
+ };
2396
+ return Object.assign(factory, {
2397
+ duration: (value, unit, locale = defaults.locale ?? 'en') => DateTick.duration(value, unit, locale),
2398
+ guessTimezone: DateTick.guessTimezone,
2399
+ parse: (input, pattern, locale = defaults.locale ?? 'en', timezone = defaults.timezone ?? DateTick.guessTimezone()) => DateTick.parse(input, pattern, locale, timezone),
2400
+ min: DateTick.min,
2401
+ max: DateTick.max,
2402
+ isValid: DateTick.isValid,
2403
+ isDateTick: DateTick.isDateTick,
2404
+ isDuration: Duration.isDuration,
2405
+ utc: (date, options = {}) => factory(date, { ...options, timezone: 'UTC' }),
2406
+ unix: (seconds, options = {}) => factory(new Date(seconds * 1000), options),
2407
+ withDefaults: (next) => createDateTickFactory({ ...defaults, ...next }),
2408
+ });
2409
+ };
2410
+ /**
2411
+ * The quickest way to create a {@link DateTick}.
2412
+ *
2413
+ * @example
2414
+ * datetick('2026-06-25T19:23Z', { timezone: 'Asia/Tokyo' }).formatPattern('DD/MM/YYYY HH:mm');
2415
+ * datetick.duration(90, 'minute').humanize(); // '2 hours'
2416
+ * datetick.utc('2026-06-25').startOf('week');
2417
+ */
2418
+ // The PURE annotation tells bundlers this call has no observable side effects, so consumers
2419
+ // that only import `DateTick` or `Duration` (not `datetick`/the default export) can tree-shake this away.
2420
+ const datetick = /* @__PURE__ */ createDateTickFactory({});
2421
+
2422
+ export { DateTick, Duration, datetick, datetick as default };