@lbd-sh/date-tz 1.0.2 → 1.0.5

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.
Files changed (2) hide show
  1. package/README.md +227 -110
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,15 +1,24 @@
1
1
  # Date TZ
2
2
 
3
- Powerful timezone-aware date utilities for JavaScript and TypeScript. `DateTz` keeps timestamps in sync with IANA timezones, handles daylight-saving changes transparently, and exposes a tiny, dependency-free API that stays close to the platform `Date` object while remaining predictable.
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
- ## Features
5
+ ## TL;DR
6
6
 
7
- - Full TypeScript support with `DateTz`, `IDateTz`, and the bundled `timezones` catalog.
8
- - Minute-precision timestamps normalised to UTC while exposing friendly getters (`year`, `month`, `day`, ...).
9
- - Formatting and parsing with familiar tokens (`YYYY`, `MM`, `DD`, `HH`, `hh`, `aa`, `tz`, and more).
10
- - Arithmetic helpers (`add`, `set`) that respect leap years, month lengths, and DST.
11
- - Instant timezone conversion with `convertToTimezone` and `cloneToTimezone`, backed by automatic DST detection (`Intl.DateTimeFormat`).
12
- - Works in Node.js and modern browsers without polyfills.
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
- ## Quick Start
33
+ ## Why DateTz?
23
34
 
24
- ```ts
25
- import { DateTz } from '@lbd-sh/date-tz';
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
- // Create a Rome-based date for 2025-03-01 09:15
28
- const meeting = new DateTz(Date.UTC(2025, 2, 1, 8, 15), 'Europe/Rome');
41
+ ## API Surface at a Glance
29
42
 
30
- meeting.toString(); // "2025-03-01 09:15:00"
31
- meeting.toString('DD MMM YYYY HH:mm'); // "01 Mar 2025 09:15"
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
- // Move the meeting forward and switch to New York time
34
- meeting.add(1, 'day').add(2, 'hour');
35
- const nyc = meeting.cloneToTimezone('America/New_York');
52
+ ## Pattern Tokens
36
53
 
37
- nyc.toString('YYYY-MM-DD HH:mm tz'); // "2025-03-02 05:15 America/New_York"
38
- nyc.isDst; // true or false depending on the date
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
- ## Usage
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
- ### Creating Dates
71
+ ## Creating Dates
44
72
 
45
73
  ```ts
46
74
  import { DateTz, IDateTz } from '@lbd-sh/date-tz';
47
75
 
48
- new DateTz(Date.now(), 'UTC');
49
- new DateTz(1719753300000, 'Europe/Rome');
50
- new DateTz({ timestamp: Date.now(), timezone: 'Asia/Tokyo' } satisfies IDateTz);
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
- DateTz.now('America/Los_Angeles');
83
+ // Using the helper
84
+ const laNow = DateTz.now('America/Los_Angeles');
53
85
  ```
54
86
 
55
- Notes:
87
+ ### Working With Plain Date Objects
56
88
 
57
- - Timestamps are stored in milliseconds since the Unix epoch and are truncated to the nearest minute (`seconds` and `milliseconds` are dropped) for deterministic arithmetic.
58
- - Timezone identifiers must exist in the bundled `timezones` map; an error is thrown otherwise.
89
+ ```ts
90
+ const native = new Date();
91
+ const madrid = new DateTz(native.getTime(), 'Europe/Madrid');
59
92
 
60
- ### Formatting
93
+ // Alternatively, keep everything UTC and convert when needed
94
+ const fromUtc = new DateTz(native.getTime(), 'UTC').cloneToTimezone('Europe/Madrid');
95
+ ```
61
96
 
62
- `DateTz.toString` accepts an optional pattern and locale. Unspecified tokens fall back to the default `DateTz.defaultFormat` (`YYYY-MM-DD HH:mm:ss`).
97
+ ## Formatting Showcases
63
98
 
64
99
  ```ts
65
- const invoice = new DateTz(Date.UTC(2025, 5, 12, 12, 0), 'Europe/Paris');
100
+ const order = new DateTz(Date.UTC(2025, 10, 5, 16, 45), 'Europe/Paris');
66
101
 
67
- invoice.toString(); // "2025-06-12 14:00:00"
68
- invoice.toString('DD/MM/YYYY HH:mm tz'); // "12/06/2025 14:00 Europe/Paris"
69
- invoice.toString('LM DD, YYYY hh:mm aa', 'it'); // "Giugno 12, 2025 02:00 pm"
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
- Available tokens:
108
+ ### Locale-sensitive Month Names
73
109
 
74
- | Token | Meaning | Example |
75
- | ----- | ------- | ------- |
76
- | `YYYY`, `yyyy` | Full year | `2025` |
77
- | `YY`, `yy` | Year, two digits | `25` |
78
- | `MM` | Month (01–12) | `06` |
79
- | `LM` | Locale month name (capitalised) | `Giugno` |
80
- | `DD` | Day of month (01–31) | `12` |
81
- | `HH` | Hours (00–23) | `14` |
82
- | `hh` | Hours (01–12) | `02` |
83
- | `mm` | Minutes (00–59) | `00` |
84
- | `ss` | Seconds (00–59) | `00` |
85
- | `aa` | `am`/`pm` marker | `pm` |
86
- | `AA` | `AM`/`PM` marker | `PM` |
87
- | `tz` | Timezone identifier | `Europe/Paris` |
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
- ### Parsing strings
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
- `DateTz.parse` mirrors `toString`: pass the source string, its pattern, and an optional timezone (default `UTC`).
127
+ ## Arithmetic Cookbook
92
128
 
93
129
  ```ts
94
- import { DateTz } from '@lbd-sh/date-tz';
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
- const parsed = DateTz.parse(
97
- '2025-06-12 02:00 PM',
98
- 'YYYY-MM-DD hh:mm AA',
99
- 'America/New_York'
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
- parsed.timestamp; // Milliseconds in UTC (minute precision)
103
- parsed.timezone; // "America/New_York"
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
- > 12-hour patterns require both `hh` and the `AA`/`aa` markers when parsing. If you only need 24-hour formats, prefer `HH`.
147
+ `add` accepts `minute`, `hour`, `day`, `month`, `year`. Compose multiple calls for complex adjustments. Overflows and leap years are handled automatically.
108
148
 
109
- ### Arithmetic
149
+ ### Immutable Patterns
110
150
 
111
- Mutating helpers operate in-place and normalise overflows:
151
+ `add`, `set`, and `convertToTimezone` mutate the instance. Use `cloneToTimezone` or spread semantics when immutability is preferred:
112
152
 
113
153
  ```ts
114
- const endOfQuarter = new DateTz(Date.UTC(2025, 2, 31, 23, 0), 'UTC');
115
-
116
- endOfQuarter.add(1, 'day'); // 2025-04-01 23:00
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
- All arithmetic respects leap years, month lengths, and daylight-saving changes via the offset cache.
159
+ ## Comparing & Sorting
122
160
 
123
- ### Comparing
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 madrid = DateTz.now('Europe/Madrid');
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
- rome.isComparable(madrid); // false (different timezones)
189
+ const takeoff = flight.cloneToTimezone('America/Los_Angeles');
190
+ const landing = flight.cloneToTimezone('Asia/Tokyo');
130
191
 
131
- const laterInRome = new DateTz(rome.timestamp + 60_000, 'Europe/Rome');
132
- rome.compare(laterInRome); // negative number
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
- `compare` throws when timezones differ to avoid accidental cross-timezone comparisons. Use `isComparable` first, or convert one date.
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
- ### Timezone conversion
211
+ ## Working with Collections
138
212
 
139
213
  ```ts
140
- const departure = new DateTz(Date.UTC(2025, 4, 15, 6, 45), 'Europe/Rome');
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
- departure.isDst; // true/false depending on the date
230
+ ## Serialization Tips
143
231
 
144
- // Modify in place
145
- departure.convertToTimezone('America/New_York');
232
+ ```ts
233
+ const payload = {
234
+ createdAt: DateTz.now('UTC').toString(),
235
+ timestamp: Date.now(),
236
+ };
146
237
 
147
- // Clone to keep the original instance
148
- const arrival = departure.cloneToTimezone('Asia/Tokyo');
238
+ // Later...
239
+ const restored = new DateTz(payload.timestamp, 'UTC');
149
240
  ```
150
241
 
151
- Timezone changes refresh the internal offset cache and leverage `Intl.DateTimeFormat` when available to detect real-world DST shifts.
242
+ Prefer storing the timestamp (UTC) and timezone id. When deserialising, feed both back into the constructor for deterministic results.
152
243
 
153
- ### Accessing components
244
+ ## Extending the Timezone Map
154
245
 
155
246
  ```ts
156
- const dt = DateTz.now('UTC');
247
+ import { timezones } from '@lbd-sh/date-tz';
157
248
 
158
- dt.year; // e.g. 2025
159
- dt.month; // 0-based (0 = January)
160
- dt.day; // 1-31
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
- ## Type Definitions
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
- The package ships with comprehensive typings:
267
+ `IDateTz` lets you accept plain objects from APIs while still benefiting from compile-time guarantees.
171
268
 
172
- - `DateTz` implements `IDateTz`.
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
- Import what you need:
271
+ ### With Fetch / APIs
177
272
 
178
273
  ```ts
179
- import { DateTz, IDateTz, timezones } from '@lbd-sh/date-tz';
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
- ## Timezone catalogue
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
- - Contains 500+ IANA identifiers with both standard (`sdt`) and daylight (`dst`) offsets in seconds.
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
- ## Publishing & Packaging
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
- - Build with `npm run build` (TypeScript emits to `dist/` with declarations and source maps).
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
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@lbd-sh/date-tz",
3
3
  "displayName": "Date TZ",
4
4
  "engineStrict": false,
5
- "version": "1.0.2",
5
+ "version": "1.0.5",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {
@@ -31,13 +31,13 @@
31
31
  ],
32
32
  "author": {
33
33
  "email": "info@lbdsh.com",
34
- "name": "lbd-sh",
34
+ "name": "LBD SRL",
35
35
  "url": "https://github.com/lbdsh"
36
36
  },
37
37
  "maintainers": [
38
38
  {
39
39
  "email": "info@lbdsh.com",
40
- "name": "lbd-sh",
40
+ "name": "LBD SRL",
41
41
  "url": "https://github.com/lbdsh"
42
42
  }
43
43
  ],