From efda48c1e4bb810276e968c4faf98c420e0403b7 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Sun, 10 May 2026 22:03:21 +0100 Subject: [PATCH 1/8] Return all ECMA-402 required fields from Intl resolvedOptions() Fix TryReadStringOption using `out` instead of `var`, which cleared constructor defaults when the option was absent. Add missing fields (numberingSystem, roundingIncrement, roundingPriority, trailingZeroDisplay, pluralCategories, calendar) and resolve fraction-digit defaults per spec across NumberFormat, DateTimeFormat, PluralRules, RelativeTimeFormat, and DurationFormat. Closes #595 Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.Intl.Helpers.pas | 4 +- .../Goccia.Values.IntlDateTimeFormat.pas | 85 ++++++++++++------- .../Goccia.Values.IntlDurationFormat.pas | 6 +- .../units/Goccia.Values.IntlNumberFormat.pas | 78 ++++++++++++++--- .../units/Goccia.Values.IntlPluralRules.pas | 30 +++++++ .../Goccia.Values.IntlRelativeTimeFormat.pas | 8 +- .../prototype/resolvedOptions.js | 58 +++++++++++++ .../prototype/resolvedOptions.js | 29 +++++++ .../NumberFormat/prototype/resolvedOptions.js | 79 +++++++++++++++++ .../PluralRules/prototype/resolvedOptions.js | 41 +++++++++ .../prototype/resolvedOptions.js | 25 ++++++ 11 files changed, 392 insertions(+), 51 deletions(-) create mode 100644 tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js create mode 100644 tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js create mode 100644 tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js create mode 100644 tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js create mode 100644 tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js diff --git a/source/units/Goccia.Intl.Helpers.pas b/source/units/Goccia.Intl.Helpers.pas index 1e79c8ed..7000c6d5 100644 --- a/source/units/Goccia.Intl.Helpers.pas +++ b/source/units/Goccia.Intl.Helpers.pas @@ -18,7 +18,7 @@ function FormatPartsToArray(const AParts: TIntlFormatPartArray): TGocciaArrayVal // Reads a string-valued property from AOptions. Returns True and sets AValue // when the property exists and is not undefined; returns False otherwise. function TryReadStringOption(const AOptions: TGocciaObjectValue; - const AName: string; out AValue: string): Boolean; + const AName: string; var AValue: string): Boolean; // Like TryReadStringOption but additionally rejects NUL characters via // ThrowRangeError (SErrorIntlInvalidOption). @@ -49,7 +49,7 @@ function FormatPartsToArray(const AParts: TIntlFormatPartArray): TGocciaArrayVal end; function TryReadStringOption(const AOptions: TGocciaObjectValue; - const AName: string; out AValue: string): Boolean; + const AName: string; var AValue: string): Boolean; var V: TGocciaValue; begin diff --git a/source/units/Goccia.Values.IntlDateTimeFormat.pas b/source/units/Goccia.Values.IntlDateTimeFormat.pas index e942c06e..4ce1d850 100644 --- a/source/units/Goccia.Values.IntlDateTimeFormat.pas +++ b/source/units/Goccia.Values.IntlDateTimeFormat.pas @@ -166,6 +166,25 @@ constructor TGocciaIntlDateTimeFormatValue.Create(const ALocale: string; const A ReadOptions(AOptions); + // Default calendar and numberingSystem + if FCalendar = '' then + FCalendar := 'gregory'; + if FNumberingSystem = '' then + FNumberingSystem := 'latn'; + + // ECMA-402: when no dateStyle/timeStyle and no component properties, + // default to { year: "numeric", month: "numeric", day: "numeric" } + if (FDateStyle = '') and (FTimeStyle = '') and + (FWeekday = '') and (FEra = '') and (FYear = '') and + (FMonth = '') and (FDay = '') and (FDayPeriod = '') and + (FHour = '') and (FMinute = '') and (FSecond = '') and + (FTimeZoneName = '') then + begin + FYear := 'numeric'; + FMonth := 'numeric'; + FDay := 'numeric'; + end; + // Build resolved ICU options FResolvedOptions := DefaultDateTimeFormatOptions; FResolvedOptions.DateStyle := DateStyleStringToEnum(FDateStyle); @@ -307,43 +326,43 @@ function TGocciaIntlDateTimeFormatValue.IntlDateTimeFormatResolvedOptions(const DTF := AsDateTimeFormat(AThisValue, 'Intl.DateTimeFormat.prototype.resolvedOptions'); Obj := TGocciaObjectValue.Create(TGocciaObjectValue.SharedObjectPrototype); Obj.AssignProperty('locale', TGocciaStringLiteralValue.Create(DTF.FLocale)); - if DTF.FDateStyle <> '' then - Obj.AssignProperty('dateStyle', TGocciaStringLiteralValue.Create(DTF.FDateStyle)); - if DTF.FTimeStyle <> '' then - Obj.AssignProperty('timeStyle', TGocciaStringLiteralValue.Create(DTF.FTimeStyle)); - if DTF.FCalendar <> '' then - Obj.AssignProperty('calendar', TGocciaStringLiteralValue.Create(DTF.FCalendar)); - if DTF.FNumberingSystem <> '' then - Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(DTF.FNumberingSystem)); - if DTF.FTimeZone <> '' then - Obj.AssignProperty('timeZone', TGocciaStringLiteralValue.Create(DTF.FTimeZone)); + Obj.AssignProperty('calendar', TGocciaStringLiteralValue.Create(DTF.FCalendar)); + Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(DTF.FNumberingSystem)); + Obj.AssignProperty('timeZone', TGocciaStringLiteralValue.Create(DTF.FTimeZone)); if DTF.FHour12 >= 0 then Obj.AssignProperty('hour12', TGocciaBooleanLiteralValue.Create(DTF.FHour12 = 1)); if DTF.FHourCycle <> '' then Obj.AssignProperty('hourCycle', TGocciaStringLiteralValue.Create(DTF.FHourCycle)); - if DTF.FWeekday <> '' then - Obj.AssignProperty('weekday', TGocciaStringLiteralValue.Create(DTF.FWeekday)); - if DTF.FEra <> '' then - Obj.AssignProperty('era', TGocciaStringLiteralValue.Create(DTF.FEra)); - if DTF.FYear <> '' then - Obj.AssignProperty('year', TGocciaStringLiteralValue.Create(DTF.FYear)); - if DTF.FMonth <> '' then - Obj.AssignProperty('month', TGocciaStringLiteralValue.Create(DTF.FMonth)); - if DTF.FDay <> '' then - Obj.AssignProperty('day', TGocciaStringLiteralValue.Create(DTF.FDay)); - if DTF.FDayPeriod <> '' then - Obj.AssignProperty('dayPeriod', TGocciaStringLiteralValue.Create(DTF.FDayPeriod)); - if DTF.FHour <> '' then - Obj.AssignProperty('hour', TGocciaStringLiteralValue.Create(DTF.FHour)); - if DTF.FMinute <> '' then - Obj.AssignProperty('minute', TGocciaStringLiteralValue.Create(DTF.FMinute)); - if DTF.FSecond <> '' then - Obj.AssignProperty('second', TGocciaStringLiteralValue.Create(DTF.FSecond)); - if DTF.FFractionalSecondDigits >= 0 then - Obj.AssignProperty('fractionalSecondDigits', - TGocciaNumberLiteralValue.Create(DTF.FFractionalSecondDigits)); - if DTF.FTimeZoneName <> '' then - Obj.AssignProperty('timeZoneName', TGocciaStringLiteralValue.Create(DTF.FTimeZoneName)); + if DTF.FDateStyle <> '' then + Obj.AssignProperty('dateStyle', TGocciaStringLiteralValue.Create(DTF.FDateStyle)); + if DTF.FTimeStyle <> '' then + Obj.AssignProperty('timeStyle', TGocciaStringLiteralValue.Create(DTF.FTimeStyle)); + if (DTF.FDateStyle = '') and (DTF.FTimeStyle = '') then + begin + if DTF.FWeekday <> '' then + Obj.AssignProperty('weekday', TGocciaStringLiteralValue.Create(DTF.FWeekday)); + if DTF.FEra <> '' then + Obj.AssignProperty('era', TGocciaStringLiteralValue.Create(DTF.FEra)); + if DTF.FYear <> '' then + Obj.AssignProperty('year', TGocciaStringLiteralValue.Create(DTF.FYear)); + if DTF.FMonth <> '' then + Obj.AssignProperty('month', TGocciaStringLiteralValue.Create(DTF.FMonth)); + if DTF.FDay <> '' then + Obj.AssignProperty('day', TGocciaStringLiteralValue.Create(DTF.FDay)); + if DTF.FDayPeriod <> '' then + Obj.AssignProperty('dayPeriod', TGocciaStringLiteralValue.Create(DTF.FDayPeriod)); + if DTF.FHour <> '' then + Obj.AssignProperty('hour', TGocciaStringLiteralValue.Create(DTF.FHour)); + if DTF.FMinute <> '' then + Obj.AssignProperty('minute', TGocciaStringLiteralValue.Create(DTF.FMinute)); + if DTF.FSecond <> '' then + Obj.AssignProperty('second', TGocciaStringLiteralValue.Create(DTF.FSecond)); + if DTF.FFractionalSecondDigits >= 0 then + Obj.AssignProperty('fractionalSecondDigits', + TGocciaNumberLiteralValue.Create(DTF.FFractionalSecondDigits)); + if DTF.FTimeZoneName <> '' then + Obj.AssignProperty('timeZoneName', TGocciaStringLiteralValue.Create(DTF.FTimeZoneName)); + end; Result := Obj; end; diff --git a/source/units/Goccia.Values.IntlDurationFormat.pas b/source/units/Goccia.Values.IntlDurationFormat.pas index 1b180f33..822d83d0 100644 --- a/source/units/Goccia.Values.IntlDurationFormat.pas +++ b/source/units/Goccia.Values.IntlDurationFormat.pas @@ -286,6 +286,9 @@ constructor TGocciaIntlDurationFormatValue.Create(const ALocale: string; const A ReadOptions(AOptions); + if FNumberingSystem = '' then + FNumberingSystem := 'latn'; + InitializePrototype; if Assigned(GetIntlDurationFormatShared) then FPrototype := GetIntlDurationFormatShared.Prototype; @@ -429,8 +432,7 @@ function TGocciaIntlDurationFormatValue.IntlDurationFormatResolvedOptions(const Obj.AssignProperty('millisecondsDisplay', TGocciaStringLiteralValue.Create(DF.FMillisecondsDisplay)); Obj.AssignProperty('microsecondsDisplay', TGocciaStringLiteralValue.Create(DF.FMicrosecondsDisplay)); Obj.AssignProperty('nanosecondsDisplay', TGocciaStringLiteralValue.Create(DF.FNanosecondsDisplay)); - if DF.FNumberingSystem <> '' then - Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(DF.FNumberingSystem)); + Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(DF.FNumberingSystem)); Result := Obj; end; diff --git a/source/units/Goccia.Values.IntlNumberFormat.pas b/source/units/Goccia.Values.IntlNumberFormat.pas index 4cc82a4d..9149ad67 100644 --- a/source/units/Goccia.Values.IntlNumberFormat.pas +++ b/source/units/Goccia.Values.IntlNumberFormat.pas @@ -33,6 +33,9 @@ TGocciaIntlNumberFormatValue = class(TGocciaObjectValue) FMaximumFractionDigits: Integer; FMinimumSignificantDigits: Integer; FMaximumSignificantDigits: Integer; + FRoundingIncrement: Integer; + FRoundingPriority: string; + FTrailingZeroDisplay: string; FNumberingSystem: string; FResolvedOptions: TIntlNumberFormatOptions; @@ -348,12 +351,18 @@ procedure TGocciaIntlNumberFormatValue.ReadOptions(const AOptions: TGocciaObject V := AOptions.GetProperty('maximumSignificantDigits'); if Assigned(V) and not (V is TGocciaUndefinedLiteralValue) then FMaximumSignificantDigits := Trunc(V.ToNumberLiteral.Value); + V := AOptions.GetProperty('roundingIncrement'); + if Assigned(V) and not (V is TGocciaUndefinedLiteralValue) then + FRoundingIncrement := Trunc(V.ToNumberLiteral.Value); + TryReadStringOption(AOptions, 'roundingPriority', FRoundingPriority); + TryReadStringOption(AOptions, 'trailingZeroDisplay', FTrailingZeroDisplay); TryReadStringOption(AOptions, 'numberingSystem', FNumberingSystem); end; constructor TGocciaIntlNumberFormatValue.Create(const ALocale: string; const AOptions: TGocciaObjectValue); var - Canonical: string; + Canonical, CurrSymbol, CurrNarrow: string; + CurrDigits: Integer; begin inherited Create; Canonical := CanonicalizeUnicodeLocaleId(ALocale); @@ -372,6 +381,9 @@ constructor TGocciaIntlNumberFormatValue.Create(const ALocale: string; const AOp FSignDisplay := 'auto'; FUseGrouping := 'auto'; FRoundingMode := 'halfExpand'; + FRoundingIncrement := 1; + FRoundingPriority := 'auto'; + FTrailingZeroDisplay := 'auto'; FMinimumIntegerDigits := 1; FMinimumFractionDigits := -1; FMaximumFractionDigits := -1; @@ -400,6 +412,45 @@ constructor TGocciaIntlNumberFormatValue.Create(const ALocale: string; const AOp ((FMaximumSignificantDigits < 1) or (FMaximumSignificantDigits > 21)) then ThrowRangeError(Format(SErrorIntlDigitsOutOfRange, ['maximumSignificantDigits', 1, 21])); + // Default numberingSystem to "latn" + if FNumberingSystem = '' then + FNumberingSystem := 'latn'; + + // Resolve fraction digit defaults when significantDigits rounding is not in use + if (FMinimumSignificantDigits < 0) and (FMaximumSignificantDigits < 0) then + begin + if (FMinimumFractionDigits < 0) and (FMaximumFractionDigits < 0) then + begin + if FStyle = 'currency' then + begin + FMinimumFractionDigits := 2; + FMaximumFractionDigits := 2; + if TryGetCurrencyInfo(FLocale, FCurrency, CurrSymbol, CurrNarrow, CurrDigits) then + begin + FMinimumFractionDigits := CurrDigits; + FMaximumFractionDigits := CurrDigits; + end; + end + else if FStyle = 'percent' then + begin + FMinimumFractionDigits := 0; + FMaximumFractionDigits := 0; + end + else + begin + FMinimumFractionDigits := 0; + FMaximumFractionDigits := 3; + end; + end + else + begin + if FMinimumFractionDigits < 0 then + FMinimumFractionDigits := 0; + if FMaximumFractionDigits < 0 then + FMaximumFractionDigits := Max(3, FMinimumFractionDigits); + end; + end; + // Build resolved ICU options FResolvedOptions := DefaultNumberFormatOptions; FResolvedOptions.Style := StyleStringToEnum(FStyle); @@ -521,22 +572,19 @@ function TGocciaIntlNumberFormatValue.IntlNumberFormatResolvedOptions(const AArg NF := AsNumberFormat(AThisValue, 'Intl.NumberFormat.prototype.resolvedOptions'); Obj := TGocciaObjectValue.Create(TGocciaObjectValue.SharedObjectPrototype); Obj.AssignProperty('locale', TGocciaStringLiteralValue.Create(NF.FLocale)); + Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(NF.FNumberingSystem)); Obj.AssignProperty('style', TGocciaStringLiteralValue.Create(NF.FStyle)); - if NF.FCurrency <> '' then - Obj.AssignProperty('currency', TGocciaStringLiteralValue.Create(NF.FCurrency)); if NF.FStyle = 'currency' then begin + Obj.AssignProperty('currency', TGocciaStringLiteralValue.Create(NF.FCurrency)); Obj.AssignProperty('currencyDisplay', TGocciaStringLiteralValue.Create(NF.FCurrencyDisplay)); Obj.AssignProperty('currencySign', TGocciaStringLiteralValue.Create(NF.FCurrencySign)); end; - if NF.FUnitIdentifier <> '' then - Obj.AssignProperty('unit', TGocciaStringLiteralValue.Create(NF.FUnitIdentifier)); if NF.FStyle = 'unit' then + begin + Obj.AssignProperty('unit', TGocciaStringLiteralValue.Create(NF.FUnitIdentifier)); Obj.AssignProperty('unitDisplay', TGocciaStringLiteralValue.Create(NF.FUnitDisplay)); - Obj.AssignProperty('notation', TGocciaStringLiteralValue.Create(NF.FNotation)); - Obj.AssignProperty('signDisplay', TGocciaStringLiteralValue.Create(NF.FSignDisplay)); - Obj.AssignProperty('useGrouping', TGocciaStringLiteralValue.Create(NF.FUseGrouping)); - Obj.AssignProperty('roundingMode', TGocciaStringLiteralValue.Create(NF.FRoundingMode)); + end; Obj.AssignProperty('minimumIntegerDigits', TGocciaNumberLiteralValue.Create(NF.FMinimumIntegerDigits)); if NF.FMinimumFractionDigits >= 0 then @@ -551,8 +599,16 @@ function TGocciaIntlNumberFormatValue.IntlNumberFormatResolvedOptions(const AArg if NF.FMaximumSignificantDigits >= 0 then Obj.AssignProperty('maximumSignificantDigits', TGocciaNumberLiteralValue.Create(NF.FMaximumSignificantDigits)); - if NF.FNumberingSystem <> '' then - Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(NF.FNumberingSystem)); + Obj.AssignProperty('useGrouping', TGocciaStringLiteralValue.Create(NF.FUseGrouping)); + Obj.AssignProperty('notation', TGocciaStringLiteralValue.Create(NF.FNotation)); + if NF.FNotation = 'compact' then + Obj.AssignProperty('compactDisplay', TGocciaStringLiteralValue.Create(NF.FCompactDisplay)); + Obj.AssignProperty('signDisplay', TGocciaStringLiteralValue.Create(NF.FSignDisplay)); + Obj.AssignProperty('roundingMode', TGocciaStringLiteralValue.Create(NF.FRoundingMode)); + Obj.AssignProperty('roundingIncrement', + TGocciaNumberLiteralValue.Create(NF.FRoundingIncrement)); + Obj.AssignProperty('roundingPriority', TGocciaStringLiteralValue.Create(NF.FRoundingPriority)); + Obj.AssignProperty('trailingZeroDisplay', TGocciaStringLiteralValue.Create(NF.FTrailingZeroDisplay)); Result := Obj; end; diff --git a/source/units/Goccia.Values.IntlPluralRules.pas b/source/units/Goccia.Values.IntlPluralRules.pas index 0aade2bb..3dddcca3 100644 --- a/source/units/Goccia.Values.IntlPluralRules.pas +++ b/source/units/Goccia.Values.IntlPluralRules.pas @@ -48,6 +48,7 @@ implementation Goccia.Intl.CLDRData, Goccia.ObjectModel.Types, Goccia.Realm, + Goccia.Values.ArrayValue, Goccia.Values.ErrorHelper, Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.SymbolValue; @@ -172,6 +173,15 @@ constructor TGocciaIntlPluralRulesValue.Create(const ALocale: string; const AOpt FMaximumSignificantDigits := Trunc(V.ToNumberLiteral.Value); end; + // Resolve fraction digit defaults when significantDigits rounding is not in use + if (FMinimumSignificantDigits < 0) and (FMaximumSignificantDigits < 0) then + begin + if FMinimumFractionDigits < 0 then + FMinimumFractionDigits := 0; + if FMaximumFractionDigits < 0 then + FMaximumFractionDigits := 3; + end; + InitializePrototype; if Assigned(GetIntlPluralRulesShared) then FPrototype := GetIntlPluralRulesShared.Prototype; @@ -275,6 +285,8 @@ function TGocciaIntlPluralRulesValue.IntlPluralRulesResolvedOptions(const AArgs: var PR: TGocciaIntlPluralRulesValue; Obj: TGocciaObjectValue; + Rules: TIntlPluralRuleSet; + CatArr: TGocciaArrayValue; begin PR := AsPluralRules(AThisValue, 'Intl.PluralRules.prototype.resolvedOptions'); Obj := TGocciaObjectValue.Create(TGocciaObjectValue.SharedObjectPrototype); @@ -294,6 +306,24 @@ function TGocciaIntlPluralRulesValue.IntlPluralRulesResolvedOptions(const AArgs: if PR.FMaximumSignificantDigits >= 0 then Obj.AssignProperty('maximumSignificantDigits', TGocciaNumberLiteralValue.Create(PR.FMaximumSignificantDigits)); + + CatArr := TGocciaArrayValue.Create; + if TryGetPluralRules(PR.FLocale, PluralTypeStringToEnum(PR.FType) = iptCardinal, Rules) then + begin + if Rules.Zero <> '' then + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('zero')); + if Rules.One <> '' then + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('one')); + if Rules.Two <> '' then + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('two')); + if Rules.Few <> '' then + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('few')); + if Rules.Many <> '' then + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('many')); + end; + CatArr.Elements.Add(TGocciaStringLiteralValue.Create('other')); + Obj.AssignProperty('pluralCategories', CatArr); + Result := Obj; end; diff --git a/source/units/Goccia.Values.IntlRelativeTimeFormat.pas b/source/units/Goccia.Values.IntlRelativeTimeFormat.pas index 802c80da..9e335bed 100644 --- a/source/units/Goccia.Values.IntlRelativeTimeFormat.pas +++ b/source/units/Goccia.Values.IntlRelativeTimeFormat.pas @@ -186,6 +186,9 @@ constructor TGocciaIntlRelativeTimeFormatValue.Create(const ALocale: string; con FNumberingSystem := V.ToStringLiteral.Value; end; + if FNumberingSystem = '' then + FNumberingSystem := 'latn'; + InitializePrototype; if Assigned(GetIntlRelativeTimeFormatShared) then FPrototype := GetIntlRelativeTimeFormatShared.Prototype; @@ -304,10 +307,9 @@ function TGocciaIntlRelativeTimeFormatValue.IntlRelativeTimeFormatResolvedOption RTF := AsRelativeTimeFormat(AThisValue, 'Intl.RelativeTimeFormat.prototype.resolvedOptions'); Obj := TGocciaObjectValue.Create(TGocciaObjectValue.SharedObjectPrototype); Obj.AssignProperty('locale', TGocciaStringLiteralValue.Create(RTF.FLocale)); - Obj.AssignProperty('numeric', TGocciaStringLiteralValue.Create(RTF.FNumeric)); + Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(RTF.FNumberingSystem)); Obj.AssignProperty('style', TGocciaStringLiteralValue.Create(RTF.FStyle)); - if RTF.FNumberingSystem <> '' then - Obj.AssignProperty('numberingSystem', TGocciaStringLiteralValue.Create(RTF.FNumberingSystem)); + Obj.AssignProperty('numeric', TGocciaStringLiteralValue.Create(RTF.FNumeric)); Result := Obj; end; diff --git a/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js new file mode 100644 index 00000000..f8572128 --- /dev/null +++ b/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js @@ -0,0 +1,58 @@ +/*--- +description: Intl.DateTimeFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +const isIntl = typeof Intl !== "undefined"; + +describe.runIf(isIntl)("Intl.DateTimeFormat.prototype.resolvedOptions", () => { + test("default includes calendar, numberingSystem, and timeZone", () => { + const opts = new Intl.DateTimeFormat("en-US").resolvedOptions(); + expect(opts.locale).toBe("en-US"); + expect(opts.calendar).toBe("gregory"); + expect(opts.numberingSystem).toBe("latn"); + expect(opts.timeZone).toBe("UTC"); + }); + + test("default resolves to year/month/day numeric", () => { + const opts = new Intl.DateTimeFormat("en-US").resolvedOptions(); + expect(opts.year).toBe("numeric"); + expect(opts.month).toBe("numeric"); + expect(opts.day).toBe("numeric"); + }); + + test("dateStyle excludes component properties", () => { + const opts = new Intl.DateTimeFormat("en-US", { + dateStyle: "full", + }).resolvedOptions(); + expect(opts.dateStyle).toBe("full"); + expect(opts.year).toBe(undefined); + expect(opts.month).toBe(undefined); + expect(opts.day).toBe(undefined); + }); + + test("component properties exclude dateStyle/timeStyle", () => { + const opts = new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "numeric", + }).resolvedOptions(); + expect(opts.hour).toBe("numeric"); + expect(opts.minute).toBe("numeric"); + expect(opts.dateStyle).toBe(undefined); + expect(opts.timeStyle).toBe(undefined); + }); + + test("explicit calendar option is preserved", () => { + const opts = new Intl.DateTimeFormat("en-US", { + calendar: "japanese", + }).resolvedOptions(); + expect(opts.calendar).toBe("japanese"); + }); + + test("timeZone option is preserved", () => { + const opts = new Intl.DateTimeFormat("en-US", { + timeZone: "America/New_York", + }).resolvedOptions(); + expect(opts.timeZone).toBe("America/New_York"); + }); +}); diff --git a/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js new file mode 100644 index 00000000..1fe23c68 --- /dev/null +++ b/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js @@ -0,0 +1,29 @@ +/*--- +description: Intl.DurationFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +const isIntl = typeof Intl !== "undefined"; + +describe.runIf(isIntl)("Intl.DurationFormat.prototype.resolvedOptions", () => { + test("default includes numberingSystem", () => { + const opts = new Intl.DurationFormat("en-US").resolvedOptions(); + expect(opts.locale).toBe("en-US"); + expect(opts.numberingSystem).toBe("latn"); + expect(opts.style).toBe("short"); + }); + + test("display fields default to auto", () => { + const opts = new Intl.DurationFormat("en-US").resolvedOptions(); + expect(opts.yearsDisplay).toBe("auto"); + expect(opts.monthsDisplay).toBe("auto"); + expect(opts.weeksDisplay).toBe("auto"); + expect(opts.daysDisplay).toBe("auto"); + expect(opts.hoursDisplay).toBe("auto"); + expect(opts.minutesDisplay).toBe("auto"); + expect(opts.secondsDisplay).toBe("auto"); + expect(opts.millisecondsDisplay).toBe("auto"); + expect(opts.microsecondsDisplay).toBe("auto"); + expect(opts.nanosecondsDisplay).toBe("auto"); + }); +}); diff --git a/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js new file mode 100644 index 00000000..80419326 --- /dev/null +++ b/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js @@ -0,0 +1,79 @@ +/*--- +description: Intl.NumberFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +const isIntl = typeof Intl !== "undefined"; + +describe.runIf(isIntl)("Intl.NumberFormat.prototype.resolvedOptions", () => { + test("decimal style includes all spec-required fields", () => { + const opts = new Intl.NumberFormat("en-US").resolvedOptions(); + expect(opts.locale).toBe("en-US"); + expect(opts.numberingSystem).toBe("latn"); + expect(opts.style).toBe("decimal"); + expect(opts.minimumIntegerDigits).toBe(1); + expect(opts.minimumFractionDigits).toBe(0); + expect(opts.maximumFractionDigits).toBe(3); + expect(opts.useGrouping).toBe("auto"); + expect(opts.notation).toBe("standard"); + expect(opts.signDisplay).toBe("auto"); + expect(opts.roundingMode).toBe("halfExpand"); + expect(opts.roundingIncrement).toBe(1); + expect(opts.roundingPriority).toBe("auto"); + expect(opts.trailingZeroDisplay).toBe("auto"); + }); + + test("currency style includes currency-specific fields", () => { + const opts = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).resolvedOptions(); + expect(opts.style).toBe("currency"); + expect(opts.currency).toBe("USD"); + expect(opts.currencyDisplay).toBe("symbol"); + expect(opts.currencySign).toBe("standard"); + expect(opts.minimumFractionDigits).toBe(2); + expect(opts.maximumFractionDigits).toBe(2); + }); + + test("currency style omits unit fields", () => { + const opts = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).resolvedOptions(); + expect(opts.unit).toBe(undefined); + expect(opts.unitDisplay).toBe(undefined); + }); + + test("percent style resolves fraction digits to 0", () => { + const opts = new Intl.NumberFormat("en-US", { + style: "percent", + }).resolvedOptions(); + expect(opts.minimumFractionDigits).toBe(0); + expect(opts.maximumFractionDigits).toBe(0); + }); + + test("compact notation includes compactDisplay", () => { + const opts = new Intl.NumberFormat("en-US", { + notation: "compact", + }).resolvedOptions(); + expect(opts.notation).toBe("compact"); + expect(opts.compactDisplay).toBe("short"); + }); + + test("standard notation omits compactDisplay", () => { + const opts = new Intl.NumberFormat("en-US").resolvedOptions(); + expect(opts.compactDisplay).toBe(undefined); + }); + + test("significantDigits rounding omits fraction digit fields", () => { + const opts = new Intl.NumberFormat("en-US", { + minimumSignificantDigits: 1, + maximumSignificantDigits: 5, + }).resolvedOptions(); + expect(opts.minimumSignificantDigits).toBe(1); + expect(opts.maximumSignificantDigits).toBe(5); + expect(opts.minimumFractionDigits).toBe(undefined); + expect(opts.maximumFractionDigits).toBe(undefined); + }); +}); diff --git a/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js b/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js new file mode 100644 index 00000000..ced3329c --- /dev/null +++ b/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js @@ -0,0 +1,41 @@ +/*--- +description: Intl.PluralRules.prototype.resolvedOptions +features: [Intl] +---*/ + +const isIntl = typeof Intl !== "undefined"; + +describe.runIf(isIntl)("Intl.PluralRules.prototype.resolvedOptions", () => { + test("default includes all spec-required fields", () => { + const opts = new Intl.PluralRules("en-US").resolvedOptions(); + expect(opts.locale).toBe("en-US"); + expect(opts.type).toBe("cardinal"); + expect(opts.minimumIntegerDigits).toBe(1); + expect(opts.minimumFractionDigits).toBe(0); + expect(opts.maximumFractionDigits).toBe(3); + }); + + test("pluralCategories is an array containing other", () => { + const opts = new Intl.PluralRules("en-US").resolvedOptions(); + expect(Array.isArray(opts.pluralCategories)).toBe(true); + expect(opts.pluralCategories.includes("other")).toBe(true); + }); + + test("ordinal type is preserved", () => { + const opts = new Intl.PluralRules("en-US", { + type: "ordinal", + }).resolvedOptions(); + expect(opts.type).toBe("ordinal"); + }); + + test("significantDigits rounding omits fraction digit fields", () => { + const opts = new Intl.PluralRules("en-US", { + minimumSignificantDigits: 1, + maximumSignificantDigits: 5, + }).resolvedOptions(); + expect(opts.minimumSignificantDigits).toBe(1); + expect(opts.maximumSignificantDigits).toBe(5); + expect(opts.minimumFractionDigits).toBe(undefined); + expect(opts.maximumFractionDigits).toBe(undefined); + }); +}); diff --git a/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js new file mode 100644 index 00000000..2cbb1e33 --- /dev/null +++ b/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js @@ -0,0 +1,25 @@ +/*--- +description: Intl.RelativeTimeFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +const isIntl = typeof Intl !== "undefined"; + +describe.runIf(isIntl)("Intl.RelativeTimeFormat.prototype.resolvedOptions", () => { + test("default includes all spec-required fields", () => { + const opts = new Intl.RelativeTimeFormat("en-US").resolvedOptions(); + expect(opts.locale).toBe("en-US"); + expect(opts.numberingSystem).toBe("latn"); + expect(opts.style).toBe("long"); + expect(opts.numeric).toBe("always"); + }); + + test("explicit options are preserved", () => { + const opts = new Intl.RelativeTimeFormat("en-US", { + style: "short", + numeric: "auto", + }).resolvedOptions(); + expect(opts.style).toBe("short"); + expect(opts.numeric).toBe("auto"); + }); +}); From 48953cae728df24c8b3c4d24b06a54700c379f1e Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Mon, 11 May 2026 08:14:58 +0100 Subject: [PATCH 2/8] Fix ICU number formatter: significant digits via pattern, rounding increment Configure ICU number formatter with ECMA-402 options by passing minimumIntegerDigits, fraction digits, significant digits, useGrouping, roundingMode, and roundingIncrement through to ICU via unum_setAttribute, unum_applyPattern, and engine-side rounding increment arithmetic. Use unum_applyPattern with '@' notation for significant digits mode, since unum_setAttribute(UNUM_SIGNIFICANT_DIGITS_USED) has no effect on Apple ICU. Implement rounding increment in Pascal using integer-scaled arithmetic to avoid IEEE 754 precision issues and ICU portability gaps. Fix ICU symbol resolution on Linux where libraries export versioned symbols (e.g. unum_open_76). Store the version suffix discovered during library loading and try versioned names when bare lookups fail. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/shared/ICU.pas | 21 ++ source/shared/IntlICU.pas | 294 +++++++++++++++++- .../units/Goccia.Values.IntlNumberFormat.pas | 24 ++ 3 files changed, 338 insertions(+), 1 deletion(-) diff --git a/source/shared/ICU.pas b/source/shared/ICU.pas index 3be4aa21..6113abaf 100644 --- a/source/shared/ICU.pas +++ b/source/shared/ICU.pas @@ -38,6 +38,7 @@ implementation InitLock: TRTLCriticalSection; {$IFDEF LINUX} UCHandle: TLibHandle; + ICUVersionSuffix: string; {$ENDIF} {$IFDEF LINUX} @@ -66,6 +67,7 @@ function TryLoadLinuxICU(out AHandle: TLibHandle): Boolean; AHandle := NilHandle; Continue; end; + ICUVersionSuffix := '_' + IntToStr(Version); Result := True; Exit; end; @@ -81,7 +83,10 @@ function TryLoadLinuxICU(out AHandle: TLibHandle): Boolean; AHandle := NilHandle; end else + begin + ICUVersionSuffix := ''; Result := True; + end; end; end; {$ENDIF} @@ -134,14 +139,29 @@ function ICULibraryAvailable: Boolean; function ICUGetProcAddress(const AName: string): Pointer; var Handle: TLibHandle; + {$IFDEF LINUX} + VersionedName: string; + {$ENDIF} begin Result := nil; if not TryGetICULibraryHandle(Handle) then Exit; Result := GetProcAddress(Handle, AName); {$IFDEF LINUX} + if (Result = nil) and (ICUVersionSuffix <> '') then + begin + VersionedName := AName + ICUVersionSuffix; + Result := GetProcAddress(Handle, VersionedName); + end; if (Result = nil) and (UCHandle <> NilHandle) then + begin Result := GetProcAddress(UCHandle, AName); + if (Result = nil) and (ICUVersionSuffix <> '') then + begin + VersionedName := AName + ICUVersionSuffix; + Result := GetProcAddress(UCHandle, VersionedName); + end; + end; {$ENDIF} end; @@ -152,6 +172,7 @@ initialization LoadSucceeded := False; {$IFDEF LINUX} UCHandle := NilHandle; + ICUVersionSuffix := ''; {$ENDIF} finalization diff --git a/source/shared/IntlICU.pas b/source/shared/IntlICU.pas index 8b6cb598..1c1bf485 100644 --- a/source/shared/IntlICU.pas +++ b/source/shared/IntlICU.pas @@ -59,6 +59,7 @@ function TryICULowerCase(const ALocale, AStr: string; out AResult: string): Bool implementation uses + Math, SysUtils, DynLibs, @@ -75,6 +76,25 @@ implementation UNUM_PERCENT_STYLE = 3; UNUM_SCIENTIFIC_STYLE = 4; UNUM_PATTERN_DECIMAL = 0; + UNUM_GROUPING_USED = 1; + UNUM_MAX_INTEGER_DIGITS = 3; + UNUM_MIN_INTEGER_DIGITS = 4; + UNUM_MAX_FRACTION_DIGITS = 6; + UNUM_MIN_FRACTION_DIGITS = 7; + UNUM_ROUNDING_MODE = 11; + UNUM_SIGNIFICANT_DIGITS_USED = 22; + UNUM_MIN_SIGNIFICANT_DIGITS = 23; + UNUM_MAX_SIGNIFICANT_DIGITS = 24; + UNUM_ROUND_CEILING = 0; + UNUM_ROUND_FLOOR = 1; + UNUM_ROUND_DOWN = 2; + UNUM_ROUND_UP = 3; + UNUM_ROUND_HALFEVEN = 4; + UNUM_ROUND_HALFDOWN = 5; + UNUM_ROUND_HALFUP = 6; + UNUM_ROUND_HALF_CEILING = 9; + UNUM_ROUND_HALF_FLOOR = 10; + UNUM_DATTR_ROUNDING_INCREMENT = 0; UDAT_FULL = 0; UDAT_LONG = 1; UDAT_MEDIUM = 2; @@ -173,6 +193,11 @@ implementation var AStatus: TICUErrorCode): LongInt; cdecl; TUnumSetAttribute = procedure(AFormat: Pointer; AAttr: LongInt; ANewValue: LongInt); cdecl; + TUnumSetDoubleAttribute = procedure(AFormat: Pointer; AAttr: LongInt; + ANewValue: Double); cdecl; + TUnumApplyPattern = procedure(AFormat: Pointer; ALocalized: ByteBool; + const APattern: PUChar; APatternLength: LongInt; + AParseError: Pointer; var AStatus: TICUErrorCode); cdecl; TUnumSetTextAttribute = procedure(AFormat: Pointer; ATag: LongInt; const ANewValue: PUChar; ANewValueLength: LongInt; var AStatus: TICUErrorCode); cdecl; @@ -246,6 +271,8 @@ TIntlICUFunctions = record UnumClose: TUnumClose; UnumFormatDouble: TUnumFormatDouble; UnumSetAttribute: TUnumSetAttribute; + UnumSetDoubleAttribute: TUnumSetDoubleAttribute; + UnumApplyPattern: TUnumApplyPattern; UnumSetTextAttribute: TUnumSetTextAttribute; UdatOpen: TUdatOpen; UdatClose: TUdatClose; @@ -380,6 +407,10 @@ function TryLoadIntlFunctions(const AHandle: TLibHandle): Boolean; S := ResolveSymbol(AHandle, 'unum_setAttribute'); if Assigned(S) then F.UnumSetAttribute := TUnumSetAttribute(S); + S := ResolveSymbol(AHandle, 'unum_setDoubleAttribute'); + if Assigned(S) then F.UnumSetDoubleAttribute := TUnumSetDoubleAttribute(S); + S := ResolveSymbol(AHandle, 'unum_applyPattern'); + if Assigned(S) then F.UnumApplyPattern := TUnumApplyPattern(S); S := ResolveSymbol(AHandle, 'unum_setTextAttribute'); if Assigned(S) then F.UnumSetTextAttribute := TUnumSetTextAttribute(S); S := ResolveSymbol(AHandle, 'uplrules_open'); @@ -698,6 +729,263 @@ function NumberStyleToICU(AStyle: TIntlNumberStyle): LongInt; end; end; +function RoundingModeToICU(AMode: TIntlNumberRoundingMode): LongInt; +begin + case AMode of + inrmCeil: Result := UNUM_ROUND_CEILING; + inrmFloor: Result := UNUM_ROUND_FLOOR; + inrmExpand: Result := UNUM_ROUND_UP; + inrmTrunc: Result := UNUM_ROUND_DOWN; + inrmHalfCeil: Result := UNUM_ROUND_HALF_CEILING; + inrmHalfFloor: Result := UNUM_ROUND_HALF_FLOOR; + inrmHalfExpand: Result := UNUM_ROUND_HALFUP; + inrmHalfTrunc: Result := UNUM_ROUND_HALFDOWN; + inrmHalfEven: Result := UNUM_ROUND_HALFEVEN; + else + Result := UNUM_ROUND_HALFUP; + end; +end; + +procedure ApplySignificantDigitsPattern(AFormatter: Pointer; + AMinSig, AMaxSig: Integer); +var + Pattern: UnicodeString; + Status: TICUErrorCode; + I: Integer; +begin + if not Assigned(IntlFunctions.UnumApplyPattern) then + Exit; + if AMinSig < 1 then AMinSig := 1; + if AMaxSig < AMinSig then AMaxSig := AMinSig; + Pattern := ''; + for I := 1 to AMinSig do + Pattern := Pattern + '@'; + for I := AMinSig + 1 to AMaxSig do + Pattern := Pattern + '#'; + Status := ICU_SUCCESS; + IntlFunctions.UnumApplyPattern(AFormatter, False, + PWideChar(Pattern), Length(Pattern), nil, Status); +end; + +procedure ConfigureNumberFormatter(AFormatter: Pointer; + const AOptions: TIntlNumberFormatOptions); +var + IncValue: Double; + MinSig, MaxSig: Integer; +begin + if not Assigned(IntlFunctions.UnumSetAttribute) then + Exit; + + if (AOptions.MinimumSignificantDigits > 0) or + (AOptions.MaximumSignificantDigits > 0) then + begin + MinSig := AOptions.MinimumSignificantDigits; + MaxSig := AOptions.MaximumSignificantDigits; + if MinSig < 1 then MinSig := 1; + if MaxSig < 1 then MaxSig := 21; + ApplySignificantDigitsPattern(AFormatter, MinSig, MaxSig); + end + else + begin + if AOptions.MinimumFractionDigits >= 0 then + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_MIN_FRACTION_DIGITS, AOptions.MinimumFractionDigits); + if AOptions.MaximumFractionDigits >= 0 then + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_MAX_FRACTION_DIGITS, AOptions.MaximumFractionDigits); + end; + + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_MIN_INTEGER_DIGITS, AOptions.MinimumIntegerDigits); + + if AOptions.UseGrouping = inugFalse then + IntlFunctions.UnumSetAttribute(AFormatter, UNUM_GROUPING_USED, 0); + + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_ROUNDING_MODE, RoundingModeToICU(AOptions.RoundingMode)); + + if (AOptions.RoundingIncrement > 1) and + Assigned(IntlFunctions.UnumSetDoubleAttribute) then + begin + IncValue := AOptions.RoundingIncrement; + if AOptions.MaximumFractionDigits = 1 then + IncValue := IncValue * 0.1 + else if AOptions.MaximumFractionDigits = 2 then + IncValue := IncValue * 0.01 + else if AOptions.MaximumFractionDigits = 3 then + IncValue := IncValue * 0.001 + else if AOptions.MaximumFractionDigits = 4 then + IncValue := IncValue * 0.0001; + IntlFunctions.UnumSetDoubleAttribute(AFormatter, + UNUM_DATTR_ROUNDING_INCREMENT, IncValue); + end; +end; + +function RoundHalfExpand(AValue: Double): Double; +begin + if AValue >= 0 then + Result := Math.Floor(AValue + 0.5) + else + Result := Math.Ceil(AValue - 0.5); +end; + +function RoundHalfTrunc(AValue: Double): Double; +var + F: Double; +begin + F := Frac(AValue); + if Abs(F) > 0.5 then + begin + if AValue >= 0 then + Result := Math.Ceil(AValue - 0.5) + else + Result := Math.Floor(AValue + 0.5); + end + else + Result := System.Int(AValue); +end; + +function RoundWithMode(AValue: Double; AMode: TIntlNumberRoundingMode): Double; +var + F: Double; +begin + case AMode of + inrmCeil: + Result := Math.Ceil(AValue); + inrmFloor: + Result := Math.Floor(AValue); + inrmTrunc: + Result := System.Int(AValue); + inrmExpand: + if AValue >= 0 then + Result := Math.Ceil(AValue) + else + Result := Math.Floor(AValue); + inrmHalfEven: + Result := Round(AValue); + inrmHalfTrunc: + Result := RoundHalfTrunc(AValue); + inrmHalfCeil: + begin + F := Frac(AValue); + if F = 0.5 then + Result := Math.Ceil(AValue) + else if F = -0.5 then + Result := System.Int(AValue) + else + Result := RoundHalfExpand(AValue); + end; + inrmHalfFloor: + begin + F := Frac(AValue); + if F = 0.5 then + Result := System.Int(AValue) + else if F = -0.5 then + Result := Math.Floor(AValue) + else + Result := RoundHalfExpand(AValue); + end; + else + Result := RoundHalfExpand(AValue); + end; +end; + +function ApplyRoundingIncrement(AValue: Double; + const AOptions: TIntlNumberFormatOptions): Double; +var + Scale, ScaledInt, Remainder, Rounded: Double; + Inc: Integer; + IsNeg: Boolean; +begin + Result := AValue; + Inc := AOptions.RoundingIncrement; + if Inc <= 1 then + Exit; + if AOptions.MaximumFractionDigits < 0 then + Exit; + + Scale := 1.0; + case AOptions.MaximumFractionDigits of + 1: Scale := 10.0; + 2: Scale := 100.0; + 3: Scale := 1000.0; + 4: Scale := 10000.0; + else + Exit; + end; + + IsNeg := AValue < 0; + ScaledInt := Abs(AValue) * Scale; + if Abs(ScaledInt - System.Round(ScaledInt)) < 1e-6 then + ScaledInt := System.Round(ScaledInt); + Remainder := ScaledInt - Math.Floor(ScaledInt / Inc) * Inc; + if Abs(Remainder - Inc) < 1e-9 then + Remainder := 0; + + if Remainder = 0 then + begin + Result := AValue; + Exit; + end; + + case AOptions.RoundingMode of + inrmCeil: + if IsNeg then + Rounded := ScaledInt - Remainder + else + Rounded := ScaledInt - Remainder + Inc; + inrmFloor: + if IsNeg then + Rounded := ScaledInt - Remainder + Inc + else + Rounded := ScaledInt - Remainder; + inrmTrunc: + Rounded := ScaledInt - Remainder; + inrmExpand: + Rounded := ScaledInt - Remainder + Inc; + inrmHalfEven, inrmHalfExpand, inrmHalfTrunc, inrmHalfCeil, inrmHalfFloor: + begin + if Remainder * 2 > Inc then + Rounded := ScaledInt - Remainder + Inc + else if Remainder * 2 < Inc then + Rounded := ScaledInt - Remainder + else + begin + case AOptions.RoundingMode of + inrmHalfExpand: + Rounded := ScaledInt - Remainder + Inc; + inrmHalfTrunc: + Rounded := ScaledInt - Remainder; + inrmHalfCeil: + if IsNeg then + Rounded := ScaledInt - Remainder + else + Rounded := ScaledInt - Remainder + Inc; + inrmHalfFloor: + if IsNeg then + Rounded := ScaledInt - Remainder + Inc + else + Rounded := ScaledInt - Remainder; + else + begin + if Trunc(Math.Floor(ScaledInt / Inc)) mod 2 = 0 then + Rounded := ScaledInt - Remainder + else + Rounded := ScaledInt - Remainder + Inc; + end; + end; + end; + end; + else + Rounded := ScaledInt - Remainder + Inc; + end; + + if IsNeg then + Result := -(Rounded / Scale) + else + Result := Rounded / Scale; +end; + function TryICUFormatNumber(const ALocale: string; AValue: Double; const AOptions: TIntlNumberFormatOptions; out AFormatted: string): Boolean; var @@ -707,6 +995,7 @@ function TryICUFormatNumber(const ALocale: string; AValue: Double; ResultLen: LongInt; LocaleAnsi: AnsiString; ICUStyle: LongInt; + FormattedValue: Double; begin Result := False; AFormatted := ''; @@ -724,9 +1013,12 @@ function TryICUFormatNumber(const ALocale: string; AValue: Double; Exit; try + ConfigureNumberFormatter(Formatter, AOptions); + FormattedValue := ApplyRoundingIncrement(AValue, AOptions); + FillChar(Buffer, SizeOf(Buffer), 0); Status := ICU_SUCCESS; - ResultLen := IntlFunctions.UnumFormatDouble(Formatter, AValue, + ResultLen := IntlFunctions.UnumFormatDouble(Formatter, FormattedValue, @Buffer[0], FORMAT_BUFFER_CAPACITY, nil, Status); if not ICUSucceeded(Status) or (ResultLen <= 0) then Exit; diff --git a/source/units/Goccia.Values.IntlNumberFormat.pas b/source/units/Goccia.Values.IntlNumberFormat.pas index 9149ad67..b0a1fa0a 100644 --- a/source/units/Goccia.Values.IntlNumberFormat.pas +++ b/source/units/Goccia.Values.IntlNumberFormat.pas @@ -164,6 +164,28 @@ function UseGroupingStringToEnum(const AValue: string): TIntlNumberUseGrouping; Result := inugAuto; end; +function RoundingModeStringToEnum(const AValue: string): TIntlNumberRoundingMode; +begin + if AValue = 'ceil' then + Result := inrmCeil + else if AValue = 'floor' then + Result := inrmFloor + else if AValue = 'expand' then + Result := inrmExpand + else if AValue = 'trunc' then + Result := inrmTrunc + else if AValue = 'halfCeil' then + Result := inrmHalfCeil + else if AValue = 'halfFloor' then + Result := inrmHalfFloor + else if AValue = 'halfTrunc' then + Result := inrmHalfTrunc + else if AValue = 'halfEven' then + Result := inrmHalfEven + else + Result := inrmHalfExpand; +end; + function InsertGroupingSeparator(const AIntPart, ASep: string): string; var Len, I, GroupCount: Integer; @@ -466,6 +488,8 @@ constructor TGocciaIntlNumberFormatValue.Create(const ALocale: string; const AOp FResolvedOptions.MaximumFractionDigits := FMaximumFractionDigits; FResolvedOptions.MinimumSignificantDigits := FMinimumSignificantDigits; FResolvedOptions.MaximumSignificantDigits := FMaximumSignificantDigits; + FResolvedOptions.RoundingMode := RoundingModeStringToEnum(FRoundingMode); + FResolvedOptions.RoundingIncrement := FRoundingIncrement; FResolvedOptions.NumberingSystem := FNumberingSystem; InitializePrototype; From 9a0ccfdc82176b971d514876fb8f15221e6ce4f7 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Mon, 11 May 2026 23:35:54 +0100 Subject: [PATCH 3/8] Prefer embedded Unicode property tables over live ICU data The ICU symbol resolution fix causes uset_* functions to load on Linux for the first time. The system ICU may ship a different Unicode version than the embedded tables were generated from, producing mismatches for 52 property-escape tests. Swap the lookup order so embedded data is tried first and ICU is the fallback for properties not in the embedded tables. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.RegExp.Compiler.pas | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/units/Goccia.RegExp.Compiler.pas b/source/units/Goccia.RegExp.Compiler.pas index b4741cc1..bf6e7429 100644 --- a/source/units/Goccia.RegExp.Compiler.pas +++ b/source/units/Goccia.RegExp.Compiler.pas @@ -409,12 +409,6 @@ procedure TRegExpCompiler.GetUnicodePropertyRanges(const APropertyName: string; ValuePart := ''; end; - if TryICUGetUnicodePropertyRanges(PropPart, ValuePart, ICURanges) then - begin - CopyICURanges; - Exit; - end; - if EqPos > 0 then begin if TryGetUnicodePropertyRanges(PropPart + '/' + ValuePart, ICURanges) then @@ -438,6 +432,12 @@ procedure TRegExpCompiler.GetUnicodePropertyRanges(const APropertyName: string; end; end; + if TryICUGetUnicodePropertyRanges(PropPart, ValuePart, ICURanges) then + begin + CopyICURanges; + Exit; + end; + raise EConvertError.Create('Invalid Unicode property name: ' + APropertyName); end; From 5856b3ca4d373f1d4958dcf8c289f139d874cdf9 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Tue, 12 May 2026 07:36:36 +0100 Subject: [PATCH 4/8] Revert "Prefer embedded Unicode property tables over live ICU data" This reverts commit 9a0ccfdc82176b971d514876fb8f15221e6ce4f7. --- source/units/Goccia.RegExp.Compiler.pas | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/units/Goccia.RegExp.Compiler.pas b/source/units/Goccia.RegExp.Compiler.pas index bf6e7429..b4741cc1 100644 --- a/source/units/Goccia.RegExp.Compiler.pas +++ b/source/units/Goccia.RegExp.Compiler.pas @@ -409,6 +409,12 @@ procedure TRegExpCompiler.GetUnicodePropertyRanges(const APropertyName: string; ValuePart := ''; end; + if TryICUGetUnicodePropertyRanges(PropPart, ValuePart, ICURanges) then + begin + CopyICURanges; + Exit; + end; + if EqPos > 0 then begin if TryGetUnicodePropertyRanges(PropPart + '/' + ValuePart, ICURanges) then @@ -432,12 +438,6 @@ procedure TRegExpCompiler.GetUnicodePropertyRanges(const APropertyName: string; end; end; - if TryICUGetUnicodePropertyRanges(PropPart, ValuePart, ICURanges) then - begin - CopyICURanges; - Exit; - end; - raise EConvertError.Create('Invalid Unicode property name: ' + APropertyName); end; From 43477b72af46dd801baebe726135fffffe779e65 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Thu, 14 May 2026 04:25:31 +0100 Subject: [PATCH 5/8] Fix double-rounding and dead code in ICU number formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ICU-side rounding increment configuration from ConfigureNumberFormatter — ApplyRoundingIncrement handles rounding in Pascal before the value reaches ICU. When roundingIncrement > 1, set ICU rounding mode to neutral halfUp so it does not interfere. Simplify ApplyRoundingIncrement: replace separate Rounded variable with direct Lower/Upper grid point selection, eliminating the uninitialized variable and redundant case branches. Remove unused RoundWithMode, RoundHalfExpand, RoundHalfTrunc functions that were left over from an earlier iteration. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/shared/IntlICU.pas | 181 ++++++++++---------------------------- 1 file changed, 48 insertions(+), 133 deletions(-) diff --git a/source/shared/IntlICU.pas b/source/shared/IntlICU.pas index 1c1bf485..b22316f3 100644 --- a/source/shared/IntlICU.pas +++ b/source/shared/IntlICU.pas @@ -770,7 +770,6 @@ procedure ApplySignificantDigitsPattern(AFormatter: Pointer; procedure ConfigureNumberFormatter(AFormatter: Pointer; const AOptions: TIntlNumberFormatOptions); var - IncValue: Double; MinSig, MaxSig: Integer; begin if not Assigned(IntlFunctions.UnumSetAttribute) then @@ -801,99 +800,19 @@ procedure ConfigureNumberFormatter(AFormatter: Pointer; if AOptions.UseGrouping = inugFalse then IntlFunctions.UnumSetAttribute(AFormatter, UNUM_GROUPING_USED, 0); - IntlFunctions.UnumSetAttribute(AFormatter, - UNUM_ROUNDING_MODE, RoundingModeToICU(AOptions.RoundingMode)); - - if (AOptions.RoundingIncrement > 1) and - Assigned(IntlFunctions.UnumSetDoubleAttribute) then - begin - IncValue := AOptions.RoundingIncrement; - if AOptions.MaximumFractionDigits = 1 then - IncValue := IncValue * 0.1 - else if AOptions.MaximumFractionDigits = 2 then - IncValue := IncValue * 0.01 - else if AOptions.MaximumFractionDigits = 3 then - IncValue := IncValue * 0.001 - else if AOptions.MaximumFractionDigits = 4 then - IncValue := IncValue * 0.0001; - IntlFunctions.UnumSetDoubleAttribute(AFormatter, - UNUM_DATTR_ROUNDING_INCREMENT, IncValue); - end; -end; - -function RoundHalfExpand(AValue: Double): Double; -begin - if AValue >= 0 then - Result := Math.Floor(AValue + 0.5) - else - Result := Math.Ceil(AValue - 0.5); -end; - -function RoundHalfTrunc(AValue: Double): Double; -var - F: Double; -begin - F := Frac(AValue); - if Abs(F) > 0.5 then - begin - if AValue >= 0 then - Result := Math.Ceil(AValue - 0.5) - else - Result := Math.Floor(AValue + 0.5); - end + if AOptions.RoundingIncrement > 1 then + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP) else - Result := System.Int(AValue); -end; - -function RoundWithMode(AValue: Double; AMode: TIntlNumberRoundingMode): Double; -var - F: Double; -begin - case AMode of - inrmCeil: - Result := Math.Ceil(AValue); - inrmFloor: - Result := Math.Floor(AValue); - inrmTrunc: - Result := System.Int(AValue); - inrmExpand: - if AValue >= 0 then - Result := Math.Ceil(AValue) - else - Result := Math.Floor(AValue); - inrmHalfEven: - Result := Round(AValue); - inrmHalfTrunc: - Result := RoundHalfTrunc(AValue); - inrmHalfCeil: - begin - F := Frac(AValue); - if F = 0.5 then - Result := Math.Ceil(AValue) - else if F = -0.5 then - Result := System.Int(AValue) - else - Result := RoundHalfExpand(AValue); - end; - inrmHalfFloor: - begin - F := Frac(AValue); - if F = 0.5 then - Result := System.Int(AValue) - else if F = -0.5 then - Result := Math.Floor(AValue) - else - Result := RoundHalfExpand(AValue); - end; - else - Result := RoundHalfExpand(AValue); - end; + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_ROUNDING_MODE, RoundingModeToICU(AOptions.RoundingMode)); end; function ApplyRoundingIncrement(AValue: Double; const AOptions: TIntlNumberFormatOptions): Double; var - Scale, ScaledInt, Remainder, Rounded: Double; + Scale, ScaledInt, Remainder: Double; + Lower, Upper: Double; Inc: Integer; IsNeg: Boolean; begin @@ -923,67 +842,63 @@ function ApplyRoundingIncrement(AValue: Double; Remainder := 0; if Remainder = 0 then - begin - Result := AValue; Exit; - end; + + Lower := ScaledInt - Remainder; + Upper := Lower + Inc; case AOptions.RoundingMode of inrmCeil: - if IsNeg then - Rounded := ScaledInt - Remainder - else - Rounded := ScaledInt - Remainder + Inc; + if IsNeg then ScaledInt := Lower else ScaledInt := Upper; inrmFloor: - if IsNeg then - Rounded := ScaledInt - Remainder + Inc - else - Rounded := ScaledInt - Remainder; + if IsNeg then ScaledInt := Upper else ScaledInt := Lower; inrmTrunc: - Rounded := ScaledInt - Remainder; + ScaledInt := Lower; inrmExpand: - Rounded := ScaledInt - Remainder + Inc; - inrmHalfEven, inrmHalfExpand, inrmHalfTrunc, inrmHalfCeil, inrmHalfFloor: + ScaledInt := Upper; + inrmHalfExpand: + if Remainder * 2 >= Inc then ScaledInt := Upper else ScaledInt := Lower; + inrmHalfTrunc: + if Remainder * 2 > Inc then ScaledInt := Upper else ScaledInt := Lower; + inrmHalfEven: begin if Remainder * 2 > Inc then - Rounded := ScaledInt - Remainder + Inc + ScaledInt := Upper else if Remainder * 2 < Inc then - Rounded := ScaledInt - Remainder + ScaledInt := Lower + else if Trunc(Lower / Inc) mod 2 = 0 then + ScaledInt := Lower else - begin - case AOptions.RoundingMode of - inrmHalfExpand: - Rounded := ScaledInt - Remainder + Inc; - inrmHalfTrunc: - Rounded := ScaledInt - Remainder; - inrmHalfCeil: - if IsNeg then - Rounded := ScaledInt - Remainder - else - Rounded := ScaledInt - Remainder + Inc; - inrmHalfFloor: - if IsNeg then - Rounded := ScaledInt - Remainder + Inc - else - Rounded := ScaledInt - Remainder; - else - begin - if Trunc(Math.Floor(ScaledInt / Inc)) mod 2 = 0 then - Rounded := ScaledInt - Remainder - else - Rounded := ScaledInt - Remainder + Inc; - end; - end; - end; + ScaledInt := Upper; + end; + inrmHalfCeil: + begin + if Remainder * 2 > Inc then + ScaledInt := Upper + else if Remainder * 2 < Inc then + ScaledInt := Lower + else if IsNeg then + ScaledInt := Lower + else + ScaledInt := Upper; + end; + inrmHalfFloor: + begin + if Remainder * 2 > Inc then + ScaledInt := Upper + else if Remainder * 2 < Inc then + ScaledInt := Lower + else if IsNeg then + ScaledInt := Upper + else + ScaledInt := Lower; end; - else - Rounded := ScaledInt - Remainder + Inc; end; if IsNeg then - Result := -(Rounded / Scale) + Result := -(ScaledInt / Scale) else - Result := Rounded / Scale; + Result := ScaledInt / Scale; end; function TryICUFormatNumber(const ALocale: string; AValue: Double; From 15698514ddc5dec80637448b96299c0839e9b051 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Thu, 14 May 2026 04:40:02 +0100 Subject: [PATCH 6/8] Clean up IntlICU: use StringOfChar, drop Math import, remove dead constant Replace loop-based '@'/'#' pattern building in ApplySignificantDigitsPattern with StringOfChar. Replace Math.Floor with Trunc (operand is always non-negative via Abs), removing the Math unit dependency. Remove unused UNUM_SIGNIFICANT_DIGITS_USED constant (significant digits use unum_applyPattern, not setAttribute). Co-Authored-By: Claude Opus 4.6 (1M context) --- source/shared/IntlICU.pas | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/source/shared/IntlICU.pas b/source/shared/IntlICU.pas index b22316f3..3f2db729 100644 --- a/source/shared/IntlICU.pas +++ b/source/shared/IntlICU.pas @@ -59,7 +59,6 @@ function TryICULowerCase(const ALocale, AStr: string; out AResult: string): Bool implementation uses - Math, SysUtils, DynLibs, @@ -82,7 +81,6 @@ implementation UNUM_MAX_FRACTION_DIGITS = 6; UNUM_MIN_FRACTION_DIGITS = 7; UNUM_ROUNDING_MODE = 11; - UNUM_SIGNIFICANT_DIGITS_USED = 22; UNUM_MIN_SIGNIFICANT_DIGITS = 23; UNUM_MAX_SIGNIFICANT_DIGITS = 24; UNUM_ROUND_CEILING = 0; @@ -751,17 +749,13 @@ procedure ApplySignificantDigitsPattern(AFormatter: Pointer; var Pattern: UnicodeString; Status: TICUErrorCode; - I: Integer; begin if not Assigned(IntlFunctions.UnumApplyPattern) then Exit; if AMinSig < 1 then AMinSig := 1; if AMaxSig < AMinSig then AMaxSig := AMinSig; - Pattern := ''; - for I := 1 to AMinSig do - Pattern := Pattern + '@'; - for I := AMinSig + 1 to AMaxSig do - Pattern := Pattern + '#'; + Pattern := UnicodeString(StringOfChar('@', AMinSig) + + StringOfChar('#', AMaxSig - AMinSig)); Status := ICU_SUCCESS; IntlFunctions.UnumApplyPattern(AFormatter, False, PWideChar(Pattern), Length(Pattern), nil, Status); @@ -837,7 +831,7 @@ function ApplyRoundingIncrement(AValue: Double; ScaledInt := Abs(AValue) * Scale; if Abs(ScaledInt - System.Round(ScaledInt)) < 1e-6 then ScaledInt := System.Round(ScaledInt); - Remainder := ScaledInt - Math.Floor(ScaledInt / Inc) * Inc; + Remainder := ScaledInt - Trunc(ScaledInt / Inc) * Inc; if Abs(Remainder - Inc) < 1e-9 then Remainder := 0; From 511f60801d8543c855ffbf6a87eb08c233e5826f Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Thu, 14 May 2026 05:51:20 +0100 Subject: [PATCH 7/8] Address CodeRabbit review: validation, defaults, and guard fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hardcoded scale switch in ApplyRoundingIncrement with a loop so maxFractionDigits 0 and >4 are handled instead of exiting. - Add FFractionalSecondDigits to the DateTimeFormat no-components guard so specifying fractionalSecondDigits alone prevents the year/month/day defaults per ECMA-402 ToDateTimeOptions. - Validate roundingIncrement against ECMA-402 allowed values {1,2,5,10,20,25,50,100,200,250,500,1000,2000,2500,5000}. - Default missing significant digit bounds (min→1, max→21) and suppress fraction digits when significant-digit rounding is active, in both NumberFormat and PluralRules constructors. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/shared/IntlICU.pas | 12 +++-------- .../Goccia.Values.IntlDateTimeFormat.pas | 2 +- .../units/Goccia.Values.IntlNumberFormat.pas | 21 +++++++++++++++++-- .../units/Goccia.Values.IntlPluralRules.pas | 12 +++++++++-- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/source/shared/IntlICU.pas b/source/shared/IntlICU.pas index 3f2db729..99340fa5 100644 --- a/source/shared/IntlICU.pas +++ b/source/shared/IntlICU.pas @@ -807,7 +807,7 @@ function ApplyRoundingIncrement(AValue: Double; var Scale, ScaledInt, Remainder: Double; Lower, Upper: Double; - Inc: Integer; + I, Inc: Integer; IsNeg: Boolean; begin Result := AValue; @@ -818,14 +818,8 @@ function ApplyRoundingIncrement(AValue: Double; Exit; Scale := 1.0; - case AOptions.MaximumFractionDigits of - 1: Scale := 10.0; - 2: Scale := 100.0; - 3: Scale := 1000.0; - 4: Scale := 10000.0; - else - Exit; - end; + for I := 1 to AOptions.MaximumFractionDigits do + Scale := Scale * 10.0; IsNeg := AValue < 0; ScaledInt := Abs(AValue) * Scale; diff --git a/source/units/Goccia.Values.IntlDateTimeFormat.pas b/source/units/Goccia.Values.IntlDateTimeFormat.pas index 36400e19..19c6c5fb 100644 --- a/source/units/Goccia.Values.IntlDateTimeFormat.pas +++ b/source/units/Goccia.Values.IntlDateTimeFormat.pas @@ -283,7 +283,7 @@ constructor TGocciaIntlDateTimeFormatValue.Create(const ALocale: string; const A (FWeekday = '') and (FEra = '') and (FYear = '') and (FMonth = '') and (FDay = '') and (FDayPeriod = '') and (FHour = '') and (FMinute = '') and (FSecond = '') and - (FTimeZoneName = '') then + (FFractionalSecondDigits < 0) and (FTimeZoneName = '') then begin FYear := 'numeric'; FMonth := 'numeric'; diff --git a/source/units/Goccia.Values.IntlNumberFormat.pas b/source/units/Goccia.Values.IntlNumberFormat.pas index b0a1fa0a..70c33832 100644 --- a/source/units/Goccia.Values.IntlNumberFormat.pas +++ b/source/units/Goccia.Values.IntlNumberFormat.pas @@ -433,13 +433,30 @@ constructor TGocciaIntlNumberFormatValue.Create(const ALocale: string; const AOp if (FMaximumSignificantDigits >= 0) and ((FMaximumSignificantDigits < 1) or (FMaximumSignificantDigits > 21)) then ThrowRangeError(Format(SErrorIntlDigitsOutOfRange, ['maximumSignificantDigits', 1, 21])); + if (FRoundingIncrement <> 1) and (FRoundingIncrement <> 2) and + (FRoundingIncrement <> 5) and (FRoundingIncrement <> 10) and + (FRoundingIncrement <> 20) and (FRoundingIncrement <> 25) and + (FRoundingIncrement <> 50) and (FRoundingIncrement <> 100) and + (FRoundingIncrement <> 200) and (FRoundingIncrement <> 250) and + (FRoundingIncrement <> 500) and (FRoundingIncrement <> 1000) and + (FRoundingIncrement <> 2000) and (FRoundingIncrement <> 2500) and + (FRoundingIncrement <> 5000) then + ThrowRangeError(Format(SErrorIntlInvalidOption, [IntToStr(FRoundingIncrement), 'roundingIncrement'])); // Default numberingSystem to "latn" if FNumberingSystem = '' then FNumberingSystem := 'latn'; - // Resolve fraction digit defaults when significantDigits rounding is not in use - if (FMinimumSignificantDigits < 0) and (FMaximumSignificantDigits < 0) then + if (FMinimumSignificantDigits >= 0) or (FMaximumSignificantDigits >= 0) then + begin + if FMinimumSignificantDigits < 0 then + FMinimumSignificantDigits := 1; + if FMaximumSignificantDigits < 0 then + FMaximumSignificantDigits := 21; + FMinimumFractionDigits := -1; + FMaximumFractionDigits := -1; + end + else begin if (FMinimumFractionDigits < 0) and (FMaximumFractionDigits < 0) then begin diff --git a/source/units/Goccia.Values.IntlPluralRules.pas b/source/units/Goccia.Values.IntlPluralRules.pas index 3dddcca3..d5c7631a 100644 --- a/source/units/Goccia.Values.IntlPluralRules.pas +++ b/source/units/Goccia.Values.IntlPluralRules.pas @@ -173,8 +173,16 @@ constructor TGocciaIntlPluralRulesValue.Create(const ALocale: string; const AOpt FMaximumSignificantDigits := Trunc(V.ToNumberLiteral.Value); end; - // Resolve fraction digit defaults when significantDigits rounding is not in use - if (FMinimumSignificantDigits < 0) and (FMaximumSignificantDigits < 0) then + if (FMinimumSignificantDigits >= 0) or (FMaximumSignificantDigits >= 0) then + begin + if FMinimumSignificantDigits < 0 then + FMinimumSignificantDigits := 1; + if FMaximumSignificantDigits < 0 then + FMaximumSignificantDigits := 21; + FMinimumFractionDigits := -1; + FMaximumFractionDigits := -1; + end + else begin if FMinimumFractionDigits < 0 then FMinimumFractionDigits := 0; From d027e382dd0a7e4c4644df18679a3be2fe80226e Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Thu, 14 May 2026 06:56:25 +0100 Subject: [PATCH 8/8] Remove dead isIntl guard from resolvedOptions tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intl is always available — the runIf(isIntl) guard was copied from older test files but never triggers. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Intl/DateTimeFormat/prototype/resolvedOptions.js | 4 +--- .../Intl/DurationFormat/prototype/resolvedOptions.js | 4 +--- .../built-ins/Intl/NumberFormat/prototype/resolvedOptions.js | 4 +--- tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js | 4 +--- .../Intl/RelativeTimeFormat/prototype/resolvedOptions.js | 4 +--- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js index 88703dcb..5bfef0ee 100644 --- a/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js @@ -3,9 +3,7 @@ description: Intl.DateTimeFormat.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; - -describe.runIf(isIntl)("Intl.DateTimeFormat.prototype.resolvedOptions", () => { +describe("Intl.DateTimeFormat.prototype.resolvedOptions", () => { test("default includes calendar, numberingSystem, and timeZone", () => { const opts = new Intl.DateTimeFormat("en-US").resolvedOptions(); expect(opts.locale).toBe("en-US"); diff --git a/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js index 1fe23c68..0c20c842 100644 --- a/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js @@ -3,9 +3,7 @@ description: Intl.DurationFormat.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; - -describe.runIf(isIntl)("Intl.DurationFormat.prototype.resolvedOptions", () => { +describe("Intl.DurationFormat.prototype.resolvedOptions", () => { test("default includes numberingSystem", () => { const opts = new Intl.DurationFormat("en-US").resolvedOptions(); expect(opts.locale).toBe("en-US"); diff --git a/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js index 80419326..18615af8 100644 --- a/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js @@ -3,9 +3,7 @@ description: Intl.NumberFormat.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; - -describe.runIf(isIntl)("Intl.NumberFormat.prototype.resolvedOptions", () => { +describe("Intl.NumberFormat.prototype.resolvedOptions", () => { test("decimal style includes all spec-required fields", () => { const opts = new Intl.NumberFormat("en-US").resolvedOptions(); expect(opts.locale).toBe("en-US"); diff --git a/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js b/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js index ced3329c..410b8bb4 100644 --- a/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js @@ -3,9 +3,7 @@ description: Intl.PluralRules.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; - -describe.runIf(isIntl)("Intl.PluralRules.prototype.resolvedOptions", () => { +describe("Intl.PluralRules.prototype.resolvedOptions", () => { test("default includes all spec-required fields", () => { const opts = new Intl.PluralRules("en-US").resolvedOptions(); expect(opts.locale).toBe("en-US"); diff --git a/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js b/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js index 2cbb1e33..fbf719f9 100644 --- a/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js @@ -3,9 +3,7 @@ description: Intl.RelativeTimeFormat.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; - -describe.runIf(isIntl)("Intl.RelativeTimeFormat.prototype.resolvedOptions", () => { +describe("Intl.RelativeTimeFormat.prototype.resolvedOptions", () => { test("default includes all spec-required fields", () => { const opts = new Intl.RelativeTimeFormat("en-US").resolvedOptions(); expect(opts.locale).toBe("en-US");