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