Skip to content

IntlDateTimeFormat: only pass -u-hc- to UDateIntervalFormat when explicitly requested - #366

Open
robobun wants to merge 1 commit into
mainfrom
robobun/intl-dtf-formatrange-hc-icu-22669
Open

IntlDateTimeFormat: only pass -u-hc- to UDateIntervalFormat when explicitly requested#366
robobun wants to merge 1 commit into
mainfrom
robobun/intl-dtf-formatrange-hc-icu-22669

Conversation

@robobun

@robobun robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

Intl.DateTimeFormat.prototype.formatRange with timeStyle: "full" (or "long") on ja-JP, ko-KR, th-TH renders time with colon separators instead of the locale's literal-separated style pattern, while format() on the same formatter renders correctly:

const a = new Date(Date.UTC(2024, 0, 15, 13, 4, 5)), b = new Date(Date.UTC(2024, 6, 4, 3, 30, 0));
const f = new Intl.DateTimeFormat("ja-JP", { timeZone: "UTC", timeStyle: "full" });
f.format(a);         // "13時04分05秒 協定世界時"
f.formatRange(a, a); // "13時04分05秒 協定世界時"           (equal-instant shortcut)
f.formatRange(a, b); // "2024/1/15 13:04:05 協定世界時~..."  (colons, wrong)
// node/V8:           "2024/1/15 13時04分05秒 協定世界時~..."

Same shape for ko-KR (오후 1시 4분 5초 becomes 오후 1:04:05) and th-TH (13 นาฬิกา 04 นาที 05 วินาที becomes 13:04:05).

Cause

createDateIntervalFormatIfNecessary appends -u-hc-<m_hourCycle> to the locale passed to udtitvfmt_open whenever m_hourCycle != None. But m_hourCycle is derived from the generated pattern in setFormatsFromPattern, so it is always populated when an hour field is present, even when the user never specified hourCycle, hour12, or a -u-hc- locale extension. For ja-JP with timeStyle: "full" the locale becomes ja-u-hc-h23 instead of ja.

In ICU < 76 (before ICU-22669), DateTimePatternGenerator::addICUPatterns loads the locale's standard style patterns by creating DateFormat instances, and when the locale carries an hours keyword those instances go through a path that regenerates their patterns from skeletons. For locales whose timeFormats/full pattern uses literal separators the regenerated pattern falls back to the colon-separated availableFormats entry. DateIntervalFormat::initializePattern then builds its fallback patterns from that generator and produces H:mm:ss for range endpoints, while the single-date UDateFormat (which was opened without -u-hc-) still has H時mm分ss秒.

Confirmed directly against stock ICU 75.1:

DateIntervalFormat("Hmmsszzzz", "ja")          -> 13時04分05秒 GMT ~ 14時30分00秒 GMT
DateIntervalFormat("Hmmsszzzz", "ja-u-hc-h23") -> 13:04:05 GMT ~ 14:30:00 GMT

Fix

Store the hour cycle resolved from the locale's -u-hc- extension and the hourCycle option separately as m_explicitHourCycle (None when neither was specified), and use that instead of the pattern-derived m_hourCycle when building the UDateIntervalFormat locale in both createDateIntervalFormatIfNecessary and createTemporalIntervalFormat.

The skeleton passed to udtitvfmt_open already encodes the 12/24-hour choice (h vs H), and the locale default covers the h11/h12 and h23/h24 preference within each group, so -u-hc- is only needed to override that preference. Explicit hourCycle: "h11" etc. continues to work.

Verification

Built Bun against local WebKit + stock ICU 75.1. Repro now matches node output byte-for-byte for ja/ko/th, and all hourCycle: "h11"|"h12"|"h23"|"h24" cases still render the correct midnight hour in formatRange. Test coverage lands in oven-sh/bun alongside the WEBKIT_VERSION bump.

…icitly requested

createDateIntervalFormatIfNecessary and createTemporalIntervalFormat were appending
`-u-hc-<m_hourCycle>` to the locale passed to udtitvfmt_open whenever m_hourCycle was
populated. m_hourCycle is derived from the generated pattern in setFormatsFromPattern,
so it is always non-None when an hour field is present, even when the user never
specified hourCycle, hour12, or a -u-hc- locale extension.

In ICU < 76 (before ICU-22669), DateTimePatternGenerator::addICUPatterns loads the
locale's style patterns by creating DateFormat instances, and when the locale carries
an hours keyword those instances regenerate their patterns from skeletons. For locales
whose timeFormats patterns use literal separators (ja `H時mm分ss秒 zzzz`, ko
`a h시 m분 s초 zzzz`, th `H นาฬิกา mm นาที ss วินาที zzzz`) the regenerated
patterns fall back to the colon-separated availableFormats entries, so the generator's
pattern map no longer contains the style pattern. DateIntervalFormat then produces
`H:mm:ss` for range endpoints while the single-date UDateFormat (which never received
-u-hc-) still produces `H時mm分ss秒`, and format(a) / formatRange(a,b) disagree.

Store the hour cycle resolved from the locale extension and hourCycle option separately
as m_explicitHourCycle (None when neither was specified) and use that when building the
UDateIntervalFormat locale. The skeleton already encodes the 12/24-hour choice, and the
locale default covers the h11/h12 and h23/h24 preference, so -u-hc- is only needed when
the user explicitly overrode that preference.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49ade63a-1254-4226-879f-7029af9fa36a

📥 Commits

Reviewing files that changed from the base of the PR and between 9839239 and d8348c3.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp
  • Source/JavaScriptCore/runtime/IntlDateTimeFormat.h

Walkthrough

Changes

The formatter now records explicit hour-cycle input separately from pattern-derived state and uses that value when constructing ICU date and temporal interval formatters.

Hour-cycle interval formatting

Layer / File(s) Summary
Track explicit hour-cycle state
Source/JavaScriptCore/runtime/IntlDateTimeFormat.h, Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp
IntlDateTimeFormatImpl adds m_explicitHourCycle, and initialization stores the resolved explicit hour cycle in it.
Apply explicit cycle to interval formatters
Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp
Date and temporal interval formatter creation now uses m_explicitHourCycle to determine whether to append the ICU -hc- extension.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the core change: only adding -u-hc- to UDateIntervalFormat when explicitly requested.
Description check ✅ Passed The description matches the diff and explains the bug, root cause, fix, and verification.
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.

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

@claude claude 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.

I didn't find any issues — the change is small, well-documented, and the root-cause analysis (ICU-22669) checks out. Deferring to a human only because this is a behavioral change in Intl spec-compliance code that interacts with ICU version-specific quirks.

What was reviewed:

  • Traced initializeDateTimeFormatresolved.extensions[Hc] is null when neither -u-hc- nor hourCycle is given (localeData default is null), so m_explicitHourCycle correctly stays None in the ja-JP repro.
  • hour12 path: sets localeOptions[Hc] = String(), so m_explicitHourCycle is None; the 12/24 choice is still carried by the skeleton's h/H, matching the PR's reasoning.
  • Both interval-format call sites updated consistently; m_hourCycle still drives resolvedOptions() and pattern rewriting, unchanged.
Extended reasoning...

Overview

Adds a new m_explicitHourCycle field to IntlDateTimeFormatImpl that captures the hour cycle as resolved from the input locale's -u-hc- extension and the hourCycle option (via resolveLocale), before setFormatsFromPattern overwrites m_hourCycle from the generated pattern. The two UDateIntervalFormat construction sites (createDateIntervalFormatIfNecessary and createTemporalIntervalFormat) now consult m_explicitHourCycle instead of m_hourCycle when deciding whether to append -u-hc- to the ICU locale string.

Security risks

None. This is pure Intl formatting output; no parsing of untrusted input beyond what already flows through resolveLocale/parseHourCycle, no memory management changes, no new allocations.

Level of scrutiny

Moderate. The diff itself is tiny (one field assignment, two use-site swaps, plus comments), but it changes observable formatRange output and works around an ICU-version-specific bug (ICU-22669, fixed in ICU 76). The correctness argument depends on resolveLocale semantics: the Hc keyLocaleData default is a null String() (IntlDateTimeFormat.cpp:225), so when the user supplies neither -u-hc- nor hourCycle, resolved.extensions[Hc] is null and parseHourCycle returns None — which is exactly the case the fix targets. When hour12 is set, localeOptions[Hc] is forced to null String() (line ~785), so m_explicitHourCycle is again None, and the 12/24 choice is carried by the skeleton's h/H character instead — consistent with the PR description's claim that the skeleton already encodes this and the locale default handles h11/h12 vs h23/h24 within each group.

Other factors

  • m_hourCycle retains all its other uses (resolvedOptions(), replaceHourCycleInPattern), so spec-visible resolvedOptions().hourCycle is unchanged.
  • The PR description includes a direct ICU 75.1 reproduction of the underlying ICU bug and byte-for-byte verification against node/V8 for ja/ko/th plus all four explicit hourCycle values, and notes tests land in oven-sh/bun.
  • Given this is a behavioral change in a spec-compliance area with ICU-version-dependent effects, I'd prefer a human familiar with the Intl code to sign off rather than auto-approving.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
d8348c3d autobuild-preview-pr-366-d8348c3d 2026-07-28 09:28:24 UTC

robobun added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
…formatRange fix)

Picks up oven-sh/WebKit#366: only pass -u-hc- to UDateIntervalFormat when the
hour cycle was explicitly requested, so formatRange keeps the locale's
timeStyle literal separators (ja/ko/th) instead of falling back to H:mm:ss.
One commit on top of 549170099226f816.
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.

2 participants