{"id":51781167,"url":"https://github.com/leeoniya/easy-tz","last_synced_at":"2026-07-20T12:04:07.856Z","repository":{"id":371187017,"uuid":"1299561865","full_name":"leeoniya/easy-tz","owner":"leeoniya","description":"Experiments in implementing a fast, dependency-free getTimeZonesAt(timestamp)","archived":false,"fork":false,"pushed_at":"2026-07-15T21:11:38.000Z","size":360,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-15T23:11:40.451Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leeoniya.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-13T17:29:46.000Z","updated_at":"2026-07-15T21:11:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/leeoniya/easy-tz","commit_stats":null,"previous_names":["leeoniya/timezones-plus","leeoniya/easy-tz"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/leeoniya/easy-tz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Feasy-tz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Feasy-tz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Feasy-tz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Feasy-tz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leeoniya","download_url":"https://codeload.github.com/leeoniya/easy-tz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leeoniya%2Feasy-tz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35685359,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"ssl_error","status_checked_at":"2026-07-20T02:08:09.736Z","response_time":111,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-20T12:04:07.165Z","updated_at":"2026-07-20T12:04:07.850Z","avatar_url":"https://github.com/leeoniya.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🌐 easy-tz\n\nExperiments in implementing a fast, dependency-free `getTimeZonesAt(timestamp)`. Scope: current-year accuracy in modern runtimes; results are independent of\nthe host timezone (`TZ`). Historical tzdata accuracy is a non-goal.\n\n```ts\nfunction getTimeZonesAt(timestamp: number): TimeZoneInfo[];\n\ninterface TimeZoneInfo {\n  name: string;     // \"America/New_York\"\n  abbr: string;     // \"EST\" / \"EDT\" (not \"GMT-5\" where avoidable)\n  offset: string;   // \"-05:00\"\n  aliasOf?: string; // canonical id when `name` is a legacy spelling (\"Asia/Kolkata\")\n}\n```\n\n## Why this exists\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│ Type to search (name, city, abbreviation)                 🔍 │\n├──────────────────────────────────────────────────────────────┤\n│  Default  UTC, GMT                             [UTC+00:00]  ▲│\n│  Browser Time  CDT                             [UTC−05:00]  █│\n│  Coordinated Universal Time  UTC, GMT          [UTC+00:00]  █│\n│ ──────────────────────────────────────────────────────────  ░│\n│  Africa                                                     ░│\n│    Abidjan  GMT                                [UTC+00:00]  ░│\n│    Accra  GMT                                  [UTC+00:00]  ░│\n│    Addis Ababa  EAT                            [UTC+03:00]  ░│\n│    Algiers  CET                                [UTC+01:00]  ░│\n│    Asmara  EAT                                 [UTC+03:00]  ▼│\n└──────────────────────────────────────────────────────────────┘\n```\n\nWhile swapping a codebase from 295KB `moment` to 68KB `luxon`, I also\nwanted to drop the 770KB `moment-timezone` dependency from a time zone picker\ncomponent. A small, fast replacement did not exist for this purpose (see\n[comparison.md](comparison.md) for the full 19-library evaluation):\n\n- `Intl` provides offsets, but not reliable abbreviations: `en` CLDR only\n  defines short names for a handful of mostly North American metazones, so\n  Intl-backed formatters (luxon, date-fns, dayjs) emit \"GMT+2\"-style labels\n  for most of the world.\n- Relying on `Intl` at runtime is also slow to initialize and memory-heavy:\n  constructing a formatter per zone is ~100x the cost of calling one, so the\n  first full-list call pays tens of milliseconds and tens of MB of ICU state.\n- Libraries with real abbreviations built in (moment-timezone,\n  timezone-support, timezonecomplete, bigeasy/timezone) bundle full tzdata —\n  0.3-1.8 MB minified — carrying deep historical transition data this use\n  case doesn't need.\n\nMy [first attempt](https://github.com/leeoniya/timezones) split the\ndifference with a generated offset→abbreviation lookup plus live Intl\noffsets. The implementations here further explore the full live-to-baked spectrum,\nending in `07-baked-rules`: vs moment-timezone it cuts cold start ~80x\n(24.8ms → 0.3ms) and memory ~3x (22MB → 6.5MB) at ~1.3% of the bundle size\n(773.5KB → 10.2KB), while passing all 62 edge-case fixtures and improving\nabbreviation coverage for 159 zones where modern tzdata dropped letter\nabbreviations (Santiago CLT/CLST, Kathmandu NPT, Chatham CHAST/CHADT,\nKiritimati LINT, Lord Howe LHST/LHDT, Istanbul TRT, …).\n\n## Implementations\n\nOrdered fastest to slowest — which is also most-baked to most-live: each\nstep down trusts the generated data less (adding runtime rigor and cost),\nuntil `04-live-intl` ships no generated data at all.\n\n| impl | trust model | cold ms | miss ms | rss MB | bundle KB |\n|---|---|--:|--:|--:|--:|\n| `07-baked-rules` | trusts baked tables completely | 0.3 | \u003c0.1 | 6.5 | 10.2 |\n| `10-audited-rules` | baked tables, Temporal-audited at first call; failing zones recovered live | 2.3 | \u003c0.1 | 7.4 | 11.7 |\n| `08-verified-sharing` | live Intl values; baked data only hints formatter sharing, Temporal-verified at first call | 22.2 | 0.6 | 18.4 | 10.6 |\n| `04-live-intl` | fully live — no generated data to trust | 38.9 | 1.4 | 25.6 | 6.3 |\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eImplementation details\u003c/b\u003e — strategies and per-impl notes\u003c/summary\u003e\n\n| impl | abbr strategy | offset strategy |\n|---|---|---|\n| `07-baked-rules` | baked into generated year schedule | baked into generated year schedule (zero Intl at runtime) |\n| `10-audited-rules` | 07's baked schedule, audited at first call | baked (audited); Temporal-live for recovered zones |\n| `08-verified-sharing` | same as 04, via rep formatters whose groups are Temporal-verified at first call | same as 04 |\n| `04-live-intl` | `'long'` name -\u003e curated map, initials fallback | derived arithmetically from zone-local wall-clock fields (1 Intl call/zone) |\n\n`07-baked-rules` trusts the generated data completely: the generator emits\n`shared/schedule.ts` — a YEAR-INDEPENDENT schedule fitted by probing three\nconsecutive years: static states, two-state nth-weekday-of-month rules\n(\"second Sunday of March at 02:00 wall\"), and current-year segments for the\nfew zones whose rules aren't Gregorian (Morocco/Palestine Ramadan rules) —\nso a call is pure date math with zero Intl usage, and stays correct across\nyear boundaries until a country actually changes policy. Fastest cold start\nand smallest memory of the four, but least resilient: a stale table means\nwrong answers until regeneration (needed on tzdata/CLDR changes, and yearly\nonly for the irregular zones). `tests/schedule.test.ts` asserts\noutput-equality with 04 including next-year instants; irregular zones clamp\noutside the generated year.\n\nHow exposed is that in practice — measured against 04, not against perfect\ndata? The events are real but rare: weighting the last decade of tzdb\nreleases (2016-2026) by who's affected, a future-effective rule change hit\na 2M+ metro zone in 7 of 11 years — Cairo twice (2016 DST cancel on 3 days'\nnotice, 2023 reintroduction), Istanbul (2016), Casablanca and Pyongyang\n(2018), Brazil's DST-observing zones incl. São Paulo and Rio (2019), the\n2022 cluster (Mexico City, Tehran, Amman, Damascus, Santiago), Almaty and\nAsunción (2024), Calgary/Edmonton (2026) — ~1.5-2 major zones/year, heavily\nclustered; 2021 and 2025 touched only small-population zones (Samoa, South\nSudan, Chilean Aysén). But 04 is not current at the effective date either:\nits data rides announcement -\u003e tzdb release (days-weeks, sometimes negative\n— Alberta 2026 shipped 3 weeks after taking effect; Egypt 2016 gave 3 days)\n-\u003e ICU/Chrome pickup (a stable cycle or two on a 4-week cadence) -\u003e each\nuser's browser actually updating. That shared upstream pipeline is\nweeks-to-months; 07's *additional* exposure is only how long\nregen+redeploy lags the generating Chrome's update, which for any app that\ndeploys monthly-or-better rounds to zero. It can even invert: baked output\ndoesn't depend on the user's runtime, so a freshly regenerated table serves\ncorrect post-change data to browsers whose own ICU is still stale — where\n04 is wrong. The one structural exception is predictable, not\nevent-driven: the Ramadan-rule zones (Casablanca/El Aaiun and Gaza/Hebron —\nall of Morocco and Palestine, ~40M people) clamp outside their generated\nyear, so skipping the January regen gets them wrong for the ~month-long\nRamadan window every single year, no policy change required.\n\n`10-audited-rules` builds on 07, adding a first-call audit for rigor: once\nper process (sound — browsers never hot-swap tzdata) every zone's\ncurrent-year behavior predicted by the baked schedule is checked against\nTemporal's exact transition walk (~2-5ms, no formatters). Zones that fail —\na policy change in a stale table, unknown zones, irregular zones outside\ntheir generated year — are recovered for the session with live Temporal\noffsets and generic GMT-style labels; everything else runs pure baked at\n07's miss cost. Never a wrong offset on Temporal runtimes; without Temporal\n(Safari, bun, Temporal-less Node builds) it degrades to exactly 07. (It\nsuperseded `09-guarded-hybrid`, which achieved the same protection with a\nper-call guard and a bundled live-Intl fallback: ~0.8ms misses and +3.4KB\nfor curated-quality recovery labels.)\n\n`08-verified-sharing` applies the same verify-at-first-call idea but flips\nthe trust model: values always come from live Intl, and the generated class\ntable is demoted to a *hint* about which zones can share one formatter (188\nformatters instead of 445, cutting 04's cold start roughly in half). At\nfirst call each group member's exact offset behavior for the year is\ncompared against its representative's via Temporal's transition walk\n(`getTimeZoneTransition`, no formatters, ~4-5ms once), and diverged members\nare split out to format themselves. One-time cost, no per-call overhead; a\nstale table can only cost speed, never correctness. Without Temporal it\ndegrades to exactly impl 04.\n\n`04-live-intl` is the fully live baseline: no generated data at all — a\ncurated long-name -\u003e abbreviation map plus one Intl formatter per zone,\nwith offsets derived arithmetically from zone-local wall-clock fields.\nSlowest cold start and heaviest memory (one formatter per zone forces the\nfull ICU cost), but nothing can go stale except the small curated abbr map;\nit's the reference the other three are tested against.\n\nAll impls memoize the full response per UTC hour bucket\n(`shared/hourCache.ts`): a single global\nslot keeps the last bucket's result and is refreshed whenever a timestamp\nfalls outside it, so only same-bucket repeats hit — suited to clock-driven\nqueries near \"now\". The underlying compute always runs at the bucket start,\nso DST transitions (hour-aligned in UTC for nearly all zones) resolve\ndeterministically at bucket boundaries. Cache hits return the same array\nreference — treat results as immutable. Hits cost ~0.1-0.3µs vs a miss's\n~1-4ms (live impls) or ~0.05-0.1ms (baked impls); `tests/cache.test.ts`\nbenches hit and miss loops separately for every impl.\n\n\u003c/details\u003e\n\n## Install\n\n```sh\nnpm install @leeoniya/easy-tz\n```\n\n## Usage\n\n```ts\nimport { getTimeZonesAt } from '@leeoniya/easy-tz';\n\nconst zones = getTimeZonesAt(Date.now());\n// [\n//   { name: 'Africa/Abidjan',     abbr: 'GMT', offset: '+00:00' },\n//   ...\n//   { name: 'America/New_York',   abbr: 'EDT', offset: '-04:00' },\n//   ...\n// ] — every IANA zone the runtime knows, sorted by name\n```\n\nThe root import is `07-baked-rules` — fastest and smallest, pure baked data\n(see [Implementations](#implementations)). The other impls are available as\nsubpath imports with the same API, in increasing order of runtime\nverification (and cost):\n\n```ts\nimport { getTimeZonesAt } from '@leeoniya/easy-tz/10-audited-rules';    // baked, Temporal-audited at first call\nimport { getTimeZonesAt } from '@leeoniya/easy-tz/08-verified-sharing'; // live values, verified sharing\nimport { getTimeZonesAt } from '@leeoniya/easy-tz/04-live-intl';        // fully live baseline\n```\n\nResults are memoized per UTC hour bucket and returned by reference — treat\nthem as immutable. Every entry also exports `clearCache()`, which drops\nthat memo so the next call recomputes (first-call init/verification work is\nnot redone); it exists for test/bench harnesses and for recovering from\naccidental mutation of a returned array.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleeoniya%2Feasy-tz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleeoniya%2Feasy-tz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleeoniya%2Feasy-tz/lists"}