Skip to content

fix(intl): #5582 — Temporal-aware DateTimeFormat range + timeZoneName part (19 test262 cases)#5770

Merged
proggeramlug merged 1 commit into
mainfrom
fix/5582-dtf-temporal-range
Jun 28, 2026
Merged

fix(intl): #5582 — Temporal-aware DateTimeFormat range + timeZoneName part (19 test262 cases)#5770
proggeramlug merged 1 commit into
mainfrom
fix/5582-dtf-temporal-range

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

#5582Intl.DateTimeFormat Temporal range + timeZoneName

Builds on main's single-value Temporal support (temporal::temporal_to_epoch_ms) and extends it to the parts that the deterministic formatter still missed: the range methods, the timeZoneName part, and the spec-mandated TypeErrors.

Problem

  • formatRange / formatRangeToParts coerced both endpoints through ToNumber, so any Temporal argument threw TypeError: Cannot convert a Temporal value to a number.
  • formatToParts never emitted a timeZoneName part, so the cases that read .find(p => p.type === 'timeZoneName').value hit undefined.value.
  • Intl.DateTimeFormat.prototype.format(zonedDateTime) formatted instead of throwing the required TypeError.

Changes (single file: crates/perry-runtime/src/intl/date_collator.rs)

  • date_time_range_clip routes each endpoint through the same Temporal-aware date_arg_to_clipped_ms as the single-value path; a plain Temporal value decodes to its epoch instant and is formatted instead of throwing.
  • range_type_tagPartitionDateTimeRangePattern: the two endpoints must be the same brand; mixing brands (e.g. PlainDate + PlainTime, or Date + a Temporal value) is a TypeError. This preserves the fails-on-distinct-temporal-types / dissimilar-types throws that previously came for free from ToNumber.
  • date_arg_to_clipped_ms rejects Temporal.ZonedDateTime with a TypeError (ECMA-402 HandleDateTimeValue), covering the single-value and same-brand range cases.
  • formatToParts emits a {type:"timeZoneName"} part when the option is set and the input denotes a real instant (Date/number); a Temporal plain value carries no zone and is suppressed.

Reuses main's temporal_to_epoch_ms — no duplicate epoch logic.

Validation

scripts/test262_subset.py --dir intl402/DateTimeFormat (pinned corpus), measured against current main:

main this PR
pass 126 145
runtime-fail 97 78

0 regressions. 19 cases flip green:

  • format / formatRange / formatRangeToParts × temporal-{plaindate,plaindatetime,plainmonthday,plaintime,plainyearmonth}-formatting-timezonename (15)
  • format / formatToParts temporal-zoneddatetime-not-supported (2)
  • formatRange / formatRangeToParts temporal-objects-no-time-clip (2)

cargo test -p perry-runtime (date module) green.

Scope

Perry ships no CLDR / Temporal calendar engine (out of scope per CLAUDE.md), so the rendered timeZoneName label and extreme-date calendar output stay best-effort; the in-scope cases observe only part presence and (non-)throwing behavior. Remaining intl402/DateTimeFormat failures (calendar systems, dayPeriod, fractionalSecondDigits, …) still need a real formatting engine and stay tracked in #5582.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Intl.DateTimeFormat now rejects Temporal.ZonedDateTime inputs with a clear TypeError instead of treating them like epoch-based dates.
    • formatToParts() now includes a timeZoneName part when requested for real instants, improving output consistency.
    • Date range formatting now validates both endpoints more strictly, preventing mixed date/Temporal values from being processed together.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Temporal.ZonedDateTime TypeError guard in date_arg_to_clipped_ms, refactors formatToParts to optionally append a timeZoneName part for non-Temporal instants, and updates date_time_range_clip to enforce matching Temporal brands on range endpoints using a new range_type_tag helper with Temporal-aware coercion.

Changes

Intl.DateTimeFormat Temporal validation and range coercion

Layer / File(s) Summary
ZonedDateTime TypeError guard
crates/perry-runtime/src/intl/date_collator.rs
date_arg_to_clipped_ms now explicitly throws a TypeError when the value is a Temporal.ZonedDateTime, short-circuiting before epoch extraction.
formatToParts timeZoneName emission
crates/perry-runtime/src/intl/date_collator.rs
formatToParts delegates to a new date_time_format_to_parts_value helper and conditionally appends a timeZoneName part for non-Temporal instant values, using time_zone_name_display with UTC/offset/GMT fallback logic.
Range endpoint brand-check and coercion
crates/perry-runtime/src/intl/date_collator.rs
New range_type_tag returns the Temporal kind or a non-Temporal sentinel. date_time_range_clip uses it to enforce that both endpoints share the same brand, then coerces both via date_arg_to_clipped_ms, removing the old numeric-only coercion and manual TimeClip.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5601: Introduces formatRange/formatRangeToParts plumbing that date_time_range_clip is now tightened to serve.
  • PerryTS/perry#5649: Overlaps directly on date_arg_to_clipped_ms, formatToParts timeZoneName, and range coercion in the same file.
  • PerryTS/perry#5765: Modifies the same Temporal-to-epoch-milliseconds conversion path in date_collator.rs that this PR's ZonedDateTime guard precedes.

Poem

🐇 A ZonedDateTime came hopping by,
But TypeError stopped it — "No epoch for you, goodbye!"
The timeZone name was stitched in neat,
And range endpoints now must match to meet.
UTC or GMT, the rabbit knows the way,
Temporal and Intl, aligned today! 🕰️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and matches the main change: Temporal-aware Intl.DateTimeFormat range handling and timeZoneName output.
Description check ✅ Passed The description covers the problem, concrete changes, related issue, and validation, though it uses custom headings instead of the template.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/5582-dtf-temporal-range

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… part (19 test262 cases)

Builds on main's single-value Temporal support (`temporal_to_epoch_ms`) and
extends it to the range methods, the `timeZoneName` part, and the
spec-mandated `TypeError`s the deterministic formatter still missed.

- `formatRange`/`formatRangeToParts` coerced both endpoints through `ToNumber`
  (`js_number_coerce`), raising "Cannot convert a Temporal value to a number"
  for any Temporal argument. `date_time_range_clip` now routes each endpoint
  through the same Temporal-aware `date_arg_to_clipped_ms` as the single-value
  path, so a plain Temporal value (PlainDate/PlainDateTime/PlainYearMonth/
  PlainMonthDay/PlainTime) decodes to its epoch instant and is formatted
  instead of throwing.
- `PartitionDateTimeRangePattern`: the two endpoints must be the same brand —
  mixing brands (e.g. PlainDate + PlainTime, or Date + a Temporal value) is a
  `TypeError` (`range_type_tag`). This preserves the existing
  `fails-on-distinct-temporal-types` / dissimilar-types throws that previously
  came for free from `ToNumber`.
- `Temporal.ZonedDateTime` is now rejected with a `TypeError` in
  `date_arg_to_clipped_ms` (ECMA-402 HandleDateTimeValue), covering both the
  single-value (`format`/`formatToParts`) and same-brand range cases.
- `formatToParts` emits a `{type:"timeZoneName"}` part when the option is set
  and the input denotes a real instant (`Date`/number); a Temporal plain value
  carries no zone and is suppressed.

test262 intl402/DateTimeFormat: runtime-fail 97 → 78 (pass 126 → 145), no
regressions. Fixes temporal-*-formatting-timezonename (x15),
temporal-zoneddatetime-not-supported (format/formatToParts), and the
temporal-objects-no-time-clip range cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug force-pushed the fix/5582-dtf-temporal-range branch from ad11553 to 512983e Compare June 28, 2026 18:47
@proggeramlug proggeramlug changed the title fix(intl): #5582 — Temporal-aware DateTimeFormat range + timeZoneName part (17 test262 cases) fix(intl): #5582 — Temporal-aware DateTimeFormat range + timeZoneName part (19 test262 cases) Jun 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/intl/date_collator.rs`:
- Around line 124-126: The early return in date_collator logic is too broad: it
skips all Temporal values and prevents Temporal.Instant from rendering a
requested timeZoneName. Update the guard in the date collator path so only the
plain Temporal types (PlainDate, PlainTime, PlainDateTime, PlainYearMonth,
PlainMonthDay) return early, while Temporal.Instant continues through the
formatting flow and can include the zone label. Use the existing temporal type
checks around the relevant collator/formatting branch to narrow this condition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44d9f752-1ca4-402c-a874-c71f208cda80

📥 Commits

Reviewing files that changed from the base of the PR and between 5d47364 and 512983e.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/intl/date_collator.rs

Comment on lines +124 to +126
if crate::temporal::is_temporal_value(value) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== date_collator.rs (focused) ==\n'
nl -ba crates/perry-runtime/src/intl/date_collator.rs | sed -n '100,170p'

printf '\n== temporal/mod.rs (focused) ==\n'
nl -ba crates/perry-runtime/src/temporal/mod.rs | sed -n '1,260p'

printf '\n== helper usages ==\n'
rg -n "fn is_temporal_value|fn temporal_kind|enum TemporalKind|is_temporal_value\(" crates/perry-runtime/src/temporal crates/perry-runtime/src -g '!**/target/**'

Repository: PerryTS/perry

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== date_collator.rs helper call sites ==\n'
sed -n '1,220p' crates/perry-runtime/src/intl/date_collator.rs | grep -n -C 4 -E 'append_time_zone_name_part|date_time_format_to_parts_value|is_temporal_value|temporal_kind'

printf '\n== temporal module symbols ==\n'
rg -n "enum TemporalKind|is_temporal_value|temporal_kind|PlainDate|PlainTime|PlainDateTime|PlainYearMonth|PlainMonthDay|Instant" crates/perry-runtime/src/temporal -g '!**/target/**'

Repository: PerryTS/perry

Length of output: 40351


Don’t suppress timeZoneName for Temporal.Instant. This guard skips every Temporal value, including Temporal.Instant; only the plain Temporal types (PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay) should return early so an Instant can still render a zone label when requested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/intl/date_collator.rs` around lines 124 - 126, The
early return in date_collator logic is too broad: it skips all Temporal values
and prevents Temporal.Instant from rendering a requested timeZoneName. Update
the guard in the date collator path so only the plain Temporal types (PlainDate,
PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay) return early, while
Temporal.Instant continues through the formatting flow and can include the zone
label. Use the existing temporal type checks around the relevant
collator/formatting branch to narrow this condition.

@proggeramlug proggeramlug merged commit 2412d05 into main Jun 28, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/5582-dtf-temporal-range branch June 28, 2026 19:00
proggeramlug added a commit that referenced this pull request Jun 28, 2026
…matToParts (#5774)

Follow-up to #5770 (CodeRabbit review). The `timeZoneName`-part guard in
`formatToParts` skipped *every* Temporal value, including `Temporal.Instant`.
An `Instant` is anchored to the timeline, so — like a `Date`/number — it must
render a zone label when `timeZoneName` is requested; only the *plain* Temporal
kinds (PlainDate/PlainTime/PlainDateTime/PlainYearMonth/PlainMonthDay) carry no
zone and stay suppressed. (`ZonedDateTime`/`Duration` are rejected upstream by
`date_arg_to_clipped_ms` and never reach this path.)

Behavior-preserving for the test262 intl402/DateTimeFormat suite (pass 145,
runtime-fail 78, unchanged); the in-scope timezonename cases use only plain
Temporal types.

Co-authored-by: Ralph <ralph@skelpo.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant