mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 14:27:35 +00:00
LibJS: Disallow mixed-sign durations in Intl.DurationFormat
This is a normative change in the Intl.DurationFormat spec.
See: 89ab1855
This commit is contained in:
parent
8a671154c3
commit
fb8c4a724e
5 changed files with 187 additions and 12 deletions
|
@ -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)
|
ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& global_object, Value input)
|
||||||
{
|
{
|
||||||
auto& vm = global_object.vm();
|
auto& vm = global_object.vm();
|
||||||
|
@ -183,7 +183,62 @@ ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& glo
|
||||||
return result;
|
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)
|
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();
|
auto& vm = global_object.vm();
|
||||||
|
@ -252,7 +307,7 @@ static String convert_number_format_pattern_to_duration_format_template(Unicode:
|
||||||
return result;
|
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)
|
ThrowCompletionOr<Vector<PatternPartition>> partition_duration_format_pattern(GlobalObject& global_object, DurationFormat const& duration_format, Temporal::DurationRecord const& duration)
|
||||||
{
|
{
|
||||||
auto& vm = global_object.vm();
|
auto& vm = global_object.vm();
|
||||||
|
|
|
@ -219,6 +219,8 @@ struct DurationUnitOptions {
|
||||||
};
|
};
|
||||||
|
|
||||||
ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(GlobalObject& global_object, Value input);
|
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<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);
|
ThrowCompletionOr<Vector<PatternPartition>> partition_duration_format_pattern(GlobalObject& global_object, DurationFormat const& duration_format, Temporal::DurationRecord const& duration);
|
||||||
|
|
||||||
|
|
|
@ -41,19 +41,23 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format)
|
||||||
// 3. Let record be ? ToDurationRecord(duration).
|
// 3. Let record be ? ToDurationRecord(duration).
|
||||||
auto record = TRY(to_duration_record(global_object, vm.argument(0)));
|
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));
|
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;
|
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) {
|
for (auto const& part : formatted) {
|
||||||
// a. Set result to the string-concatenation of result and part.[[Value]].
|
// a. Set result to the string-concatenation of result and part.[[Value]].
|
||||||
result.append(part.value);
|
result.append(part.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Return result.
|
// 8. Return result.
|
||||||
return js_string(vm, result.build());
|
return js_string(vm, result.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,14 +71,18 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
|
||||||
// 3. Let record be ? ToDurationRecord(duration).
|
// 3. Let record be ? ToDurationRecord(duration).
|
||||||
auto record = TRY(to_duration_record(global_object, vm.argument(0)));
|
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));
|
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));
|
auto* result = MUST(Array::create(global_object, 0));
|
||||||
|
|
||||||
// 6. Let n be 0.
|
// 7. Let n be 0.
|
||||||
// 7. For each element part in formatted, in List order, do
|
// 8. For each element part in formatted, in List order, do
|
||||||
for (size_t n = 0; n < formatted.size(); ++n) {
|
for (size_t n = 0; n < formatted.size(); ++n) {
|
||||||
auto const& part = formatted[n];
|
auto const& part = formatted[n];
|
||||||
|
|
||||||
|
@ -93,7 +101,7 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
|
||||||
// e. Increment n by 1.
|
// e. Increment n by 1.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Return result.
|
// 9. Return result.
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
).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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue