diff options
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"); + }); +}); |