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..99340fa5 100644 --- a/source/shared/IntlICU.pas +++ b/source/shared/IntlICU.pas @@ -75,6 +75,24 @@ 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_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 +191,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 +269,8 @@ TIntlICUFunctions = record UnumClose: TUnumClose; UnumFormatDouble: TUnumFormatDouble; UnumSetAttribute: TUnumSetAttribute; + UnumSetDoubleAttribute: TUnumSetDoubleAttribute; + UnumApplyPattern: TUnumApplyPattern; UnumSetTextAttribute: TUnumSetTextAttribute; UdatOpen: TUdatOpen; UdatClose: TUdatClose; @@ -380,6 +405,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 +727,168 @@ 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; +begin + if not Assigned(IntlFunctions.UnumApplyPattern) then + Exit; + if AMinSig < 1 then AMinSig := 1; + if AMaxSig < AMinSig then AMaxSig := AMinSig; + Pattern := UnicodeString(StringOfChar('@', AMinSig) + + StringOfChar('#', AMaxSig - AMinSig)); + Status := ICU_SUCCESS; + IntlFunctions.UnumApplyPattern(AFormatter, False, + PWideChar(Pattern), Length(Pattern), nil, Status); +end; + +procedure ConfigureNumberFormatter(AFormatter: Pointer; + const AOptions: TIntlNumberFormatOptions); +var + 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); + + if AOptions.RoundingIncrement > 1 then + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP) + else + IntlFunctions.UnumSetAttribute(AFormatter, + UNUM_ROUNDING_MODE, RoundingModeToICU(AOptions.RoundingMode)); +end; + +function ApplyRoundingIncrement(AValue: Double; + const AOptions: TIntlNumberFormatOptions): Double; +var + Scale, ScaledInt, Remainder: Double; + Lower, Upper: Double; + I, Inc: Integer; + IsNeg: Boolean; +begin + Result := AValue; + Inc := AOptions.RoundingIncrement; + if Inc <= 1 then + Exit; + if AOptions.MaximumFractionDigits < 0 then + Exit; + + Scale := 1.0; + for I := 1 to AOptions.MaximumFractionDigits do + Scale := Scale * 10.0; + + IsNeg := AValue < 0; + ScaledInt := Abs(AValue) * Scale; + if Abs(ScaledInt - System.Round(ScaledInt)) < 1e-6 then + ScaledInt := System.Round(ScaledInt); + Remainder := ScaledInt - Trunc(ScaledInt / Inc) * Inc; + if Abs(Remainder - Inc) < 1e-9 then + Remainder := 0; + + if Remainder = 0 then + Exit; + + Lower := ScaledInt - Remainder; + Upper := Lower + Inc; + + case AOptions.RoundingMode of + inrmCeil: + if IsNeg then ScaledInt := Lower else ScaledInt := Upper; + inrmFloor: + if IsNeg then ScaledInt := Upper else ScaledInt := Lower; + inrmTrunc: + ScaledInt := Lower; + inrmExpand: + 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 + ScaledInt := Upper + else if Remainder * 2 < Inc then + ScaledInt := Lower + else if Trunc(Lower / Inc) mod 2 = 0 then + ScaledInt := Lower + else + 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; + end; + + if IsNeg then + Result := -(ScaledInt / Scale) + else + Result := ScaledInt / Scale; +end; + function TryICUFormatNumber(const ALocale: string; AValue: Double; const AOptions: TIntlNumberFormatOptions; out AFormatted: string): Boolean; var @@ -707,6 +898,7 @@ function TryICUFormatNumber(const ALocale: string; AValue: Double; ResultLen: LongInt; LocaleAnsi: AnsiString; ICUStyle: LongInt; + FormattedValue: Double; begin Result := False; AFormatted := ''; @@ -724,9 +916,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.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 ec0b8774..19c6c5fb 100644 --- a/source/units/Goccia.Values.IntlDateTimeFormat.pas +++ b/source/units/Goccia.Values.IntlDateTimeFormat.pas @@ -271,6 +271,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 + (FFractionalSecondDigits < 0) and (FTimeZoneName = '') then + begin + FYear := 'numeric'; + FMonth := 'numeric'; + FDay := 'numeric'; + end; + // Build resolved ICU options FResolvedOptions := DefaultDateTimeFormatOptions; FResolvedOptions.DateStyle := DateStyleStringToEnum(FDateStyle); @@ -412,43 +431,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..70c33832 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; @@ -161,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; @@ -348,12 +373,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 +403,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; @@ -399,6 +433,62 @@ 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'; + + 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 + 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; @@ -415,6 +505,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; @@ -521,22 +613,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 +640,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..d5c7631a 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,23 @@ constructor TGocciaIntlPluralRulesValue.Create(const ALocale: string; const AOpt FMaximumSignificantDigits := Trunc(V.ToNumberLiteral.Value); end; + 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; + if FMaximumFractionDigits < 0 then + FMaximumFractionDigits := 3; + end; + InitializePrototype; if Assigned(GetIntlPluralRulesShared) then FPrototype := GetIntlPluralRulesShared.Prototype; @@ -275,6 +293,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 +314,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 index 18aa3214..5bfef0ee 100644 --- a/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js +++ b/tests/built-ins/Intl/DateTimeFormat/prototype/resolvedOptions.js @@ -3,9 +3,57 @@ description: Intl.DateTimeFormat.prototype.resolvedOptions features: [Intl] ---*/ -const isIntl = typeof Intl !== "undefined"; +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"); + 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"); + }); -describe.runIf(isIntl)("Intl.DateTimeFormat.prototype.resolvedOptions", () => { test("normalizes offset time zones to HH:MM form", () => { const cases = [ ["+03", "+03:00"], 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..0c20c842 --- /dev/null +++ b/tests/built-ins/Intl/DurationFormat/prototype/resolvedOptions.js @@ -0,0 +1,27 @@ +/*--- +description: Intl.DurationFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +describe("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..18615af8 --- /dev/null +++ b/tests/built-ins/Intl/NumberFormat/prototype/resolvedOptions.js @@ -0,0 +1,77 @@ +/*--- +description: Intl.NumberFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +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"); + 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..410b8bb4 --- /dev/null +++ b/tests/built-ins/Intl/PluralRules/prototype/resolvedOptions.js @@ -0,0 +1,39 @@ +/*--- +description: Intl.PluralRules.prototype.resolvedOptions +features: [Intl] +---*/ + +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"); + 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..fbf719f9 --- /dev/null +++ b/tests/built-ins/Intl/RelativeTimeFormat/prototype/resolvedOptions.js @@ -0,0 +1,23 @@ +/*--- +description: Intl.RelativeTimeFormat.prototype.resolvedOptions +features: [Intl] +---*/ + +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"); + 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"); + }); +});