@lbd-sh/date-tz 1.0.11 → 1.0.13
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/README.md +53 -3
- package/dist/date-tz.d.ts +71 -0
- package/dist/date-tz.js +866 -0
- package/dist/date-tz.js.map +1 -0
- package/dist/idate-tz.d.ts +57 -0
- package/dist/idate-tz.js +3 -0
- package/dist/idate-tz.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +0 -1
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/timezones.d.ts +8 -0
- package/dist/timezones.js +606 -0
- package/dist/timezones.js.map +1 -0
- package/package.json +7 -3
- package/.github/workflows/production.yaml +0 -70
- package/.vscode/launch.json +0 -15
- package/.vscode/settings.json +0 -9
- package/.vscode/tasks.json +0 -13
- package/.vscode/tsconfig.json +0 -12
- package/GitVersion.yml +0 -108
- package/merge.cmd +0 -18
- package/src/date-tz.spec.ts +0 -206
- package/src/date-tz.ts +0 -752
- package/src/idate-tz.ts +0 -23
- package/src/timezones.ts +0 -604
- package/tsconfig.json +0 -30
package/dist/date-tz.js
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DateTz = void 0;
|
|
4
|
+
const timezones_1 = require("./timezones");
|
|
5
|
+
const MS_PER_SECOND = 1000;
|
|
6
|
+
const MS_PER_MINUTE = 60000;
|
|
7
|
+
const MS_PER_HOUR = 3600000;
|
|
8
|
+
const MS_PER_DAY = 86400000;
|
|
9
|
+
const MS_PER_WEEK = MS_PER_DAY * 7;
|
|
10
|
+
const epochYear = 1970;
|
|
11
|
+
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
12
|
+
const UNIT_ALIASES = {
|
|
13
|
+
ms: 'millisecond',
|
|
14
|
+
millisecond: 'millisecond',
|
|
15
|
+
milliseconds: 'millisecond',
|
|
16
|
+
s: 'second',
|
|
17
|
+
sec: 'second',
|
|
18
|
+
second: 'second',
|
|
19
|
+
seconds: 'second',
|
|
20
|
+
m: 'minute',
|
|
21
|
+
min: 'minute',
|
|
22
|
+
minute: 'minute',
|
|
23
|
+
minutes: 'minute',
|
|
24
|
+
h: 'hour',
|
|
25
|
+
hr: 'hour',
|
|
26
|
+
hour: 'hour',
|
|
27
|
+
hours: 'hour',
|
|
28
|
+
d: 'day',
|
|
29
|
+
day: 'day',
|
|
30
|
+
days: 'day',
|
|
31
|
+
w: 'week',
|
|
32
|
+
wk: 'week',
|
|
33
|
+
week: 'week',
|
|
34
|
+
weeks: 'week',
|
|
35
|
+
M: 'month',
|
|
36
|
+
mon: 'month',
|
|
37
|
+
month: 'month',
|
|
38
|
+
months: 'month',
|
|
39
|
+
y: 'year',
|
|
40
|
+
yr: 'year',
|
|
41
|
+
year: 'year',
|
|
42
|
+
years: 'year'
|
|
43
|
+
};
|
|
44
|
+
const GRANULARITY_UNITS = ['minute', 'hour', 'day', 'week', 'month', 'year'];
|
|
45
|
+
class DateTz {
|
|
46
|
+
constructor(value, tz) {
|
|
47
|
+
if (typeof value === 'object') {
|
|
48
|
+
this.timestamp = value.timestamp;
|
|
49
|
+
this.timezone = value.timezone || 'UTC';
|
|
50
|
+
if (!timezones_1.timezones[this.timezone]) {
|
|
51
|
+
throw new Error(`Invalid timezone: ${value.timezone}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.timezone = tz || 'UTC';
|
|
56
|
+
if (!timezones_1.timezones[this.timezone]) {
|
|
57
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
58
|
+
}
|
|
59
|
+
this.timestamp = this.stripSMs(value);
|
|
60
|
+
}
|
|
61
|
+
this.invalidateOffsetCache();
|
|
62
|
+
}
|
|
63
|
+
get timezoneOffset() {
|
|
64
|
+
return timezones_1.timezones[this.timezone];
|
|
65
|
+
}
|
|
66
|
+
compare(other) {
|
|
67
|
+
if (this.isComparable(other)) {
|
|
68
|
+
return this.timestamp - other.timestamp;
|
|
69
|
+
}
|
|
70
|
+
throw new Error('Cannot compare dates with different timezones');
|
|
71
|
+
}
|
|
72
|
+
isComparable(other) {
|
|
73
|
+
return this.timezone === other.timezone;
|
|
74
|
+
}
|
|
75
|
+
toString(pattern, locale) {
|
|
76
|
+
if (!pattern)
|
|
77
|
+
pattern = 'YYYY-MM-DD HH:mm:ss';
|
|
78
|
+
const offsetInfo = this.getOffsetInfo();
|
|
79
|
+
const offset = offsetInfo.offsetSeconds * 1000;
|
|
80
|
+
let remainingMs = this.timestamp + offset;
|
|
81
|
+
let year = epochYear;
|
|
82
|
+
while (true) {
|
|
83
|
+
const daysInYear = this.isLeapYear(year) ? 366 : 365;
|
|
84
|
+
const msInYear = daysInYear * MS_PER_DAY;
|
|
85
|
+
if (remainingMs >= msInYear) {
|
|
86
|
+
remainingMs -= msInYear;
|
|
87
|
+
year++;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let month = 0;
|
|
94
|
+
while (month < 12) {
|
|
95
|
+
const daysInMonth = month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
96
|
+
const msInMonth = daysInMonth * MS_PER_DAY;
|
|
97
|
+
if (remainingMs >= msInMonth) {
|
|
98
|
+
remainingMs -= msInMonth;
|
|
99
|
+
month++;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const day = Math.floor(remainingMs / MS_PER_DAY) + 1;
|
|
106
|
+
remainingMs %= MS_PER_DAY;
|
|
107
|
+
const hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
108
|
+
remainingMs %= MS_PER_HOUR;
|
|
109
|
+
const minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
110
|
+
remainingMs %= MS_PER_MINUTE;
|
|
111
|
+
const second = Math.floor(remainingMs / 1000);
|
|
112
|
+
const pm = hour >= 12 ? 'PM' : 'AM';
|
|
113
|
+
const hour12 = hour % 12 || 12;
|
|
114
|
+
if (!locale)
|
|
115
|
+
locale = 'en';
|
|
116
|
+
let monthStr = new Date(year, month, 3).toLocaleString(locale || 'en', { month: 'long' });
|
|
117
|
+
monthStr = monthStr.charAt(0).toUpperCase() + monthStr.slice(1);
|
|
118
|
+
const tokens = {
|
|
119
|
+
YYYY: year,
|
|
120
|
+
YY: String(year).slice(-2),
|
|
121
|
+
yyyy: year.toString(),
|
|
122
|
+
yy: String(year).slice(-2),
|
|
123
|
+
MM: String(month + 1).padStart(2, '0'),
|
|
124
|
+
LM: monthStr,
|
|
125
|
+
DD: String(day).padStart(2, '0'),
|
|
126
|
+
HH: String(hour).padStart(2, '0'),
|
|
127
|
+
mm: String(minute).padStart(2, '0'),
|
|
128
|
+
ss: String(second).padStart(2, '0'),
|
|
129
|
+
aa: pm.toLowerCase(),
|
|
130
|
+
AA: pm,
|
|
131
|
+
hh: hour12.toString().padStart(2, '0'),
|
|
132
|
+
tz: this.timezone,
|
|
133
|
+
};
|
|
134
|
+
return pattern.replace(/YYYY|yyyy|YY|yy|MM|LM|DD|HH|hh|mm|ss|aa|AA|tz/g, (match) => tokens[match]);
|
|
135
|
+
}
|
|
136
|
+
add(value, unit) {
|
|
137
|
+
let remainingMs = this.timestamp;
|
|
138
|
+
let year = 1970;
|
|
139
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
140
|
+
remainingMs %= MS_PER_DAY;
|
|
141
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
142
|
+
remainingMs %= MS_PER_HOUR;
|
|
143
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
144
|
+
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
145
|
+
while (days >= this.daysInYear(year)) {
|
|
146
|
+
days -= this.daysInYear(year);
|
|
147
|
+
year++;
|
|
148
|
+
}
|
|
149
|
+
let month = 0;
|
|
150
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
151
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
152
|
+
month++;
|
|
153
|
+
}
|
|
154
|
+
let day = days + 1;
|
|
155
|
+
switch (unit) {
|
|
156
|
+
case 'minute':
|
|
157
|
+
minute += value;
|
|
158
|
+
break;
|
|
159
|
+
case 'hour':
|
|
160
|
+
hour += value;
|
|
161
|
+
break;
|
|
162
|
+
case 'day':
|
|
163
|
+
day += value;
|
|
164
|
+
break;
|
|
165
|
+
case 'month':
|
|
166
|
+
month += value;
|
|
167
|
+
break;
|
|
168
|
+
case 'year':
|
|
169
|
+
year += value;
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
173
|
+
}
|
|
174
|
+
while (minute >= 60) {
|
|
175
|
+
minute -= 60;
|
|
176
|
+
hour++;
|
|
177
|
+
}
|
|
178
|
+
while (hour >= 24) {
|
|
179
|
+
hour -= 24;
|
|
180
|
+
day++;
|
|
181
|
+
}
|
|
182
|
+
while (month >= 12) {
|
|
183
|
+
month -= 12;
|
|
184
|
+
year++;
|
|
185
|
+
}
|
|
186
|
+
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
187
|
+
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
188
|
+
month++;
|
|
189
|
+
if (month >= 12) {
|
|
190
|
+
month = 0;
|
|
191
|
+
year++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const newTimestamp = (() => {
|
|
195
|
+
let totalMs = 0;
|
|
196
|
+
for (let y = 1970; y < year; y++) {
|
|
197
|
+
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
198
|
+
}
|
|
199
|
+
for (let m = 0; m < month; m++) {
|
|
200
|
+
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
201
|
+
}
|
|
202
|
+
totalMs += (day - 1) * MS_PER_DAY;
|
|
203
|
+
totalMs += hour * MS_PER_HOUR;
|
|
204
|
+
totalMs += minute * MS_PER_MINUTE;
|
|
205
|
+
totalMs += second * 1000;
|
|
206
|
+
return totalMs;
|
|
207
|
+
})();
|
|
208
|
+
this.timestamp = newTimestamp;
|
|
209
|
+
this.invalidateOffsetCache();
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
subtract(value, unit) {
|
|
213
|
+
this.shift(unit, -value);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
plus(duration = {}) {
|
|
217
|
+
for (const [rawUnit, rawValue] of Object.entries(duration)) {
|
|
218
|
+
if (rawValue === undefined || rawValue === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const normalized = this.normalizeDiffUnit(rawUnit);
|
|
222
|
+
const value = rawValue;
|
|
223
|
+
if (normalized === 'millisecond' || normalized === 'second') {
|
|
224
|
+
throw new Error(`Unsupported duration unit: ${rawUnit}`);
|
|
225
|
+
}
|
|
226
|
+
this.shift(normalized, value);
|
|
227
|
+
}
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
minus(duration = {}) {
|
|
231
|
+
const inverted = {};
|
|
232
|
+
for (const [unit, rawValue] of Object.entries(duration)) {
|
|
233
|
+
if (rawValue === undefined || rawValue === 0) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
inverted[unit] = -rawValue;
|
|
237
|
+
}
|
|
238
|
+
return this.plus(inverted);
|
|
239
|
+
}
|
|
240
|
+
diff(other, unit = 'millisecond', asFloat = false) {
|
|
241
|
+
const normalized = this.normalizeDiffUnit(unit);
|
|
242
|
+
const comparable = this.ensureComparable(other);
|
|
243
|
+
const delta = this.timestamp - comparable.timestamp;
|
|
244
|
+
switch (normalized) {
|
|
245
|
+
case 'millisecond':
|
|
246
|
+
return this.roundDiff(delta, asFloat);
|
|
247
|
+
case 'second':
|
|
248
|
+
return this.roundDiff(delta / MS_PER_SECOND, asFloat);
|
|
249
|
+
case 'minute':
|
|
250
|
+
return this.roundDiff(delta / MS_PER_MINUTE, asFloat);
|
|
251
|
+
case 'hour':
|
|
252
|
+
return this.roundDiff(delta / MS_PER_HOUR, asFloat);
|
|
253
|
+
case 'day':
|
|
254
|
+
return this.roundDiff(delta / MS_PER_DAY, asFloat);
|
|
255
|
+
case 'week':
|
|
256
|
+
return this.roundDiff(delta / MS_PER_WEEK, asFloat);
|
|
257
|
+
case 'month': {
|
|
258
|
+
const months = this.diffInMonths(comparable, true);
|
|
259
|
+
return this.roundDiff(months, asFloat);
|
|
260
|
+
}
|
|
261
|
+
case 'year': {
|
|
262
|
+
const years = this.diffInMonths(comparable, true) / 12;
|
|
263
|
+
return this.roundDiff(years, asFloat);
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
return this.roundDiff(delta, asFloat);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
startOf(unit) {
|
|
270
|
+
const granularity = this.normalizeGranularity(unit);
|
|
271
|
+
switch (granularity) {
|
|
272
|
+
case 'minute':
|
|
273
|
+
this.setLocalComponents({ second: 0 });
|
|
274
|
+
break;
|
|
275
|
+
case 'hour':
|
|
276
|
+
this.setLocalComponents({ minute: 0, second: 0 });
|
|
277
|
+
break;
|
|
278
|
+
case 'day':
|
|
279
|
+
this.setLocalComponents({ hour: 0, minute: 0, second: 0 });
|
|
280
|
+
break;
|
|
281
|
+
case 'week': {
|
|
282
|
+
this.startOf('day');
|
|
283
|
+
const dayOfWeek = this.dayOfWeek;
|
|
284
|
+
if (dayOfWeek !== 0) {
|
|
285
|
+
this.shift('day', -dayOfWeek);
|
|
286
|
+
this.startOf('day');
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 'month':
|
|
291
|
+
this.setLocalComponents({ day: 1, hour: 0, minute: 0, second: 0 });
|
|
292
|
+
break;
|
|
293
|
+
case 'year':
|
|
294
|
+
this.setLocalComponents({ month: 0, day: 1, hour: 0, minute: 0, second: 0 });
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
endOf(unit) {
|
|
300
|
+
const granularity = this.normalizeGranularity(unit);
|
|
301
|
+
switch (granularity) {
|
|
302
|
+
case 'minute':
|
|
303
|
+
return this;
|
|
304
|
+
case 'hour':
|
|
305
|
+
this.startOf('hour');
|
|
306
|
+
this.shift('hour', 1);
|
|
307
|
+
this.shift('minute', -1);
|
|
308
|
+
break;
|
|
309
|
+
case 'day':
|
|
310
|
+
this.startOf('day');
|
|
311
|
+
this.shift('day', 1);
|
|
312
|
+
this.shift('minute', -1);
|
|
313
|
+
break;
|
|
314
|
+
case 'week':
|
|
315
|
+
this.startOf('week');
|
|
316
|
+
this.shift('week', 1);
|
|
317
|
+
this.shift('minute', -1);
|
|
318
|
+
break;
|
|
319
|
+
case 'month':
|
|
320
|
+
this.startOf('month');
|
|
321
|
+
this.shift('month', 1);
|
|
322
|
+
this.shift('minute', -1);
|
|
323
|
+
break;
|
|
324
|
+
case 'year':
|
|
325
|
+
this.startOf('year');
|
|
326
|
+
this.shift('year', 1);
|
|
327
|
+
this.shift('minute', -1);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
clone() {
|
|
333
|
+
return new DateTz(this);
|
|
334
|
+
}
|
|
335
|
+
toJSDate() {
|
|
336
|
+
return new Date(this.timestamp);
|
|
337
|
+
}
|
|
338
|
+
toISOString() {
|
|
339
|
+
return this.toJSDate().toISOString();
|
|
340
|
+
}
|
|
341
|
+
toISO() {
|
|
342
|
+
return this.toISOString();
|
|
343
|
+
}
|
|
344
|
+
toUnix() {
|
|
345
|
+
return Math.floor(this.timestamp / 1000);
|
|
346
|
+
}
|
|
347
|
+
valueOf() {
|
|
348
|
+
return this.timestamp;
|
|
349
|
+
}
|
|
350
|
+
isBefore(other, unit = 'millisecond') {
|
|
351
|
+
return this.compareWithUnit(other, unit) < 0;
|
|
352
|
+
}
|
|
353
|
+
isAfter(other, unit = 'millisecond') {
|
|
354
|
+
return this.compareWithUnit(other, unit) > 0;
|
|
355
|
+
}
|
|
356
|
+
isSame(other, unit = 'millisecond') {
|
|
357
|
+
return this.compareWithUnit(other, unit) === 0;
|
|
358
|
+
}
|
|
359
|
+
isSameOrBefore(other, unit = 'millisecond') {
|
|
360
|
+
return this.compareWithUnit(other, unit) <= 0;
|
|
361
|
+
}
|
|
362
|
+
isSameOrAfter(other, unit = 'millisecond') {
|
|
363
|
+
return this.compareWithUnit(other, unit) >= 0;
|
|
364
|
+
}
|
|
365
|
+
isBetween(start, end, unit = 'millisecond', inclusivity = '()') {
|
|
366
|
+
if (inclusivity.length !== 2 || !['(', '['].includes(inclusivity[0]) || ![')', ']'].includes(inclusivity[1])) {
|
|
367
|
+
throw new Error(`Invalid inclusivity token: ${inclusivity}`);
|
|
368
|
+
}
|
|
369
|
+
const normalized = this.normalizeDiffUnit(unit);
|
|
370
|
+
const rangeStart = this.ensureComparable(start);
|
|
371
|
+
const rangeEnd = this.ensureComparable(end);
|
|
372
|
+
if (rangeStart.timestamp > rangeEnd.timestamp) {
|
|
373
|
+
throw new Error('Start date must be before end date');
|
|
374
|
+
}
|
|
375
|
+
const [lower, upper] = inclusivity.split('');
|
|
376
|
+
const lowerCmp = this.compareWithUnitDate(rangeStart, normalized);
|
|
377
|
+
const upperCmp = this.compareWithUnitDate(rangeEnd, normalized);
|
|
378
|
+
const lowerPass = lower === '(' ? lowerCmp > 0 : lowerCmp >= 0;
|
|
379
|
+
const upperPass = upper === ')' ? upperCmp < 0 : upperCmp <= 0;
|
|
380
|
+
return lowerPass && upperPass;
|
|
381
|
+
}
|
|
382
|
+
_year(considerDst = false) {
|
|
383
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
384
|
+
let remainingMs = this.timestamp + offset;
|
|
385
|
+
let year = 1970;
|
|
386
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
387
|
+
while (days >= this.daysInYear(year)) {
|
|
388
|
+
days -= this.daysInYear(year);
|
|
389
|
+
year++;
|
|
390
|
+
}
|
|
391
|
+
return year;
|
|
392
|
+
}
|
|
393
|
+
_month(considerDst = false) {
|
|
394
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
395
|
+
let remainingMs = this.timestamp + offset;
|
|
396
|
+
let year = 1970;
|
|
397
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
398
|
+
while (days >= this.daysInYear(year)) {
|
|
399
|
+
days -= this.daysInYear(year);
|
|
400
|
+
year++;
|
|
401
|
+
}
|
|
402
|
+
let month = 0;
|
|
403
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
404
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
405
|
+
month++;
|
|
406
|
+
}
|
|
407
|
+
return month;
|
|
408
|
+
}
|
|
409
|
+
_day(considerDst = false) {
|
|
410
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
411
|
+
let remainingMs = this.timestamp + offset;
|
|
412
|
+
let year = 1970;
|
|
413
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
414
|
+
while (days >= this.daysInYear(year)) {
|
|
415
|
+
days -= this.daysInYear(year);
|
|
416
|
+
year++;
|
|
417
|
+
}
|
|
418
|
+
let month = 0;
|
|
419
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
420
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
421
|
+
month++;
|
|
422
|
+
}
|
|
423
|
+
return days + 1;
|
|
424
|
+
}
|
|
425
|
+
_hour(considerDst = false) {
|
|
426
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
427
|
+
let remainingMs = this.timestamp + offset;
|
|
428
|
+
remainingMs %= MS_PER_DAY;
|
|
429
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
430
|
+
return hour;
|
|
431
|
+
}
|
|
432
|
+
_minute(considerDst = false) {
|
|
433
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
434
|
+
let remainingMs = this.timestamp + offset;
|
|
435
|
+
remainingMs %= MS_PER_HOUR;
|
|
436
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
437
|
+
return minute;
|
|
438
|
+
}
|
|
439
|
+
_dayOfWeek(considerDst = false) {
|
|
440
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
441
|
+
let remainingMs = this.timestamp + offset;
|
|
442
|
+
const date = new Date(remainingMs);
|
|
443
|
+
return date.getDay();
|
|
444
|
+
}
|
|
445
|
+
convertToTimezone(tz) {
|
|
446
|
+
if (!timezones_1.timezones[tz]) {
|
|
447
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
448
|
+
}
|
|
449
|
+
this.timezone = tz;
|
|
450
|
+
this.invalidateOffsetCache();
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
cloneToTimezone(tz) {
|
|
454
|
+
if (!timezones_1.timezones[tz]) {
|
|
455
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
456
|
+
}
|
|
457
|
+
const clone = new DateTz(this);
|
|
458
|
+
clone.timezone = tz;
|
|
459
|
+
clone.invalidateOffsetCache();
|
|
460
|
+
return clone;
|
|
461
|
+
}
|
|
462
|
+
stripSMs(timestamp) {
|
|
463
|
+
const days = Math.floor(timestamp / MS_PER_DAY);
|
|
464
|
+
const remainingAfterDays = timestamp % MS_PER_DAY;
|
|
465
|
+
const hours = Math.floor(remainingAfterDays / MS_PER_HOUR);
|
|
466
|
+
const remainingAfterHours = remainingAfterDays % MS_PER_HOUR;
|
|
467
|
+
const minutes = Math.floor(remainingAfterHours / MS_PER_MINUTE);
|
|
468
|
+
return days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE;
|
|
469
|
+
}
|
|
470
|
+
invalidateOffsetCache() {
|
|
471
|
+
this.offsetCache = undefined;
|
|
472
|
+
}
|
|
473
|
+
getOffsetSeconds(considerDst) {
|
|
474
|
+
const tzInfo = timezones_1.timezones[this.timezone];
|
|
475
|
+
if (!tzInfo) {
|
|
476
|
+
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
477
|
+
}
|
|
478
|
+
if (!considerDst) {
|
|
479
|
+
return tzInfo.sdt;
|
|
480
|
+
}
|
|
481
|
+
return this.getOffsetInfo().offsetSeconds;
|
|
482
|
+
}
|
|
483
|
+
getOffsetInfo() {
|
|
484
|
+
if (this.offsetCache && this.offsetCache.timestamp === this.timestamp) {
|
|
485
|
+
return this.offsetCache.info;
|
|
486
|
+
}
|
|
487
|
+
const info = this.computeOffsetInfo();
|
|
488
|
+
this.offsetCache = { timestamp: this.timestamp, info };
|
|
489
|
+
return info;
|
|
490
|
+
}
|
|
491
|
+
computeOffsetInfo() {
|
|
492
|
+
const tzInfo = timezones_1.timezones[this.timezone];
|
|
493
|
+
if (!tzInfo) {
|
|
494
|
+
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
495
|
+
}
|
|
496
|
+
if (tzInfo.dst === tzInfo.sdt) {
|
|
497
|
+
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
498
|
+
}
|
|
499
|
+
const actual = this.getIntlOffsetSeconds(this.timestamp);
|
|
500
|
+
if (actual !== null) {
|
|
501
|
+
if (actual !== tzInfo.sdt && actual !== tzInfo.dst) {
|
|
502
|
+
return { offsetSeconds: actual, isDst: actual > tzInfo.sdt };
|
|
503
|
+
}
|
|
504
|
+
return { offsetSeconds: actual, isDst: actual === tzInfo.dst };
|
|
505
|
+
}
|
|
506
|
+
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
507
|
+
}
|
|
508
|
+
getIntlOffsetSeconds(timestamp) {
|
|
509
|
+
try {
|
|
510
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
511
|
+
timeZone: this.timezone,
|
|
512
|
+
hour12: false,
|
|
513
|
+
year: 'numeric',
|
|
514
|
+
month: '2-digit',
|
|
515
|
+
day: '2-digit',
|
|
516
|
+
hour: '2-digit',
|
|
517
|
+
minute: '2-digit',
|
|
518
|
+
second: '2-digit'
|
|
519
|
+
});
|
|
520
|
+
const parts = formatter.formatToParts(new Date(timestamp));
|
|
521
|
+
const lookup = (type) => {
|
|
522
|
+
const part = parts.find(p => p.type === type);
|
|
523
|
+
if (!part) {
|
|
524
|
+
throw new Error(`Missing part ${type}`);
|
|
525
|
+
}
|
|
526
|
+
return Number(part.value);
|
|
527
|
+
};
|
|
528
|
+
const adjusted = Date.UTC(lookup('year'), lookup('month') - 1, lookup('day'), lookup('hour'), lookup('minute'), lookup('second'));
|
|
529
|
+
return Math.round((adjusted - timestamp) / 1000);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
getLocalParts(considerDst = true) {
|
|
536
|
+
return {
|
|
537
|
+
year: this._year(considerDst),
|
|
538
|
+
month: this._month(considerDst),
|
|
539
|
+
day: this._day(considerDst),
|
|
540
|
+
hour: this._hour(considerDst),
|
|
541
|
+
minute: this._minute(considerDst),
|
|
542
|
+
second: 0
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
setLocalComponents(update) {
|
|
546
|
+
const current = this.getLocalParts(true);
|
|
547
|
+
const next = {
|
|
548
|
+
...current,
|
|
549
|
+
...update
|
|
550
|
+
};
|
|
551
|
+
const pattern = 'YYYY-MM-DD HH:mm:ss';
|
|
552
|
+
const dateString = [
|
|
553
|
+
String(next.year).padStart(4, '0'),
|
|
554
|
+
String(next.month + 1).padStart(2, '0'),
|
|
555
|
+
String(next.day).padStart(2, '0')
|
|
556
|
+
].join('-') + ' ' + [
|
|
557
|
+
String(next.hour).padStart(2, '0'),
|
|
558
|
+
String(next.minute).padStart(2, '0'),
|
|
559
|
+
String(next.second).padStart(2, '0')
|
|
560
|
+
].join(':');
|
|
561
|
+
const parsed = DateTz.parse(dateString, pattern, this.timezone);
|
|
562
|
+
this.timestamp = parsed.timestamp;
|
|
563
|
+
this.invalidateOffsetCache();
|
|
564
|
+
}
|
|
565
|
+
toDateInstance(other) {
|
|
566
|
+
if (other instanceof DateTz) {
|
|
567
|
+
return other;
|
|
568
|
+
}
|
|
569
|
+
const tz = other.timezone ?? this.timezone;
|
|
570
|
+
return new DateTz(other.timestamp, tz);
|
|
571
|
+
}
|
|
572
|
+
ensureComparable(other) {
|
|
573
|
+
const instance = this.toDateInstance(other);
|
|
574
|
+
if (!this.isComparable(instance)) {
|
|
575
|
+
throw new Error('Cannot compare dates with different timezones');
|
|
576
|
+
}
|
|
577
|
+
return instance;
|
|
578
|
+
}
|
|
579
|
+
normalizeDiffUnit(unit) {
|
|
580
|
+
if (!unit) {
|
|
581
|
+
return 'millisecond';
|
|
582
|
+
}
|
|
583
|
+
const normalized = UNIT_ALIASES[unit.toLowerCase()];
|
|
584
|
+
if (!normalized) {
|
|
585
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
586
|
+
}
|
|
587
|
+
return normalized;
|
|
588
|
+
}
|
|
589
|
+
normalizeGranularity(unit) {
|
|
590
|
+
const normalized = this.normalizeDiffUnit(unit);
|
|
591
|
+
if (normalized === 'millisecond' || normalized === 'second') {
|
|
592
|
+
throw new Error(`Unsupported granularity: ${unit}`);
|
|
593
|
+
}
|
|
594
|
+
return normalized;
|
|
595
|
+
}
|
|
596
|
+
diffInMonths(other, asFloat) {
|
|
597
|
+
const earlier = this.timestamp < other.timestamp ? new DateTz(this) : new DateTz(other);
|
|
598
|
+
const later = this.timestamp < other.timestamp ? new DateTz(other) : new DateTz(this);
|
|
599
|
+
let anchor = new DateTz(earlier);
|
|
600
|
+
let months = 0;
|
|
601
|
+
let next = new DateTz(anchor).add(1, 'month');
|
|
602
|
+
while (next.timestamp <= later.timestamp) {
|
|
603
|
+
anchor = next;
|
|
604
|
+
months++;
|
|
605
|
+
next = new DateTz(anchor).add(1, 'month');
|
|
606
|
+
}
|
|
607
|
+
if (!asFloat) {
|
|
608
|
+
return this.timestamp < other.timestamp ? -months : months;
|
|
609
|
+
}
|
|
610
|
+
const spanStart = anchor.timestamp;
|
|
611
|
+
const spanEnd = next.timestamp;
|
|
612
|
+
const span = spanEnd - spanStart;
|
|
613
|
+
const remainder = later.timestamp - spanStart;
|
|
614
|
+
const fractional = span !== 0 ? remainder / span : 0;
|
|
615
|
+
const value = months + fractional;
|
|
616
|
+
return this.timestamp < other.timestamp ? -value : value;
|
|
617
|
+
}
|
|
618
|
+
roundDiff(value, asFloat) {
|
|
619
|
+
if (asFloat) {
|
|
620
|
+
return value;
|
|
621
|
+
}
|
|
622
|
+
return value < 0 ? Math.ceil(value) : Math.floor(value);
|
|
623
|
+
}
|
|
624
|
+
compareWithUnitDate(other, unit) {
|
|
625
|
+
const normalized = this.normalizeDiffUnit(unit);
|
|
626
|
+
if (normalized === 'millisecond' || normalized === 'second') {
|
|
627
|
+
return this.timestamp - other.timestamp;
|
|
628
|
+
}
|
|
629
|
+
const granularity = normalized;
|
|
630
|
+
const left = this.clone().startOf(granularity);
|
|
631
|
+
const right = other.clone().startOf(granularity);
|
|
632
|
+
return left.timestamp - right.timestamp;
|
|
633
|
+
}
|
|
634
|
+
compareWithUnit(other, unit) {
|
|
635
|
+
const instance = this.ensureComparable(other);
|
|
636
|
+
return this.compareWithUnitDate(instance, unit);
|
|
637
|
+
}
|
|
638
|
+
shift(unit, value) {
|
|
639
|
+
if (value === 0) {
|
|
640
|
+
return this;
|
|
641
|
+
}
|
|
642
|
+
switch (unit) {
|
|
643
|
+
case 'minute':
|
|
644
|
+
this.timestamp += value * MS_PER_MINUTE;
|
|
645
|
+
break;
|
|
646
|
+
case 'hour':
|
|
647
|
+
this.timestamp += value * MS_PER_HOUR;
|
|
648
|
+
break;
|
|
649
|
+
case 'day':
|
|
650
|
+
this.timestamp += value * MS_PER_DAY;
|
|
651
|
+
break;
|
|
652
|
+
case 'week':
|
|
653
|
+
this.timestamp += value * MS_PER_WEEK;
|
|
654
|
+
break;
|
|
655
|
+
case 'month':
|
|
656
|
+
this.shiftCalendar('month', value);
|
|
657
|
+
return this;
|
|
658
|
+
case 'year':
|
|
659
|
+
this.shiftCalendar('year', value);
|
|
660
|
+
return this;
|
|
661
|
+
default:
|
|
662
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
663
|
+
}
|
|
664
|
+
this.invalidateOffsetCache();
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
shiftCalendar(unit, value) {
|
|
668
|
+
if (value === 0) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const current = this.getLocalParts(true);
|
|
672
|
+
let year = current.year;
|
|
673
|
+
let month = current.month;
|
|
674
|
+
let day = current.day;
|
|
675
|
+
if (unit === 'year') {
|
|
676
|
+
year += value;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
let totalMonths = year * 12 + month + value;
|
|
680
|
+
year = Math.floor(totalMonths / 12);
|
|
681
|
+
month = totalMonths % 12;
|
|
682
|
+
if (month < 0) {
|
|
683
|
+
month += 12;
|
|
684
|
+
year -= 1;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const maxDay = month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
688
|
+
if (day > maxDay) {
|
|
689
|
+
day = maxDay;
|
|
690
|
+
}
|
|
691
|
+
this.setLocalComponents({ year, month, day });
|
|
692
|
+
}
|
|
693
|
+
set(value, unit) {
|
|
694
|
+
let remainingMs = this.timestamp;
|
|
695
|
+
let year = 1970;
|
|
696
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
697
|
+
remainingMs %= MS_PER_DAY;
|
|
698
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
699
|
+
remainingMs %= MS_PER_HOUR;
|
|
700
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
701
|
+
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
702
|
+
while (days >= this.daysInYear(year)) {
|
|
703
|
+
days -= this.daysInYear(year);
|
|
704
|
+
year++;
|
|
705
|
+
}
|
|
706
|
+
let month = 0;
|
|
707
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
708
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
709
|
+
month++;
|
|
710
|
+
}
|
|
711
|
+
let day = days + 1;
|
|
712
|
+
switch (unit) {
|
|
713
|
+
case 'year':
|
|
714
|
+
year = value;
|
|
715
|
+
break;
|
|
716
|
+
case 'month':
|
|
717
|
+
month = value - 1;
|
|
718
|
+
break;
|
|
719
|
+
case 'day':
|
|
720
|
+
day = value;
|
|
721
|
+
break;
|
|
722
|
+
case 'hour':
|
|
723
|
+
hour = value;
|
|
724
|
+
break;
|
|
725
|
+
case 'minute':
|
|
726
|
+
minute = value;
|
|
727
|
+
break;
|
|
728
|
+
default:
|
|
729
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
730
|
+
}
|
|
731
|
+
while (month >= 12) {
|
|
732
|
+
month -= 12;
|
|
733
|
+
year++;
|
|
734
|
+
}
|
|
735
|
+
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
736
|
+
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
737
|
+
month++;
|
|
738
|
+
if (month >= 12) {
|
|
739
|
+
month = 0;
|
|
740
|
+
year++;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const newTimestamp = (() => {
|
|
744
|
+
let totalMs = 0;
|
|
745
|
+
for (let y = 1970; y < year; y++) {
|
|
746
|
+
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
747
|
+
}
|
|
748
|
+
for (let m = 0; m < month; m++) {
|
|
749
|
+
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
750
|
+
}
|
|
751
|
+
totalMs += (day - 1) * MS_PER_DAY;
|
|
752
|
+
totalMs += hour * MS_PER_HOUR;
|
|
753
|
+
totalMs += minute * MS_PER_MINUTE;
|
|
754
|
+
totalMs += second * 1000;
|
|
755
|
+
return totalMs;
|
|
756
|
+
})();
|
|
757
|
+
this.timestamp = newTimestamp;
|
|
758
|
+
this.invalidateOffsetCache();
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
761
|
+
isLeapYear(year) {
|
|
762
|
+
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
763
|
+
}
|
|
764
|
+
daysInYear(year) {
|
|
765
|
+
return this.isLeapYear(year) ? 366 : 365;
|
|
766
|
+
}
|
|
767
|
+
static parse(dateString, pattern, tz) {
|
|
768
|
+
if (!pattern)
|
|
769
|
+
pattern = DateTz.defaultFormat;
|
|
770
|
+
if (!tz)
|
|
771
|
+
tz = 'UTC';
|
|
772
|
+
if (!timezones_1.timezones[tz]) {
|
|
773
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
774
|
+
}
|
|
775
|
+
if (pattern.includes('hh') && (!pattern.includes('aa') || !pattern.includes('AA'))) {
|
|
776
|
+
throw new Error('AM/PM marker (aa or AA) is required when using 12-hour format (hh)');
|
|
777
|
+
}
|
|
778
|
+
const regex = /YYYY|yyyy|MM|DD|HH|hh|mm|ss|aa|AA/g;
|
|
779
|
+
const dateComponents = {
|
|
780
|
+
YYYY: 1970,
|
|
781
|
+
yyyy: 1970,
|
|
782
|
+
MM: 0,
|
|
783
|
+
DD: 0,
|
|
784
|
+
HH: 0,
|
|
785
|
+
hh: 0,
|
|
786
|
+
aa: 'am',
|
|
787
|
+
AA: "AM",
|
|
788
|
+
mm: 0,
|
|
789
|
+
ss: 0,
|
|
790
|
+
};
|
|
791
|
+
let match;
|
|
792
|
+
let index = 0;
|
|
793
|
+
while ((match = regex.exec(pattern)) !== null) {
|
|
794
|
+
const token = match[0];
|
|
795
|
+
const value = parseInt(dateString.substring(match.index, match.index + token.length), 10);
|
|
796
|
+
dateComponents[token] = value;
|
|
797
|
+
index += token.length + 1;
|
|
798
|
+
}
|
|
799
|
+
const year = dateComponents.YYYY || dateComponents.yyyy;
|
|
800
|
+
const month = dateComponents.MM - 1;
|
|
801
|
+
const day = dateComponents.DD;
|
|
802
|
+
let hour = 0;
|
|
803
|
+
const ampm = (dateComponents.a || dateComponents.A);
|
|
804
|
+
if (ampm) {
|
|
805
|
+
hour = ampm.toUpperCase() === 'AM' ? dateComponents.hh : dateComponents.hh + 12;
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
hour = dateComponents.HH;
|
|
809
|
+
}
|
|
810
|
+
const minute = dateComponents.mm;
|
|
811
|
+
const second = dateComponents.ss;
|
|
812
|
+
const daysInYear = (year) => (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 366 : 365;
|
|
813
|
+
const daysInMonth = (year, month) => month === 1 && daysInYear(year) === 366 ? 29 : daysPerMonth[month];
|
|
814
|
+
let timestamp = 0;
|
|
815
|
+
for (let y = 1970; y < year; y++) {
|
|
816
|
+
timestamp += daysInYear(y) * MS_PER_DAY;
|
|
817
|
+
}
|
|
818
|
+
for (let m = 0; m < month; m++) {
|
|
819
|
+
timestamp += daysInMonth(year, m) * MS_PER_DAY;
|
|
820
|
+
}
|
|
821
|
+
timestamp += (day - 1) * MS_PER_DAY;
|
|
822
|
+
timestamp += hour * MS_PER_HOUR;
|
|
823
|
+
timestamp += minute * MS_PER_MINUTE;
|
|
824
|
+
timestamp += second * 1000;
|
|
825
|
+
const offset = (timezones_1.timezones[tz].sdt) * 1000;
|
|
826
|
+
let remainingMs = timestamp - offset;
|
|
827
|
+
const date = new DateTz(remainingMs, tz);
|
|
828
|
+
date.timestamp -= date.isDst ? (timezones_1.timezones[tz].dst - timezones_1.timezones[tz].sdt) * 1000 : 0;
|
|
829
|
+
date.invalidateOffsetCache();
|
|
830
|
+
return date;
|
|
831
|
+
}
|
|
832
|
+
static now(tz) {
|
|
833
|
+
if (!tz)
|
|
834
|
+
tz = 'UTC';
|
|
835
|
+
const timezone = timezones_1.timezones[tz];
|
|
836
|
+
if (!timezone) {
|
|
837
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
838
|
+
}
|
|
839
|
+
const date = new DateTz(Date.now(), tz);
|
|
840
|
+
return date;
|
|
841
|
+
}
|
|
842
|
+
get isDst() {
|
|
843
|
+
return this.getOffsetInfo().isDst;
|
|
844
|
+
}
|
|
845
|
+
get year() {
|
|
846
|
+
return this._year(true);
|
|
847
|
+
}
|
|
848
|
+
get month() {
|
|
849
|
+
return this._month(true);
|
|
850
|
+
}
|
|
851
|
+
get day() {
|
|
852
|
+
return this._day(true);
|
|
853
|
+
}
|
|
854
|
+
get hour() {
|
|
855
|
+
return this._hour(true);
|
|
856
|
+
}
|
|
857
|
+
get minute() {
|
|
858
|
+
return this._minute(true);
|
|
859
|
+
}
|
|
860
|
+
get dayOfWeek() {
|
|
861
|
+
return this._dayOfWeek(true);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
exports.DateTz = DateTz;
|
|
865
|
+
DateTz.defaultFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
866
|
+
//# sourceMappingURL=date-tz.js.map
|