@lbd-sh/date-tz 1.0.6 → 1.0.7

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 +194 -168
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,25 +1,47 @@
1
- # Date TZ
1
+ # Date TZ ⏰✨
2
2
 
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.
3
+ `DateTz` is the timezone swiss‑army knife for modern JavaScript and TypeScript projects. It keeps minute-precision timestamps aligned with IANA zones, gracefully glides across daylight-saving transitions, and exposes a lightweight API that feels familiar yet powerful. Whether you are building dashboards, schedulers, or automation pipelines, DateTz keeps your time math honest. 🌍
4
4
 
5
- ## TL;DR
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Quick Start](#quick-start)
10
+ 2. [Installation](#installation)
11
+ 3. [Why DateTz?](#why-datetz)
12
+ 4. [API Surface Overview](#api-surface-overview)
13
+ 5. [Formatting Tokens](#formatting-tokens)
14
+ 6. [Core Concepts & Recipes](#core-concepts--recipes)
15
+ 7. [Daylight Saving Time Deep Dive](#daylight-saving-time-deep-dive)
16
+ 8. [Real-World Playbook](#real-world-playbook)
17
+ 9. [TypeScript & Tooling](#typescript--tooling)
18
+ 10. [Performance Tips](#performance-tips)
19
+ 11. [FAQ & Troubleshooting](#faq--troubleshooting)
20
+ 12. [Contributing](#contributing)
21
+ 13. [License](#license)
22
+
23
+ ---
24
+
25
+ ## Quick Start
6
26
 
7
27
  ```ts
8
28
  import { DateTz } from '@lbd-sh/date-tz';
9
29
 
30
+ // Create a meeting in Rome and preview it elsewhere
10
31
  const rome = new DateTz(Date.UTC(2025, 5, 15, 7, 30), 'Europe/Rome');
32
+ const nyc = rome.cloneToTimezone('America/New_York');
11
33
 
12
34
  rome.toString(); // "2025-06-15 09:30:00"
13
35
  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
36
  nyc.toString('YYYY-MM-DD HH:mm tz'); // "2025-06-15 03:30 America/New_York"
18
37
 
19
- rome.add(2, 'day').set(11, 'hour');
20
- rome.toString('YYYY-MM-DD HH:mm'); // "2025-06-17 11:30"
38
+ rome.add(2, 'day').set(11, 'hour'); // Mutating, still DST-safe
21
39
  ```
22
40
 
41
+ Need a full workflow? Jump to the [Real-World Playbook](#real-world-playbook). 👇
42
+
43
+ ---
44
+
23
45
  ## Installation
24
46
 
25
47
  ```bash
@@ -30,31 +52,41 @@ yarn add @lbd-sh/date-tz
30
52
  pnpm add @lbd-sh/date-tz
31
53
  ```
32
54
 
55
+ DateTz ships as CommonJS with TypeScript declarations. No runtime dependencies, no polyfills.
56
+
57
+ ---
58
+
33
59
  ## Why DateTz?
34
60
 
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.
61
+ - **Predictable math** 🧮 – timestamps are truncated to minutes so cron-like workflows never drift by milliseconds.
62
+ - **DST aware** 🌓 – offsets are sourced from the bundled `timezones` map and double-checked via `Intl.DateTimeFormat`.
63
+ - **Expressive formatting** 🖨️ – familiar tokens (`YYYY`, `MM`, `hh`, `AA`, `tz`, `LM`, etc.) with locale-aware month names.
64
+ - **Simple conversions** 🔁 `convertToTimezone` and `cloneToTimezone` make cross-zone comparisons painless.
65
+ - **TypeScript-first** 📘 strong typings, `IDateTz` contract, and declaration files baked in.
66
+ - **Zero dependencies** 🪶 – keep bundles lean while gaining runtime confidence.
67
+
68
+ ---
40
69
 
41
- ## API Surface at a Glance
70
+ ## API Surface Overview
42
71
 
43
72
  | Member | Description |
44
73
  | ------ | ----------- |
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)`. |
74
+ | `new DateTz(value, tz?)` | Build from a timestamp or an `IDateTz`-compatible object (timezone defaults to `UTC`). |
75
+ | `DateTz.now(tz?)` | Current moment in the requested timezone. |
76
+ | `DateTz.parse(str, pattern?, tz?)` | Parse formatted strings into `DateTz` instances. |
77
+ | `DateTz.defaultFormat` | Default pattern used by `toString()` when no arguments are provided. |
78
+ | Getters | `year`, `month`, `day`, `hour`, `minute`, `dayOfWeek`, `isDst`, `timezoneOffset`. |
79
+ | Mutators | `add(value, unit)`, `set(value, unit)`, `convertToTimezone(tz)` (mutating), `cloneToTimezone(tz)` (immutable). |
80
+ | Comparison | `compare(other)`, `isComparable(other)` guard against cross-zone mistakes. |
51
81
 
52
- ## Pattern Tokens
82
+ ---
83
+
84
+ ## Formatting Tokens
53
85
 
54
86
  | Token | Meaning | Example |
55
87
  | ----- | ------- | ------- |
56
- | `YYYY`, `yyyy` | Four digit year | `2025` |
57
- | `YY`, `yy` | Two digit year | `25` |
88
+ | `YYYY`, `yyyy` | Four-digit year | `2025` |
89
+ | `YY`, `yy` | Two-digit year | `25` |
58
90
  | `MM` | Month (01–12) | `06` |
59
91
  | `LM` | Locale month name (capitalised) | `June` |
60
92
  | `DD` | Day of month (01–31) | `15` |
@@ -62,251 +94,245 @@ pnpm add @lbd-sh/date-tz
62
94
  | `hh` | Hour (01–12) | `03` |
63
95
  | `mm` | Minute (00–59) | `30` |
64
96
  | `ss` | Second (00–59) | `00` |
65
- | `aa` | `am`/`pm` marker | `am` |
66
- | `AA` | `AM`/`PM` marker | `PM` |
97
+ | `aa` | Lowercase am/pm marker | `pm` |
98
+ | `AA` | Uppercase AM/PM marker | `PM` |
67
99
  | `tz` | Timezone identifier | `Europe/Rome` |
68
100
 
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.
101
+ > Literal text? Wrap it in square brackets: `YYYY-MM-DD[ @ ]HH:mm` → `2025-06-15 @ 09:30`.
70
102
 
71
- ## Creating Dates
103
+ ---
72
104
 
73
- ```ts
74
- import { DateTz, IDateTz } from '@lbd-sh/date-tz';
105
+ ## Core Concepts & Recipes
75
106
 
76
- // From a unix timestamp (milliseconds)
77
- const utcMeeting = new DateTz(Date.UTC(2025, 0, 1, 12, 0), 'UTC');
107
+ ### Creating Dates
78
108
 
79
- // From another DateTz-like object
80
- const seed: IDateTz = { timestamp: Date.now(), timezone: 'Asia/Tokyo' };
81
- const tokyo = new DateTz(seed);
109
+ ```ts
110
+ import { DateTz, IDateTz } from '@lbd-sh/date-tz';
82
111
 
83
- // Using the helper
84
- const laNow = DateTz.now('America/Los_Angeles');
112
+ const utc = new DateTz(Date.now(), 'UTC');
113
+ const tokyo = new DateTz({ timestamp: Date.now(), timezone: 'Asia/Tokyo' } satisfies IDateTz);
114
+ const la = DateTz.now('America/Los_Angeles');
85
115
  ```
86
116
 
87
- ### Working With Plain Date Objects
117
+ ### From Native Date
88
118
 
89
119
  ```ts
90
120
  const native = new Date();
91
121
  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');
122
+ const alwaysUtc = new DateTz(native.getTime(), 'UTC').cloneToTimezone('Europe/Madrid');
95
123
  ```
96
124
 
97
- ## Formatting Showcases
125
+ ### Formatting Showcases
98
126
 
99
127
  ```ts
100
- const order = new DateTz(Date.UTC(2025, 10, 5, 16, 45), 'Europe/Paris');
128
+ const invoice = new DateTz(Date.UTC(2025, 10, 5, 16, 45), 'Europe/Paris');
101
129
 
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"
130
+ invoice.toString(); // "2025-11-05 17:45:00"
131
+ invoice.toString('DD/MM/YYYY HH:mm'); // "05/11/2025 17:45"
132
+ invoice.toString('LM DD, YYYY hh:mm aa', 'fr'); // "Novembre 05, 2025 05:45 pm"
133
+ invoice.toString('[Order timezone:] tz'); // "Order timezone: Europe/Paris"
106
134
  ```
107
135
 
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.
111
-
112
- ## Parsing Scenarios
136
+ ### Parsing Scenarios
113
137
 
114
138
  ```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');
139
+ DateTz.parse('2025-09-01 02:30', 'YYYY-MM-DD HH:mm', 'Asia/Singapore'); // 24h format
140
+ DateTz.parse('03-18-2025 07:15 PM', 'MM-DD-YYYY hh:mm AA', 'America/New_York'); // 12h
141
+ DateTz.parse('Sale closes 2025/03/31 @ 23:59', 'Sale closes YYYY/MM/DD [@] HH:mm', 'UTC'); // Literals
123
142
  ```
124
143
 
125
- Parsing throws when the timezone id is missing or invalid, or when pattern/token combos are incompatible (for example `hh` without `aa`/`AA`).
144
+ Parsing throws on invalid zones or incompatible patterns (e.g. `hh` without `aa`/`AA`).
126
145
 
127
- ## Arithmetic Cookbook
146
+ ### Arithmetic Cookbook
128
147
 
129
148
  ```ts
130
149
  const sprint = new DateTz(Date.UTC(2025, 1, 1, 9, 0), 'Europe/Amsterdam');
131
150
 
132
- sprint.add(2, 'week'); // week not supported
133
- // Use compositions instead:
134
- sprint.add(14, 'day');
135
-
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
- }
142
-
143
- // Shift to 10:00 local time
144
- sprint.set(10, 'hour').set(0, 'minute');
151
+ sprint.add(14, 'day'); // Compose to simulate weeks
152
+ sprint.set(sprint.month + 1, 'month').set(1, 'day'); // First day of next month
153
+ while ([0, 6].includes(sprint.dayOfWeek)) sprint.add(1, 'day'); // Skip weekend
154
+ sprint.set(10, 'hour').set(0, 'minute'); // Move to 10:00
145
155
  ```
146
156
 
147
- `add` accepts `minute`, `hour`, `day`, `month`, `year`. Compose multiple calls for complex adjustments. Overflows and leap years are handled automatically.
148
-
149
- ### Immutable Patterns
150
-
151
- `add`, `set`, and `convertToTimezone` mutate the instance. Use `cloneToTimezone` or spread semantics when immutability is preferred:
157
+ ### Immutability Pattern
152
158
 
153
159
  ```ts
154
160
  const base = DateTz.now('UTC');
155
- const iteration = new DateTz(base);
156
- iteration.add(1, 'day');
161
+ const nextRun = new DateTz(base).add(1, 'day');
157
162
  ```
158
163
 
159
- ## Comparing & Sorting
164
+ Mutators change the instance; use cloning when you need persistence.
165
+
166
+ ### Comparing & Sorting
160
167
 
161
168
  ```ts
162
169
  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'),
170
+ DateTz.parse('2025-06-15 08:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
171
+ DateTz.parse('2025-06-15 09:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
172
+ DateTz.parse('2025-06-14 18:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome'),
166
173
  ];
167
174
 
168
175
  slots.sort((a, b) => a.compare(b));
169
176
  ```
170
177
 
171
- `compare` throws if timezones differ:
178
+ Always guard cross-zone comparisons:
172
179
 
173
180
  ```ts
174
181
  const rome = DateTz.now('Europe/Rome');
175
182
  const ny = DateTz.now('America/New_York');
176
183
 
177
- if (!rome.isComparable(ny)) {
178
- ny.convertToTimezone(rome.timezone);
179
- }
180
-
181
- rome.compare(ny);
184
+ if (!rome.isComparable(ny)) ny.convertToTimezone(rome.timezone);
182
185
  ```
183
186
 
184
- ## Timezone Conversion Deep Dive
187
+ ---
185
188
 
186
- ```ts
187
- const flight = new DateTz(Date.UTC(2025, 3, 28, 20, 0), 'Europe/London');
189
+ ## Daylight Saving Time Deep Dive
188
190
 
189
- const takeoff = flight.cloneToTimezone('America/Los_Angeles');
190
- const landing = flight.cloneToTimezone('Asia/Tokyo');
191
+ ```ts
192
+ const dstEdge = new DateTz(Date.UTC(2025, 2, 30, 0, 30), 'Europe/Rome'); // DST start night
191
193
 
192
- takeoff.isDst; // false (London DST might not have started yet)
193
- landing.isDst; // true or false depending on Tokyo rules
194
+ dstEdge.toString(); // "2025-03-30 01:30:00"
195
+ dstEdge.add(1, 'hour');
196
+ dstEdge.toString(); // "2025-03-30 03:30:00" (skips 02:30)
197
+ dstEdge.isDst; // true
194
198
  ```
195
199
 
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).
200
+ Under the hood:
201
+
202
+ - Offsets are cached per timestamp (`offsetCache`) for speed.
203
+ - If `Intl.DateTimeFormat` is available, DateTz validates the actual offset at runtime.
204
+ - Without `Intl`, DateTz falls back to the static `timezones` map, so you can safely run on serverless or sandboxed environments.
199
205
 
200
- ### DST Transition Example
206
+ ### Timezone Conversion
201
207
 
202
208
  ```ts
203
- const dstEdge = new DateTz(Date.UTC(2025, 2, 30, 0, 30), 'Europe/Rome'); // Night DST starts
209
+ const flight = new DateTz(Date.UTC(2025, 3, 28, 20, 0), 'Europe/London');
204
210
 
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
211
+ const takeoff = flight.cloneToTimezone('America/Los_Angeles'); // immutable
212
+ const landing = flight.cloneToTimezone('Asia/Tokyo');
209
213
  ```
210
214
 
211
- ## Working with Collections
215
+ Use `convertToTimezone` if you want to mutate the instance in-place.
216
+
217
+ ---
218
+
219
+ ## Real-World Playbook
220
+
221
+ ### Scheduling Emails Across Offices 📧
212
222
 
213
223
  ```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'),
224
+ const offices = [
225
+ { tz: 'Europe/Rome', hour: 9 },
226
+ { tz: 'America/New_York', hour: 9 },
227
+ { tz: 'Asia/Tokyo', hour: 9 },
218
228
  ];
219
229
 
220
- const sorted = timeline.slice().sort((a, b) => a.compare(b));
230
+ const base = DateTz.now('UTC').set(0, 'minute');
221
231
 
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
- }, {});
232
+ const sends = offices.map(({ tz, hour }) => {
233
+ const local = new DateTz(base).convertToTimezone(tz);
234
+ local.set(hour, 'hour');
235
+ if (local.compare(base) < 0) local.add(1, 'day');
236
+ return local;
237
+ });
228
238
  ```
229
239
 
230
- ## Serialization Tips
240
+ ### React Component Integration ⚛️
231
241
 
232
- ```ts
233
- const payload = {
234
- createdAt: DateTz.now('UTC').toString(),
235
- timestamp: Date.now(),
236
- };
242
+ ```tsx
243
+ import { useMemo } from 'react';
244
+ import { DateTz } from '@lbd-sh/date-tz';
237
245
 
238
- // Later...
239
- const restored = new DateTz(payload.timestamp, 'UTC');
246
+ export function Countdown({ timestamp, tz }: { timestamp: number; tz: string }) {
247
+ const target = useMemo(() => new DateTz(timestamp, tz), [timestamp, tz]);
248
+ return (
249
+ <span title={target.toString('YYYY-MM-DD HH:mm tz')}>
250
+ {target.toString('DD MMM YYYY, HH:mm', navigator.language)}
251
+ </span>
252
+ );
253
+ }
240
254
  ```
241
255
 
242
- Prefer storing the timestamp (UTC) and timezone id. When deserialising, feed both back into the constructor for deterministic results.
243
-
244
- ## Extending the Timezone Map
256
+ ### Express Middleware 🛤️
245
257
 
246
258
  ```ts
247
- import { timezones } from '@lbd-sh/date-tz';
259
+ app.use((req, _res, next) => {
260
+ const headerTz = req.header('x-user-tz') ?? 'UTC';
261
+ req.context = {
262
+ now: () => DateTz.now(headerTz),
263
+ };
264
+ next();
265
+ });
266
+ ```
248
267
 
249
- timezones['Custom/Island'] = { sdt: 32_400, dst: 36_000 }; // Offsets in seconds
268
+ ### Testing Automation with Jest 🧪
269
+
270
+ ```ts
271
+ import { DateTz } from '@lbd-sh/date-tz';
250
272
 
251
- const island = new DateTz(Date.now(), 'Custom/Island');
273
+ describe('billing cutoff', () => {
274
+ it('moves to next business day when on weekend', () => {
275
+ const friday = DateTz.parse('2025-06-13 17:00', 'YYYY-MM-DD HH:mm', 'Europe/Rome');
276
+ friday.add(1, 'day');
277
+ expect(friday.dayOfWeek).toBe(6);
278
+ friday.add(2, 'day');
279
+ expect(friday.dayOfWeek).toBe(1);
280
+ });
281
+ });
252
282
  ```
253
283
 
254
- > Ensure keys follow IANA naming conventions. Offsets are seconds from UTC (negative for west, positive for east).
284
+ ---
255
285
 
256
- ## TypeScript Excellence
286
+ ## TypeScript & Tooling
287
+
288
+ - `IDateTz` describes constructor-friendly shapes.
289
+ - `timezones` exports the full offset map (`Record<string, { sdt: number; dst: number }>`).
290
+ - Compatible with bundlers (Webpack, Vite, TurboPack). For ESM projects, use transpilation or `dynamic import`.
257
291
 
258
292
  ```ts
259
293
  import type { IDateTz } from '@lbd-sh/date-tz';
260
294
 
261
- function normalise(date: IDateTz): IDateTz {
262
- const instance = new DateTz(date);
263
- return instance.cloneToTimezone('UTC');
295
+ function normalise(input: IDateTz): IDateTz {
296
+ return new DateTz(input).cloneToTimezone('UTC');
264
297
  }
265
298
  ```
266
299
 
267
- `IDateTz` lets you accept plain objects from APIs while still benefiting from compile-time guarantees.
300
+ ---
268
301
 
269
- ## Interoperability Patterns
302
+ ## Performance Tips
270
303
 
271
- ### With Fetch / APIs
304
+ 1. **Reuse instances** when adjusting a datetime in a tight loop (mutating via `set`/`add` avoids allocations).
305
+ 2. **Cache conversions**: if you often convert the same timestamp across timezones, keep the clones.
306
+ 3. **Disable locale formatting** (`toString(pattern)` without the `locale` argument) for the fastest path.
307
+ 4. **Tree-shake**: import only what you need (`import { DateTz }` instead of wildcard).
272
308
 
273
- ```ts
274
- const response = await fetch('/api/events');
275
- const body: { timestamp: number; timezone: string }[] = await response.json();
309
+ ---
276
310
 
277
- const events = body.map(({ timestamp, timezone }) => new DateTz(timestamp, timezone));
278
- ```
311
+ ## FAQ & Troubleshooting
279
312
 
280
- ### With Cron-like Scheduling
313
+ **Q: Can I format with seconds or milliseconds?**
314
+ A: Seconds (`ss`) are supported. Milliseconds are intentionally dropped to keep arithmetic deterministic. Store them separately if needed.
281
315
 
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
- ```
316
+ **Q: Why can’t I compare two dates with different timezones?**
317
+ A: `compare` guards against mistakes. Convert one date (`cloneToTimezone`) before comparing.
289
318
 
290
- ## Packaging Notes
319
+ **Q: How do I add weeks?**
320
+ A: Compose calls (`add(7, 'day')`). Weeks are not a native unit to keep the API small and explicit.
291
321
 
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:
322
+ **Q: Do I need to ship the entire `timezones` map?**
323
+ A: The map is ~40 KB minified. For extreme optimisation you can pre-prune zones before bundling.
295
324
 
296
- ```bash
297
- npm install
298
- npm run build
299
- ```
325
+ ---
300
326
 
301
- - The GitHub Action (`.github/workflows/production.yaml`) produces the build, versions using GitVersion, and publishes to npm as `@lbd-sh/date-tz`.
327
+ ## Contributing
302
328
 
303
- ## Roadmap Ideas
329
+ - 💡 Open feature ideas or bug reports at [github.com/lbdsh/date-tz/issues](https://github.com/lbdsh/date-tz/issues).
330
+ - 🔁 Submit pull requests following the existing linting/build steps (`npm ci && npm run build`).
331
+ - 📦 Releases are automated through the GitHub workflow in `.github/workflows/production.yaml`.
304
332
 
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).
333
+ Community feedback keeps DateTz sharp—thanks for being part of it! 🙌
308
334
 
309
- Contributions and feature requests are welcome — open an issue at [github.com/lbdsh/date-tz/issues](https://github.com/lbdsh/date-tz/issues).
335
+ ---
310
336
 
311
337
  ## License
312
338
 
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.6",
5
+ "version": "1.0.7",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {