mmntjs-timezone 0.0.1

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,138 @@
1
+ # mmntjs-timezone
2
+
3
+ Drop-in replacement for `moment-timezone`.
4
+
5
+ ## Architecture
6
+
7
+ mmntjs-timezone is moving toward a compatibility-first architecture:
8
+
9
+ - Moment Timezone compatible packed-data APIs remain the public boundary
10
+ - runtime-loaded zones/links/countries are stored in an internal registry
11
+ - built-in IANA zone fallback currently still uses `Intl` until bundled authoritative data lands
12
+ - the long-term target is full tzdata-backed compatibility with lazy decode and compact storage
13
+
14
+ ```
15
+ moment-timezone mmntjs-timezone
16
+ │ │
17
+ ├─ packed tzdb ── Intl.DateTimeFormat
18
+ ├─ add/link data ── Intl.supportedValuesOf
19
+ ├─ zone().abbr() ── timeZoneName: "short"
20
+ └─ zone().offset() ── formatToParts → UTC comparison
21
+ ```
22
+
23
+ ## Behavioral Compatibility
24
+
25
+ | Feature | Status | Notes |
26
+ |---------|--------|-------|
27
+ | `moment.tz(input, zone)` | ✅ | Parse wall-clock in zone |
28
+ | `moment.tz(input, format, zone)` | ✅ | Parse with format in zone |
29
+ | `moment.tz(input, format, strict, zone)` | ✅ | Strict format dispatch |
30
+ | `moment(ts).tz(zone)` | ✅ | Convert instant to zone |
31
+ | `moment.utc(ts).tz(zone)` | ✅ | Convert UTC to zone |
32
+ | `moment.parseZone(s).tz(zone)` | ✅ | Convert parsed offset to zone |
33
+ | `moment.tz().format("z")` | ✅ | Timezone abbreviation |
34
+ | `moment.tz().format("Z")` | ✅ | Offset display |
35
+ | `moment.tz().utcOffset()` | ✅ | Numeric offset |
36
+ | `moment.tz().zoneAbbr()` | ✅ | Abbreviation API |
37
+ | `moment.tz().zoneName()` | ✅ | Long zone name API |
38
+ | `moment.tz.zone(name)` | ✅ | Zone object API |
39
+ | `moment.tz.names()` | ✅ | List all zone names |
40
+ | `moment.tz.guess()` | ✅ | Runtime timezone detection |
41
+ | `moment.tz.setDefault(z)` | ⚠️ Partial | Stores zone name; apply requires core changes |
42
+ | DST spring-forward | ✅ | Adjusted forward by 1h |
43
+ | DST fall-back | ✅ | First-occurrence (DST side) |
44
+ | `moment.tz(input, zone).valueOf()` | ✅ | Matches moment-timezone |
45
+ | `zone.abbr(ts)` | ✅ | Matches moment-timezone |
46
+ | `zone.offset(ts)` | ✅ | Matches moment-timezone |
47
+ | `zone.utcOffset(ts)` | ✅ | Matches moment-timezone |
48
+
49
+ ### Oracle verification
50
+
51
+ All behavioral tests compare mmntjs-timezone output against moment-timezone.
52
+ Hand-written expected strings are NOT used for timezone-specific values.
53
+
54
+ ### Deterministic
55
+
56
+ - Fixed random seed for property tests
57
+ - Cached `Intl.DateTimeFormat` per timezone
58
+ - Offset cache uses `Math.floor(timestamp / 1000)` — deterministic per-second
59
+ - All tests pass across 6 timezone environments (UTC, America/New_York, Europe/Berlin, Asia/Tokyo, Australia/Sydney, America/Los_Angeles)
60
+
61
+ ## Current Status
62
+
63
+ The package now exposes the core packed-data compatibility APIs and preloads bundled authoritative tzdata generated from `moment-timezone` at build time:
64
+
65
+ - `moment.tz.add(data)`
66
+ - `moment.tz.link(links)`
67
+ - `moment.tz.load(bundle)`
68
+ - `moment.tz.unpack(data)`
69
+ - `moment.tz.unpackBase60(input)`
70
+ - `moment.tz.countries()`
71
+ - `moment.tz.zonesForCountry(code)`
72
+
73
+ Current limitation:
74
+
75
+ - internal storage still uses unpacked JS arrays/objects rather than the planned compact typed-array / lazy-decode representation
76
+
77
+ ### Intl-backed abbreviation limitations
78
+
79
+ `zone.abbr()` uses `Intl.DateTimeFormat` with `timeZoneName: "short"`. This differs from moment-timezone's packed tzdb:
80
+
81
+ - **Historical abbreviations**: Intl may not know pre-1970 abbreviations (e.g., "BST" for pre-WW2 London). The oracle tests in `zone-object.test.ts` verify this for epoch=0.
82
+ - **Generic vs standard/daylight**: Some zones return generic "CT" or "MT" instead of "CST"/"CDT" or "EST"/"EDT". `tryLocaleAbbr()` filters out non-standard results via heuristic (2-5 uppercase letters, no "GMT" prefix).
83
+ - **Known overrides**: Zones like `Asia/Taipei`, `Africa/Cairo` use `KNOWN_ABBR` table (line 217) where Intl doesn't provide a short name.
84
+ - **Fallback**: When Intl fails to produce a short name, the abbreviation is synthesized as `GMT{±HHMM}` (e.g., "GMT+0530" for Asia/Kolkata).
85
+ - **Locale probing**: Multiple locales (`en-US`, `ja-JP`, `zh-CN`, etc.) are tried since different locales sometimes yield a short name where others produce a long name. The winning locale is cached per zone.
86
+
87
+ Abbreviation behavior is oracle-tested against moment-timezone in `regression.test.ts` (including DST transitions and cache ordering).
88
+
89
+ - **Default timezone**: `moment.tz.setDefault()` stores the zone name. Full integration (making `moment()` respect the default) requires changes to the mmntjs core. The current behavior is:
90
+ - `moment.defaultZone` is set
91
+ - `moment.tz(explicit, zone)` still uses the explicit zone
92
+ - `moment.utc()` is unaffected
93
+ - `moment()` does NOT automatically create in the default zone
94
+
95
+ ## Testing
96
+
97
+ ```bash
98
+ # Run all tests
99
+ bun test
100
+
101
+ # Run across 6 timezones
102
+ bash ../../scripts/run-timezone-tests.sh
103
+
104
+ # Run property tests
105
+ bun test test/property.test.ts
106
+ bun test test/properties-intensive.test.ts
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT
112
+
113
+ ## Bundle Size
114
+
115
+ Comparison with original `moment-timezone` (all minified, `bunx tsup --minify`).
116
+
117
+ ### Full data (all historical zones)
118
+
119
+ | | raw | gzip |
120
+ |---|---|---|
121
+ | `moment-timezone-with-data.min.js` | 710KB | 38KB |
122
+ | `mmntjs-timezone/index.js` (ESM) | **296KB** (−58%) | **35KB** (−8%) |
123
+
124
+ ### 1970–2030 subset
125
+
126
+ | | raw | gzip |
127
+ |---|---|---|
128
+ | `moment-timezone-with-data-1970-2030.min.js` | 131KB | 20KB |
129
+ | `mmntjs-timezone/1970-2030.js` (ESM) | **78KB** (−40%) | **22KB** (+10%) |
130
+
131
+ ### Logic only (no tzdata, load-your-own)
132
+
133
+ | | raw | gzip |
134
+ |---|---|---|
135
+ | `moment-timezone.min.js` | 7KB | 3KB |
136
+ | `mmntjs-timezone/logic.js` (ESM) | **12KB** | **4.7KB** |
137
+
138
+ Both full and 1970–2030 use the same `!D|` delta-encoded binary blob format generated from the upstream `moment-timezone` npm package at build time. The 1970–2030 gzip is 2KB larger because the filter is mechanical (timestamp window) rather than hand-picked like the original.