@lbd-sh/date-tz 1.0.1
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 +147 -0
- package/date-tz.d.ts +41 -0
- package/date-tz.js +503 -0
- package/date-tz.js.map +1 -0
- package/idate-tz.d.ts +22 -0
- package/idate-tz.js +3 -0
- package/idate-tz.js.map +1 -0
- package/index.d.ts +3 -0
- package/index.js +20 -0
- package/index.js.map +1 -0
- package/package.json +55 -0
- package/timezones.d.ts +8 -0
- package/timezones.js +594 -0
- package/timezones.js.map +1 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# DateTz Class Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `DateTz` class represents a date and time in a specific timezone. It provides utilities for formatting, parsing, comparing, and manipulating date and time values with full timezone support.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Constructor
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
new DateTz(value: IDateTz)
|
|
13
|
+
new DateTz(value: number, tz?: string)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Accepts either an `IDateTz` object or a Unix timestamp with an optional timezone.
|
|
17
|
+
- Throws an error if the provided timezone is invalid.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Properties
|
|
22
|
+
|
|
23
|
+
### Instance Properties
|
|
24
|
+
|
|
25
|
+
- `timestamp: number` — Milliseconds since Unix epoch (UTC).
|
|
26
|
+
- `timezone: string` — The timezone identifier (e.g., `"UTC"`, `"Europe/Rome"`).
|
|
27
|
+
|
|
28
|
+
### Static Properties
|
|
29
|
+
|
|
30
|
+
- `DateTz.defaultFormat: string` — Default string format pattern: `'YYYY-MM-DD HH:mm:ss'`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Getters
|
|
35
|
+
|
|
36
|
+
- `timezoneOffset: number` — Returns the timezone offset in minutes.
|
|
37
|
+
- `year: number` — Returns the full year.
|
|
38
|
+
- `month: number` — Returns the month (0–11).
|
|
39
|
+
- `day: number` — Returns the day of the month (1–31).
|
|
40
|
+
- `hour: number` — Returns the hour (0–23).
|
|
41
|
+
- `minute: number` — Returns the minute (0–59).
|
|
42
|
+
- `dayOfWeek: number` — Returns the day of the week (0–6).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Methods
|
|
47
|
+
|
|
48
|
+
### `compare(other: IDateTz): number`
|
|
49
|
+
|
|
50
|
+
Compares this instance with another `DateTz`. Throws if timezones differ.
|
|
51
|
+
|
|
52
|
+
### `isComparable(other: IDateTz): boolean`
|
|
53
|
+
|
|
54
|
+
Returns `true` if the two instances share the same timezone.
|
|
55
|
+
|
|
56
|
+
### `toString(pattern?: string): string`
|
|
57
|
+
|
|
58
|
+
Returns the string representation using the provided format.
|
|
59
|
+
|
|
60
|
+
#### Format Tokens
|
|
61
|
+
|
|
62
|
+
| Token | Meaning |
|
|
63
|
+
| ---------- | --------------- |
|
|
64
|
+
| YYYY, yyyy | Full year |
|
|
65
|
+
| YY, yy | Last 2 digits |
|
|
66
|
+
| MM | Month (01–12) |
|
|
67
|
+
| DD | Day (01–31) |
|
|
68
|
+
| HH | Hour (00–23) |
|
|
69
|
+
| hh | Hour (01–12) |
|
|
70
|
+
| mm | Minute (00–59) |
|
|
71
|
+
| ss | Second (00–59) |
|
|
72
|
+
| aa, AA | AM/PM marker |
|
|
73
|
+
| tz | Timezone string |
|
|
74
|
+
|
|
75
|
+
### `add(value: number, unit: 'minute' | 'hour' | 'day' | 'month' | 'year'): this`
|
|
76
|
+
|
|
77
|
+
Adds the given time to the instance.
|
|
78
|
+
|
|
79
|
+
### `set(value: number, unit: 'year' | 'month' | 'day' | 'hour' | 'minute'): this`
|
|
80
|
+
|
|
81
|
+
Sets a specific part of the date/time.
|
|
82
|
+
|
|
83
|
+
### `convertToTimezone(tz: string): this`
|
|
84
|
+
|
|
85
|
+
Changes the timezone of the instance in place.
|
|
86
|
+
|
|
87
|
+
### `cloneToTimezone(tz: string): DateTz`
|
|
88
|
+
|
|
89
|
+
Returns a new instance in the specified timezone.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Static Methods
|
|
94
|
+
|
|
95
|
+
### `DateTz.parse(dateString: string, pattern?: string, tz?: string): DateTz`
|
|
96
|
+
|
|
97
|
+
Parses a string to a `DateTz` instance.
|
|
98
|
+
|
|
99
|
+
### `DateTz.now(tz?: string): DateTz`
|
|
100
|
+
|
|
101
|
+
Returns the current date/time as a `DateTz` instance.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Utility Methods (Private)
|
|
106
|
+
|
|
107
|
+
- `stripSMs(timestamp: number): number` — Removes seconds and milliseconds.
|
|
108
|
+
- `isLeapYear(year: number): boolean` — Determines if the year is a leap year.
|
|
109
|
+
- `daysInYear(year: number): number` — Returns days in a year.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Example
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const dt = new DateTz(1719146400000, 'Europe/Rome');
|
|
117
|
+
console.log(dt.toString());
|
|
118
|
+
|
|
119
|
+
dt.add(1, 'day');
|
|
120
|
+
console.log(dt.day);
|
|
121
|
+
|
|
122
|
+
const parsed = DateTz.parse("2025-06-23 14:00:00", "YYYY-MM-DD HH:mm:ss", "Europe/Rome");
|
|
123
|
+
console.log(parsed.toString());
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Error Handling
|
|
129
|
+
|
|
130
|
+
- Throws on invalid timezone.
|
|
131
|
+
- Throws if trying to compare across timezones.
|
|
132
|
+
- Throws if parsing a 12-hour format without AM/PM marker.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Requirements
|
|
137
|
+
|
|
138
|
+
Requires:
|
|
139
|
+
- `timezones` object mapping timezone identifiers to UTC offsets.
|
|
140
|
+
- `daysPerMonth`, `MS_PER_DAY`, `MS_PER_HOUR`, `MS_PER_MINUTE`, `epochYear` constants.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Summary
|
|
145
|
+
|
|
146
|
+
`DateTz` is ideal for handling precise and consistent date/time values across multiple timezones with customizable formatting, parsing, and manipulation options.
|
|
147
|
+
|
package/date-tz.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IDateTz } from "./idate-tz";
|
|
2
|
+
export declare class DateTz implements IDateTz {
|
|
3
|
+
timestamp: number;
|
|
4
|
+
timezone: string;
|
|
5
|
+
private offsetCache?;
|
|
6
|
+
static defaultFormat: string;
|
|
7
|
+
constructor(value: IDateTz);
|
|
8
|
+
constructor(value: number, tz?: string);
|
|
9
|
+
get timezoneOffset(): import("./timezones").TimezoneOffset;
|
|
10
|
+
compare(other: IDateTz): number;
|
|
11
|
+
isComparable(other: IDateTz): boolean;
|
|
12
|
+
toString(): string;
|
|
13
|
+
toString(pattern: string): string;
|
|
14
|
+
add(value: number, unit: 'minute' | 'hour' | 'day' | 'month' | 'year'): this;
|
|
15
|
+
private _year;
|
|
16
|
+
private _month;
|
|
17
|
+
private _day;
|
|
18
|
+
private _hour;
|
|
19
|
+
private _minute;
|
|
20
|
+
private _dayOfWeek;
|
|
21
|
+
convertToTimezone(tz: string): this;
|
|
22
|
+
cloneToTimezone(tz: string): DateTz;
|
|
23
|
+
private stripSMs;
|
|
24
|
+
private invalidateOffsetCache;
|
|
25
|
+
private getOffsetSeconds;
|
|
26
|
+
private getOffsetInfo;
|
|
27
|
+
private computeOffsetInfo;
|
|
28
|
+
private getIntlOffsetSeconds;
|
|
29
|
+
set(value: number, unit: 'year' | 'month' | 'day' | 'hour' | 'minute'): this;
|
|
30
|
+
private isLeapYear;
|
|
31
|
+
private daysInYear;
|
|
32
|
+
static parse(dateString: string, pattern?: string, tz?: string): DateTz;
|
|
33
|
+
static now(tz?: string): DateTz;
|
|
34
|
+
get isDst(): boolean;
|
|
35
|
+
get year(): number;
|
|
36
|
+
get month(): number;
|
|
37
|
+
get day(): number;
|
|
38
|
+
get hour(): number;
|
|
39
|
+
get minute(): number;
|
|
40
|
+
get dayOfWeek(): number;
|
|
41
|
+
}
|
package/date-tz.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
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_MINUTE = 60000;
|
|
6
|
+
const MS_PER_HOUR = 3600000;
|
|
7
|
+
const MS_PER_DAY = 86400000;
|
|
8
|
+
const epochYear = 1970;
|
|
9
|
+
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
10
|
+
class DateTz {
|
|
11
|
+
constructor(value, tz) {
|
|
12
|
+
if (typeof value === 'object') {
|
|
13
|
+
this.timestamp = value.timestamp;
|
|
14
|
+
this.timezone = value.timezone || 'UTC';
|
|
15
|
+
if (!timezones_1.timezones[this.timezone]) {
|
|
16
|
+
throw new Error(`Invalid timezone: ${value.timezone}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.timezone = tz || 'UTC';
|
|
21
|
+
if (!timezones_1.timezones[this.timezone]) {
|
|
22
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
23
|
+
}
|
|
24
|
+
this.timestamp = this.stripSMs(value);
|
|
25
|
+
}
|
|
26
|
+
this.invalidateOffsetCache();
|
|
27
|
+
}
|
|
28
|
+
get timezoneOffset() {
|
|
29
|
+
return timezones_1.timezones[this.timezone];
|
|
30
|
+
}
|
|
31
|
+
compare(other) {
|
|
32
|
+
if (this.isComparable(other)) {
|
|
33
|
+
return this.timestamp - other.timestamp;
|
|
34
|
+
}
|
|
35
|
+
throw new Error('Cannot compare dates with different timezones');
|
|
36
|
+
}
|
|
37
|
+
isComparable(other) {
|
|
38
|
+
return this.timezone === other.timezone;
|
|
39
|
+
}
|
|
40
|
+
toString(pattern, locale) {
|
|
41
|
+
if (!pattern)
|
|
42
|
+
pattern = 'YYYY-MM-DD HH:mm:ss';
|
|
43
|
+
const offsetInfo = this.getOffsetInfo();
|
|
44
|
+
const offset = offsetInfo.offsetSeconds * 1000;
|
|
45
|
+
let remainingMs = this.timestamp + offset;
|
|
46
|
+
let year = epochYear;
|
|
47
|
+
while (true) {
|
|
48
|
+
const daysInYear = this.isLeapYear(year) ? 366 : 365;
|
|
49
|
+
const msInYear = daysInYear * MS_PER_DAY;
|
|
50
|
+
if (remainingMs >= msInYear) {
|
|
51
|
+
remainingMs -= msInYear;
|
|
52
|
+
year++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let month = 0;
|
|
59
|
+
while (month < 12) {
|
|
60
|
+
const daysInMonth = month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
61
|
+
const msInMonth = daysInMonth * MS_PER_DAY;
|
|
62
|
+
if (remainingMs >= msInMonth) {
|
|
63
|
+
remainingMs -= msInMonth;
|
|
64
|
+
month++;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const day = Math.floor(remainingMs / MS_PER_DAY) + 1;
|
|
71
|
+
remainingMs %= MS_PER_DAY;
|
|
72
|
+
const hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
73
|
+
remainingMs %= MS_PER_HOUR;
|
|
74
|
+
const minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
75
|
+
remainingMs %= MS_PER_MINUTE;
|
|
76
|
+
const second = Math.floor(remainingMs / 1000);
|
|
77
|
+
const pm = hour >= 12 ? 'PM' : 'AM';
|
|
78
|
+
const hour12 = hour % 12 || 12;
|
|
79
|
+
if (!locale)
|
|
80
|
+
locale = 'en';
|
|
81
|
+
let monthStr = new Date(year, month, 3).toLocaleString(locale || 'en', { month: 'long' });
|
|
82
|
+
monthStr = monthStr.charAt(0).toUpperCase() + monthStr.slice(1);
|
|
83
|
+
const tokens = {
|
|
84
|
+
YYYY: year,
|
|
85
|
+
YY: String(year).slice(-2),
|
|
86
|
+
yyyy: year.toString(),
|
|
87
|
+
yy: String(year).slice(-2),
|
|
88
|
+
MM: String(month + 1).padStart(2, '0'),
|
|
89
|
+
LM: monthStr,
|
|
90
|
+
DD: String(day).padStart(2, '0'),
|
|
91
|
+
HH: String(hour).padStart(2, '0'),
|
|
92
|
+
mm: String(minute).padStart(2, '0'),
|
|
93
|
+
ss: String(second).padStart(2, '0'),
|
|
94
|
+
aa: pm.toLowerCase(),
|
|
95
|
+
AA: pm,
|
|
96
|
+
hh: hour12.toString().padStart(2, '0'),
|
|
97
|
+
tz: this.timezone,
|
|
98
|
+
};
|
|
99
|
+
return pattern.replace(/YYYY|yyyy|YY|yy|MM|LM|DD|HH|hh|mm|ss|aa|AA|tz/g, (match) => tokens[match]);
|
|
100
|
+
}
|
|
101
|
+
add(value, unit) {
|
|
102
|
+
let remainingMs = this.timestamp;
|
|
103
|
+
let year = 1970;
|
|
104
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
105
|
+
remainingMs %= MS_PER_DAY;
|
|
106
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
107
|
+
remainingMs %= MS_PER_HOUR;
|
|
108
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
109
|
+
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
110
|
+
while (days >= this.daysInYear(year)) {
|
|
111
|
+
days -= this.daysInYear(year);
|
|
112
|
+
year++;
|
|
113
|
+
}
|
|
114
|
+
let month = 0;
|
|
115
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
116
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
117
|
+
month++;
|
|
118
|
+
}
|
|
119
|
+
let day = days + 1;
|
|
120
|
+
switch (unit) {
|
|
121
|
+
case 'minute':
|
|
122
|
+
minute += value;
|
|
123
|
+
break;
|
|
124
|
+
case 'hour':
|
|
125
|
+
hour += value;
|
|
126
|
+
break;
|
|
127
|
+
case 'day':
|
|
128
|
+
day += value;
|
|
129
|
+
break;
|
|
130
|
+
case 'month':
|
|
131
|
+
month += value;
|
|
132
|
+
break;
|
|
133
|
+
case 'year':
|
|
134
|
+
year += value;
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
138
|
+
}
|
|
139
|
+
while (minute >= 60) {
|
|
140
|
+
minute -= 60;
|
|
141
|
+
hour++;
|
|
142
|
+
}
|
|
143
|
+
while (hour >= 24) {
|
|
144
|
+
hour -= 24;
|
|
145
|
+
day++;
|
|
146
|
+
}
|
|
147
|
+
while (month >= 12) {
|
|
148
|
+
month -= 12;
|
|
149
|
+
year++;
|
|
150
|
+
}
|
|
151
|
+
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
152
|
+
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
153
|
+
month++;
|
|
154
|
+
if (month >= 12) {
|
|
155
|
+
month = 0;
|
|
156
|
+
year++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const newTimestamp = (() => {
|
|
160
|
+
let totalMs = 0;
|
|
161
|
+
for (let y = 1970; y < year; y++) {
|
|
162
|
+
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
163
|
+
}
|
|
164
|
+
for (let m = 0; m < month; m++) {
|
|
165
|
+
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
166
|
+
}
|
|
167
|
+
totalMs += (day - 1) * MS_PER_DAY;
|
|
168
|
+
totalMs += hour * MS_PER_HOUR;
|
|
169
|
+
totalMs += minute * MS_PER_MINUTE;
|
|
170
|
+
totalMs += second * 1000;
|
|
171
|
+
return totalMs;
|
|
172
|
+
})();
|
|
173
|
+
this.timestamp = newTimestamp;
|
|
174
|
+
this.invalidateOffsetCache();
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
_year(considerDst = false) {
|
|
178
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
179
|
+
let remainingMs = this.timestamp + offset;
|
|
180
|
+
let year = 1970;
|
|
181
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
182
|
+
while (days >= this.daysInYear(year)) {
|
|
183
|
+
days -= this.daysInYear(year);
|
|
184
|
+
year++;
|
|
185
|
+
}
|
|
186
|
+
return year;
|
|
187
|
+
}
|
|
188
|
+
_month(considerDst = false) {
|
|
189
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
190
|
+
let remainingMs = this.timestamp + offset;
|
|
191
|
+
let year = 1970;
|
|
192
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
193
|
+
while (days >= this.daysInYear(year)) {
|
|
194
|
+
days -= this.daysInYear(year);
|
|
195
|
+
year++;
|
|
196
|
+
}
|
|
197
|
+
let month = 0;
|
|
198
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
199
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
200
|
+
month++;
|
|
201
|
+
}
|
|
202
|
+
return month;
|
|
203
|
+
}
|
|
204
|
+
_day(considerDst = false) {
|
|
205
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
206
|
+
let remainingMs = this.timestamp + offset;
|
|
207
|
+
let year = 1970;
|
|
208
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
209
|
+
while (days >= this.daysInYear(year)) {
|
|
210
|
+
days -= this.daysInYear(year);
|
|
211
|
+
year++;
|
|
212
|
+
}
|
|
213
|
+
let month = 0;
|
|
214
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
215
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
216
|
+
month++;
|
|
217
|
+
}
|
|
218
|
+
return days + 1;
|
|
219
|
+
}
|
|
220
|
+
_hour(considerDst = false) {
|
|
221
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
222
|
+
let remainingMs = this.timestamp + offset;
|
|
223
|
+
remainingMs %= MS_PER_DAY;
|
|
224
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
225
|
+
return hour;
|
|
226
|
+
}
|
|
227
|
+
_minute(considerDst = false) {
|
|
228
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
229
|
+
let remainingMs = this.timestamp + offset;
|
|
230
|
+
remainingMs %= MS_PER_HOUR;
|
|
231
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
232
|
+
return minute;
|
|
233
|
+
}
|
|
234
|
+
_dayOfWeek(considerDst = false) {
|
|
235
|
+
const offset = this.getOffsetSeconds(considerDst) * 1000;
|
|
236
|
+
let remainingMs = this.timestamp + offset;
|
|
237
|
+
const date = new Date(remainingMs);
|
|
238
|
+
return date.getDay();
|
|
239
|
+
}
|
|
240
|
+
convertToTimezone(tz) {
|
|
241
|
+
if (!timezones_1.timezones[tz]) {
|
|
242
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
243
|
+
}
|
|
244
|
+
this.timezone = tz;
|
|
245
|
+
this.invalidateOffsetCache();
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
cloneToTimezone(tz) {
|
|
249
|
+
if (!timezones_1.timezones[tz]) {
|
|
250
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
251
|
+
}
|
|
252
|
+
const clone = new DateTz(this);
|
|
253
|
+
clone.timezone = tz;
|
|
254
|
+
clone.invalidateOffsetCache();
|
|
255
|
+
return clone;
|
|
256
|
+
}
|
|
257
|
+
stripSMs(timestamp) {
|
|
258
|
+
const days = Math.floor(timestamp / MS_PER_DAY);
|
|
259
|
+
const remainingAfterDays = timestamp % MS_PER_DAY;
|
|
260
|
+
const hours = Math.floor(remainingAfterDays / MS_PER_HOUR);
|
|
261
|
+
const remainingAfterHours = remainingAfterDays % MS_PER_HOUR;
|
|
262
|
+
const minutes = Math.floor(remainingAfterHours / MS_PER_MINUTE);
|
|
263
|
+
return days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE;
|
|
264
|
+
}
|
|
265
|
+
invalidateOffsetCache() {
|
|
266
|
+
this.offsetCache = undefined;
|
|
267
|
+
}
|
|
268
|
+
getOffsetSeconds(considerDst) {
|
|
269
|
+
const tzInfo = timezones_1.timezones[this.timezone];
|
|
270
|
+
if (!tzInfo) {
|
|
271
|
+
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
272
|
+
}
|
|
273
|
+
if (!considerDst) {
|
|
274
|
+
return tzInfo.sdt;
|
|
275
|
+
}
|
|
276
|
+
return this.getOffsetInfo().offsetSeconds;
|
|
277
|
+
}
|
|
278
|
+
getOffsetInfo() {
|
|
279
|
+
if (this.offsetCache && this.offsetCache.timestamp === this.timestamp) {
|
|
280
|
+
return this.offsetCache.info;
|
|
281
|
+
}
|
|
282
|
+
const info = this.computeOffsetInfo();
|
|
283
|
+
this.offsetCache = { timestamp: this.timestamp, info };
|
|
284
|
+
return info;
|
|
285
|
+
}
|
|
286
|
+
computeOffsetInfo() {
|
|
287
|
+
const tzInfo = timezones_1.timezones[this.timezone];
|
|
288
|
+
if (!tzInfo) {
|
|
289
|
+
throw new Error(`Invalid timezone: ${this.timezone}`);
|
|
290
|
+
}
|
|
291
|
+
if (tzInfo.dst === tzInfo.sdt) {
|
|
292
|
+
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
293
|
+
}
|
|
294
|
+
const actual = this.getIntlOffsetSeconds(this.timestamp);
|
|
295
|
+
if (actual !== null) {
|
|
296
|
+
if (actual !== tzInfo.sdt && actual !== tzInfo.dst) {
|
|
297
|
+
return { offsetSeconds: actual, isDst: actual > tzInfo.sdt };
|
|
298
|
+
}
|
|
299
|
+
return { offsetSeconds: actual, isDst: actual === tzInfo.dst };
|
|
300
|
+
}
|
|
301
|
+
return { offsetSeconds: tzInfo.sdt, isDst: false };
|
|
302
|
+
}
|
|
303
|
+
getIntlOffsetSeconds(timestamp) {
|
|
304
|
+
try {
|
|
305
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
306
|
+
timeZone: this.timezone,
|
|
307
|
+
hour12: false,
|
|
308
|
+
year: 'numeric',
|
|
309
|
+
month: '2-digit',
|
|
310
|
+
day: '2-digit',
|
|
311
|
+
hour: '2-digit',
|
|
312
|
+
minute: '2-digit',
|
|
313
|
+
second: '2-digit'
|
|
314
|
+
});
|
|
315
|
+
const parts = formatter.formatToParts(new Date(timestamp));
|
|
316
|
+
const lookup = (type) => {
|
|
317
|
+
const part = parts.find(p => p.type === type);
|
|
318
|
+
if (!part) {
|
|
319
|
+
throw new Error(`Missing part ${type}`);
|
|
320
|
+
}
|
|
321
|
+
return Number(part.value);
|
|
322
|
+
};
|
|
323
|
+
const adjusted = Date.UTC(lookup('year'), lookup('month') - 1, lookup('day'), lookup('hour'), lookup('minute'), lookup('second'));
|
|
324
|
+
return Math.round((adjusted - timestamp) / 1000);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
set(value, unit) {
|
|
331
|
+
let remainingMs = this.timestamp;
|
|
332
|
+
let year = 1970;
|
|
333
|
+
let days = Math.floor(remainingMs / MS_PER_DAY);
|
|
334
|
+
remainingMs %= MS_PER_DAY;
|
|
335
|
+
let hour = Math.floor(remainingMs / MS_PER_HOUR);
|
|
336
|
+
remainingMs %= MS_PER_HOUR;
|
|
337
|
+
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
|
|
338
|
+
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
|
|
339
|
+
while (days >= this.daysInYear(year)) {
|
|
340
|
+
days -= this.daysInYear(year);
|
|
341
|
+
year++;
|
|
342
|
+
}
|
|
343
|
+
let month = 0;
|
|
344
|
+
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
345
|
+
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
346
|
+
month++;
|
|
347
|
+
}
|
|
348
|
+
let day = days + 1;
|
|
349
|
+
switch (unit) {
|
|
350
|
+
case 'year':
|
|
351
|
+
year = value;
|
|
352
|
+
break;
|
|
353
|
+
case 'month':
|
|
354
|
+
month = value - 1;
|
|
355
|
+
break;
|
|
356
|
+
case 'day':
|
|
357
|
+
day = value;
|
|
358
|
+
break;
|
|
359
|
+
case 'hour':
|
|
360
|
+
hour = value;
|
|
361
|
+
break;
|
|
362
|
+
case 'minute':
|
|
363
|
+
minute = value;
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
367
|
+
}
|
|
368
|
+
while (month >= 12) {
|
|
369
|
+
month -= 12;
|
|
370
|
+
year++;
|
|
371
|
+
}
|
|
372
|
+
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
|
|
373
|
+
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
|
|
374
|
+
month++;
|
|
375
|
+
if (month >= 12) {
|
|
376
|
+
month = 0;
|
|
377
|
+
year++;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const newTimestamp = (() => {
|
|
381
|
+
let totalMs = 0;
|
|
382
|
+
for (let y = 1970; y < year; y++) {
|
|
383
|
+
totalMs += this.daysInYear(y) * MS_PER_DAY;
|
|
384
|
+
}
|
|
385
|
+
for (let m = 0; m < month; m++) {
|
|
386
|
+
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
|
|
387
|
+
}
|
|
388
|
+
totalMs += (day - 1) * MS_PER_DAY;
|
|
389
|
+
totalMs += hour * MS_PER_HOUR;
|
|
390
|
+
totalMs += minute * MS_PER_MINUTE;
|
|
391
|
+
totalMs += second * 1000;
|
|
392
|
+
return totalMs;
|
|
393
|
+
})();
|
|
394
|
+
this.timestamp = newTimestamp;
|
|
395
|
+
this.invalidateOffsetCache();
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
398
|
+
isLeapYear(year) {
|
|
399
|
+
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
400
|
+
}
|
|
401
|
+
daysInYear(year) {
|
|
402
|
+
return this.isLeapYear(year) ? 366 : 365;
|
|
403
|
+
}
|
|
404
|
+
static parse(dateString, pattern, tz) {
|
|
405
|
+
if (!pattern)
|
|
406
|
+
pattern = DateTz.defaultFormat;
|
|
407
|
+
if (!tz)
|
|
408
|
+
tz = 'UTC';
|
|
409
|
+
if (!timezones_1.timezones[tz]) {
|
|
410
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
411
|
+
}
|
|
412
|
+
if (pattern.includes('hh') && (!pattern.includes('aa') || !pattern.includes('AA'))) {
|
|
413
|
+
throw new Error('AM/PM marker (aa or AA) is required when using 12-hour format (hh)');
|
|
414
|
+
}
|
|
415
|
+
const regex = /YYYY|yyyy|MM|DD|HH|hh|mm|ss|aa|AA/g;
|
|
416
|
+
const dateComponents = {
|
|
417
|
+
YYYY: 1970,
|
|
418
|
+
yyyy: 1970,
|
|
419
|
+
MM: 0,
|
|
420
|
+
DD: 0,
|
|
421
|
+
HH: 0,
|
|
422
|
+
hh: 0,
|
|
423
|
+
aa: 'am',
|
|
424
|
+
AA: "AM",
|
|
425
|
+
mm: 0,
|
|
426
|
+
ss: 0,
|
|
427
|
+
};
|
|
428
|
+
let match;
|
|
429
|
+
let index = 0;
|
|
430
|
+
while ((match = regex.exec(pattern)) !== null) {
|
|
431
|
+
const token = match[0];
|
|
432
|
+
const value = parseInt(dateString.substring(match.index, match.index + token.length), 10);
|
|
433
|
+
dateComponents[token] = value;
|
|
434
|
+
index += token.length + 1;
|
|
435
|
+
}
|
|
436
|
+
const year = dateComponents.YYYY || dateComponents.yyyy;
|
|
437
|
+
const month = dateComponents.MM - 1;
|
|
438
|
+
const day = dateComponents.DD;
|
|
439
|
+
let hour = 0;
|
|
440
|
+
const ampm = (dateComponents.a || dateComponents.A);
|
|
441
|
+
if (ampm) {
|
|
442
|
+
hour = ampm.toUpperCase() === 'AM' ? dateComponents.hh : dateComponents.hh + 12;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
hour = dateComponents.HH;
|
|
446
|
+
}
|
|
447
|
+
const minute = dateComponents.mm;
|
|
448
|
+
const second = dateComponents.ss;
|
|
449
|
+
const daysInYear = (year) => (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 366 : 365;
|
|
450
|
+
const daysInMonth = (year, month) => month === 1 && daysInYear(year) === 366 ? 29 : daysPerMonth[month];
|
|
451
|
+
let timestamp = 0;
|
|
452
|
+
for (let y = 1970; y < year; y++) {
|
|
453
|
+
timestamp += daysInYear(y) * MS_PER_DAY;
|
|
454
|
+
}
|
|
455
|
+
for (let m = 0; m < month; m++) {
|
|
456
|
+
timestamp += daysInMonth(year, m) * MS_PER_DAY;
|
|
457
|
+
}
|
|
458
|
+
timestamp += (day - 1) * MS_PER_DAY;
|
|
459
|
+
timestamp += hour * MS_PER_HOUR;
|
|
460
|
+
timestamp += minute * MS_PER_MINUTE;
|
|
461
|
+
timestamp += second * 1000;
|
|
462
|
+
const offset = (timezones_1.timezones[tz].sdt) * 1000;
|
|
463
|
+
let remainingMs = timestamp - offset;
|
|
464
|
+
const date = new DateTz(remainingMs, tz);
|
|
465
|
+
date.timestamp -= date.isDst ? (timezones_1.timezones[tz].dst - timezones_1.timezones[tz].sdt) * 1000 : 0;
|
|
466
|
+
date.invalidateOffsetCache();
|
|
467
|
+
return date;
|
|
468
|
+
}
|
|
469
|
+
static now(tz) {
|
|
470
|
+
if (!tz)
|
|
471
|
+
tz = 'UTC';
|
|
472
|
+
const timezone = timezones_1.timezones[tz];
|
|
473
|
+
if (!timezone) {
|
|
474
|
+
throw new Error(`Invalid timezone: ${tz}`);
|
|
475
|
+
}
|
|
476
|
+
const date = new DateTz(Date.now(), tz);
|
|
477
|
+
return date;
|
|
478
|
+
}
|
|
479
|
+
get isDst() {
|
|
480
|
+
return this.getOffsetInfo().isDst;
|
|
481
|
+
}
|
|
482
|
+
get year() {
|
|
483
|
+
return this._year(true);
|
|
484
|
+
}
|
|
485
|
+
get month() {
|
|
486
|
+
return this._month(true);
|
|
487
|
+
}
|
|
488
|
+
get day() {
|
|
489
|
+
return this._day(true);
|
|
490
|
+
}
|
|
491
|
+
get hour() {
|
|
492
|
+
return this._hour(true);
|
|
493
|
+
}
|
|
494
|
+
get minute() {
|
|
495
|
+
return this._minute(true);
|
|
496
|
+
}
|
|
497
|
+
get dayOfWeek() {
|
|
498
|
+
return this._dayOfWeek(true);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
exports.DateTz = DateTz;
|
|
502
|
+
DateTz.defaultFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
503
|
+
//# sourceMappingURL=date-tz.js.map
|