@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.
Files changed (2) hide show
  1. package/README.md +252 -86
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,147 +1,313 @@
1
- # DateTz Class Documentation
1
+ # Date TZ
2
2
 
3
- ## Overview
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
- 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.
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
- ## Constructor
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(value: IDateTz)
13
- new DateTz(value: number, tz?: string)
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
- - Accepts either an `IDateTz` object or a Unix timestamp with an optional timezone.
17
- - Throws an error if the provided timezone is invalid.
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
- ## Properties
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
- ### Instance Properties
127
+ ## Arithmetic Cookbook
24
128
 
25
- - `timestamp: number` — Milliseconds since Unix epoch (UTC).
26
- - `timezone: string` The timezone identifier (e.g., `"UTC"`, `"Europe/Rome"`).
129
+ ```ts
130
+ const sprint = new DateTz(Date.UTC(2025, 1, 1, 9, 0), 'Europe/Amsterdam');
27
131
 
28
- ### Static Properties
132
+ sprint.add(2, 'week'); // ❌ week not supported
133
+ // Use compositions instead:
134
+ sprint.add(14, 'day');
29
135
 
30
- - `DateTz.defaultFormat: string` Default string format pattern: `'YYYY-MM-DD HH:mm:ss'`.
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
- ## Getters
147
+ `add` accepts `minute`, `hour`, `day`, `month`, `year`. Compose multiple calls for complex adjustments. Overflows and leap years are handled automatically.
35
148
 
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).
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
- ## Methods
153
+ ```ts
154
+ const base = DateTz.now('UTC');
155
+ const iteration = new DateTz(base);
156
+ iteration.add(1, 'day');
157
+ ```
47
158
 
48
- ### `compare(other: IDateTz): number`
159
+ ## Comparing & Sorting
49
160
 
50
- Compares this instance with another `DateTz`. Throws if timezones differ.
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
- ### `isComparable(other: IDateTz): boolean`
168
+ slots.sort((a, b) => a.compare(b));
169
+ ```
53
170
 
54
- Returns `true` if the two instances share the same timezone.
171
+ `compare` throws if timezones differ:
55
172
 
56
- ### `toString(pattern?: string): string`
173
+ ```ts
174
+ const rome = DateTz.now('Europe/Rome');
175
+ const ny = DateTz.now('America/New_York');
57
176
 
58
- Returns the string representation using the provided format.
177
+ if (!rome.isComparable(ny)) {
178
+ ny.convertToTimezone(rome.timezone);
179
+ }
59
180
 
60
- #### Format Tokens
181
+ rome.compare(ny);
182
+ ```
61
183
 
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 |
184
+ ## Timezone Conversion Deep Dive
74
185
 
75
- ### `add(value: number, unit: 'minute' | 'hour' | 'day' | 'month' | 'year'): this`
186
+ ```ts
187
+ const flight = new DateTz(Date.UTC(2025, 3, 28, 20, 0), 'Europe/London');
76
188
 
77
- Adds the given time to the instance.
189
+ const takeoff = flight.cloneToTimezone('America/Los_Angeles');
190
+ const landing = flight.cloneToTimezone('Asia/Tokyo');
78
191
 
79
- ### `set(value: number, unit: 'year' | 'month' | 'day' | 'hour' | 'minute'): this`
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
- Sets a specific part of the date/time.
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
- ### `convertToTimezone(tz: string): this`
200
+ ### DST Transition Example
84
201
 
85
- Changes the timezone of the instance in place.
202
+ ```ts
203
+ const dstEdge = new DateTz(Date.UTC(2025, 2, 30, 0, 30), 'Europe/Rome'); // Night DST starts
86
204
 
87
- ### `cloneToTimezone(tz: string): DateTz`
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
- Returns a new instance in the specified timezone.
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
- ## Static Methods
230
+ ## Serialization Tips
94
231
 
95
- ### `DateTz.parse(dateString: string, pattern?: string, tz?: string): DateTz`
232
+ ```ts
233
+ const payload = {
234
+ createdAt: DateTz.now('UTC').toString(),
235
+ timestamp: Date.now(),
236
+ };
96
237
 
97
- Parses a string to a `DateTz` instance.
238
+ // Later...
239
+ const restored = new DateTz(payload.timestamp, 'UTC');
240
+ ```
98
241
 
99
- ### `DateTz.now(tz?: string): DateTz`
242
+ Prefer storing the timestamp (UTC) and timezone id. When deserialising, feed both back into the constructor for deterministic results.
100
243
 
101
- Returns the current date/time as a `DateTz` instance.
244
+ ## Extending the Timezone Map
102
245
 
103
- ---
246
+ ```ts
247
+ import { timezones } from '@lbd-sh/date-tz';
104
248
 
105
- ## Utility Methods (Private)
249
+ timezones['Custom/Island'] = { sdt: 32_400, dst: 36_000 }; // Offsets in seconds
106
250
 
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.
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
- ## Example
256
+ ## TypeScript Excellence
114
257
 
115
258
  ```ts
116
- const dt = new DateTz(1719146400000, 'Europe/Rome');
117
- console.log(dt.toString());
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
- dt.add(1, 'day');
120
- console.log(dt.day);
269
+ ## Interoperability Patterns
121
270
 
122
- const parsed = DateTz.parse("2025-06-23 14:00:00", "YYYY-MM-DD HH:mm:ss", "Europe/Rome");
123
- console.log(parsed.toString());
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
- ## Error Handling
290
+ ## Packaging Notes
129
291
 
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.
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
- ## Requirements
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
- Requires:
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
- ## Summary
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
- `DateTz` is ideal for handling precise and consistent date/time values across multiple timezones with customizable formatting, parsing, and manipulation options.
311
+ ## License
147
312
 
313
+ ISC © lbd-sh
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.1",
5
+ "version": "1.0.3",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {