@arkv/temporal 0.2.0

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 ADDED
@@ -0,0 +1,492 @@
1
+ # @arkv/temporal
2
+
3
+ > The zero-timezone-bug drop-in replacement for Day.js.
4
+ > Keep your exact same chainable API. Swap the engine to the modern, native JavaScript **Temporal** API.
5
+
6
+ ---
7
+
8
+ ## The Problem
9
+
10
+ JavaScript's `Date` object is broken by design — and most of the popular date libraries (Day.js, Moment.js) are built on top of it. That means they inherit all the same fundamental bugs:
11
+
12
+ ### What `Date` gets wrong
13
+
14
+ **1. Timezone traps**
15
+ `Date` only understands two timezones: UTC and "whatever the machine is set to". There is no way to work with an arbitrary named timezone without a third-party dataset and manual offset arithmetic.
16
+
17
+ ```js
18
+ // Day.js — no timezone support in core
19
+ dayjs('2024-03-10').add(1, 'day').format('YYYY-MM-DD')
20
+ // Returns '2024-03-11' ✓ — but only because it avoids the DST edge
21
+ // On the night of a DST "spring forward", adding 1 "day" adds 23 hours.
22
+ // "23 hours later" is not "tomorrow".
23
+ ```
24
+
25
+ **2. Mutable state leaking through**
26
+ `Date` methods like `setMonth()` mutate in place. Libraries paper over this with cloning, but bugs still slip through.
27
+
28
+ **3. Month indexing insanity**
29
+ `new Date(2024, 0, 1)` is January 1st. January is month `0`. December is month `11`. This is a design decision from 1995 that has confused every JavaScript developer since.
30
+
31
+ **4. String parsing is implementation-defined**
32
+ `new Date('2024-03-07')` returns midnight UTC on some runtimes and midnight local time on others.
33
+
34
+ **5. No sub-millisecond precision**
35
+ `Date` is capped at millisecond precision. Modern systems need microsecond and nanosecond timestamps.
36
+
37
+ ---
38
+
39
+ ## The Solution: Temporal
40
+
41
+ The **TC39 Temporal proposal** is a complete, ground-up redesign of date and time in JavaScript. It solves every one of the problems above:
42
+
43
+ - ✅ **Named timezone support** built in — `Temporal.ZonedDateTime` always carries its timezone
44
+ - ✅ **Immutable by design** — every operation returns a new object
45
+ - ✅ **Calendar-aware arithmetic** — adding 1 month to January 31st gives February 28th, not March 3rd
46
+ - ✅ **DST-safe** — adding "1 day" moves the wall clock by exactly 1 day, even across DST boundaries
47
+ - ✅ **Nanosecond precision** — `epochNanoseconds` returns a `BigInt`
48
+ - ✅ **Explicit timezone handling** — you cannot accidentally ignore a timezone
49
+
50
+ ### But Temporal is verbose
51
+
52
+ The Temporal API is incredibly powerful. It is also incredibly verbose:
53
+
54
+ ```js
55
+ // Temporal — correct but exhausting
56
+ Temporal.Now.plainDateISO().toString()
57
+ // vs
58
+ dayjs().format('YYYY-MM-DD')
59
+
60
+ // Temporal — adding 1 month
61
+ Temporal.Now.zonedDateTimeISO().add({ months: 1 }).toPlainDate().toString()
62
+ // vs
63
+ dayjs().add(1, 'month').format('YYYY-MM-DD')
64
+ ```
65
+
66
+ Nobody wants to rewrite 10,000 lines of clean `dayjs().format('YYYY-MM-DD')` code into Temporal's multi-step API.
67
+
68
+ ---
69
+
70
+ ## What `@arkv/temporal` Does
71
+
72
+ `@arkv/temporal` is an **adapter**. It exposes the **exact same chainable API as Day.js**, but maintains a `Temporal.ZonedDateTime` internally instead of a `Date`. You get:
73
+
74
+ - The **DX of Day.js** — same methods, same chaining, same format tokens
75
+ - The **correctness of Temporal** — timezone safety, DST-aware arithmetic, nanosecond timestamps
76
+ - **Zero code changes** for the vast majority of use cases
77
+
78
+ ```js
79
+ // Before — Day.js with Date underneath
80
+ import dayjs from 'dayjs'
81
+ dayjs('2026-03-07').add(1, 'month').format('YYYY-MM-DD')
82
+
83
+ // After — @arkv/temporal with Temporal underneath
84
+ import tdayjs from '@arkv/temporal'
85
+ tdayjs('2026-03-07').add(1, 'month').format('YYYY-MM-DD')
86
+ // API is identical. The engine is not.
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Installation
92
+
93
+ ```sh
94
+ npm install @arkv/temporal
95
+ # or
96
+ bun add @arkv/temporal
97
+ ```
98
+
99
+ > **Note:** The Temporal API is not yet available natively in all runtimes (Bun, older Node.js). This package automatically installs and imports [`temporal-polyfill`](https://github.com/fullcalendar/temporal-polyfill) to fill the gap. No configuration required.
100
+
101
+ ---
102
+
103
+ ## Quick Start
104
+
105
+ ```ts
106
+ import tdayjs from '@arkv/temporal'
107
+
108
+ // Current time
109
+ tdayjs()
110
+
111
+ // Parse a date string
112
+ tdayjs('2026-03-07')
113
+ tdayjs('2026-03-07T10:30:00')
114
+ tdayjs('2026-03-07T10:30:00+05:00')
115
+ tdayjs('2026-03-07T10:30:00[America/New_York]')
116
+
117
+ // Parse a unix timestamp (milliseconds)
118
+ tdayjs(1741305600000)
119
+
120
+ // Parse a native Date
121
+ tdayjs(new Date())
122
+
123
+ // Clone another instance
124
+ tdayjs(tdayjs())
125
+
126
+ // Unix seconds
127
+ tdayjs.unix(1741305600)
128
+
129
+ // Invalid date (dayjs-compatible)
130
+ tdayjs(null).isValid() // false
131
+ ```
132
+
133
+ ---
134
+
135
+ ## API Reference
136
+
137
+ All methods are identical to Day.js unless explicitly noted. For detailed documentation, refer to the [Day.js docs](https://day.js.org/docs/en/parse/parse).
138
+
139
+ ### Parsing
140
+
141
+ | Input | Behavior |
142
+ |-------|----------|
143
+ | `undefined` / no argument | Current local time |
144
+ | `null` | Invalid date |
145
+ | `number` | Unix timestamp in **milliseconds** |
146
+ | `string` (date only) | e.g. `'2026-03-07'` — midnight local time |
147
+ | `string` (datetime) | e.g. `'2026-03-07T10:30:00'` — local time |
148
+ | `string` (with offset) | e.g. `'2026-03-07T10:30:00+05:00'` — instant, displayed in local tz |
149
+ | `string` (with annotation) | e.g. `'2026-03-07T10:30:00[Europe/London]'` — full ZonedDateTime |
150
+ | `Date` | Native JS Date |
151
+ | `TDayjs` | Clones the instance |
152
+
153
+ ### Display / Validity
154
+
155
+ ```ts
156
+ tdayjs().isValid() // true
157
+ tdayjs(null).isValid() // false
158
+ tdayjs().clone() // new identical instance
159
+ ```
160
+
161
+ ### Getters
162
+
163
+ ```ts
164
+ const d = tdayjs('2026-03-07T10:30:45.123')
165
+
166
+ d.year() // 2026
167
+ d.month() // 2 ← 0-indexed (0 = January, 11 = December)
168
+ d.date() // 7 ← day of month (1–31)
169
+ d.day() // 6 ← day of week (0 = Sunday, 6 = Saturday)
170
+ d.hour() // 10
171
+ d.minute() // 30
172
+ d.second() // 45
173
+ d.millisecond() // 123
174
+
175
+ // Generic getter
176
+ d.get('year') // 2026
177
+ d.get('month') // 2
178
+ ```
179
+
180
+ > **Note on `month()`:** Like Day.js, months are **0-indexed**. January = `0`, December = `11`. Internally, Temporal uses 1-indexed months — the conversion is handled automatically.
181
+
182
+ ### Setters
183
+
184
+ All setters return a **new instance**. The original is never modified.
185
+
186
+ ```ts
187
+ tdayjs('2026-03-07').year(2030) // → 2030-03-07
188
+ tdayjs('2026-03-07').month(0) // → 2026-01-07 (January)
189
+ tdayjs('2026-03-07').month(11) // → 2026-12-07 (December)
190
+ tdayjs('2026-03-07').date(15) // → 2026-03-15
191
+ tdayjs('2026-03-07').hour(9) // → 2026-03-07T09:xx:xx
192
+ tdayjs('2026-03-07').minute(0)
193
+ tdayjs('2026-03-07').second(0)
194
+ tdayjs('2026-03-07').millisecond(0)
195
+
196
+ // Generic setter
197
+ tdayjs('2026-03-07').set('year', 2030)
198
+ tdayjs('2026-03-07').set('month', 5) // June
199
+ ```
200
+
201
+ ### Add / Subtract
202
+
203
+ ```ts
204
+ // Supported units (case-insensitive, singular/plural/short all work)
205
+ // 'year' | 'years' | 'y'
206
+ // 'month' | 'months' | 'M'
207
+ // 'quarter' | 'quarters' | 'Q' ← native, no plugin needed
208
+ // 'week' | 'weeks' | 'w'
209
+ // 'day' | 'days' | 'd'
210
+ // 'hour' | 'hours' | 'h'
211
+ // 'minute' | 'minutes' | 'm'
212
+ // 'second' | 'seconds' | 's'
213
+ // 'millisecond' | 'milliseconds' | 'ms'
214
+
215
+ tdayjs('2026-03-07').add(1, 'day') // 2026-03-08
216
+ tdayjs('2026-01-31').add(1, 'month') // 2026-02-28 (clamped, not March 3)
217
+ tdayjs('2026-03-07').add(1, 'year') // 2027-03-07
218
+ tdayjs('2026-03-07').add(1, 'quarter') // 2026-06-07
219
+ tdayjs('2026-03-07').subtract(1, 'week') // 2026-02-28
220
+ ```
221
+
222
+ > **Why `add(1, 'month')` is different here:**
223
+ > Day.js (using `Date`) can return `March 3` for `January 31 + 1 month` depending on the engine.
224
+ > `@arkv/temporal` uses Temporal's calendar-aware arithmetic which correctly **clamps** to the last day of the target month.
225
+
226
+ ### Start Of / End Of
227
+
228
+ ```ts
229
+ const d = tdayjs('2026-03-15T10:30:45')
230
+
231
+ // Start of unit
232
+ d.startOf('year') // 2026-01-01T00:00:00
233
+ d.startOf('month') // 2026-03-01T00:00:00
234
+ d.startOf('week') // 2026-03-09T00:00:00 (Sunday)
235
+ d.startOf('day') // 2026-03-15T00:00:00
236
+ d.startOf('hour') // 2026-03-15T10:00:00
237
+ d.startOf('minute') // 2026-03-15T10:30:00
238
+ d.startOf('second') // 2026-03-15T10:30:45.000
239
+
240
+ // End of unit
241
+ d.endOf('year') // 2026-12-31T23:59:59.999
242
+ d.endOf('month') // 2026-03-31T23:59:59.999
243
+ d.endOf('day') // 2026-03-15T23:59:59.999
244
+ d.endOf('hour') // 2026-03-15T10:59:59.999
245
+ ```
246
+
247
+ ### Format
248
+
249
+ Uses the same token syntax as Day.js:
250
+
251
+ ```ts
252
+ const d = tdayjs('2026-03-07T09:05:03')
253
+
254
+ d.format() // '2026-03-07T09:05:03+HH:mm' (default)
255
+ d.format('YYYY-MM-DD') // '2026-03-07'
256
+ d.format('DD/MM/YYYY') // '07/03/2026'
257
+ d.format('MMM D, YYYY') // 'Mar 7, 2026'
258
+ d.format('MMMM Do, YYYY') // 'March 7, 2026' (no ordinal plugin needed)
259
+ d.format('HH:mm:ss') // '09:05:03'
260
+ d.format('h:mm A') // '9:05 AM'
261
+ d.format('h:mm a') // '9:05 am'
262
+ d.format('[Today is] dddd') // 'Today is Saturday'
263
+ d.format('ddd, MMM D') // 'Sat, Mar 7'
264
+ ```
265
+
266
+ | Token | Output | Description |
267
+ |-------|--------|-------------|
268
+ | `YYYY` | `2026` | 4-digit year |
269
+ | `YY` | `26` | 2-digit year |
270
+ | `M` | `3` | Month (1–12) |
271
+ | `MM` | `03` | Month, zero-padded |
272
+ | `MMM` | `Mar` | Abbreviated month name |
273
+ | `MMMM` | `March` | Full month name |
274
+ | `D` | `7` | Day of month (1–31) |
275
+ | `DD` | `07` | Day of month, zero-padded |
276
+ | `d` | `6` | Day of week (0=Sun, 6=Sat) |
277
+ | `dd` | `Sa` | Min weekday name |
278
+ | `ddd` | `Sat` | Short weekday name |
279
+ | `dddd` | `Saturday` | Full weekday name |
280
+ | `H` | `9` | Hour, 24h (0–23) |
281
+ | `HH` | `09` | Hour, 24h, zero-padded |
282
+ | `h` | `9` | Hour, 12h (1–12) |
283
+ | `hh` | `09` | Hour, 12h, zero-padded |
284
+ | `A` | `AM` | Meridiem, uppercase |
285
+ | `a` | `am` | Meridiem, lowercase |
286
+ | `m` | `5` | Minute (0–59) |
287
+ | `mm` | `05` | Minute, zero-padded |
288
+ | `s` | `3` | Second (0–59) |
289
+ | `ss` | `03` | Second, zero-padded |
290
+ | `SSS` | `000` | Milliseconds |
291
+ | `Z` | `+05:00` | UTC offset with colon |
292
+ | `ZZ` | `+0500` | UTC offset without colon |
293
+ | `[text]` | `text` | Escaped literal text |
294
+
295
+ ### Difference
296
+
297
+ ```ts
298
+ const a = tdayjs('2026-03-07')
299
+ const b = tdayjs('2025-01-01')
300
+
301
+ a.diff(b, 'year') // 1
302
+ a.diff(b, 'month') // 14
303
+ a.diff(b, 'day') // 430
304
+ a.diff(b, 'hour') // 10320
305
+ a.diff(b) // milliseconds (default)
306
+
307
+ // Float for fractional result
308
+ a.diff(b, 'year', true) // 1.17...
309
+
310
+ // Negative when self is before the argument
311
+ tdayjs('2025-01-01').diff(tdayjs('2026-03-07'), 'year') // -1
312
+ ```
313
+
314
+ ### Comparison
315
+
316
+ ```ts
317
+ const past = tdayjs('2025-01-01')
318
+ const now = tdayjs('2026-03-07')
319
+ const future = tdayjs('2027-12-31')
320
+
321
+ now.isBefore(future) // true
322
+ now.isAfter(past) // true
323
+ now.isSame(tdayjs('2026-03-07')) // true (millisecond precision)
324
+
325
+ // With unit — compares within the granularity of the unit
326
+ const a = tdayjs('2026-03-07T10:00:00')
327
+ const b = tdayjs('2026-03-07T22:00:00')
328
+
329
+ a.isSame(b, 'day') // true — same day
330
+ a.isSame(b, 'hour') // false — different hour
331
+ a.isBefore(b, 'day') // false — same day, not before
332
+ ```
333
+
334
+ ### Conversion
335
+
336
+ ```ts
337
+ const d = tdayjs('2026-03-07T10:30:00')
338
+
339
+ d.valueOf() // 1741343400000 — ms since Unix epoch
340
+ d.unix() // 1741343400 — seconds since Unix epoch
341
+ d.daysInMonth() // 31 — days in March
342
+ d.utcOffset() // e.g. 60 — offset in minutes
343
+ d.toDate() // native Date object
344
+ d.toISOString() // '2026-03-07T10:30:00Z'
345
+ d.toJSON() // '2026-03-07T10:30:00Z' (null if invalid)
346
+ d.toString() // 'Sat, 07 Mar 2026 10:30:00 GMT'
347
+ ```
348
+
349
+ ### Locale
350
+
351
+ ```ts
352
+ import tdayjs from '@arkv/temporal'
353
+ import type { ILocale } from '@arkv/temporal'
354
+
355
+ // Get current global locale
356
+ tdayjs.locale() // 'en'
357
+
358
+ // Set global locale (register your own)
359
+ const fr: ILocale = {
360
+ name: 'fr',
361
+ months: ['Janvier', 'Février', /* ... */ 'Décembre'],
362
+ monthsShort: ['Jan', 'Fév', /* ... */ 'Déc'],
363
+ weekdays: ['Dimanche', 'Lundi', /* ... */ 'Samedi'],
364
+ weekdaysShort: ['Dim', 'Lun', /* ... */ 'Sam'],
365
+ weekdaysMin: ['Di', 'Lu', /* ... */ 'Sa'],
366
+ weekStart: 1, // Monday
367
+ }
368
+ tdayjs.locale(fr)
369
+
370
+ // Per-instance locale (does not affect global)
371
+ tdayjs('2026-03-07').locale('en').format('MMMM') // 'March'
372
+ tdayjs('2026-03-07').locale(fr).format('MMMM') // 'Mars'
373
+ ```
374
+
375
+ ### Plugin System
376
+
377
+ The plugin interface is compatible with Day.js plugins so that existing plugin code can be adapted with minimal effort:
378
+
379
+ ```ts
380
+ import tdayjs, { TDayjs } from '@arkv/temporal'
381
+
382
+ const myPlugin = (option, Cls, factory) => {
383
+ Cls.prototype.yesterday = function () {
384
+ return this.subtract(1, 'day')
385
+ }
386
+ }
387
+
388
+ tdayjs.extend(myPlugin)
389
+ tdayjs().yesterday().format('YYYY-MM-DD')
390
+ ```
391
+
392
+ > **Note:** Day.js plugins that directly access `this.$d` (the internal native `Date`) will not work because `@arkv/temporal` uses `Temporal.ZonedDateTime` internally (`this.$zdt`). Most formatting and arithmetic plugins can be rewritten to use the public API.
393
+
394
+ ### Static Methods
395
+
396
+ ```ts
397
+ tdayjs.isDayjs(tdayjs()) // true
398
+ tdayjs.isDayjs(new Date()) // false
399
+ tdayjs.unix(1741305600) // same as tdayjs(1741305600 * 1000)
400
+ tdayjs.locale() // get global locale name
401
+ tdayjs.locale('en') // set global locale
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Key Differences from Day.js
407
+
408
+ | Feature | Day.js | @arkv/temporal |
409
+ |---------|--------|----------------|
410
+ | Internal engine | `Date` object | `Temporal.ZonedDateTime` |
411
+ | Month indexing | 0-indexed (Jan=0) | **Same** — 0-indexed for compatibility |
412
+ | Timezone support | Plugin required | Built-in (always zone-aware) |
413
+ | DST handling | Inaccurate (millisecond math) | Correct (calendar arithmetic) |
414
+ | Month overflow | Platform-dependent | Clamped to month end |
415
+ | Quarter support | Plugin required | **Native** — no plugin needed |
416
+ | Nanosecond precision | No | Yes (via Temporal) |
417
+ | Immutability | Via cloning | Fully immutable |
418
+ | `this.$d` (internal Date) | ✓ | ✗ — use `this.$zdt` instead |
419
+ | Plugin compatibility | Full | Partial (public API only) |
420
+
421
+ ---
422
+
423
+ ## Why Named Timezone Safety Matters
424
+
425
+ Consider this code:
426
+
427
+ ```ts
428
+ // Setting a recurring alarm for 8:00 AM
429
+ const alarm = tdayjs('2026-11-01T08:00:00[America/New_York]')
430
+
431
+ // Adding 1 day (crosses the DST "fall back" on Nov 1, 2026)
432
+ alarm.add(1, 'day').format('HH:mm')
433
+ // Day.js with `Date`: '07:00' — wrong! (23 hours, not 24)
434
+ // @arkv/temporal: '08:00' — correct! (wall-clock 8AM next day)
435
+ ```
436
+
437
+ `Temporal` adds "1 calendar day" which means "tomorrow at the same wall-clock time", which is what users expect. `Date`-based libraries add "86,400,000 milliseconds" which is only correct when there's no DST transition.
438
+
439
+ ---
440
+
441
+ ## Compatibility Matrix
442
+
443
+ | Method | Status | Notes |
444
+ |--------|--------|-------|
445
+ | `dayjs()` / `tdayjs()` | ✅ Full | All input types supported |
446
+ | `.format()` | ✅ Full | All standard tokens |
447
+ | `.add()` / `.subtract()` | ✅ Full | Including quarters (no plugin needed) |
448
+ | `.startOf()` / `.endOf()` | ✅ Full | All time units |
449
+ | `.diff()` | ✅ Full | Including float mode |
450
+ | `.isBefore()` / `.isAfter()` / `.isSame()` | ✅ Full | With and without unit granularity |
451
+ | `.year()` / `.month()` / ... | ✅ Full | All getters and setters |
452
+ | `.get()` / `.set()` | ✅ Full | |
453
+ | `.locale()` | ✅ Full | Custom locale registration |
454
+ | `.utc()` / `.local()` | 🔜 Planned | UTC mode plugin |
455
+ | `.tz()` | 🔜 Planned | Named timezone switching |
456
+ | `.fromNow()` / `.to()` | 🔜 Planned | Relative time plugin |
457
+ | `.isLeapYear()` | 🔜 Planned | Via `$zdt.inLeapYear` |
458
+ | `.duration()` | 🔜 Planned | Via `Temporal.Duration` |
459
+
460
+ ---
461
+
462
+ ## TypeScript
463
+
464
+ All types are exported and match the Day.js type surface:
465
+
466
+ ```ts
467
+ import tdayjs, {
468
+ type TDayjs,
469
+ type ConfigType,
470
+ type UnitType,
471
+ type OpUnitType,
472
+ type QUnitType,
473
+ type ManipulateType,
474
+ type ILocale,
475
+ } from '@arkv/temporal'
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Runtime Requirements
481
+
482
+ - **Node.js**: 22+ (with Temporal polyfill included)
483
+ - **Bun**: Any version (with Temporal polyfill included)
484
+ - **Browsers**: Evergreen (with Temporal polyfill included)
485
+
486
+ The `temporal-polyfill` dependency is automatically included — you don't need to configure anything. Once native Temporal lands in all runtimes, the polyfill will become a no-op.
487
+
488
+ ---
489
+
490
+ ## License
491
+
492
+ MIT © [Petar Zarkov](https://github.com/petarzarkov)
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REGEX_FORMAT = exports.INVALID_DATE = exports.FORMAT_DEFAULT = void 0;
4
+ exports.FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ';
5
+ exports.INVALID_DATE = 'Invalid Date';
6
+ // Matches format tokens and [escaped] text
7
+ exports.REGEX_FORMAT = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g;