@lbd-sh/date-tz 1.0.1 → 1.0.3
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 +252 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,147 +1,313 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Date TZ
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Powerful, dependency-free timezone utilities for JavaScript and TypeScript. `DateTz` keeps minute-precision timestamps aligned with IANA timezones, detects daylight-saving transitions automatically, and pairs a tiny API with comprehensive TypeScript definitions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## TL;DR
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```ts
|
|
8
|
+
import { DateTz } from '@lbd-sh/date-tz';
|
|
9
|
+
|
|
10
|
+
const rome = new DateTz(Date.UTC(2025, 5, 15, 7, 30), 'Europe/Rome');
|
|
11
|
+
|
|
12
|
+
rome.toString(); // "2025-06-15 09:30:00"
|
|
13
|
+
rome.toString('DD LM YYYY HH:mm tz', 'en'); // "15 June 2025 09:30 Europe/Rome"
|
|
14
|
+
rome.isDst; // true
|
|
15
|
+
|
|
16
|
+
const nyc = rome.cloneToTimezone('America/New_York');
|
|
17
|
+
nyc.toString('YYYY-MM-DD HH:mm tz'); // "2025-06-15 03:30 America/New_York"
|
|
18
|
+
|
|
19
|
+
rome.add(2, 'day').set(11, 'hour');
|
|
20
|
+
rome.toString('YYYY-MM-DD HH:mm'); // "2025-06-17 11:30"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @lbd-sh/date-tz
|
|
27
|
+
# or
|
|
28
|
+
yarn add @lbd-sh/date-tz
|
|
29
|
+
# or
|
|
30
|
+
pnpm add @lbd-sh/date-tz
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Why DateTz?
|
|
34
|
+
|
|
35
|
+
- **Predictable arithmetic** – timestamps are truncated to minutes to avoid millisecond drift.
|
|
36
|
+
- **DST aware** – offsets come from the bundled `timezones` map and are verified via `Intl.DateTimeFormat` when available.
|
|
37
|
+
- **Rich formatting/parsing** – reuse familiar tokens (`YYYY`, `MM`, `hh`, `AA`, `tz`, `LM` for locale month names, and more).
|
|
38
|
+
- **Lean footprint** – no runtime dependencies, CommonJS output plus type declarations.
|
|
39
|
+
- **Ergonomic conversions** – move or clone dates across timezones in one call.
|
|
40
|
+
|
|
41
|
+
## API Surface at a Glance
|
|
42
|
+
|
|
43
|
+
| Member | Description |
|
|
44
|
+
| ------ | ----------- |
|
|
45
|
+
| `new DateTz(value, tz?)` | Accepts a timestamp or an `IDateTz` compliant object plus an optional timezone id. |
|
|
46
|
+
| `DateTz.now(tz?)` | Returns the current moment in the given timezone (default `UTC`). |
|
|
47
|
+
| `DateTz.parse(string, pattern?, tz?)` | Creates a date from a formatted string. |
|
|
48
|
+
| `DateTz.defaultFormat` | Global default pattern used by `toString()` with no args. |
|
|
49
|
+
| Instance getters | `year`, `month`, `day`, `hour`, `minute`, `dayOfWeek`, `isDst`, `timezoneOffset`. |
|
|
50
|
+
| Instance methods | `toString(pattern?, locale?)`, `compare(other)`, `isComparable(other)`, `add(value, unit)`, `set(value, unit)`, `convertToTimezone(tz)`, `cloneToTimezone(tz)`. |
|
|
51
|
+
|
|
52
|
+
## Pattern Tokens
|
|
53
|
+
|
|
54
|
+
| Token | Meaning | Example |
|
|
55
|
+
| ----- | ------- | ------- |
|
|
56
|
+
| `YYYY`, `yyyy` | Four digit year | `2025` |
|
|
57
|
+
| `YY`, `yy` | Two digit year | `25` |
|
|
58
|
+
| `MM` | Month (01–12) | `06` |
|
|
59
|
+
| `LM` | Locale month name (capitalised) | `June` |
|
|
60
|
+
| `DD` | Day of month (01–31) | `15` |
|
|
61
|
+
| `HH` | Hour (00–23) | `09` |
|
|
62
|
+
| `hh` | Hour (01–12) | `03` |
|
|
63
|
+
| `mm` | Minute (00–59) | `30` |
|
|
64
|
+
| `ss` | Second (00–59) | `00` |
|
|
65
|
+
| `aa` | `am`/`pm` marker | `am` |
|
|
66
|
+
| `AA` | `AM`/`PM` marker | `PM` |
|
|
67
|
+
| `tz` | Timezone identifier | `Europe/Rome` |
|
|
68
|
+
|
|
69
|
+
> Need literal text? Keep it inside square brackets. Example: `YYYY-MM-DD[ @ ]HH:mm` → `2025-06-15 @ 09:30`. Characters inside brackets remain unchanged.
|
|
70
|
+
|
|
71
|
+
## Creating Dates
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { DateTz, IDateTz } from '@lbd-sh/date-tz';
|
|
75
|
+
|
|
76
|
+
// From a unix timestamp (milliseconds)
|
|
77
|
+
const utcMeeting = new DateTz(Date.UTC(2025, 0, 1, 12, 0), 'UTC');
|
|
78
|
+
|
|
79
|
+
// From another DateTz-like object
|
|
80
|
+
const seed: IDateTz = { timestamp: Date.now(), timezone: 'Asia/Tokyo' };
|
|
81
|
+
const tokyo = new DateTz(seed);
|
|
8
82
|
|
|
9
|
-
|
|
83
|
+
// Using the helper
|
|
84
|
+
const laNow = DateTz.now('America/Los_Angeles');
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Working With Plain Date Objects
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const native = new Date();
|
|
91
|
+
const madrid = new DateTz(native.getTime(), 'Europe/Madrid');
|
|
92
|
+
|
|
93
|
+
// Alternatively, keep everything UTC and convert when needed
|
|
94
|
+
const fromUtc = new DateTz(native.getTime(), 'UTC').cloneToTimezone('Europe/Madrid');
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Formatting Showcases
|
|
10
98
|
|
|
11
99
|
```ts
|
|
12
|
-
new DateTz(
|
|
13
|
-
|
|
100
|
+
const order = new DateTz(Date.UTC(2025, 10, 5, 16, 45), 'Europe/Paris');
|
|
101
|
+
|
|
102
|
+
order.toString(); // "2025-11-05 17:45:00"
|
|
103
|
+
order.toString('DD/MM/YYYY HH:mm'); // "05/11/2025 17:45"
|
|
104
|
+
order.toString('LM DD, YYYY hh:mm aa', 'fr'); // "Novembre 05, 2025 05:45 pm"
|
|
105
|
+
order.toString('[Order timezone:] tz'); // "Order timezone: Europe/Paris"
|
|
14
106
|
```
|
|
15
107
|
|
|
16
|
-
-
|
|
17
|
-
|
|
108
|
+
### Locale-sensitive Month Names
|
|
109
|
+
|
|
110
|
+
`LM` maps to `new Date(year, month, 3).toLocaleString(locale, { month: 'long' })` ensuring accurate localisation without full `Intl` formatting.
|
|
18
111
|
|
|
19
|
-
|
|
112
|
+
## Parsing Scenarios
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Standard 24h format
|
|
116
|
+
const release = DateTz.parse('2025-09-01 02:30', 'YYYY-MM-DD HH:mm', 'Asia/Singapore');
|
|
117
|
+
|
|
118
|
+
// 12h format (requires AM/PM marker)
|
|
119
|
+
const dinner = DateTz.parse('03-18-2025 07:15 PM', 'MM-DD-YYYY hh:mm AA', 'America/New_York');
|
|
120
|
+
|
|
121
|
+
// Custom tokens with literal text
|
|
122
|
+
const promo = DateTz.parse('Sale closes 2025/03/31 @ 23:59', 'Sale closes YYYY/MM/DD [@] HH:mm', 'UTC');
|
|
123
|
+
```
|
|
20
124
|
|
|
21
|
-
|
|
125
|
+
Parsing throws when the timezone id is missing or invalid, or when pattern/token combos are incompatible (for example `hh` without `aa`/`AA`).
|
|
22
126
|
|
|
23
|
-
|
|
127
|
+
## Arithmetic Cookbook
|
|
24
128
|
|
|
25
|
-
|
|
26
|
-
|
|
129
|
+
```ts
|
|
130
|
+
const sprint = new DateTz(Date.UTC(2025, 1, 1, 9, 0), 'Europe/Amsterdam');
|
|
27
131
|
|
|
28
|
-
|
|
132
|
+
sprint.add(2, 'week'); // ❌ week not supported
|
|
133
|
+
// Use compositions instead:
|
|
134
|
+
sprint.add(14, 'day');
|
|
29
135
|
|
|
30
|
-
|
|
136
|
+
// Move to first business day of next month
|
|
137
|
+
sprint.set(sprint.month + 1, 'month');
|
|
138
|
+
sprint.set(1, 'day');
|
|
139
|
+
while ([0, 6].includes(sprint.dayOfWeek)) {
|
|
140
|
+
sprint.add(1, 'day');
|
|
141
|
+
}
|
|
31
142
|
|
|
32
|
-
|
|
143
|
+
// Shift to 10:00 local time
|
|
144
|
+
sprint.set(10, 'hour').set(0, 'minute');
|
|
145
|
+
```
|
|
33
146
|
|
|
34
|
-
|
|
147
|
+
`add` accepts `minute`, `hour`, `day`, `month`, `year`. Compose multiple calls for complex adjustments. Overflows and leap years are handled automatically.
|
|
35
148
|
|
|
36
|
-
|
|
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).
|
|
149
|
+
### Immutable Patterns
|
|
43
150
|
|
|
44
|
-
|
|
151
|
+
`add`, `set`, and `convertToTimezone` mutate the instance. Use `cloneToTimezone` or spread semantics when immutability is preferred:
|
|
45
152
|
|
|
46
|
-
|
|
153
|
+
```ts
|
|
154
|
+
const base = DateTz.now('UTC');
|
|
155
|
+
const iteration = new DateTz(base);
|
|
156
|
+
iteration.add(1, 'day');
|
|
157
|
+
```
|
|
47
158
|
|
|
48
|
-
|
|
159
|
+
## Comparing & Sorting
|
|
49
160
|
|
|
50
|
-
|
|
161
|
+
```ts
|
|
162
|
+
const slots = [
|
|
163
|
+
new DateTz(Date.UTC(2025, 6, 10, 8, 0), 'Europe/Rome'),
|
|
164
|
+
new DateTz(Date.UTC(2025, 6, 10, 9, 0), 'Europe/Rome'),
|
|
165
|
+
new DateTz(Date.UTC(2025, 6, 9, 18, 0), 'Europe/Rome'),
|
|
166
|
+
];
|
|
51
167
|
|
|
52
|
-
|
|
168
|
+
slots.sort((a, b) => a.compare(b));
|
|
169
|
+
```
|
|
53
170
|
|
|
54
|
-
|
|
171
|
+
`compare` throws if timezones differ:
|
|
55
172
|
|
|
56
|
-
|
|
173
|
+
```ts
|
|
174
|
+
const rome = DateTz.now('Europe/Rome');
|
|
175
|
+
const ny = DateTz.now('America/New_York');
|
|
57
176
|
|
|
58
|
-
|
|
177
|
+
if (!rome.isComparable(ny)) {
|
|
178
|
+
ny.convertToTimezone(rome.timezone);
|
|
179
|
+
}
|
|
59
180
|
|
|
60
|
-
|
|
181
|
+
rome.compare(ny);
|
|
182
|
+
```
|
|
61
183
|
|
|
62
|
-
|
|
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 |
|
|
184
|
+
## Timezone Conversion Deep Dive
|
|
74
185
|
|
|
75
|
-
|
|
186
|
+
```ts
|
|
187
|
+
const flight = new DateTz(Date.UTC(2025, 3, 28, 20, 0), 'Europe/London');
|
|
76
188
|
|
|
77
|
-
|
|
189
|
+
const takeoff = flight.cloneToTimezone('America/Los_Angeles');
|
|
190
|
+
const landing = flight.cloneToTimezone('Asia/Tokyo');
|
|
78
191
|
|
|
79
|
-
|
|
192
|
+
takeoff.isDst; // false (London DST might not have started yet)
|
|
193
|
+
landing.isDst; // true or false depending on Tokyo rules
|
|
194
|
+
```
|
|
80
195
|
|
|
81
|
-
|
|
196
|
+
- `convertToTimezone` mutates the instance.
|
|
197
|
+
- `cloneToTimezone` returns a new instance.
|
|
198
|
+
- Both refresh the cached offset and leverage the `Intl` API to detect real-world DST offsets (falling back to the static map).
|
|
82
199
|
|
|
83
|
-
###
|
|
200
|
+
### DST Transition Example
|
|
84
201
|
|
|
85
|
-
|
|
202
|
+
```ts
|
|
203
|
+
const dstEdge = new DateTz(Date.UTC(2025, 2, 30, 0, 30), 'Europe/Rome'); // Night DST starts
|
|
86
204
|
|
|
87
|
-
|
|
205
|
+
dstEdge.toString(); // "2025-03-30 01:30:00"
|
|
206
|
+
dstEdge.add(1, 'hour');
|
|
207
|
+
dstEdge.toString(); // "2025-03-30 03:30:00" (skips 02:30 automatically)
|
|
208
|
+
dstEdge.isDst; // true
|
|
209
|
+
```
|
|
88
210
|
|
|
89
|
-
|
|
211
|
+
## Working with Collections
|
|
90
212
|
|
|
91
|
-
|
|
213
|
+
```ts
|
|
214
|
+
const timeline = [
|
|
215
|
+
DateTz.parse('2025-06-15 09:30', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
|
|
216
|
+
DateTz.parse('2025-06-15 10:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
|
|
217
|
+
DateTz.parse('2025-06-15 09:45', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const sorted = timeline.slice().sort((a, b) => a.compare(b));
|
|
221
|
+
|
|
222
|
+
// Group by day
|
|
223
|
+
const byDate = sorted.reduce<Record<string, DateTz[]>>((acc, slot) => {
|
|
224
|
+
const key = slot.toString('YYYY-MM-DD');
|
|
225
|
+
(acc[key] ||= []).push(slot);
|
|
226
|
+
return acc;
|
|
227
|
+
}, {});
|
|
228
|
+
```
|
|
92
229
|
|
|
93
|
-
##
|
|
230
|
+
## Serialization Tips
|
|
94
231
|
|
|
95
|
-
|
|
232
|
+
```ts
|
|
233
|
+
const payload = {
|
|
234
|
+
createdAt: DateTz.now('UTC').toString(),
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
};
|
|
96
237
|
|
|
97
|
-
|
|
238
|
+
// Later...
|
|
239
|
+
const restored = new DateTz(payload.timestamp, 'UTC');
|
|
240
|
+
```
|
|
98
241
|
|
|
99
|
-
|
|
242
|
+
Prefer storing the timestamp (UTC) and timezone id. When deserialising, feed both back into the constructor for deterministic results.
|
|
100
243
|
|
|
101
|
-
|
|
244
|
+
## Extending the Timezone Map
|
|
102
245
|
|
|
103
|
-
|
|
246
|
+
```ts
|
|
247
|
+
import { timezones } from '@lbd-sh/date-tz';
|
|
104
248
|
|
|
105
|
-
|
|
249
|
+
timezones['Custom/Island'] = { sdt: 32_400, dst: 36_000 }; // Offsets in seconds
|
|
106
250
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- `daysInYear(year: number): number` — Returns days in a year.
|
|
251
|
+
const island = new DateTz(Date.now(), 'Custom/Island');
|
|
252
|
+
```
|
|
110
253
|
|
|
111
|
-
|
|
254
|
+
> Ensure keys follow IANA naming conventions. Offsets are seconds from UTC (negative for west, positive for east).
|
|
112
255
|
|
|
113
|
-
##
|
|
256
|
+
## TypeScript Excellence
|
|
114
257
|
|
|
115
258
|
```ts
|
|
116
|
-
|
|
117
|
-
|
|
259
|
+
import type { IDateTz } from '@lbd-sh/date-tz';
|
|
260
|
+
|
|
261
|
+
function normalise(date: IDateTz): IDateTz {
|
|
262
|
+
const instance = new DateTz(date);
|
|
263
|
+
return instance.cloneToTimezone('UTC');
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`IDateTz` lets you accept plain objects from APIs while still benefiting from compile-time guarantees.
|
|
118
268
|
|
|
119
|
-
|
|
120
|
-
console.log(dt.day);
|
|
269
|
+
## Interoperability Patterns
|
|
121
270
|
|
|
122
|
-
|
|
123
|
-
|
|
271
|
+
### With Fetch / APIs
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
const response = await fetch('/api/events');
|
|
275
|
+
const body: { timestamp: number; timezone: string }[] = await response.json();
|
|
276
|
+
|
|
277
|
+
const events = body.map(({ timestamp, timezone }) => new DateTz(timestamp, timezone));
|
|
124
278
|
```
|
|
125
279
|
|
|
126
|
-
|
|
280
|
+
### With Cron-like Scheduling
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const job = new DateTz(Date.UTC(2025, 5, 1, 5, 0), 'America/New_York');
|
|
284
|
+
|
|
285
|
+
while (job.compare(DateTz.now('America/New_York')) < 0) {
|
|
286
|
+
job.add(1, 'day');
|
|
287
|
+
}
|
|
288
|
+
```
|
|
127
289
|
|
|
128
|
-
##
|
|
290
|
+
## Packaging Notes
|
|
129
291
|
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
292
|
+
- The published CommonJS bundle lives in `dist/index.js`; declarations and maps ship alongside (`index.d.ts`, `*.map`).
|
|
293
|
+
- `package.json` exposes `main` and `types` from the compiled output, so consumers do not need to run TypeScript.
|
|
294
|
+
- Build locally with:
|
|
133
295
|
|
|
134
|
-
|
|
296
|
+
```bash
|
|
297
|
+
npm install
|
|
298
|
+
npm run build
|
|
299
|
+
```
|
|
135
300
|
|
|
136
|
-
|
|
301
|
+
- The GitHub Action (`.github/workflows/production.yaml`) produces the build, versions using GitVersion, and publishes to npm as `@lbd-sh/date-tz`.
|
|
137
302
|
|
|
138
|
-
|
|
139
|
-
- `timezones` object mapping timezone identifiers to UTC offsets.
|
|
140
|
-
- `daysPerMonth`, `MS_PER_DAY`, `MS_PER_HOUR`, `MS_PER_MINUTE`, `epochYear` constants.
|
|
303
|
+
## Roadmap Ideas
|
|
141
304
|
|
|
142
|
-
|
|
305
|
+
- Add optional ESM build targets.
|
|
306
|
+
- Ship a companion utilities layer (diffs, ranges, calendars).
|
|
307
|
+
- Expand token set with `W` (ISO week) and `ddd` (weekday short names).
|
|
143
308
|
|
|
144
|
-
|
|
309
|
+
Contributions and feature requests are welcome — open an issue at [github.com/lbdsh/date-tz/issues](https://github.com/lbdsh/date-tz/issues).
|
|
145
310
|
|
|
146
|
-
|
|
311
|
+
## License
|
|
147
312
|
|
|
313
|
+
ISC © lbd-sh
|