8000 [JSC] Update Intl.DurationFormat based on Jan ECMA402 consensus · WebKit/WebKit@4bfba24 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4bfba24

Browse files
committed
[JSC] Update Intl.DurationFormat based on Jan ECMA402 consensus
https://bugs.webkit.org/show_bug.cgi?id=251115 rdar://104582024 Reviewed by Mark Lam. This patch upgrades Intl.DurationFormat implementation to align it to Jan ECMA402 meeting consensus. The changes are three-fold. 1. format and formatToParts should accept String too. And throwing a range error when string is not valid[1]. 2. formatToParts should use singular form of unit names instead of plural form. 3. formatToParts should split each unit's representation to make numeric part and unit part accessible[3]. Previously, we just grouped "1 hour" as "hour" part. But after this, we split it into "1" integer, " " literal, and "hour" unit, with unit = "hour". [1]: tc39/proposal-intl-duration-format#128 [2]: tc39/proposal-intl-duration-format#44 [3]: tc39/proposal-intl-duration-format#55 * JSTests/stress/intl-durationformat-format-to-parts.js: (Intl.DurationFormat.shouldBe.JSON.stringify.fmt.formatToParts): (Intl.DurationFormat.shouldBeOneOf): * JSTests/stress/intl-durationformat.js: (test): * Source/JavaScriptCore/runtime/IntlDurationFormat.cpp: (JSC::collectElements): (JSC::IntlDurationFormat::formatToParts const): * Source/JavaScriptCore/runtime/IntlDurationFormatPrototype.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): Canonical link: https://commits.webkit.org/259317@main
1 parent 102ec03 commit 4bfba24

File tree

4 files changed

+77
-37
lines changed

4 files changed

+77
-37
lines changed

JSTests/stress/intl-durationformat-format-to-parts.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//@ requireOptions("--useIntlDurationFormat=1")
2+
//@ skip if $hostOS != "darwin" # We are testing Intl features based on Darwin's ICU. The other port owners can extend it by testing it in their platforms and removing this condition.
23

34
function shouldBe(actual, expected) {
45
if (actual !== expected)
@@ -43,7 +44,7 @@ if (Intl.DurationFormat) {
4344
hours: 22,
4445
minutes: 30,
4546
seconds: 34,
46-
})), `[{"type":"years","value":"1 yr"},{"type":"literal","value":", "},{"type":"hours","value":"22 hr"},{"type":"literal","value":", "},{"type":"minutes","value":"30 min"},{"type":"literal","value":", "},{"type":"seconds","value":"34 sec"}]`);
47+
})), `[{"type":"integer","value":"1","unit":"year"},{"type":"literal","value":" ","unit":"year"},{"type":"unit","value":"yr","unit":"year"},{"type":"literal","value":", "},{"type":"integer","value":"22","unit":"hour"},{"type":"literal","value":" ","unit":"hour"},{"type":"unit","value":"hr","unit":"hour"},{"type":"literal","value":", "},{"type":"integer","value":"30","unit":"minute"},{"type":"literal","value":" ","unit":"minute"},{"type":"unit","value":"min","unit":"minute"},{"type":"literal","value":", "},{"type":"integer","value":"34","unit":"second"},{"type":"literal","value":" ","unit":"second"},{"type":"unit","value":"sec","unit":"second"}]`);
4748
}
4849
{
4950
var fmt = new Intl.DurationFormat('en', {
@@ -54,8 +55,13 @@ if (Intl.DurationFormat) {
5455
});
5556

5657
shouldBeOneOf(JSON.stringify(fmt.formatToParts({ years: 1, months: 2, weeks: 3, days: 4, hours: 10, minutes: 34, seconds: 33, milliseconds: 32 })), [
57-
`[{"type":"years","value":"1y"},{"type":"literal","value":", "},{"type":"months","value":"2mo"},{"type":"literal","value":", "},{"type":"weeks","value":"3w"},{"type":"literal","value":", "},{"type":"days","value":"4d"},{"type":"literal","value":", "},{"type":"hours","value":"10"},{"type":"literal","value":":"},{"type":"minutes","value":"34"},{"type":"literal","value":":"},{"type":"seconds","value":"33.03"}]`,
58-
`[{"type":"years","value":"1y"},{"type":"literal","value":", "},{"type":"months","value":"2m"},{"type":"literal","value":", "},{"type":"weeks","value":"3w"},{"type":"literal","value":", "},{"type":"days","value":"4d"},{"type":"literal","value":", "},{"type":"hours","value":"10"},{"type":"literal","value":":"},{"type":"minutes","value":"34"},{"type":"literal","value":":"},{"type":"seconds","value":"33.03"}]`,
58+
`[{"type":"integer","value":"1","unit":"year"},{"type":"unit","value":"y","unit":"year"},{"type":"literal","value":", "},{"type":"integer","value":"2","unit":"month"},{"type":"unit","value":"mo","unit":"month"},{"type":"literal","value":", "},{"type":"integer","value":"3","unit":"week"},{"type":"unit","value":"w","unit":"week"},{"type":"literal","value":", "},{"type":"integer","value":"4","unit":"day"},{"type":"unit","value":"d","unit":"day"},{"type":"literal","value":", "},{"type":"integer","value":"10","unit":"hour"},{"type":"literal","value":":"},{"type":"integer","value":"34","unit":"minute"},{"type":"literal","value":":"},{"type":"integer","value":"33","unit":"second"},{"type":"decimal","value":".","unit":"second"},{"type":"fraction","value":"03","unit":"second"}]`,
59+
]);
60+
}
61+
{
62+
var fmt = new Intl.DurationFormat('en-US', { style: 'digital', fractionalDigits: 9, millseconds: 'numeric' });
63+
shouldBeOneOf(JSON.stringify(fmt.formatToParts({ hours: 7, minutes: 8, seconds: 9, milliseconds: 123, microseconds: 456, nanoseconds: 789 })), [
64+
`[{"type":"integer","value":"7","unit":"hour"},{"type":"literal","value":":"},{"type":"integer","value":"08","unit":"minute"},{"type":"literal","value":":"},{"type":"integer","value":"09","unit":"second"},{"type":"decimal","value":".","unit":"second"},{"type":"fraction","value":"123456789","unit":"second"}]`,
5965
]);
6066
}
6167
}

JSTests/stress/intl-durationformat.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ function test() {
166166
shouldThrow(() => df.format({}), TypeError);
167167
shouldThrow(() => df.format([]), TypeError);
168168
shouldThrow(() => df.format(42), TypeError);
169-
shouldThrow(() => df.format("Apple"), TypeError);
169+
shouldThrow(() => df.format("Apple"), RangeError);
170170
shouldThrow(() => df.format(null), TypeError);
171171
shouldThrow(() => df.format(undefined), TypeError);
172172
shouldThrow(() => df.format([null]), TypeError);
@@ -194,11 +194,17 @@ function test() {
194194
{
195195
const duration = { hours: 1, minutes: 2, seconds: 3 };
196196
const expected = [
197-
{"type":"hours","value":"1 hour"},
197+
{"type":"integer","value":"1","unit":"hour"},
198+
{"type":"literal","value":" ","unit":"hour"},
199+
{"type":"unit","value":"hour","unit":"hour"},
198200
{"type":"literal","value":", "},
199-
{"type":"minutes","value":"2 minutes"},
201+
{"type":"integer","value":"2","unit":"minute"},
202+
{"type":"literal","value":" ","unit":"minute"},
203+
{"type":"unit","value":"minutes","unit":"minute"},
200204
{"type":"literal","value":", "},
201-
{"type":"seconds","value":"3 seconds"}
205+
{"type":"integer","value":"3","unit":"second"},
206+
{"type":"literal","value":" ","unit":"second"},
207+
{"type":"unit","value":"seconds","unit":"second"}
202208
];
203209

204210
const df = new Intl.DurationFormat('en-GB', { style: 'long' });
@@ -211,7 +217,7 @@ function test() {
211217

212218
shouldThrow(() => df.formatToParts(), TypeError);
213219
shouldThrow(() => df.formatToParts([]), TypeError);
214-
shouldThrow(() => df.formatToParts("Apple"), TypeError);
220+
shouldThrow(() => df.formatToParts("Apple"), RangeError);
215221
shouldThrow(() => df.formatToParts(42), TypeError);
216222
shouldThrow(() => df.formatToParts(null), TypeError);
217223
shouldThrow(() => df.formatToParts(undefined), TypeError);

Source/JavaScriptCore/runtime/IntlDurationFormat.cpp

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ struct Element {
318318
ElementType m_type;
319319
TemporalUnit m_unit;
320320
String m_string;
321+
double m_value;
322+
std::unique_ptr<UFormattedNumber, ICUDeleter<unumf_closeResult>> m_formattedNumber;
321323
};
322324

323325
static Vector<Element> collectElements(JSGlobalObject* globalObject, const IntlDurationFormat* durationFormat, ISO8601::Duration duration)
@@ -384,7 +386,21 @@ static Vector<Element> collectElements(JSGlobalObject* globalObject, const IntlD
384386
// 3.l. If value is not 0 or display is not "auto", then
385387
value = purifyNaN(value);
386388
if (value != 0 || unitData.display() != IntlDurationFormat::Display::Auto) {
387-
auto formatDouble = [&](const String& skeleton) -> String {
389+
auto formatToString = [&](UFormattedNumber* formattedNumber) -> String {
390+
auto scope = DECLARE_THROW_SCOPE(vm);
391+
392+
UErrorCode status = U_ZERO_ERROR;
393+
Vector<UChar, 32> buffer;
394+
status = callBufferProducingFunction(unumf_resultToString, formattedNumber, buffer);
395+
if (U_FAILURE(status)) {
396+
throwTypeError(globalObject, scope, "Failed to format a number."_s);
397+
return { };
398+
}
399+
400+
return String(WTFMove(buffer));
401+
};
402+
403+
auto formatDouble = [&](const String& skeleton) -> std::unique_ptr<UFormattedNumber, ICUDeleter<unumf_closeResult>> {
388404
auto scope = DECLARE_THROW_SCOPE(vm);
389405

390406
dataLogLnIf(IntlDurationFormatInternal::verbose, skeleton);
@@ -408,24 +424,21 @@ static Vector<Element> collectElements(JSGlobalObject* globalObject, const IntlD
408424
throwTypeError(globalObject, scope, "Failed to format a number."_s);
409425
return { };
410426
}
411-
Vector<UChar, 32> buffer;
412-
status = callBufferProducingFunction(unumf_resultToString, formattedNumber.get(), buffer);
413-
if (U_FAILURE(status)) {
414-
throwTypeError(globalObject, scope, "Failed to format a number."_s);
415-
return { };
416-
}
417427

418-
return String(WTFMove(buffer));
428+
return formattedNumber;
419429
};
420430

421431
switch (unitData.style()) {
422432
// 3.l.i. If style is "2-digit" or "numeric", then
423433
case IntlDurationFormat::UnitStyle::TwoDigit:
424434
case IntlDurationFormat::UnitStyle::Numeric: {
425-
String formatted = formatDouble(skeletonBuilder.toString());
435+
auto formattedNumber = formatDouble(skeletonBuilder.toString());
426 6D38 436
RETURN_IF_EXCEPTION(scope, { });
427437

428-
elements.append({ ElementType::Element, unit, WTFMove(formatted) });
438+
auto formatted = formatToString(formattedNumber.get());
439+
RETURN_IF_EXCEPTION(scope, { });
440+
441+
elements.append({ ElementType::Element, unit, WTFMove(formatted), value, WTFMove(formattedNumber) });
429442

430443
if (unit == TemporalUnit::Hour || unit == TemporalUnit::Minute) {
431444
IntlDurationFormat::UnitData nextUnit;
@@ -443,7 +456,7 @@ static Vector<Element> collectElements(JSGlobalObject* globalObject, const IntlD
443456
if (nextValue != 0 || nextUnit.display() != IntlDurationFormat::Display::Auto) {
444457
if (separator.isNull())
445458
separator = retrieveSeparator(durationFormat->dataLocaleWithExtensions(), durationFormat->numberingSystem());
446-
elements.append({ ElementType::Literal, unit, separator });
459+
elements.append({ ElementType::Literal, unit, separator, value, nullptr });
447460
}
448461
}
449462
break;
@@ -463,10 +476,13 @@ static Vector<Element> collectElements(JSGlobalObject* globalObject, const IntlD
463476
skeletonBuilder.append(" unit-width-narrow");
464477
}
465478

466-
String formatted = formatDouble(skeletonBuilder.toString());
479+
auto formattedNumber = formatDouble(skeletonBuilder.toString());
480+
RETURN_IF_EXCEPTION(scope, { });
481+
482+
auto formatted = formatToString(formattedNumber.get());
467483
RETURN_IF_EXCEPTION(scope, { });
468484

469-
elements.append({ ElementType::Element, unit, WTFMove(formatted) });
485+
elements.append({ ElementType::Element, unit, WTFMove(formatted), value, WTFMove(formattedNumber) });
470486
break;
471487
}
472488
}
@@ -614,26 +630,38 @@ JSValue IntlDurationFormat::formatToParts(JSGlobalObject* globalObject, ISO8601:
614630
return part;
615631
};
616632

617-
auto pushElements = [&](JSArray* parts, unsigned elementIndex) {
633+
auto pushElements = [&](JSArray* parts, unsigned elementIndex) -> void {
618634
auto scope = DECLARE_THROW_SCOPE(vm);
619635
if (elementIndex < groupedElements.size()) {
620636
auto& elements = groupedElements[elementIndex];
621637
for (auto& element : elements) {
622-
JSString* type = nullptr;
623-
JSString* value = jsString(vm, element.m_string);
624638
switch (element.m_type) {
625639
case ElementType::Element: {
626-
type = jsString(vm, String(temporalUnitPluralPropertyName(vm, element.m_unit).uid()));
640+
UErrorCode status = U_ZERO_ERROR;
641+
auto fieldItr = std::unique_ptr<UFieldPositionIterator, UFieldPositionIteratorDeleter>(ufieldpositer_open(&status));
642+
if (U_FAILURE(status)) {
643+
throwTypeError(globalObject, scope, "failed to open field position iterator"_s);
644+
return;
645+
}
646+
unumf_resultGetAllFieldPositions(element.m_formattedNumber.get(), fieldItr.get(), &status);
647+
if (U_FAILURE(status)) {
648+
throwTypeError(globalObject, scope, "Failed to format a number."_s);
649+
return;
650+
}
651+
IntlFieldIterator iterator(*fieldItr.get());
652+
JSString* type = jsString(vm, String(temporalUnitSingularPropertyName(vm, element.m_unit).uid()));
653+
IntlNumberFormat::formatToPartsInternal(globalObject, IntlNumberFormat::Style::Unit, std::signbit(element.m_value), IntlMathematicalValue::numberTypeFromDouble(element.m_value), element.m_string, iterator, parts, nullptr, type);
654+
RETURN_IF_EXCEPTION(scope, void());
627655
break;
628656
}
629657
case ElementType::Literal: {
630-
type = literalString;
658+
JSString* value = jsString(vm, element.m_string);
659+
JSObject* part = createPart(literalString, value);
660+
parts->push(globalObject, part);
661+
RETURN_IF_EXCEPTION(scope, void());
631662
break;
632663
}
633664
}
634-
JSObject* part = createPart(type, value);
635-
parts->push(globalObject, part);
636-
RETURN_IF_EXCEPTION(scope, void());
637665
}
638666
}
639667
};

Source/JavaScriptCore/runtime/IntlDurationFormatPrototype.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ JSC_DEFINE_HOST_FUNCTION(intlDurationFormatPrototypeFuncFormat, (JSGlobalObject*
8686
if (!durationFormat)
8787
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.format called on value that's not a DurationFormat"_s);
8888

89-
auto* object = jsDynamicCast<JSObject*>(callFrame->argument(0));
90-
if (UNLIKELY(!object))
91-
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.format argument needs to be an object"_s);
89+
JSValue argument = callFrame->argument(0);
90+
if (UNLIKELY(!argument.isObject() && !argument.isString()))
91+
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.format argument needs to be an object or a string"_s);
9292

93-
auto duration = TemporalDuration::toISO8601Duration(globalObject, object);
93+
auto duration = TemporalDuration::toISO8601Duration(globalObject, argument);
9494
RETURN_IF_EXCEPTION(scope, { });
9595

9696
RELEASE_AND_RETURN(scope, JSValue::encode(durationFormat->format(globalObject, WTFMove(duration))));
@@ -106,11 +106,11 @@ JSC_DEFINE_HOST_FUNCTION(intlDurationFormatPrototypeFuncFormatToParts, (JSGlobal
106106
if (!durationFormat)
107107
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.formatToParts called on value that's not a DurationFormat"_s);
108108

109-
auto* object = jsDynamicCast<JSObject*>(callFrame->argument(0));
110-
if (UNLIKELY(!object))
111-
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.formatToParts argument needs to be an object"_s);
109+
JSValue argument = callFrame->argument(0);
110+
if (UNLIKELY(!argument.isObject() && !argument.isString()))
111+
return throwVMTypeError(globalObject, scope, "Intl.DurationFormat.prototype.formatToParts argument needs to be an object or a string"_s);
112112

113-
auto duration = TemporalDuration::toISO8601Duration(globalObject, object);
113+
auto duration = TemporalDuration::toISO8601Duration(globalObject, argument);
114114
RETURN_IF_EXCEPTION(scope, { });
115115

116116
RELEASE_AND_RETURN(scope, JSValue::encode(durationFormat->formatToParts(globalObject, WTFMove(duration))));

0 commit comments

Comments
 (0)
0