Use temporal-polyfill for zone-aware date arithmetic
D6 puts every day boundary, week start, and snap through the dataset's IANA zone, so time/zone.ts has to convert between Instants and plain (wall-clock) times correctly in any zone. The original hand-rolled implementation (Intl.DateTimeFormat.formatToParts plus fixed-point iteration) had no documented behavior for the two cases that matter — a DST fold, where a plain time is ambiguous (1:30 AM on the fall-back day happens twice), and a DST gap, where it does not exist at all (2:30 AM on the spring-forward day). It resolved them incidentally rather than deliberately, and nothing tested which way. We adopted temporal-polyfill because it implements Temporal's explicit disambiguation model (compatible / earlier / later / reject), which turns fold and gap from undefined behavior into a policy we chose and can test.
This is a deliberate exception to the one-runtime-dependency budget, and the second dependency the project has ever taken.
Considered options
- Keep the hand-rolled
formatToPartsimplementation. Zero dependencies, but we would have had to specify and implement fold/gap semantics ourselves — the genuinely hard part — and every future calendar unit (month, year) would repeat that work. @js-temporal/polyfill. Right shape and the most conservative semantics, being the reference polyfill, but it does not tree-shake (single class hierarchy) and costs ~2.3–2.9x the gzipped size oftemporal-polyfill's/fns/*build for the same need.luxon/date-fns+tz/dayjs. None expose explicit fold/gap disambiguation control, which was the whole reason for the change.d3-time. Does its arithmetic in the environment's local zone or UTC only — structurally incapable of working in an arbitrary dataset-owned zone, which is exactly what D6 requires.
Consequences
- The escape hatch stays open. Only
time/zone.tsimports the package, via/fns/*entry points, andtime/'s public surface (instant,toPlain,fromPlain,startOfDay,addDays,diffDays) did not change. Swapping to nativeTemporalwhen it ships is a one-file change. Header and a11y date formatting go throughIntl.DateTimeFormatdirectly, not the polyfill'stoLocaleString, so that single-import-site consequence still holds;weekOfYearis the one new polyfill call, and it lives inzone.ts(S1.12, D-S1.12-13). - Accepted bus-factor risk. ~95% of the package's commits come from a single maintainer. We took this knowingly rather than by oversight; the pin is
^1(not exact, and not floored to one patch) so patch and minor updates — security fixes included — land automatically and are caught by the DST fold/gap and multi-zone round-trip tests inzone.test.tsbefore merge. - No live workaround.
temporal-polyfill@1.0.4's zoned day-unit diff threw (prepareZonedEpochDiff is not a function) in every zone, UTC included — a packaging bug in that build, worked around by routingdiffDaysthroughPlainDate.diffDays.1.0.5fixed the bug (fullcalendar/temporal-polyfill#103, #104);diffDaysnow callsZonedDateTimeFns.diffDaysdirectly.
Full dependency reasoning, including the rejected candidates above, is in plans/04 §1 and §1.1.