diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 9d3d2c40dba..3f0b3f4d737 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -8,10 +8,12 @@ */ #include +#include #include #include #include #include +#include namespace JS::Temporal { @@ -41,6 +43,227 @@ StringView temporal_unit_to_string(Unit unit) return temporal_units[to_underlying(unit)].singular_property_name; } +// 13.15 GetTemporalFractionalSecondDigitsOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption +ThrowCompletionOr get_temporal_fractional_second_digits_option(VM& vm, Object const& options) +{ + // 1. Let digitsValue be ? Get(options, "fractionalSecondDigits"). + auto digits_value = TRY(options.get(vm.names.fractionalSecondDigits)); + + // 2. If digitsValue is undefined, return AUTO. + if (digits_value.is_undefined()) + return Precision { Auto {} }; + + // 3. If digitsValue is not a Number, then + if (!digits_value.is_number()) { + // a. If ? ToString(digitsValue) is not "auto", throw a RangeError exception. + auto digits_value_string = TRY(digits_value.to_string(vm)); + + if (digits_value_string != "auto"sv) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, digits_value, vm.names.fractionalSecondDigits); + + // b. Return AUTO. + return Precision { Auto {} }; + } + + // 4. If digitsValue is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + if (digits_value.is_nan() || digits_value.is_infinity()) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, digits_value, vm.names.fractionalSecondDigits); + + // 5. Let digitCount be floor(ℝ(digitsValue)). + auto digit_count = floor(digits_value.as_double()); + + // 6. If digitCount < 0 or digitCount > 9, throw a RangeError exception. + if (digit_count < 0 || digit_count > 9) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, digits_value, vm.names.fractionalSecondDigits); + + // 7. Return digitCount. + return Precision { static_cast(digit_count) }; +} + +// 13.16 ToSecondsStringPrecisionRecord ( smallestUnit, fractionalDigitCount ), https://tc39.es/proposal-temporal/#sec-temporal-tosecondsstringprecisionrecord +SecondsStringPrecision to_seconds_string_precision_record(UnitValue smallest_unit, Precision fractional_digit_count) +{ + if (auto const* unit = smallest_unit.get_pointer()) { + // 1. If smallestUnit is MINUTE, then + if (*unit == Unit::Minute) { + // a. Return the Record { [[Precision]]: MINUTE, [[Unit]]: MINUTE, [[Increment]]: 1 }. + return { .precision = SecondsStringPrecision::Minute {}, .unit = Unit::Minute, .increment = 1 }; + } + + // 2. If smallestUnit is SECOND, then + if (*unit == Unit::Second) { + // a. Return the Record { [[Precision]]: 0, [[Unit]]: SECOND, [[Increment]]: 1 }. + return { .precision = 0, .unit = Unit::Second, .increment = 1 }; + } + + // 3. If smallestUnit is MILLISECOND, then + if (*unit == Unit::Millisecond) { + // a. Return the Record { [[Precision]]: 3, [[Unit]]: MILLISECOND, [[Increment]]: 1 }. + return { .precision = 3, .unit = Unit::Millisecond, .increment = 1 }; + } + + // 4. If smallestUnit is MICROSECOND, then + if (*unit == Unit::Microsecond) { + // a. Return the Record { [[Precision]]: 6, [[Unit]]: MICROSECOND, [[Increment]]: 1 }. + return { .precision = 6, .unit = Unit::Microsecond, .increment = 1 }; + } + + // 5. If smallestUnit is NANOSECOND, then + if (*unit == Unit::Nanosecond) { + // a. Return the Record { [[Precision]]: 9, [[Unit]]: NANOSECOND, [[Increment]]: 1 }. + return { .precision = 9, .unit = Unit::Nanosecond, .increment = 1 }; + } + } + + // 6. Assert: smallestUnit is UNSET. + VERIFY(smallest_unit.has()); + + // 7. If fractionalDigitCount is auto, then + if (fractional_digit_count.has()) { + // a. Return the Record { [[Precision]]: AUTO, [[Unit]]: NANOSECOND, [[Increment]]: 1 }. + return { .precision = Auto {}, .unit = Unit::Nanosecond, .increment = 1 }; + } + + auto fractional_digits = fractional_digit_count.get(); + + // 8. If fractionalDigitCount = 0, then + if (fractional_digits == 0) { + // a. Return the Record { [[Precision]]: 0, [[Unit]]: SECOND, [[Increment]]: 1 }. + return { .precision = 0, .unit = Unit::Second, .increment = 1 }; + } + + // 9. If fractionalDigitCount is in the inclusive interval from 1 to 3, then + if (fractional_digits >= 1 && fractional_digits <= 3) { + // a. Return the Record { [[Precision]]: fractionalDigitCount, [[Unit]]: MILLISECOND, [[Increment]]: 10**(3 - fractionalDigitCount) }. + return { .precision = fractional_digits, .unit = Unit::Millisecond, .increment = static_cast(pow(10, 3 - fractional_digits)) }; + } + + // 10. If fractionalDigitCount is in the inclusive interval from 4 to 6, then + if (fractional_digits >= 4 && fractional_digits <= 6) { + // a. Return the Record { [[Precision]]: fractionalDigitCount, [[Unit]]: MICROSECOND, [[Increment]]: 10**(6 - fractionalDigitCount) }. + return { .precision = fractional_digits, .unit = Unit::Microsecond, .increment = static_cast(pow(10, 6 - fractional_digits)) }; + } + + // 11. Assert: fractionalDigitCount is in the inclusive interval from 7 to 9. + VERIFY(fractional_digits >= 7 && fractional_digits <= 9); + + // 12. Return the Record { [[Precision]]: fractionalDigitCount, [[Unit]]: NANOSECOND, [[Increment]]: 10**(9 - fractionalDigitCount) }. + return { .precision = fractional_digits, .unit = Unit::Nanosecond, .increment = static_cast(pow(10, 9 - fractional_digits)) }; +} + +// 13.17 GetTemporalUnitValuedOption ( options, key, unitGroup, default [ , extraValues ] ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalunitvaluedoption +ThrowCompletionOr get_temporal_unit_valued_option(VM& vm, Object const& options, PropertyKey const& key, UnitGroup unit_group, UnitDefault const& default_, ReadonlySpan extra_values) +{ + // 1. Let allowedValues be a new empty List. + Vector allowed_values; + + // 2. For each row of Table 21, except the header row, in table order, do + for (auto const& row : temporal_units) { + // a. Let unit be the value in the "Value" column of the row. + auto unit = row.value; + + // b. If the "Category" column of the row is DATE and unitGroup is DATE or DATETIME, append unit to allowedValues. + if (row.category == UnitCategory::Date && (unit_group == UnitGroup::Date || unit_group == UnitGroup::DateTime)) + allowed_values.append(unit); + + // c. Else if the "Category" column of the row is TIME and unitGroup is TIME or DATETIME, append unit to allowedValues. + if (row.category == UnitCategory::Time && (unit_group == UnitGroup::Time || unit_group == UnitGroup::DateTime)) + allowed_values.append(unit); + } + + // 3. If extraValues is present, then + if (!extra_values.is_empty()) { + // a. Set allowedValues to the list-concatenation of allowedValues and extraValues. + for (auto value : extra_values) + allowed_values.append(value); + } + + OptionDefault default_value; + + // 4. If default is UNSET, then + if (default_.has()) { + // a. Let defaultValue be undefined. + default_value = {}; + } + // 5. Else if default is REQUIRED, then + else if (default_.has()) { + // a. Let defaultValue be REQUIRED. + default_value = Required {}; + } + // 6. Else if default is AUTO, then + else if (default_.has()) { + // a. Append default to allowedValues. + allowed_values.append(Auto {}); + + // b. Let defaultValue be "auto". + default_value = "auto"sv; + } + // 7. Else, + else { + auto unit = default_.get(); + + // a. Assert: allowedValues contains default. + + // b. Let defaultValue be the value in the "Singular property name" column of Table 21 corresponding to the row + // with default in the "Value" column. + default_value = temporal_units[to_underlying(unit)].singular_property_name; + } + + // 8. Let allowedStrings be a new empty List. + Vector allowed_strings; + + // 9. For each element value of allowedValues, do + for (auto value : allowed_values) { + // a. If value is auto, then + if (value.has()) { + // i. Append "auto" to allowedStrings. + allowed_strings.append("auto"sv); + } + // b. Else, + else { + auto unit = value.get(); + + // i. Let singularName be the value in the "Singular property name" column of Table 21 corresponding to the + // row with value in the "Value" column. + auto singular_name = temporal_units[to_underlying(unit)].singular_property_name; + + // ii. Append singularName to allowedStrings. + allowed_strings.append(singular_name); + + // iii. Let pluralName be the value in the "Plural property name" column of the corresponding row. + auto plural_name = temporal_units[to_underlying(unit)].plural_property_name; + + // iv. Append pluralName to allowedStrings. + allowed_strings.append(plural_name); + } + } + + // 10. NOTE: For each singular Temporal unit name that is contained within allowedStrings, the corresponding plural + // name is also contained within it. + + // 11. Let value be ? GetOption(options, key, STRING, allowedStrings, defaultValue). + auto value = TRY(get_option(vm, options, key, OptionType::String, allowed_strings, default_value)); + + // 12. If value is undefined, return UNSET. + if (value.is_undefined()) + return UnitValue { Unset {} }; + + auto value_string = value.as_string().utf8_string_view(); + + // 13. If value is "auto", return AUTO. + if (value_string == "auto"sv) + return UnitValue { Auto {} }; + + // 14. Return the value in the "Value" column of Table 21 corresponding to the row with value in its "Singular + // property name" or "Plural property name" column. + for (auto const& row : temporal_units) { + if (value_string.is_one_of(row.singular_property_name, row.plural_property_name)) + return UnitValue { row.value }; + } + + VERIFY_NOT_REACHED(); +} + // 13.18 GetTemporalRelativeToOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalrelativetooption ThrowCompletionOr get_temporal_relative_to_option(VM& vm, Object const& options) { @@ -101,6 +324,305 @@ UnitCategory temporal_unit_category(Unit unit) return temporal_units[to_underlying(unit)].category; } +// AD-HOC +Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit) +{ + switch (unit) { + case Unit::Day: + return NANOSECONDS_PER_DAY; + case Unit::Hour: + return NANOSECONDS_PER_HOUR; + case Unit::Minute: + return NANOSECONDS_PER_MINUTE; + case Unit::Second: + return NANOSECONDS_PER_SECOND; + case Unit::Millisecond: + return NANOSECONDS_PER_MILLISECOND; + case Unit::Microsecond: + return NANOSECONDS_PER_MICROSECOND; + case Unit::Nanosecond: + return NANOSECONDS_PER_NANOSECOND; + default: + VERIFY_NOT_REACHED(); + } +} + +// 13.24 FormatFractionalSeconds ( subSecondNanoseconds, precision ), https://tc39.es/proposal-temporal/#sec-temporal-formatfractionalseconds +String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision) +{ + String fraction_string; + + // 1. If precision is auto, then + if (precision.has()) { + // a. If subSecondNanoseconds = 0, return the empty String. + if (sub_second_nanoseconds == 0) + return String {}; + + // b. Let fractionString be ToZeroPaddedDecimalString(subSecondNanoseconds, 9). + fraction_string = MUST(String::formatted("{:09}", sub_second_nanoseconds)); + + // c. Set fractionString to the longest prefix of fractionString ending with a code unit other than 0x0030 (DIGIT ZERO). + fraction_string = MUST(fraction_string.trim("0"sv, TrimMode::Right)); + } + // 2. Else, + else { + // a. If precision = 0, return the empty String. + if (precision.get() == 0) + return String {}; + + // b. Let fractionString be ToZeroPaddedDecimalString(subSecondNanoseconds, 9). + fraction_string = MUST(String::formatted("{:09}", sub_second_nanoseconds)); + + // c. Set fractionString to the substring of fractionString from 0 to precision. + fraction_string = MUST(fraction_string.substring_from_byte_offset(0, precision.get())); + } + + // 3. Return the string-concatenation of the code unit 0x002E (FULL STOP) and fractionString. + return MUST(String::formatted(".{}", fraction_string)); +} + +// 13.26 GetUnsignedRoundingMode ( roundingMode, sign ), https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode +UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode rounding_mode, Sign sign) +{ + // 1. Return the specification type in the "Unsigned Rounding Mode" column of Table 22 for the row where the value + // in the "Rounding Mode" column is roundingMode and the value in the "Sign" column is sign. + switch (rounding_mode) { + case RoundingMode::Ceil: + return sign == Sign::Positive ? UnsignedRoundingMode::Infinity : UnsignedRoundingMode::Zero; + case RoundingMode::Floor: + return sign == Sign::Positive ? UnsignedRoundingMode::Zero : UnsignedRoundingMode::Infinity; + case RoundingMode::Expand: + return UnsignedRoundingMode::Infinity; + case RoundingMode::Trunc: + return UnsignedRoundingMode::Zero; + case RoundingMode::HalfCeil: + return sign == Sign::Positive ? UnsignedRoundingMode::HalfInfinity : UnsignedRoundingMode::HalfZero; + case RoundingMode::HalfFloor: + return sign == Sign::Positive ? UnsignedRoundingMode::HalfZero : UnsignedRoundingMode::HalfInfinity; + case RoundingMode::HalfExpand: + return UnsignedRoundingMode::HalfInfinity; + case RoundingMode::HalfTrunc: + return UnsignedRoundingMode::HalfZero; + case RoundingMode::HalfEven: + return UnsignedRoundingMode::HalfEven; + } + + VERIFY_NOT_REACHED(); +} + +// 13.27 ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode ), https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode +double apply_unsigned_rounding_mode(double x, double r1, double r2, UnsignedRoundingMode unsigned_rounding_mode) +{ + // 1. If x = r1, return r1. + if (x == r1) + return r1; + + // 2. Assert: r1 < x < r2. + VERIFY(r1 < x && x < r2); + + // 3. Assert: unsignedRoundingMode is not undefined. + + // 4. If unsignedRoundingMode is ZERO, return r1. + if (unsigned_rounding_mode == UnsignedRoundingMode::Zero) + return r1; + + // 5. If unsignedRoundingMode is INFINITY, return r2. + if (unsigned_rounding_mode == UnsignedRoundingMode::Infinity) + return r2; + + // 6. Let d1 be x – r1. + auto d1 = x - r1; + + // 7. Let d2 be r2 – x. + auto d2 = r2 - x; + + // 8. If d1 < d2, return r1. + if (d1 < d2) + return r1; + + // 9. If d2 < d1, return r2. + if (d2 < d1) + return r2; + + // 10. Assert: d1 is equal to d2. + VERIFY(d1 == d2); + + // 11. If unsignedRoundingMode is HALF-ZERO, return r1. + if (unsigned_rounding_mode == UnsignedRoundingMode::HalfZero) + return r1; + + // 12. If unsignedRoundingMode is HALF-INFINITY, return r2. + if (unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity) + return r2; + + // 13. Assert: unsignedRoundingMode is HALF-EVEN. + VERIFY(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven); + + // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. + auto cardinality = modulo((r1 / (r2 - r1)), 2); + + // 15. If cardinality = 0, return r1. + if (cardinality == 0) + return r1; + + // 16. Return r2. + return r2; +} + +// 13.27 ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode ), https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode +Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const& x, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode unsigned_rounding_mode, Crypto::UnsignedBigInteger const& increment) +{ + // 1. If x = r1, return r1. + if (x.quotient == r1 && x.remainder.unsigned_value().is_zero()) + return r1; + + // 2. Assert: r1 < x < r2. + // NOTE: Skipped for the sake of performance. + + // 3. Assert: unsignedRoundingMode is not undefined. + + // 4. If unsignedRoundingMode is ZERO, return r1. + if (unsigned_rounding_mode == UnsignedRoundingMode::Zero) + return r1; + + // 5. If unsignedRoundingMode is INFINITY, return r2. + if (unsigned_rounding_mode == UnsignedRoundingMode::Infinity) + return r2; + + // 6. Let d1 be x – r1. + auto d1 = x.remainder.unsigned_value(); + + // 7. Let d2 be r2 – x. + auto d2 = increment.minus(x.remainder.unsigned_value()); + + // 8. If d1 < d2, return r1. + if (d1 < d2) + return r1; + + // 9. If d2 < d1, return r2. + if (d2 < d1) + return r2; + + // 10. Assert: d1 is equal to d2. + // NOTE: Skipped for the sake of performance. + + // 11. If unsignedRoundingMode is HALF-ZERO, return r1. + if (unsigned_rounding_mode == UnsignedRoundingMode::HalfZero) + return r1; + + // 12. If unsignedRoundingMode is HALF-INFINITY, return r2. + if (unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity) + return r2; + + // 13. Assert: unsignedRoundingMode is HALF-EVEN. + VERIFY(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven); + + // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. + auto cardinality = modulo(r1.divided_by(r2.minus(r1)).quotient, "2"_bigint); + + // 15. If cardinality = 0, return r1. + if (cardinality.unsigned_value().is_zero()) + return r1; + + // 16. Return r2. + return r2; +} + +// 13.28 RoundNumberToIncrement ( x, increment, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement +double round_number_to_increment(double x, u64 increment, RoundingMode rounding_mode) +{ + // 1. Let quotient be x / increment. + auto quotient = x / static_cast(increment); + + Sign is_negative; + + // 2. If quotient < 0, then + if (quotient < 0) { + // a. Let isNegative be NEGATIVE. + is_negative = Sign::Negative; + + // b. Set quotient to -quotient. + quotient = -quotient; + } + // 3. Else, + else { + // a. Let isNegative be POSITIVE. + is_negative = Sign::Positive; + } + + // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + auto unsigned_rounding_mode = get_unsigned_rounding_mode(rounding_mode, is_negative); + + // 5. Let r1 be the largest integer such that r1 ≤ quotient. + auto r1 = floor(quotient); + + // 6. Let r2 be the smallest integer such that r2 > quotient. + auto r2 = ceil(quotient); + if (quotient == r2) + r2++; + + // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). + auto rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); + + // 8. If isNegative is NEGATIVE, set rounded to -rounded. + if (is_negative == Sign::Negative) + rounded = -rounded; + + // 9. Return rounded × increment. + return rounded * static_cast(increment); +} + +// 13.28 RoundNumberToIncrement ( x, increment, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundnumbertoincrement +Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger const& x, Crypto::UnsignedBigInteger const& increment, RoundingMode rounding_mode) +{ + // OPTIMIZATION: If the increment is 1 the number is always rounded. + if (increment == 1) + return x; + + // 1. Let quotient be x / increment. + auto division_result = x.divided_by(increment); + + // OPTIMIZATION: If there's no remainder the number is already rounded. + if (division_result.remainder.unsigned_value().is_zero()) + return x; + + Sign is_negative; + + // 2. If quotient < 0, then + if (division_result.quotient.is_negative()) { + // a. Let isNegative be NEGATIVE. + is_negative = Sign::Negative; + + // b. Set quotient to -quotient. + division_result.quotient.negate(); + division_result.remainder.negate(); + } + // 3. Else, + else { + // a. Let isNegative be POSITIVE. + is_negative = Sign::Positive; + } + + // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + auto unsigned_rounding_mode = get_unsigned_rounding_mode(rounding_mode, is_negative); + + // 5. Let r1 be the largest integer such that r1 ≤ quotient. + auto r1 = division_result.quotient; + + // 6. Let r2 be the smallest integer such that r2 > quotient. + auto r2 = division_result.quotient.plus(1_bigint); + + // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). + auto rounded = apply_unsigned_rounding_mode(division_result, r1, r2, unsigned_rounding_mode, increment); + + // 8. If isNegative is NEGATIVE, set rounded to -rounded. + if (is_negative == Sign::Negative) + rounded.negate(); + + // 9. Return rounded × increment. + return rounded.multiplied_by(increment); +} + // 13.35 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring ThrowCompletionOr> parse_temporal_duration_string(VM& vm, StringView iso_string) { @@ -353,12 +875,12 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c // 2. If value is undefined, then if (value.is_undefined()) { // a. If default is REQUIRED, throw a RangeError exception. - if (default_.has()) + if (default_.has()) return vm.throw_completion(ErrorType::OptionIsNotValidValue, "undefined"sv, property.as_string()); // b. Return default. return default_.visit( - [](DefaultRequired) -> Value { VERIFY_NOT_REACHED(); }, + [](Required) -> Value { VERIFY_NOT_REACHED(); }, [](Empty) -> Value { return js_undefined(); }, [](bool default_) -> Value { return Value { default_ }; }, [](double default_) -> Value { return Value { default_ }; }, @@ -392,4 +914,20 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c return value; } +// 14.4.1.3 GetRoundingModeOption ( options, fallback ), https://tc39.es/proposal-temporal/#sec-temporal-getroundingmodeoption +ThrowCompletionOr get_rounding_mode_option(VM& vm, Object const& options, RoundingMode fallback) +{ + // 1. Let allowedStrings be the List of Strings from the "String Identifier" column of Table 26. + static constexpr auto allowed_strings = to_array({ "ceil"sv, "floor"sv, "expand"sv, "trunc"sv, "halfCeil"sv, "halfFloor"sv, "halfExpand"sv, "halfTrunc"sv, "halfEven"sv }); + + // 2. Let stringFallback be the value from the "String Identifier" column of the row with fallback in its "Rounding Mode" column. + auto string_fallback = allowed_strings[to_underlying(fallback)]; + + // 3. Let stringValue be ? GetOption(options, "roundingMode", STRING, allowedStrings, stringFallback). + auto string_value = TRY(get_option(vm, options, vm.names.roundingMode, OptionType::String, allowed_strings, string_fallback)); + + // 4. Return the value from the "Rounding Mode" column of the row with stringValue in its "String Identifier" column. + return static_cast(allowed_strings.first_index_of(string_value.as_string().utf8_string_view()).value()); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index ac20850e738..ec028356479 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -9,6 +9,8 @@ #pragma once #include +#include +#include #include #include #include @@ -51,8 +53,49 @@ enum class UnitGroup { DateTime, }; +// https://tc39.es/proposal-temporal/#table-unsigned-rounding-modes +enum class RoundingMode { + Ceil, + Floor, + Expand, + Trunc, + HalfCeil, + HalfFloor, + HalfExpand, + HalfTrunc, + HalfEven, +}; + +// https://tc39.es/proposal-temporal/#table-unsigned-rounding-modes +enum class UnsignedRoundingMode { + HalfEven, + HalfInfinity, + HalfZero, + Infinity, + Zero, +}; + +// https://tc39.es/proposal-temporal/#table-unsigned-rounding-modes +enum class Sign { + Negative, + Positive, +}; + +struct Auto { }; +struct Required { }; struct Unset { }; +using Precision = Variant; using RoundingIncrement = Variant; +using UnitDefault = Variant; +using UnitValue = Variant; + +struct SecondsStringPrecision { + struct Minute { }; + + Variant precision; + Unit unit; + u8 increment { 0 }; +}; struct RelativeTo { // FIXME: Make these objects represent their actual types when we re-implement them. @@ -60,10 +103,20 @@ struct RelativeTo { GC::Ptr zoned_relative_to; // [[ZonedRelativeTo]] }; +ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); +SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); +ThrowCompletionOr get_temporal_unit_valued_option(VM&, Object const& options, PropertyKey const&, UnitGroup, UnitDefault const&, ReadonlySpan extra_values = {}); ThrowCompletionOr get_temporal_relative_to_option(VM&, Object const& options); Unit larger_of_two_temporal_units(Unit, Unit); bool is_calendar_unit(Unit); UnitCategory temporal_unit_category(Unit); +Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); +String format_fractional_seconds(u64, Precision); +UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); +double apply_unsigned_rounding_mode(double, double r1, double r2, UnsignedRoundingMode); +Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const&, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode, Crypto::UnsignedBigInteger const& increment); +double round_number_to_increment(double, u64 increment, RoundingMode); +Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); ThrowCompletionOr> parse_temporal_duration_string(VM&, StringView iso_string); // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation @@ -118,8 +171,7 @@ enum class OptionType { String, }; -struct DefaultRequired { }; -using OptionDefault = Variant; +using OptionDefault = Variant; ThrowCompletionOr> get_options_object(VM&, Value options); ThrowCompletionOr get_option(VM&, Object const& options, PropertyKey const& property, OptionType type, ReadonlySpan values, OptionDefault const&); @@ -130,4 +182,6 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c return get_option(vm, options, property, type, ReadonlySpan { values }, default_); } +ThrowCompletionOr get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback); + } diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index d74bc15301f..9485424205b 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -701,6 +701,20 @@ i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two) return 0; } +// 7.5.27 RoundTimeDurationToIncrement ( d, increment, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtimedurationtoincrement +ThrowCompletionOr round_time_duration_to_increment(VM& vm, TimeDuration const& duration, Crypto::UnsignedBigInteger const& increment, RoundingMode rounding_mode) +{ + // 1. Let rounded be RoundNumberToIncrement(d, increment, roundingMode). + auto rounded = round_number_to_increment(duration, increment, rounding_mode); + + // 2. If abs(rounded) > maxTimeDuration, throw a RangeError exception. + if (rounded.unsigned_value() > MAX_TIME_DURATION.unsigned_value()) + return vm.throw_completion(ErrorType::TemporalInvalidDuration); + + // 3. Return rounded. + return rounded; +} + // 7.5.28 TimeDurationSign ( d ), https://tc39.es/proposal-temporal/#sec-temporal-timedurationsign i8 time_duration_sign(TimeDuration const& time_duration) { @@ -716,6 +730,108 @@ i8 time_duration_sign(TimeDuration const& time_duration) return 0; } +// 7.5.30 RoundTimeDuration ( timeDuration, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtimeduration +ThrowCompletionOr round_time_duration(VM& vm, TimeDuration const& time_duration, Crypto::UnsignedBigInteger const& increment, Unit unit, RoundingMode rounding_mode) +{ + // 1. Let divisor be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit. + auto const& divisor = temporal_unit_length_in_nanoseconds(unit); + + // 2. Return ? RoundTimeDurationToIncrement(timeDuration, divisor × increment, roundingMode). + return TRY(round_time_duration_to_increment(vm, time_duration, divisor.multiplied_by(increment), rounding_mode)); +} + +// 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring +String temporal_duration_to_string(Duration const& duration, Precision precision) +{ + // 1. Let sign be DurationSign(duration). + auto sign = duration_sign(duration); + + // 2. Let datePart be the empty String. + StringBuilder date_part; + + // 3. If duration.[[Years]] ≠ 0, then + if (duration.years() != 0) { + // a. Set datePart to the string concatenation of abs(duration.[[Years]]) formatted as a decimal number and the + // code unit 0x0059 (LATIN CAPITAL LETTER Y). + date_part.appendff("{}Y", AK::fabs(duration.years())); + } + // 4. If duration.[[Months]] ≠ 0, then + if (duration.months() != 0) { + // a. Set datePart to the string concatenation of datePart, abs(duration.[[Months]]) formatted as a decimal number, + // and the code unit 0x004D (LATIN CAPITAL LETTER M). + date_part.appendff("{}M", AK::fabs(duration.months())); + } + // 5. If duration.[[Weeks]] ≠ 0, then + if (duration.weeks() != 0) { + // a. Set datePart to the string concatenation of datePart, abs(duration.[[Weeks]]) formatted as a decimal number, + // and the code unit 0x0057 (LATIN CAPITAL LETTER W). + date_part.appendff("{}W", AK::fabs(duration.weeks())); + } + // 6. If duration.[[Days]] ≠ 0, then + if (duration.days() != 0) { + // a. Set datePart to the string concatenation of datePart, abs(duration.[[Days]]) formatted as a decimal number, + // and the code unit 0x0044 (LATIN CAPITAL LETTER D). + date_part.appendff("{}D", AK::fabs(duration.days())); + } + + // 7. Let timePart be the empty String. + StringBuilder time_part; + + // 8. If duration.[[Hours]] ≠ 0, then + if (duration.hours() != 0) { + // a. Set timePart to the string concatenation of abs(duration.[[Hours]]) formatted as a decimal number and the + // code unit 0x0048 (LATIN CAPITAL LETTER H). + time_part.appendff("{}H", AK::fabs(duration.hours())); + } + // 9. If duration.[[Minutes]] ≠ 0, then + if (duration.minutes() != 0) { + // a. Set timePart to the string concatenation of timePart, abs(duration.[[Minutes]]) formatted as a decimal number, + // and the code unit 0x004D (LATIN CAPITAL LETTER M). + time_part.appendff("{}M", AK::fabs(duration.minutes())); + } + + // 10. Let zeroMinutesAndHigher be false. + auto zero_minutes_and_higher = false; + + // 11. If DefaultTemporalLargestUnit(duration) is SECOND, MILLISECOND, MICROSECOND, or NANOSECOND, set zeroMinutesAndHigher to true. + if (auto unit = default_temporal_largest_unit(duration); unit == Unit::Second || unit == Unit::Millisecond || unit == Unit::Microsecond || unit == Unit::Nanosecond) + zero_minutes_and_higher = true; + + // 12. Let secondsDuration be TimeDurationFromComponents(0, 0, duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + auto seconds_duration = time_duration_from_components(0, 0, duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds()); + + // 13. If secondsDuration ≠ 0, or zeroMinutesAndHigher is true, or precision is not auto, then + if (!seconds_duration.is_zero() || zero_minutes_and_higher || !precision.has()) { + auto division_result = seconds_duration.divided_by(NANOSECONDS_PER_SECOND); + + // a. Let secondsPart be abs(truncate(secondsDuration / 10**9)) formatted as a decimal number. + auto seconds_part = MUST(division_result.quotient.unsigned_value().to_base(10)); + + // b. Let subSecondsPart be FormatFractionalSeconds(abs(remainder(secondsDuration, 10**9)), precision). + auto sub_seconds_part = format_fractional_seconds(division_result.remainder.unsigned_value().to_u64(), precision); + + // c. Set timePart to the string concatenation of timePart, secondsPart, subSecondsPart, and the code unit + // 0x0053 (LATIN CAPITAL LETTER S). + time_part.appendff("{}{}S", seconds_part, sub_seconds_part); + } + + // 14. Let signPart be the code unit 0x002D (HYPHEN-MINUS) if sign < 0, and otherwise the empty String. + auto sign_part = sign < 0 ? "-"sv : ""sv; + + // 15. Let result be the string concatenation of signPart, the code unit 0x0050 (LATIN CAPITAL LETTER P) and datePart. + StringBuilder result; + result.appendff("{}P{}", sign_part, date_part.string_view()); + + // 16. If timePart is not the empty String, then + if (!time_part.is_empty()) { + // a. Set result to the string concatenation of result, the code unit 0x0054 (LATIN CAPITAL LETTER T), and timePart. + result.appendff("T{}", time_part.string_view()); + } + + // 17. Return result. + return MUST(result.to_string()); +} + // 7.5.40 AddDurations ( operation, duration, other ), https://tc39.es/proposal-temporal/#sec-temporal-adddurations ThrowCompletionOr> add_durations(VM& vm, ArithmeticOperation operation, Duration const& duration, Value other_value) { diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index 33d3818a8b4..c9f174edde8 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -118,7 +118,10 @@ TimeDuration time_duration_from_components(double hours, double minutes, double ThrowCompletionOr add_time_duration(VM&, TimeDuration const&, TimeDuration const&); ThrowCompletionOr add_24_hour_days_to_time_duration(VM&, TimeDuration const&, double days); i8 compare_time_duration(TimeDuration const&, TimeDuration const&); +ThrowCompletionOr round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); i8 time_duration_sign(TimeDuration const&); +ThrowCompletionOr round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode); +String temporal_duration_to_string(Duration const&, Precision); ThrowCompletionOr> add_durations(VM&, ArithmeticOperation, Duration const&, Value); } diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index 1c1e3069967..a747bb167dd 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -41,6 +41,9 @@ void DurationPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.abs, abs, 0, attr); define_native_function(realm, vm.names.add, add, 1, attr); define_native_function(realm, vm.names.subtract, subtract, 1, attr); + define_native_function(realm, vm.names.toString, to_string, 0, attr); + define_native_function(realm, vm.names.toJSON, to_json, 0, attr); + define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); } // 7.3.3 get Temporal.Duration.prototype.years, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.years @@ -214,4 +217,85 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::subtract) return TRY(add_durations(vm, ArithmeticOperation::Subtract, duration, other)); } +// 7.3.22 Temporal.Duration.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string) +{ + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + auto duration = TRY(typed_this_object(vm)); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(0))); + + // 4. NOTE: The following steps read options and perform independent validation in alphabetical order + // (GetTemporalFractionalSecondDigitsOption reads "fractionalSecondDigits" and GetRoundingModeOption reads + // "roundingMode"). + + // 5. Let digits be ? GetTemporalFractionalSecondDigitsOption(resolvedOptions). + auto digits = TRY(get_temporal_fractional_second_digits_option(vm, resolved_options)); + + // 6. Let roundingMode be ? GetRoundingModeOption(resolvedOptions, TRUNC). + auto rounding_mode = TRY(get_rounding_mode_option(vm, resolved_options, RoundingMode::Trunc)); + + // 7. Let smallestUnit be ? GetTemporalUnitValuedOption(resolvedOptions, "smallestUnit", TIME, UNSET). + auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, resolved_options, vm.names.smallestUnit, UnitGroup::Time, Unset {})); + + // 8. If smallestUnit is HOUR or MINUTE, throw a RangeError exception. + if (auto const* unit = smallest_unit.get_pointer(); unit && (*unit == Unit::Hour || *unit == Unit::Minute)) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(*unit), vm.names.smallestUnit); + + // 9. Let precision be ToSecondsStringPrecisionRecord(smallestUnit, digits). + auto precision = to_seconds_string_precision_record(smallest_unit, digits); + + // 10. If precision.[[Unit]] is NANOSECOND and precision.[[Increment]] = 1, then + if (precision.unit == Unit::Nanosecond && precision.increment == 1) { + // a. Return TemporalDurationToString(duration, precision.[[Precision]]). + return PrimitiveString::create(vm, temporal_duration_to_string(duration, precision.precision.downcast())); + } + + // 11. Let largestUnit be DefaultTemporalLargestUnit(duration). + auto largest_unit = default_temporal_largest_unit(duration); + + // 12. Let internalDuration be ToInternalDurationRecord(duration). + auto internal_duration = to_internal_duration_record(vm, duration); + + // 13. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], precision.[[Increment]], precision.[[Unit]], roundingMode). + auto time_duration = TRY(round_time_duration(vm, internal_duration.time, precision.increment, precision.unit, rounding_mode)); + + // 14. Set internalDuration to ! CombineDateAndTimeDuration(internalDuration.[[Date]], timeDuration). + internal_duration = MUST(combine_date_and_time_duration(vm, internal_duration.date, move(time_duration))); + + // 15. Let roundedLargestUnit be LargerOfTwoTemporalUnits(largestUnit, SECOND). + auto rounded_largest_unit = larger_of_two_temporal_units(largest_unit, Unit::Second); + + // 16. Let roundedDuration be ? TemporalDurationFromInternal(internalDuration, roundedLargestUnit). + auto rounded_duration = TRY(temporal_duration_from_internal(vm, internal_duration, rounded_largest_unit)); + + // 17. Return TemporalDurationToString(roundedDuration, precision.[[Precision]]). + return PrimitiveString::create(vm, temporal_duration_to_string(rounded_duration, precision.precision.downcast())); +} + +// 7.3.23 Temporal.Duration.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tojson +JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_json) +{ + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + auto duration = TRY(typed_this_object(vm)); + + // 3. Return TemporalDurationToString(duration, AUTO). + return PrimitiveString::create(vm, temporal_duration_to_string(duration, Auto {})); +} + +// 7.3.24 Temporal.Duration.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_locale_string) +{ + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + auto duration = TRY(typed_this_object(vm)); + + // 3. Return TemporalDurationToString(duration, AUTO). + return PrimitiveString::create(vm, temporal_duration_to_string(duration, Auto {})); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h index bdfe489aa8f..f28c1794ca8 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h @@ -35,6 +35,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(abs); JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); + JS_DECLARE_NATIVE_FUNCTION(to_string); + JS_DECLARE_NATIVE_FUNCTION(to_json); + JS_DECLARE_NATIVE_FUNCTION(to_locale_string); }; } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toJSON.js new file mode 100644 index 00000000000..d324e957d28 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toJSON.js @@ -0,0 +1,19 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.Duration.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + expect(new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).toJSON()).toBe( + "P1Y2M3W4DT5H6M7.00800901S" + ); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Temporal.Duration.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toLocaleString.js new file mode 100644 index 00000000000..189c3b6846b --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toLocaleString.js @@ -0,0 +1,19 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.Duration.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + expect(new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).toLocaleString()).toBe( + "P1Y2M3W4DT5H6M7.00800901S" + ); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Temporal.Duration.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toString.js new file mode 100644 index 00000000000..1e9ea6a284c --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.toString.js @@ -0,0 +1,94 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.Duration.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + const values = [ + [[0], "PT0S"], + [[1], "P1Y"], + [[0, 1], "P1M"], + [[0, 0, 1], "P1W"], + [[0, 0, 0, 1], "P1D"], + [[0, 0, 0, 0, 1], "PT1H"], + [[0, 0, 0, 0, 0, 1], "PT1M"], + [[0, 0, 0, 0, 0, 0, 1], "PT1S"], + [[0, 0, 0, 0, 0, 0, 0, 1], "PT0.001S"], + [[0, 0, 0, 0, 0, 0, 0, 0, 1], "PT0.000001S"], + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1], "PT0.000000001S"], + [[1, 2], "P1Y2M"], + [[1, 2, 3], "P1Y2M3W"], + [[1, 2, 3, 4], "P1Y2M3W4D"], + [[1, 2, 3, 4, 5], "P1Y2M3W4DT5H"], + [[1, 2, 3, 4, 5, 6], "P1Y2M3W4DT5H6M"], + [[1, 2, 3, 4, 5, 6, 7], "P1Y2M3W4DT5H6M7S"], + [[1, 2, 3, 4, 5, 6, 7, 8], "P1Y2M3W4DT5H6M7.008S"], + [[1, 2, 3, 4, 5, 6, 7, 8, 9], "P1Y2M3W4DT5H6M7.008009S"], + [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "P1Y2M3W4DT5H6M7.00800901S"], + [ + [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], + "P100Y200M300W400DT500H600M700.800901S", + ], + [[-1], "-P1Y"], + ]; + for (const [args, expected] of values) { + expect(new Temporal.Duration(...args).toString()).toBe(expected); + } + }); + + test("smallestUnit option", () => { + const values = [ + ["second", "P1Y2M3W4DT5H6M7S"], + ["millisecond", "P1Y2M3W4DT5H6M7.008S"], + ["microsecond", "P1Y2M3W4DT5H6M7.008010S"], + ["nanosecond", "P1Y2M3W4DT5H6M7.008010000S"], + ]; + for (const [smallestUnit, expected] of values) { + expect( + new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 10).toString({ smallestUnit }) + ).toBe(expected); + } + }); + + test("fractionalSecondDigits option", () => { + const values = [ + [0, "P1Y2M3W4DT5H6M7S"], + [1, "P1Y2M3W4DT5H6M7.0S"], + [2, "P1Y2M3W4DT5H6M7.00S"], + [3, "P1Y2M3W4DT5H6M7.008S"], + [4, "P1Y2M3W4DT5H6M7.0080S"], + [5, "P1Y2M3W4DT5H6M7.00801S"], + [6, "P1Y2M3W4DT5H6M7.008010S"], + [7, "P1Y2M3W4DT5H6M7.0080100S"], + [8, "P1Y2M3W4DT5H6M7.00801000S"], + [9, "P1Y2M3W4DT5H6M7.008010000S"], + ]; + for (const [fractionalSecondDigits, expected] of values) { + expect( + new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 10).toString({ + fractionalSecondDigits, + }) + ).toBe(expected); + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Temporal.Duration.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); + + test("disallowed smallestUnit option values", () => { + const values = ["year", "month", "week", "day", "hour", "minute"]; + for (const smallestUnit of values) { + expect(() => { + new Temporal.Duration(0).toString({ smallestUnit }); + }).toThrowWithMessage( + RangeError, + `${smallestUnit} is not a valid value for option smallestUnit` + ); + } + }); +});