summaryrefslogtreecommitdiff
path: root/Userland/Libraries
diff options
context:
space:
mode:
authorIdan Horowitz <idan.horowitz@gmail.com>2022-07-02 15:55:14 +0300
committerIdan Horowitz <idan.horowitz@gmail.com>2022-07-02 18:32:35 +0300
commitfb8c4a724eb126a6d50fe608d6af0b7827902f76 (patch)
treed139537073b397dbb9863ca56c52ad5f9c4494e8 /Userland/Libraries
parent8a671154c3d756325180a0ab5e69c0ce11d03021 (diff)
downloadserenity-fb8c4a724eb126a6d50fe608d6af0b7827902f76.zip
LibJS: Disallow mixed-sign durations in Intl.DurationFormat
This is a normative change in the Intl.DurationFormat spec. See: https://github.com/tc39/proposal-intl-duration-format/commit/89ab1855
Diffstat (limited to 'Userland/Libraries')
-rw-r--r--Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp61
-rw-r--r--Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h2
-rw-r--r--Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp26
-rw-r--r--Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js55
-rw-r--r--Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js55
5 files changed, 187 insertions, 12 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp
index bb48da5750..7ec32dcb52 100644
--- a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp
+++ b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp
@@ -130,7 +130,7 @@ StringView DurationFormat::display_to_string(Display display)
}
}
-// 1.1.1 ToDurationRecord ( input ), https://tc39.es/proposal-intl-duration-format/#sec-todurationrecord
+// 1.1.3 ToDurationRecord ( input ), https://tc39.es/proposal-intl-duration-format/#sec-todurationrecord
ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& global_object, Value input)
{
auto& vm = global_object.vm();
@@ -183,7 +183,62 @@ ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& glo
return result;
}
-// 1.1.2 GetDurationUnitOptions ( unit, options, baseStyle, stylesList, digitalBase, prevStyle ), https://tc39.es/proposal-intl-duration-format/#sec-getdurationunitoptions
+// 1.1.4 DurationSign ( record ), https://tc39.es/proposal-intl-duration-format/#sec-durationsign
+i8 duration_sign(Temporal::DurationRecord const& record)
+{
+ // 1. For each row in Table 1, except the header row, in table order, do
+ for (auto const& duration_instances_component : duration_instances_components) {
+ // a. Let valueSlot be the Value Slot value.
+ auto value_slot = duration_instances_component.value_slot;
+
+ // b. Let v be value of the valueSlot slot of record.
+ auto value = record.*value_slot;
+
+ // c. If v < 0, return -1.
+ if (value < 0)
+ return -1;
+
+ // d. If v > 0, return 1.
+ if (value > 0)
+ return 1;
+ }
+
+ // 2. Return 0.
+ return 0;
+}
+
+// 1.1.5 IsValidDurationRecord ( record ), https://tc39.es/proposal-intl-duration-format/#sec-isvaliddurationrecord
+bool is_valid_duration_record(Temporal::DurationRecord const& record)
+{
+ // 1. Let sign be ! DurationSign(record).
+ auto sign = duration_sign(record);
+
+ // 2. For each row in Table 1, except the header row, in table order, do
+ for (auto const& duration_instances_component : duration_instances_components) {
+ // a. Let valueSlot be the Value Slot value.
+ auto value_slot = duration_instances_component.value_slot;
+
+ // b. Let v be value of the valueSlot slot of record.
+ auto value = record.*value_slot;
+
+ // c. If ð”―(v) is not finite, return false.
+ if (!isfinite(value))
+ return false;
+
+ // d. If v < 0 and sign > 0, return false.
+ if (value < 0 && sign > 0)
+ return false;
+
+ // e. If v > 0 and sign < 0, return false.
+ if (value > 0 && sign < 0)
+ return false;
+ }
+
+ // 3. Return true.
+ return true;
+}
+
+// 1.1.6 GetDurationUnitOptions ( unit, options, baseStyle, stylesList, digitalBase, prevStyle ), https://tc39.es/proposal-intl-duration-format/#sec-getdurationunitoptions
ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(GlobalObject& global_object, String const& unit, Object const& options, StringView base_style, Span<StringView const> styles_list, StringView digital_base, Optional<String> const& previous_style)
{
auto& vm = global_object.vm();
@@ -252,7 +307,7 @@ static String convert_number_format_pattern_to_duration_format_template(Unicode:
return result;
}
-// 1.1.3 PartitionDurationFormatPattern ( durationFormat, duration ), https://tc39.es/proposal-intl-duration-format/#sec-partitiondurationformatpattern
+// 1.1.7 PartitionDurationFormatPattern ( durationFormat, duration ), https://tc39.es/proposal-intl-duration-format/#sec-partitiondurationformatpattern
ThrowCompletionOr<Vector<PatternPartition>> partition_duration_format_pattern(GlobalObject& global_object, DurationFormat const& duration_format, Temporal::DurationRecord const& duration)
{
auto& vm = global_object.vm();
diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h
index 4654155ec2..6bf3926774 100644
--- a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h
+++ b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h
@@ -219,6 +219,8 @@ struct DurationUnitOptions {
};
ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& global_object, Value input);
+i8 duration_sign(Temporal::DurationRecord const&);
+bool is_valid_duration_record(Temporal::DurationRecord const&);
ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(GlobalObject& global_object, String const& unit, Object const& options, StringView base_style, Span<StringView const> styles_list, StringView digital_base, Optional<String> const& previous_style);
ThrowCompletionOr<Vector<PatternPartition>> partition_duration_format_pattern(GlobalObject& global_object, DurationFormat const& duration_format, Temporal::DurationRecord const& duration);
diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp
index a8862093d3..d01aa9ba07 100644
--- a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp
+++ b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp
@@ -41,19 +41,23 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format)
// 3. Let record be ? ToDurationRecord(duration).
auto record = TRY(to_duration_record(global_object, vm.argument(0)));
- // 4. Let formatted be ? PartitionDurationFormatPattern(df, record).
+ // 4. If IsValidDurationRecord(record) is false, throw a RangeError exception.
+ if (!is_valid_duration_record(record))
+ return vm.throw_completion<RangeError>(global_object, ErrorType::TemporalInvalidDurationLikeObject);
+
+ // 5. Let formatted be ? PartitionDurationFormatPattern(df, record).
auto formatted = TRY(partition_duration_format_pattern(global_object, *duration_format, record));
- // 5. Let result be a new empty String.
+ // 6. Let result be a new empty String.
StringBuilder result;
- // 6. For each element part in formatted, in List order, do
+ // 7. For each element part in formatted, in List order, do
for (auto const& part : formatted) {
// a. Set result to the string-concatenation of result and part.[[Value]].
result.append(part.value);
}
- // 7. Return result.
+ // 8. Return result.
return js_string(vm, result.build());
}
@@ -67,14 +71,18 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
// 3. Let record be ? ToDurationRecord(duration).
auto record = TRY(to_duration_record(global_object, vm.argument(0)));
- // 4. Let formatted be ? PartitionDurationFormatPattern(df, record).
+ // 4. If IsValidDurationRecord(record) is false, throw a RangeError exception.
+ if (!is_valid_duration_record(record))
+ return vm.throw_completion<RangeError>(global_object, ErrorType::TemporalInvalidDurationLikeObject);
+
+ // 5. Let formatted be ? PartitionDurationFormatPattern(df, record).
auto formatted = TRY(partition_duration_format_pattern(global_object, *duration_format, record));
- // 5. Let result be ! ArrayCreate(0).
+ // 6. Let result be ! ArrayCreate(0).
auto* result = MUST(Array::create(global_object, 0));
- // 6. Let n be 0.
- // 7. For each element part in formatted, in List order, do
+ // 7. Let n be 0.
+ // 8. For each element part in formatted, in List order, do
for (size_t n = 0; n < formatted.size(); ++n) {
auto const& part = formatted[n];
@@ -93,7 +101,7 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
// e. Increment n by 1.
}
- // 7. Return result.
+ // 9. Return result.
return result;
}
diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js
index a9ead8bec3..578e696681 100644
--- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js
+++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js
@@ -63,3 +63,58 @@ describe("correct behavior", () => {
).toBe("1 J, 2 M, 3 W, 3 T, 4 Std., 5 Min., 6 Sek., 7 ms und 8,009 Ξs");
});
});
+
+describe("errors", () => {
+ test("non-object duration records", () => {
+ [-100, Infinity, NaN, "hello", 152n, Symbol("foo")].forEach(value => {
+ expect(() => {
+ new Intl.DurationFormat().format(value);
+ }).toThrowWithMessage(TypeError, "is not an object");
+ });
+ });
+
+ test("empty duration record", () => {
+ expect(() => {
+ new Intl.DurationFormat().format({});
+ }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+
+ expect(() => {
+ new Intl.DurationFormat().format({ foo: 123 });
+ }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+ });
+
+ test("non-integral duration fields", () => {
+ [
+ "years",
+ "months",
+ "weeks",
+ "days",
+ "hours",
+ "minutes",
+ "seconds",
+ "milliseconds",
+ "microseconds",
+ "nanoseconds",
+ ].forEach(field => {
+ expect(() => {
+ new Intl.DurationFormat().format({ [field]: 1.5 });
+ }).toThrowWithMessage(
+ RangeError,
+ `Invalid value for duration property '${field}': must be an integer, got 1.5`
+ );
+
+ expect(() => {
+ new Intl.DurationFormat().format({ [field]: -Infinity });
+ }).toThrowWithMessage(
+ RangeError,
+ `Invalid value for duration property '${field}': must be an integer, got -Infinity`
+ );
+ });
+ });
+
+ test("inconsistent field signs", () => {
+ expect(() => {
+ new Intl.DurationFormat().format({ years: 1, months: -1 });
+ }).toThrowWithMessage(RangeError, "Invalid duration-like object");
+ });
+});
diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js
index 56c96a4aaa..61b903ce60 100644
--- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js
+++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js
@@ -263,3 +263,58 @@ describe("correct behavior", () => {
]);
});
});
+
+describe("errors", () => {
+ test("non-object duration records", () => {
+ [-100, Infinity, NaN, "hello", 152n, Symbol("foo")].forEach(value => {
+ expect(() => {
+ new Intl.DurationFormat().formatToParts(value);
+ }).toThrowWithMessage(TypeError, "is not an object");
+ });
+ });
+
+ test("empty duration record", () => {
+ expect(() => {
+ new Intl.DurationFormat().formatToParts({});
+ }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+
+ expect(() => {
+ new Intl.DurationFormat().formatToParts({ foo: 123 });
+ }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+ });
+
+ test("non-integral duration fields", () => {
+ [
+ "years",
+ "months",
+ "weeks",
+ "days",
+ "hours",
+ "minutes",
+ "seconds",
+ "milliseconds",
+ "microseconds",
+ "nanoseconds",
+ ].forEach(field => {
+ expect(() => {
+ new Intl.DurationFormat().formatToParts({ [field]: 1.5 });
+ }).toThrowWithMessage(
+ RangeError,
+ `Invalid value for duration property '${field}': must be an integer, got 1.5`
+ );
+
+ expect(() => {
+ new Intl.DurationFormat().formatToParts({ [field]: -Infinity });
+ }).toThrowWithMessage(
+ RangeError,
+ `Invalid value for duration property '${field}': must be an integer, got -Infinity`
+ );
+ });
+ });
+
+ test("inconsistent field signs", () => {
+ expect(() => {
+ new Intl.DurationFormat().formatToParts({ years: 1, months: -1 });
+ }).toThrowWithMessage(RangeError, "Invalid duration-like object");
+ });
+});