@lbd-sh/date-tz 1.0.2 → 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 +227 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# Date TZ
|
|
2
2
|
|
|
3
|
-
Powerful
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
```
|
|
13
22
|
|
|
14
23
|
## Installation
|
|
15
24
|
|
|
@@ -17,179 +26,287 @@ Powerful timezone-aware date utilities for JavaScript and TypeScript. `DateTz` k
|
|
|
17
26
|
npm install @lbd-sh/date-tz
|
|
18
27
|
# or
|
|
19
28
|
yarn add @lbd-sh/date-tz
|
|
29
|
+
# or
|
|
30
|
+
pnpm add @lbd-sh/date-tz
|
|
20
31
|
```
|
|
21
32
|
|
|
22
|
-
##
|
|
33
|
+
## Why DateTz?
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
const meeting = new DateTz(Date.UTC(2025, 2, 1, 8, 15), 'Europe/Rome');
|
|
41
|
+
## API Surface at a Glance
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
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)`. |
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
meeting.add(1, 'day').add(2, 'hour');
|
|
35
|
-
const nyc = meeting.cloneToTimezone('America/New_York');
|
|
52
|
+
## Pattern Tokens
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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` |
|
|
40
68
|
|
|
41
|
-
|
|
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.
|
|
42
70
|
|
|
43
|
-
|
|
71
|
+
## Creating Dates
|
|
44
72
|
|
|
45
73
|
```ts
|
|
46
74
|
import { DateTz, IDateTz } from '@lbd-sh/date-tz';
|
|
47
75
|
|
|
48
|
-
|
|
49
|
-
new DateTz(
|
|
50
|
-
|
|
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);
|
|
51
82
|
|
|
52
|
-
|
|
83
|
+
// Using the helper
|
|
84
|
+
const laNow = DateTz.now('America/Los_Angeles');
|
|
53
85
|
```
|
|
54
86
|
|
|
55
|
-
|
|
87
|
+
### Working With Plain Date Objects
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
```ts
|
|
90
|
+
const native = new Date();
|
|
91
|
+
const madrid = new DateTz(native.getTime(), 'Europe/Madrid');
|
|
59
92
|
|
|
60
|
-
|
|
93
|
+
// Alternatively, keep everything UTC and convert when needed
|
|
94
|
+
const fromUtc = new DateTz(native.getTime(), 'UTC').cloneToTimezone('Europe/Madrid');
|
|
95
|
+
```
|
|
61
96
|
|
|
62
|
-
|
|
97
|
+
## Formatting Showcases
|
|
63
98
|
|
|
64
99
|
```ts
|
|
65
|
-
const
|
|
100
|
+
const order = new DateTz(Date.UTC(2025, 10, 5, 16, 45), 'Europe/Paris');
|
|
66
101
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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"
|
|
70
106
|
```
|
|
71
107
|
|
|
72
|
-
|
|
108
|
+
### Locale-sensitive Month Names
|
|
73
109
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
`LM` maps to `new Date(year, month, 3).toLocaleString(locale, { month: 'long' })` ensuring accurate localisation without full `Intl` formatting.
|
|
111
|
+
|
|
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
|
+
```
|
|
88
124
|
|
|
89
|
-
|
|
125
|
+
Parsing throws when the timezone id is missing or invalid, or when pattern/token combos are incompatible (for example `hh` without `aa`/`AA`).
|
|
90
126
|
|
|
91
|
-
|
|
127
|
+
## Arithmetic Cookbook
|
|
92
128
|
|
|
93
129
|
```ts
|
|
94
|
-
|
|
130
|
+
const sprint = new DateTz(Date.UTC(2025, 1, 1, 9, 0), 'Europe/Amsterdam');
|
|
131
|
+
|
|
132
|
+
sprint.add(2, 'week'); // ❌ week not supported
|
|
133
|
+
// Use compositions instead:
|
|
134
|
+
sprint.add(14, 'day');
|
|
95
135
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
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
|
+
}
|
|
101
142
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
parsed.isDst; // true/false depending on the moment
|
|
143
|
+
// Shift to 10:00 local time
|
|
144
|
+
sprint.set(10, 'hour').set(0, 'minute');
|
|
105
145
|
```
|
|
106
146
|
|
|
107
|
-
|
|
147
|
+
`add` accepts `minute`, `hour`, `day`, `month`, `year`. Compose multiple calls for complex adjustments. Overflows and leap years are handled automatically.
|
|
108
148
|
|
|
109
|
-
###
|
|
149
|
+
### Immutable Patterns
|
|
110
150
|
|
|
111
|
-
|
|
151
|
+
`add`, `set`, and `convertToTimezone` mutate the instance. Use `cloneToTimezone` or spread semantics when immutability is preferred:
|
|
112
152
|
|
|
113
153
|
```ts
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
endOfQuarter.set(6, 'month'); // 2025-06-01 23:00
|
|
118
|
-
endOfQuarter.set(2026, 'year'); // 2026-06-01 23:00
|
|
154
|
+
const base = DateTz.now('UTC');
|
|
155
|
+
const iteration = new DateTz(base);
|
|
156
|
+
iteration.add(1, 'day');
|
|
119
157
|
```
|
|
120
158
|
|
|
121
|
-
|
|
159
|
+
## Comparing & Sorting
|
|
122
160
|
|
|
123
|
-
|
|
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
|
+
];
|
|
167
|
+
|
|
168
|
+
slots.sort((a, b) => a.compare(b));
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`compare` throws if timezones differ:
|
|
124
172
|
|
|
125
173
|
```ts
|
|
126
174
|
const rome = DateTz.now('Europe/Rome');
|
|
127
|
-
const
|
|
175
|
+
const ny = DateTz.now('America/New_York');
|
|
176
|
+
|
|
177
|
+
if (!rome.isComparable(ny)) {
|
|
178
|
+
ny.convertToTimezone(rome.timezone);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
rome.compare(ny);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Timezone Conversion Deep Dive
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const flight = new DateTz(Date.UTC(2025, 3, 28, 20, 0), 'Europe/London');
|
|
128
188
|
|
|
129
|
-
|
|
189
|
+
const takeoff = flight.cloneToTimezone('America/Los_Angeles');
|
|
190
|
+
const landing = flight.cloneToTimezone('Asia/Tokyo');
|
|
130
191
|
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
takeoff.isDst; // false (London DST might not have started yet)
|
|
193
|
+
landing.isDst; // true or false depending on Tokyo rules
|
|
133
194
|
```
|
|
134
195
|
|
|
135
|
-
|
|
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).
|
|
199
|
+
|
|
200
|
+
### DST Transition Example
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const dstEdge = new DateTz(Date.UTC(2025, 2, 30, 0, 30), 'Europe/Rome'); // Night DST starts
|
|
204
|
+
|
|
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
|
+
```
|
|
136
210
|
|
|
137
|
-
|
|
211
|
+
## Working with Collections
|
|
138
212
|
|
|
139
213
|
```ts
|
|
140
|
-
const
|
|
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
|
+
```
|
|
141
229
|
|
|
142
|
-
|
|
230
|
+
## Serialization Tips
|
|
143
231
|
|
|
144
|
-
|
|
145
|
-
|
|
232
|
+
```ts
|
|
233
|
+
const payload = {
|
|
234
|
+
createdAt: DateTz.now('UTC').toString(),
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
};
|
|
146
237
|
|
|
147
|
-
//
|
|
148
|
-
const
|
|
238
|
+
// Later...
|
|
239
|
+
const restored = new DateTz(payload.timestamp, 'UTC');
|
|
149
240
|
```
|
|
150
241
|
|
|
151
|
-
|
|
242
|
+
Prefer storing the timestamp (UTC) and timezone id. When deserialising, feed both back into the constructor for deterministic results.
|
|
152
243
|
|
|
153
|
-
|
|
244
|
+
## Extending the Timezone Map
|
|
154
245
|
|
|
155
246
|
```ts
|
|
156
|
-
|
|
247
|
+
import { timezones } from '@lbd-sh/date-tz';
|
|
157
248
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
dt.hour; // 0-23
|
|
162
|
-
dt.minute; // 0-59
|
|
163
|
-
dt.dayOfWeek; // 0 (Sunday) to 6 (Saturday)
|
|
164
|
-
dt.timezone; // Timezone id provided at creation
|
|
165
|
-
dt.timezoneOffset; // { sdt, dst } seconds from UTC
|
|
249
|
+
timezones['Custom/Island'] = { sdt: 32_400, dst: 36_000 }; // Offsets in seconds
|
|
250
|
+
|
|
251
|
+
const island = new DateTz(Date.now(), 'Custom/Island');
|
|
166
252
|
```
|
|
167
253
|
|
|
168
|
-
|
|
254
|
+
> Ensure keys follow IANA naming conventions. Offsets are seconds from UTC (negative for west, positive for east).
|
|
255
|
+
|
|
256
|
+
## TypeScript Excellence
|
|
257
|
+
|
|
258
|
+
```ts
|
|
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
|
+
```
|
|
169
266
|
|
|
170
|
-
|
|
267
|
+
`IDateTz` lets you accept plain objects from APIs while still benefiting from compile-time guarantees.
|
|
171
268
|
|
|
172
|
-
|
|
173
|
-
- `IDateTz` describes objects that can seed the constructor.
|
|
174
|
-
- `timezones` is a `Record<string, { sdt: number; dst: number }>` that exposes offsets in seconds.
|
|
269
|
+
## Interoperability Patterns
|
|
175
270
|
|
|
176
|
-
|
|
271
|
+
### With Fetch / APIs
|
|
177
272
|
|
|
178
273
|
```ts
|
|
179
|
-
|
|
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));
|
|
180
278
|
```
|
|
181
279
|
|
|
182
|
-
|
|
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
|
+
```
|
|
289
|
+
|
|
290
|
+
## Packaging Notes
|
|
291
|
+
|
|
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:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npm install
|
|
298
|
+
npm run build
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- The GitHub Action (`.github/workflows/production.yaml`) produces the build, versions using GitVersion, and publishes to npm as `@lbd-sh/date-tz`.
|
|
183
302
|
|
|
184
|
-
|
|
185
|
-
- When `dst === sdt`, the zone does not observe daylight saving time.
|
|
186
|
-
- You can inspect or extend the map in `timezones.ts` before bundling into your application.
|
|
303
|
+
## Roadmap Ideas
|
|
187
304
|
|
|
188
|
-
|
|
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).
|
|
189
308
|
|
|
190
|
-
|
|
191
|
-
- `package.json` maps `main` and `types` to the compiled output, so consumers do not need TypeScript.
|
|
192
|
-
- The GitHub Action (`.github/workflows/production.yaml`) compiles, versions, and publishes to npm as `@lbd-sh/date-tz`.
|
|
309
|
+
Contributions and feature requests are welcome — open an issue at [github.com/lbdsh/date-tz/issues](https://github.com/lbdsh/date-tz/issues).
|
|
193
310
|
|
|
194
311
|
## License
|
|
195
312
|
|