@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/src/date-tz.ts
DELETED
|
@@ -1,752 +0,0 @@
|
|
|
1
|
-
import { IDateTz } from "./idate-tz";
|
|
2
|
-
import { timezones } from "./timezones";
|
|
3
|
-
|
|
4
|
-
const MS_PER_MINUTE = 60000;
|
|
5
|
-
const MS_PER_HOUR = 3600000;
|
|
6
|
-
const MS_PER_DAY = 86400000;
|
|
7
|
-
|
|
8
|
-
// Epoch time constants
|
|
9
|
-
const epochYear = 1970;
|
|
10
|
-
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Represents a date and time with a specific timezone.
|
|
14
|
-
*/
|
|
15
|
-
export class DateTz implements IDateTz {
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The timestamp in milliseconds since the Unix epoch.
|
|
19
|
-
*/
|
|
20
|
-
timestamp: number;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The timezone of the date.
|
|
24
|
-
*/
|
|
25
|
-
timezone: string;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Cache for the most recently resolved timezone offset.
|
|
29
|
-
*/
|
|
30
|
-
private offsetCache?: { timestamp: number; info: { offsetSeconds: number; isDst: boolean; }; };
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The default date format used when converting to string.
|
|
34
|
-
*/
|
|
35
|
-
public static defaultFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Creates an instance of DateTz.
|
|
39
|
-
* @param value - The timestamp or an object implementing IDateTz.
|
|
40
|
-
* @param tz - The timezone identifier (optional).
|
|
41
|
-
*/
|
|
42
|
-
constructor(value: IDateTz);
|
|
43
|
-
constructor(value: number, tz?: string);
|
|
44
|
-
constructor(value: number | IDateTz, tz?: string) {
|
|
45
|
-
if (typeof value === 'object') {
|
|
46
|
-
this.timestamp = value.timestamp;
|
|
47
|
-
this.timezone = value.timezone || 'UTC';
|
|
48
|
-
if (!timezones[this.timezone]) {
|
|
49
|
-
throw new Error(`Invalid timezone: ${value.timezone}`);
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
this.timezone = tz || 'UTC';
|
|
53
|
-
if (!timezones[this.timezone]) {
|
|
54
|
-
throw new Error(`Invalid timezone: ${tz}`);
|
|
55
|
-
}
|
|
56
|
-
this.timestamp = this.stripSMs(value);
|
|
57
|
-
}
|
|
58
|
-
this.invalidateOffsetCache();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Gets the timezone offset in minutes.
|
|
63
|
-
*/
|
|
64
|
-
get timezoneOffset() {
|
|
65
|
-
return timezones[this.timezone];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Compares this DateTz instance with another.
|
|
70
|
-
* @param other - The other DateTz instance to compare with.
|
|
71
|
-
* @returns The difference in timestamps.
|
|
72
|
-
* @throws Error if the timezones are different.
|
|
73
|
-
*/
|
|
74
|
-
compare(other: IDateTz): number {
|
|
75
|
-
if (this.isComparable(other)) {
|
|
76
|
-
return this.timestamp - other.timestamp;
|
|
77
|
-
}
|
|
78
|
-
throw new Error('Cannot compare dates with different timezones');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Checks if this DateTz instance is comparable with another.
|
|
83
|
-
* @param other - The other DateTz instance to check.
|
|
84
|
-
* @returns True if the timezones are the same, otherwise false.
|
|
85
|
-
*/
|
|
86
|
-
isComparable(other: IDateTz): boolean {
|
|
87
|
-
return this.timezone === other.timezone;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Converts the DateTz instance to a string representation.
|
|
92
|
-
* @param pattern - The format pattern (optional).
|
|
93
|
-
* @returns The formatted date string.
|
|
94
|
-
*/
|
|
95
|
-
toString(): string;
|
|
96
|
-
toString(pattern: string): string;
|
|
97
|
-
toString(pattern?: string, locale?: string): string {
|
|
98
|
-
if (!pattern) pattern = 'YYYY-MM-DD HH:mm:ss';
|
|
99
|
-
|
|
100
|
-
// Calculate year, month, day, hours, minutes, seconds
|
|
101
|
-
const offsetInfo = this.getOffsetInfo();
|
|
102
|
-
const offset = offsetInfo.offsetSeconds * 1000;
|
|
103
|
-
let remainingMs = this.timestamp + offset;
|
|
104
|
-
let year = epochYear;
|
|
105
|
-
|
|
106
|
-
// Calculate year
|
|
107
|
-
while (true) {
|
|
108
|
-
const daysInYear = this.isLeapYear(year) ? 366 : 365;
|
|
109
|
-
const msInYear = daysInYear * MS_PER_DAY;
|
|
110
|
-
|
|
111
|
-
if (remainingMs >= msInYear) {
|
|
112
|
-
remainingMs -= msInYear;
|
|
113
|
-
year++;
|
|
114
|
-
} else {
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Calculate month
|
|
120
|
-
let month = 0;
|
|
121
|
-
while (month < 12) {
|
|
122
|
-
const daysInMonth = month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
123
|
-
const msInMonth = daysInMonth * MS_PER_DAY;
|
|
124
|
-
|
|
125
|
-
if (remainingMs >= msInMonth) {
|
|
126
|
-
remainingMs -= msInMonth;
|
|
127
|
-
month++;
|
|
128
|
-
} else {
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Calculate day
|
|
134
|
-
const day = Math.floor(remainingMs / MS_PER_DAY) + 1;
|
|
135
|
-
remainingMs %= MS_PER_DAY;
|
|
136
|
-
|
|
137
|
-
// Calculate hour
|
|
138
|
-
const hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
139
|
-
remainingMs %= MS_PER_HOUR;
|
|
140
|
-
|
|
141
|
-
// Calculate minute
|
|
142
|
-
const minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
143
|
-
remainingMs %= MS_PER_MINUTE;
|
|
144
|
-
|
|
145
|
-
// Calculate second
|
|
146
|
-
const second = Math.floor(remainingMs / 1000);
|
|
147
|
-
|
|
148
|
-
const pm = hour >= 12 ? 'PM' : 'AM';
|
|
149
|
-
const hour12 = hour % 12 || 12; // Convert to 12-hour format
|
|
150
|
-
|
|
151
|
-
if (!locale) locale = 'en';
|
|
152
|
-
let monthStr = new Date(year, month, 3).toLocaleString(locale || 'en', { month: 'long' });
|
|
153
|
-
monthStr = monthStr.charAt(0).toUpperCase() + monthStr.slice(1);
|
|
154
|
-
|
|
155
|
-
// Map components to pattern tokens
|
|
156
|
-
const tokens: Record<string, any> = {
|
|
157
|
-
YYYY: year,
|
|
158
|
-
YY: String(year).slice(-2),
|
|
159
|
-
yyyy: year.toString(),
|
|
160
|
-
yy: String(year).slice(-2),
|
|
161
|
-
MM: String(month + 1).padStart(2, '0'),
|
|
162
|
-
LM: monthStr,
|
|
163
|
-
DD: String(day).padStart(2, '0'),
|
|
164
|
-
HH: String(hour).padStart(2, '0'),
|
|
165
|
-
mm: String(minute).padStart(2, '0'),
|
|
166
|
-
ss: String(second).padStart(2, '0'),
|
|
167
|
-
aa: pm.toLowerCase(),
|
|
168
|
-
AA: pm,
|
|
169
|
-
hh: hour12.toString().padStart(2, '0'),
|
|
170
|
-
tz: this.timezone,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Replace pattern tokens with actual values
|
|
174
|
-
return pattern.replace(/YYYY|yyyy|YY|yy|MM|LM|DD|HH|hh|mm|ss|aa|AA|tz/g, (match) => tokens[match]);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Adds a specified amount of time to the DateTz instance.
|
|
179
|
-
* @param value - The amount of time to add.
|
|
180
|
-
* @param unit - The unit of time ('minute', 'hour', 'day', 'month', 'year').
|
|
181
|
-
* @returns The updated DateTz instance.
|
|
182
|
-
* @throws Error if the unit is unsupported.
|
|
183
|
-
*/
|
|
184
|
-
add(value: number, unit: 'minute' | 'hour' | 'day' | 'month' | 'year') {
|
|
185
|
-
let remainingMs = this.timestamp;
|
|
186
|
-
|
|
187
|
-
// Extract current date components
|
|
188
|
-
let year = 1970;
|
|
189
|
-
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
190
|
-
remainingMs %= MS_PER_DAY;
|
|
191
|
-
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
192
|
-
remainingMs %= MS_PER_HOUR;
|
|
193
|
-
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
194
|
-
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
195
|
-
|
|
196
|
-
// Calculate current year
|
|
197
|
-
while (days >= this.daysInYear(year)) {
|
|
198
|
-
days -= this.daysInYear(year);
|
|
199
|
-
year++;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Calculate current month
|
|
203
|
-
let month = 0;
|
|
204
|
-
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
205
|
-
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
206
|
-
month++;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let day = days + 1;
|
|
210
|
-
|
|
211
|
-
// Add time based on the unit
|
|
212
|
-
switch (unit) {
|
|
213
|
-
case 'minute':
|
|
214
|
-
minute += value;
|
|
215
|
-
break;
|
|
216
|
-
case 'hour':
|
|
217
|
-
hour += value;
|
|
218
|
-
break;
|
|
219
|
-
case 'day':
|
|
220
|
-
day += value;
|
|
221
|
-
break;
|
|
222
|
-
case 'month':
|
|
223
|
-
month += value;
|
|
224
|
-
break;
|
|
225
|
-
case 'year':
|
|
226
|
-
year += value;
|
|
227
|
-
break;
|
|
228
|
-
default:
|
|
229
|
-
throw new Error(`Unsupported unit: ${unit}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Normalize overflow for minutes, hours, and days
|
|
233
|
-
while (minute >= 60) {
|
|
234
|
-
minute -= 60;
|
|
235
|
-
hour++;
|
|
236
|
-
}
|
|
237
|
-
while (hour >= 24) {
|
|
238
|
-
hour -= 24;
|
|
239
|
-
day++;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Normalize overflow for months and years
|
|
243
|
-
while (month >= 12) {
|
|
244
|
-
month -= 12;
|
|
245
|
-
year++;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Normalize day overflow
|
|
249
|
-
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
250
|
-
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
251
|
-
month++;
|
|
252
|
-
if (month >= 12) {
|
|
253
|
-
month = 0;
|
|
254
|
-
year++;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Convert back to timestamp
|
|
259
|
-
const newTimestamp = (() => {
|
|
260
|
-
let totalMs = 0;
|
|
261
|
-
|
|
262
|
-
// Add years
|
|
263
|
-
for (let y = 1970; y < year; y++) {
|
|
264
|
-
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Add months
|
|
268
|
-
for (let m = 0; m < month; m++) {
|
|
269
|
-
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Add days, hours, minutes, and seconds
|
|
273
|
-
totalMs += (day - 1) * MS_PER_DAY;
|
|
274
|
-
totalMs += hour * MS_PER_HOUR;
|
|
275
|
-
totalMs += minute * MS_PER_MINUTE;
|
|
276
|
-
totalMs += second * 1000;
|
|
277
|
-
|
|
278
|
-
return totalMs;
|
|
279
|
-
})();
|
|
280
|
-
|
|
281
|
-
this.timestamp = newTimestamp;
|
|
282
|
-
this.invalidateOffsetCache();
|
|
283
|
-
return this;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
private _year(considerDst = false) {
|
|
288
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
289
|
-
let remainingMs = this.timestamp + offset;
|
|
290
|
-
let year = 1970;
|
|
291
|
-
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
292
|
-
|
|
293
|
-
while (days >= this.daysInYear(year)) {
|
|
294
|
-
days -= this.daysInYear(year);
|
|
295
|
-
year++;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return year;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private _month(considerDst = false) {
|
|
302
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
303
|
-
let remainingMs = this.timestamp + offset;
|
|
304
|
-
let year = 1970;
|
|
305
|
-
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
306
|
-
|
|
307
|
-
while (days >= this.daysInYear(year)) {
|
|
308
|
-
days -= this.daysInYear(year);
|
|
309
|
-
year++;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
let month = 0;
|
|
313
|
-
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
314
|
-
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
315
|
-
month++;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return month;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private _day(considerDst = false) {
|
|
322
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
323
|
-
let remainingMs = this.timestamp + offset;
|
|
324
|
-
let year = 1970;
|
|
325
|
-
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
326
|
-
|
|
327
|
-
while (days >= this.daysInYear(year)) {
|
|
328
|
-
days -= this.daysInYear(year);
|
|
329
|
-
year++;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
let month = 0;
|
|
333
|
-
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
334
|
-
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
335
|
-
month++;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return days + 1;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private _hour(considerDst = false) {
|
|
342
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
343
|
-
let remainingMs = this.timestamp + offset;
|
|
344
|
-
remainingMs %= MS_PER_DAY;
|
|
345
|
-
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
346
|
-
return hour;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private _minute(considerDst = false) {
|
|
350
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
351
|
-
let remainingMs = this.timestamp + offset;
|
|
352
|
-
remainingMs %= MS_PER_HOUR;
|
|
353
|
-
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
354
|
-
return minute;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private _dayOfWeek(considerDst = false) {
|
|
358
|
-
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
359
|
-
let remainingMs = this.timestamp + offset;
|
|
360
|
-
const date = new Date(remainingMs);
|
|
361
|
-
return date.getDay();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Converts the DateTz instance to a different timezone.
|
|
366
|
-
* @param tz - The target timezone identifier.
|
|
367
|
-
* @returns The updated DateTz instance.
|
|
368
|
-
* @throws Error if the timezone is invalid.
|
|
369
|
-
*/
|
|
370
|
-
convertToTimezone(tz: string) {
|
|
371
|
-
if (!timezones[tz]) {
|
|
372
|
-
throw new Error(`Invalid timezone: ${tz}`);
|
|
373
|
-
}
|
|
374
|
-
this.timezone = tz;
|
|
375
|
-
this.invalidateOffsetCache();
|
|
376
|
-
return this;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Clones the DateTz instance to a different timezone.
|
|
381
|
-
* @param tz - The target timezone identifier.
|
|
382
|
-
* @returns A new DateTz instance in the target timezone.
|
|
383
|
-
* @throws Error if the timezone is invalid.
|
|
384
|
-
*/
|
|
385
|
-
cloneToTimezone(tz: string) {
|
|
386
|
-
if (!timezones[tz]) {
|
|
387
|
-
throw new Error(`Invalid timezone: ${tz}`);
|
|
388
|
-
}
|
|
389
|
-
const clone = new DateTz(this);
|
|
390
|
-
clone.timezone = tz;
|
|
391
|
-
clone.invalidateOffsetCache();
|
|
392
|
-
return clone;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Strips seconds and milliseconds from the timestamp.
|
|
397
|
-
* @param timestamp - The original timestamp.
|
|
398
|
-
* @returns The timestamp without seconds and milliseconds.
|
|
399
|
-
*/
|
|
400
|
-
private stripSMs(timestamp: number): number {
|
|
401
|
-
// Calculate the time components
|
|
402
|
-
const days = Math.floor(timestamp / MS_PER_DAY);
|
|
403
|
-
const remainingAfterDays = timestamp % MS_PER_DAY;
|
|
404
|
-
|
|
405
|
-
const hours = Math.floor(remainingAfterDays / MS_PER_HOUR);
|
|
406
|
-
const remainingAfterHours = remainingAfterDays % MS_PER_HOUR;
|
|
407
|
-
|
|
408
|
-
const minutes = Math.floor(remainingAfterHours / MS_PER_MINUTE);
|
|
409
|
-
|
|
410
|
-
// Reconstruct the timestamp without seconds and milliseconds
|
|
411
|
-
return days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private invalidateOffsetCache() {
|
|
415
|
-
this.offsetCache = undefined;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
private getOffsetSeconds(considerDst: boolean): number {
|
|
419
|
-
const tzInfo = timezones[this.timezone];
|
|
420
|
-
if (!tzInfo) {
|
|
421
|
-
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
422
|
-
}
|
|
423
|
-
if (!considerDst) {
|
|
424
|
-
return tzInfo.sdt;
|
|
425
|
-
}
|
|
426
|
-
return this.getOffsetInfo().offsetSeconds;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private getOffsetInfo(): { offsetSeconds: number; isDst: boolean; } {
|
|
430
|
-
if (this.offsetCache && this.offsetCache.timestamp === this.timestamp) {
|
|
431
|
-
return this.offsetCache.info;
|
|
432
|
-
}
|
|
433
|
-
const info = this.computeOffsetInfo();
|
|
434
|
-
this.offsetCache = { timestamp: this.timestamp, info };
|
|
435
|
-
return info;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
private computeOffsetInfo(): { offsetSeconds: number; isDst: boolean; } {
|
|
439
|
-
const tzInfo = timezones[this.timezone];
|
|
440
|
-
if (!tzInfo) {
|
|
441
|
-
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
442
|
-
}
|
|
443
|
-
if (tzInfo.dst === tzInfo.sdt) {
|
|
444
|
-
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
445
|
-
}
|
|
446
|
-
const actual = this.getIntlOffsetSeconds(this.timestamp);
|
|
447
|
-
if (actual !== null) {
|
|
448
|
-
if (actual !== tzInfo.sdt && actual !== tzInfo.dst) {
|
|
449
|
-
return { offsetSeconds: actual, isDst: actual > tzInfo.sdt };
|
|
450
|
-
}
|
|
451
|
-
return { offsetSeconds: actual, isDst: actual === tzInfo.dst };
|
|
452
|
-
}
|
|
453
|
-
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private getIntlOffsetSeconds(timestamp: number): number | null {
|
|
457
|
-
try {
|
|
458
|
-
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
459
|
-
timeZone: this.timezone,
|
|
460
|
-
hour12: false,
|
|
461
|
-
year: 'numeric',
|
|
462
|
-
month: '2-digit',
|
|
463
|
-
day: '2-digit',
|
|
464
|
-
hour: '2-digit',
|
|
465
|
-
minute: '2-digit',
|
|
466
|
-
second: '2-digit'
|
|
467
|
-
});
|
|
468
|
-
const parts = formatter.formatToParts(new Date(timestamp));
|
|
469
|
-
const lookup = (type: string) => {
|
|
470
|
-
const part = parts.find(p => p.type === type);
|
|
471
|
-
if (!part) {
|
|
472
|
-
throw new Error(`Missing part ${type}`);
|
|
473
|
-
}
|
|
474
|
-
return Number(part.value);
|
|
475
|
-
};
|
|
476
|
-
const adjusted = Date.UTC(
|
|
477
|
-
lookup('year'),
|
|
478
|
-
lookup('month') - 1,
|
|
479
|
-
lookup('day'),
|
|
480
|
-
lookup('hour'),
|
|
481
|
-
lookup('minute'),
|
|
482
|
-
lookup('second')
|
|
483
|
-
);
|
|
484
|
-
return Math.round((adjusted - timestamp) / 1000);
|
|
485
|
-
} catch {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Sets a specific component of the date or time.
|
|
492
|
-
* @param value - The value to set.
|
|
493
|
-
* @param unit - The unit to set ('year', 'month', 'day', 'hour', 'minute').
|
|
494
|
-
* @returns The updated DateTz instance.
|
|
495
|
-
* @throws Error if the unit is unsupported.
|
|
496
|
-
*/
|
|
497
|
-
set(value: number, unit: 'year' | 'month' | 'day' | 'hour' | 'minute') {
|
|
498
|
-
let remainingMs = this.timestamp;
|
|
499
|
-
|
|
500
|
-
// Extract current date components
|
|
501
|
-
let year = 1970;
|
|
502
|
-
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
503
|
-
remainingMs %= MS_PER_DAY;
|
|
504
|
-
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
505
|
-
remainingMs %= MS_PER_HOUR;
|
|
506
|
-
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
507
|
-
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
508
|
-
|
|
509
|
-
// Calculate current year
|
|
510
|
-
while (days >= this.daysInYear(year)) {
|
|
511
|
-
days -= this.daysInYear(year);
|
|
512
|
-
year++;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Calculate current month
|
|
516
|
-
let month = 0;
|
|
517
|
-
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
518
|
-
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
519
|
-
month++;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
let day = days + 1;
|
|
523
|
-
|
|
524
|
-
// Set the value based on the unit
|
|
525
|
-
switch (unit) {
|
|
526
|
-
case 'year':
|
|
527
|
-
year = value;
|
|
528
|
-
break;
|
|
529
|
-
case 'month':
|
|
530
|
-
month = value - 1;
|
|
531
|
-
break;
|
|
532
|
-
case 'day':
|
|
533
|
-
day = value;
|
|
534
|
-
break;
|
|
535
|
-
case 'hour':
|
|
536
|
-
hour = value;
|
|
537
|
-
break;
|
|
538
|
-
case 'minute':
|
|
539
|
-
minute = value;
|
|
540
|
-
break;
|
|
541
|
-
default:
|
|
542
|
-
throw new Error(`Unsupported unit: ${unit}`);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Normalize overflow for months and years
|
|
546
|
-
while (month >= 12) {
|
|
547
|
-
month -= 12;
|
|
548
|
-
year++;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Normalize day overflow
|
|
552
|
-
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
553
|
-
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
554
|
-
month++;
|
|
555
|
-
if (month >= 12) {
|
|
556
|
-
month = 0;
|
|
557
|
-
year++;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Convert back to timestamp
|
|
562
|
-
const newTimestamp = (() => {
|
|
563
|
-
let totalMs = 0;
|
|
564
|
-
|
|
565
|
-
// Add years
|
|
566
|
-
for (let y = 1970; y < year; y++) {
|
|
567
|
-
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Add months
|
|
571
|
-
for (let m = 0; m < month; m++) {
|
|
572
|
-
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Add days, hours, minutes, and seconds
|
|
576
|
-
totalMs += (day - 1) * MS_PER_DAY;
|
|
577
|
-
totalMs += hour * MS_PER_HOUR;
|
|
578
|
-
totalMs += minute * MS_PER_MINUTE;
|
|
579
|
-
totalMs += second * 1000;
|
|
580
|
-
|
|
581
|
-
return totalMs;
|
|
582
|
-
})();
|
|
583
|
-
|
|
584
|
-
this.timestamp = newTimestamp;
|
|
585
|
-
this.invalidateOffsetCache();
|
|
586
|
-
return this;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Checks if a given year is a leap year.
|
|
591
|
-
* @param year - The year to check.
|
|
592
|
-
* @returns True if the year is a leap year, otherwise false.
|
|
593
|
-
*/
|
|
594
|
-
private isLeapYear(year: number) {
|
|
595
|
-
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Gets the number of days in a given year.
|
|
600
|
-
* @param year - The year to check.
|
|
601
|
-
* @returns The number of days in the year.
|
|
602
|
-
*/
|
|
603
|
-
private daysInYear(year: number) {
|
|
604
|
-
return this.isLeapYear(year) ? 366 : 365;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Parses a date string into a DateTz instance.
|
|
609
|
-
* @param dateString - The date string to parse.
|
|
610
|
-
* @param pattern - The format pattern (optional).
|
|
611
|
-
* @param tz - The timezone identifier (optional).
|
|
612
|
-
* @returns A new DateTz instance.
|
|
613
|
-
*/
|
|
614
|
-
static parse(dateString: string, pattern?: string, tz?: string): DateTz {
|
|
615
|
-
if (!pattern) pattern = DateTz.defaultFormat;
|
|
616
|
-
if (!tz) tz = 'UTC';
|
|
617
|
-
if (!timezones[tz]) {
|
|
618
|
-
throw new Error(`Invalid timezone: ${tz}`);
|
|
619
|
-
}
|
|
620
|
-
if (pattern.includes('hh') && (!pattern.includes('aa') || !pattern.includes('AA'))) {
|
|
621
|
-
throw new Error('AM/PM marker (aa or AA) is required when using 12-hour format (hh)');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const regex = /YYYY|yyyy|MM|DD|HH|hh|mm|ss|aa|AA/g;
|
|
625
|
-
const dateComponents: { [key: string]: number | string; } = {
|
|
626
|
-
YYYY: 1970,
|
|
627
|
-
yyyy: 1970,
|
|
628
|
-
MM: 0,
|
|
629
|
-
DD: 0,
|
|
630
|
-
HH: 0,
|
|
631
|
-
hh: 0,
|
|
632
|
-
aa: 'am',
|
|
633
|
-
AA: "AM",
|
|
634
|
-
mm: 0,
|
|
635
|
-
ss: 0,
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
let match: RegExpExecArray | null;
|
|
639
|
-
let index = 0;
|
|
640
|
-
while ((match = regex.exec(pattern)) !== null) {
|
|
641
|
-
const token = match[0];
|
|
642
|
-
const value = parseInt(dateString.substring(match.index, match.index + token.length), 10);
|
|
643
|
-
dateComponents[token] = value;
|
|
644
|
-
index += token.length + 1;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const year = (dateComponents.YYYY as number) || (dateComponents.yyyy as number);
|
|
648
|
-
const month = (dateComponents.MM as number) - 1; // Months are zero-based
|
|
649
|
-
const day = dateComponents.DD as number;
|
|
650
|
-
let hour = 0;
|
|
651
|
-
const ampm = (dateComponents.a || dateComponents.A) as string;
|
|
652
|
-
if (ampm) {
|
|
653
|
-
hour = ampm.toUpperCase() === 'AM' ? (dateComponents.hh as number) : (dateComponents.hh as number) + 12;
|
|
654
|
-
} else {
|
|
655
|
-
hour = dateComponents.HH as number;
|
|
656
|
-
}
|
|
657
|
-
const minute = dateComponents.mm as number;
|
|
658
|
-
const second = dateComponents.ss as number;
|
|
659
|
-
|
|
660
|
-
const daysInYear = (year: number) => (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 366 : 365;
|
|
661
|
-
const daysInMonth = (year: number, month: number) => month === 1 && daysInYear(year) === 366 ? 29 : daysPerMonth[month];
|
|
662
|
-
|
|
663
|
-
let timestamp = 0;
|
|
664
|
-
|
|
665
|
-
// Add years
|
|
666
|
-
for (let y = 1970; y < year; y++) {
|
|
667
|
-
timestamp += daysInYear(y) * MS_PER_DAY;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Add months
|
|
671
|
-
for (let m = 0; m < month; m++) {
|
|
672
|
-
timestamp += daysInMonth(year, m) * MS_PER_DAY;
|
|
673
|
-
}
|
|
674
|
-
// Add days, hours, minutes, and seconds
|
|
675
|
-
timestamp += (day - 1) * MS_PER_DAY;
|
|
676
|
-
timestamp += hour * MS_PER_HOUR;
|
|
677
|
-
timestamp += minute * MS_PER_MINUTE;
|
|
678
|
-
timestamp += second * 1000;
|
|
679
|
-
|
|
680
|
-
const offset = (timezones[tz].sdt) * 1000;
|
|
681
|
-
let remainingMs = timestamp - offset;
|
|
682
|
-
const date = new DateTz(remainingMs, tz);
|
|
683
|
-
date.timestamp -= date.isDst ? (timezones[tz].dst - timezones[tz].sdt) * 1000 : 0;
|
|
684
|
-
date.invalidateOffsetCache();
|
|
685
|
-
return date;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Gets the current date and time as a DateTz instance.
|
|
690
|
-
* @param tz - The timezone identifier (optional). Defaults to 'UTC'.
|
|
691
|
-
* @returns A new DateTz instance representing the current date and time.
|
|
692
|
-
*/
|
|
693
|
-
static now(tz?: string): DateTz {
|
|
694
|
-
if (!tz) tz = 'UTC';
|
|
695
|
-
const timezone = timezones[tz];
|
|
696
|
-
if (!timezone) {
|
|
697
|
-
throw new Error(`Invalid timezone: ${tz}`);
|
|
698
|
-
}
|
|
699
|
-
const date = new DateTz(Date.now(), tz);
|
|
700
|
-
return date;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
get isDst(): boolean {
|
|
704
|
-
return this.getOffsetInfo().isDst;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Gets the year component of the date.
|
|
712
|
-
*/
|
|
713
|
-
get year() {
|
|
714
|
-
return this._year(true);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Gets the month component of the date.
|
|
719
|
-
*/
|
|
720
|
-
get month() {
|
|
721
|
-
return this._month(true);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Gets the day component of the date.
|
|
726
|
-
*/
|
|
727
|
-
get day() {
|
|
728
|
-
return this._day(true);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Gets the hour component of the time.
|
|
733
|
-
*/
|
|
734
|
-
get hour() {
|
|
735
|
-
return this._hour(true);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Gets the minute component of the time.
|
|
740
|
-
*/
|
|
741
|
-
get minute() {
|
|
742
|
-
return this._minute(true);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Gets the day of the week.
|
|
747
|
-
*/
|
|
748
|
-
get dayOfWeek(): number {
|
|
749
|
-
return this._dayOfWeek(true);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
}
|