Skip to content

ADR-0053 Phase 2 · Slice 3: timezone-aware today()/daysFromNow()/daysAgo() #1980

Closed

Description

@os-zhuang

Part of ADR-0053 Phase 2. Design: #1975 · Parent: #1928.

Slice 3 of 6 — compute-tz core. Make the time functions timezone-aware. Gated behind "reference tz unset → UTC".

Decision D1 (see #1975)

today()/daysFromNow()/daysAgo() return a Date at UTC-midnight of the reference-tz calendar day — new Date(Date.UTC(y, m, d)) with (y,m,d) computed in the reference tz. Not a date-only string; not local-midnight-as-instant (that trap re-introduces the silent-miss bug — cel-js's hydrateOverloadStrings rehydrates the field's date-only string to UTC midnight, so today() must match that).

Scope

  • Add timezone?: string to EvalContext — packages/formula/src/types.ts.
  • registerStdLib(env, now) → registerStdLib(env, now, timezone); buildEnv(now) → buildEnv(now, ctx.timezone) — packages/formula/src/stdlib.ts:40-57, packages/formula/src/cel-engine.ts:35-42 and :189-191.
  • Replace startOfDayUtc/addDaysUtc (stdlib.ts:18-29) with tz-aware equivalents built on a shared partsInTz/calendarDayUtc util extracted from the proven Intl.formatToParts pattern in packages/services/service-messaging/src/preference-resolver.ts:347. No hand-rolled offset math (DST).
  • Side-effect: also fixes the "keeps wall-clock time" defect of daysFromNow/daysAgo.

Acceptance criteria

  • With reference tz America/Los_Angeles, at 2026-06-16T02:00Z, today() == the UTC-midnight Date of 2026-06-15.
  • record.due_date == today() and == daysFromNow(n) match the right calendar day across DST boundaries.
  • tz unset / 'UTC' → byte-for-byte today's behavior.

Prerequisite

Confirm cel-js supports duration() + Timestamp arithmetic (the documented now() + duration("Nh") escape hatch). No duration usage exists in the formula package today.

Depends on

Slice 1 (timezone value); produces the shared partsInTz util reused by slice 5.

Activity

  1. self-assigned this
    on Jul 18, 2026
  2. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    Slice 3 delivered — closing as completed

    The compute-tz core shipped across #1998 (the slice), #2001 (shared calendarPartsInTz util), and #2006 (localization settings making the reference tz configurable end-to-end). Acceptance tests are added in #3181.

    Scope ✔︎

    • EvalContext.timezone added — packages/formula/src/types.ts.
    • registerStdLib(env, now, timezone) / buildEnv(now, ctx.timezone) — stdlib.ts, cel-engine.ts.
    • startOfDayUtc/addDaysUtc replaced by the DST-safe partsInTz/calendarDayUtc (Intl.formatToParts, no hand-rolled offset math). Shared calendarPartsInTz extracted to @objectstack/core (reused by slice 5); formula keeps a private copy to stay a leaf.
    • "keeps wall-clock time" defect of daysFromNow/daysAgo fixed (now midnight-truncated).
    • cel-js duration() prerequisite confirmed (cel-engine.test.ts).

    Acceptance criteria

    Closing as completed — Slice 3's deliverable (timezone-aware today()/daysFromNow()/daysAgo()) is done and verified. The equality-hydration gap continues in #3183.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions