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/LICENSE +21 -0
- package/README.md +118 -0
- package/commitlint.config.mjs +3 -0
- package/dist/index.d.ts +1227 -0
- package/dist/index.js +2422 -0
- package/dist/index.umd.js +2435 -0
- package/dist/index.umd.min.js +1 -0
- package/junit.xml +311 -0
- package/package.json +145 -0
|
@@ -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
|
+
}));
|