浏览代码

LibJS: Implement the latest Intl.DurationFormat proposal

The proposal has undergone quite a few normative changes since we last
synced with it. There was a time when it could not be implemented as it
was written, which is no longer the case. The resulting proposal has had
so many changes compared to our implementation, that it wouldn't make
sense to implement them commit-by-commit as we normally do. So instead,
this just implements the HEAD revision of the spec in one pass.
Timothy Flynn 1 年之前
父节点
当前提交
d634039c10

+ 4 - 2
Userland/Libraries/LibJS/Runtime/ErrorTypes.h

@@ -47,6 +47,8 @@
     M(IndexOutOfRange, "Index {} is out of range of array length {}")                                                                   \
     M(InOperatorWithObject, "'in' operator must be used on an object")                                                                  \
     M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object")                                                    \
+    M(IntlFractionalUnitFollowedByNonFractionalUnit, "Non-fractional unit {} is not allowed after a fractional unit")                   \
+    M(IntlFractionalUnitsMixedWithAlwaysDisplay, "Fractional unit {} may not be used with {} value of 'always'")                        \
     M(IntlInvalidDateTimeFormatOption, "Option {} cannot be set when also providing {}")                                                \
     M(IntlInvalidKey, "{} is not a valid key")                                                                                          \
     M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag")                                                            \
@@ -58,8 +60,8 @@
     M(IntlMinimumExceedsMaximum, "Minimum value {} is larger than maximum value {}")                                                    \
     M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}")                                                       \
     M(IntlOptionUndefined, "Option {} must be defined when option {} is {}")                                                            \
-    M(IntlNonNumericOr2DigitAfterNumericOr2Digit, "Styles other than 'numeric' and '2-digit' may not be used in smaller units after "   \
-                                                  "being used in larger units")                                                         \
+    M(IntlNonNumericOr2DigitAfterNumericOr2Digit, "Styles other than 'fractional', numeric', or '2-digit' may not be used in smaller "  \
+                                                  "units after being used in larger units")                                             \
     M(InvalidAssignToConst, "Invalid assignment to const variable")                                                                     \
     M(InvalidCodePoint, "Invalid code point {}, must be an integer no less than 0 and no greater than 0x10FFFF")                        \
     M(InvalidFormat, "Invalid {} format")                                                                                               \

+ 633 - 252
Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp

@@ -1,6 +1,6 @@
 /*
  * Copyright (c) 2022, Idan Horowitz <idan.horowitz@serenityos.org>
- * Copyright (c) 2022-2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -80,6 +80,8 @@ DurationFormat::ValueStyle DurationFormat::time_style_from_string(StringView tim
         return ValueStyle::Numeric;
     if (time_style == "2-digit"sv)
         return ValueStyle::TwoDigit;
+    if (time_style == "fractional"sv)
+        return ValueStyle::Fractional;
     VERIFY_NOT_REACHED();
 }
 
@@ -93,6 +95,8 @@ DurationFormat::ValueStyle DurationFormat::sub_second_style_from_string(StringVi
         return ValueStyle::Narrow;
     if (sub_second_style == "numeric"sv)
         return ValueStyle::Numeric;
+    if (sub_second_style == "fractional"sv)
+        return ValueStyle::Fractional;
     VERIFY_NOT_REACHED();
 }
 
@@ -118,9 +122,10 @@ StringView DurationFormat::value_style_to_string(ValueStyle value_style)
         return "numeric"sv;
     case ValueStyle::TwoDigit:
         return "2-digit"sv;
-    default:
-        VERIFY_NOT_REACHED();
+    case ValueStyle::Fractional:
+        return "fractional"sv;
     }
+    VERIFY_NOT_REACHED();
 }
 
 StringView DurationFormat::display_to_string(Display display)
@@ -135,6 +140,22 @@ StringView DurationFormat::display_to_string(Display display)
     }
 }
 
+static NonnullGCPtr<NumberFormat> construct_number_format(VM& vm, DurationFormat const& duration_format, NonnullGCPtr<Object> options)
+{
+    auto& realm = *vm.current_realm();
+
+    auto number_format = MUST(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), options));
+    return static_cast<NumberFormat&>(*number_format);
+}
+
+static NonnullGCPtr<ListFormat> construct_list_format(VM& vm, DurationFormat const& duration_format, NonnullGCPtr<Object> options)
+{
+    auto& realm = *vm.current_realm();
+
+    auto list_format = MUST(construct(vm, realm.intrinsics().intl_list_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), options));
+    return static_cast<ListFormat&>(*list_format);
+}
+
 // 1.1.3 ToDurationRecord ( input ), https://tc39.es/proposal-intl-duration-format/#sec-todurationrecord
 ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(VM& vm, Value input)
 {
@@ -209,79 +230,26 @@ ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(VM& vm, Value inp
     if (!any_defined)
         return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidDurationLikeObject);
 
-    // 24. If IsValidDurationRecord(result) is false, throw a RangeError exception.
-    if (!is_valid_duration_record(result))
+    // 24. If IsValidDuration( result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]) is false, then
+    if (!Temporal::is_valid_duration(result.years, result.months, result.weeks, result.days, result.hours, result.minutes, result.seconds, result.milliseconds, result.microseconds, result.nanoseconds)) {
+        // a. Throw a RangeError exception.
         return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidDurationLikeObject);
+    }
 
     // 25. Return result.
     return result;
 }
 
-// 1.1.4 DurationRecordSign ( record ), https://tc39.es/proposal-intl-duration-format/#sec-durationrecordsign
-i8 duration_record_sign(Temporal::DurationRecord const& record)
-{
-    // 1. For each row of 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 of the current row.
-        auto value_slot = duration_instances_component.value_slot;
-
-        // b. Let v be record.[[<valueSlot>]].
-        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 DurationRecordSign(record).
-    auto sign = duration_record_sign(record);
-
-    // 2. For each row of 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 of the current row.
-        auto value_slot = duration_instances_component.value_slot;
-
-        // b. Let v be record.[[<valueSlot>]].
-        auto value = record.*value_slot;
-
-        // c. Assert: 𝔽(v) is finite.
-        VERIFY(isfinite(value));
-
-        // 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(VM& vm, String const& unit, Object const& options, StringView base_style, ReadonlySpan<StringView> styles_list, StringView digital_base, StringView previous_style)
+// 1.1.6 GetDurationUnitOptions ( unit, options, baseStyle, stylesList, digitalBase, prevStyle, twoDigitHours ), https://tc39.es/proposal-intl-duration-format/#sec-getdurationunitoptions
+ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(VM& vm, String const& unit, Object const& options, StringView base_style, ReadonlySpan<StringView> styles_list, StringView digital_base, StringView previous_style, bool two_digit_hours)
 {
     // 1. Let style be ? GetOption(options, unit, string, stylesList, undefined).
     auto style_value = TRY(get_option(vm, options, unit.to_byte_string(), OptionType::String, styles_list, Empty {}));
+    StringView style;
 
     // 2. Let displayDefault be "always".
     auto display_default = "always"sv;
 
-    StringView style;
-
     // 3. If style is undefined, then
     if (style_value.is_undefined()) {
         // a. If baseStyle is "digital", then
@@ -297,17 +265,23 @@ ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(VM& vm, String
         }
         // b. Else,
         else {
-            // i. Set displayDefault to "auto".
-            display_default = "auto"sv;
+            // i. If prevStyle is "fractional", "numeric" or "2-digit", then
+            if (previous_style.is_one_of("fractional"sv, "numeric"sv, "2-digit"sv)) {
+                // 1. If unit is not one of "minutes" or "seconds", then
+                if (!unit.is_one_of("minutes"sv, "seconds"sv)) {
+                    // a. Set displayDefault to "auto".
+                    display_default = "auto"sv;
+                }
 
-            // ii. If prevStyle is "numeric" or "2-digit", then
-            if (previous_style == "numeric"sv || previous_style == "2-digit"sv) {
-                // 1. Set style to "numeric".
+                // 2. Set style to "numeric".
                 style = "numeric"sv;
             }
-            // iii. Else,
+            // ii. Else,
             else {
-                // 1. Set style to baseStyle.
+                // 1. Set displayDefault to "auto".
+                display_default = "auto"sv;
+
+                // 2. Set style to baseStyle.
                 style = base_style;
             }
         }
@@ -315,263 +289,670 @@ ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(VM& vm, String
         style = style_value.as_string().utf8_string_view();
     }
 
-    // 4. Let displayField be the string-concatenation of unit and "Display".
+    // 4. If style is "numeric", then
+    if (style == "numeric"sv) {
+        // a. If unit is one of "milliseconds", "microseconds", or "nanoseconds", then
+        if (unit.is_one_of("milliseconds"sv, "microseconds"sv, "nanoseconds"sv)) {
+            // i. Set style to "fractional".
+            style = "fractional"sv;
+
+            // ii. Set displayDefault to "auto".
+            display_default = "auto"sv;
+        }
+    }
+
+    // 5. Let displayField be the string-concatenation of unit and "Display".
     auto display_field = MUST(String::formatted("{}Display", unit));
 
-    // 5. Let display be ? GetOption(options, displayField, string, « "auto", "always" », displayDefault).
-    auto display = TRY(get_option(vm, options, display_field.to_byte_string(), OptionType::String, { "auto"sv, "always"sv }, display_default));
+    // 6. Let display be ? GetOption(options, displayField, string, « "auto", "always" », displayDefault).
+    auto display_value = TRY(get_option(vm, options, display_field.to_byte_string(), OptionType::String, { "auto"sv, "always"sv }, display_default));
+    auto display = display_value.as_string().utf8_string();
+
+    // 7. If display is "always" and style is "fractional", then
+    if (display == "always"sv && style == "fractional"sv) {
+        // a. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(ErrorType::IntlFractionalUnitsMixedWithAlwaysDisplay, unit, display_field);
+    }
+
+    // 8. If prevStyle is "fractional", then
+    if (previous_style == "fractional"sv) {
+        // a. If style is not "fractional", then
+        if (style != "fractional"sv) {
+            // i. Throw a RangeError exception.
+            return vm.throw_completion<RangeError>(ErrorType::IntlFractionalUnitFollowedByNonFractionalUnit, unit);
+        }
+    }
 
-    // 6. If prevStyle is "numeric" or "2-digit", then
-    if (previous_style == "numeric"sv || previous_style == "2-digit"sv) {
-        // a. If style is not "numeric" or "2-digit", then
-        if (style != "numeric"sv && style != "2-digit"sv) {
+    // 9. If prevStyle is "numeric" or "2-digit", then
+    if (previous_style.is_one_of("numeric"sv, "2-digit"sv)) {
+        // a. If style is not "fractional", "numeric" or "2-digit", then
+        if (!style.is_one_of("fractional"sv, "numeric"sv, "2-digit"sv)) {
             // i. Throw a RangeError exception.
             return vm.throw_completion<RangeError>(ErrorType::IntlNonNumericOr2DigitAfterNumericOr2Digit);
         }
-        // b. Else if unit is "minutes" or "seconds", then
-        else if (unit == "minutes"sv || unit == "seconds"sv) {
+
+        // b. If unit is "minutes" or "seconds", then
+        if (unit.is_one_of("minutes"sv, "seconds"sv)) {
             // i. Set style to "2-digit".
             style = "2-digit"sv;
         }
     }
 
-    // 7. Return the Record { [[Style]]: style, [[Display]]: display }.
-    return DurationUnitOptions { .style = MUST(String::from_utf8(style)), .display = display.as_string().utf8_string() };
+    // 10. If unit is "hours" and twoDigitHours is true, then
+    if (unit == "hours"sv && two_digit_hours) {
+        // a. Set style to "2-digit".
+        style = "2-digit"sv;
+    }
+
+    // 11. Return the Record { [[Style]]: style, [[Display]]: display  }.
+    return DurationUnitOptions { .style = MUST(String::from_utf8(style)), .display = move(display) };
 }
 
-// 1.1.7 PartitionDurationFormatPattern ( durationFormat, duration ), https://tc39.es/proposal-intl-duration-format/#sec-partitiondurationformatpattern
-Vector<::Locale::ListFormatPart> partition_duration_format_pattern(VM& vm, DurationFormat const& duration_format, Temporal::DurationRecord const& duration)
+// 1.1.7 AddFractionalDigits ( durationFormat, duration ), https://tc39.es/proposal-intl-duration-format/#sec-addfractionaldigits
+double add_fractional_digits(DurationFormat const& duration_format, Temporal::DurationRecord const& duration)
+{
+    // 1. Let result be 0.
+    double result = 0;
+
+    // 2. Let exponent be 3.
+    double exponent = 3;
+
+    // 3. For each row of Table 2, except the header row, in table order, do
+    for (auto const& duration_instances_component : duration_instances_components) {
+        // a. Let style be the value of durationFormat's internal slot whose name is the Style Slot value of the current row.
+        auto style = (duration_format.*duration_instances_component.get_style_slot)();
+
+        // b. If style is "fractional", then
+        if (style == DurationFormat::ValueStyle::Fractional) {
+            // i. Assert: The Unit value of the current row is "milliseconds", "microseconds", or "nanoseconds".
+            VERIFY(duration_instances_component.unit.is_one_of("milliseconds"sv, "microseconds"sv, "nanoseconds"sv));
+
+            // ii. Let value be the value of duration's field whose name is the Value Field value of the current row.
+            auto value = duration.*duration_instances_component.value_slot;
+
+            // iii. Set value to value / 10^exponent.
+            value = value / pow(10, exponent);
+
+            // iv. Set result to result + value.
+            result += value;
+
+            // v. Set exponent to exponent + 3.
+            exponent += 3;
+        }
+    }
+
+    // 4. Return result.
+    return result;
+}
+
+// 1.1.8 NextUnitFractional ( durationFormat, unit ), https://tc39.es/proposal-intl-duration-format/#sec-nextunitfractional
+bool next_unit_fractional(DurationFormat const& duration_format, StringView unit)
+{
+    // 1. Assert: unit is "seconds", "milliseconds", or "microseconds".
+    VERIFY(unit.is_one_of("seconds"sv, "milliseconds"sv, "microseconds"sv));
+
+    // 2. If unit is "seconds" and durationFormat.[[MillisecondsStyle]] is "fractional", return true.
+    if (unit == "seconds"sv && duration_format.milliseconds_style() == DurationFormat::ValueStyle::Fractional)
+        return true;
+
+    // 3. Else if unit is "milliseconds" and durationFormat.[[MicrosecondsStyle]] is "fractional", return true.
+    if (unit == "milliseconds"sv && duration_format.microseconds_style() == DurationFormat::ValueStyle::Fractional)
+        return true;
+
+    // 4. Else if unit is "microseconds" and durationFormat.[[NanosecondsStyle]] is "fractional", return true.
+    if (unit == "microseconds"sv && duration_format.nanoseconds_style() == DurationFormat::ValueStyle::Fractional)
+        return true;
+
+    // 5. Return false.
+    return false;
+}
+
+// 1.1.9 FormatNumericHours ( durationFormat, hoursValue, signDisplayed ), https://tc39.es/proposal-intl-duration-format/#sec-formatnumerichours
+Vector<DurationFormatPart> format_numeric_hours(VM& vm, DurationFormat const& duration_format, double hours_value, bool sign_displayed)
+{
+    auto& realm = *vm.current_realm();
+
+    // 1. Let hoursStyle be durationFormat.[[HoursStyle]].
+    auto hours_style = duration_format.hours_style();
+
+    // 2. Assert: hoursStyle is "numeric" or hoursStyle is "2-digit".
+    VERIFY(hours_style == DurationFormat::ValueStyle::Numeric || hours_style == DurationFormat::ValueStyle::TwoDigit);
+
+    // 3. Let result be a new empty List.
+    Vector<DurationFormatPart> result;
+
+    // 4. Let nfOpts be OrdinaryObjectCreate(null).
+    auto number_format_options = Object::create(realm, nullptr);
+
+    // 5. Let numberingSystem be durationFormat.[[NumberingSystem]].
+    auto const& numbering_system = duration_format.numbering_system();
+
+    // 6. Perform ! CreateDataPropertyOrThrow(nfOpts, "numberingSystem", numberingSystem).
+    MUST(number_format_options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, numbering_system)));
+
+    // 7. If hoursStyle is "2-digit", then
+    if (hours_style == DurationFormat::ValueStyle::TwoDigit) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumIntegerDigits", 2𝔽).
+        MUST(number_format_options->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value { 2 }));
+    }
+
+    // 8. If signDisplayed is false, then
+    if (!sign_displayed) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "signDisplay", "never").
+        MUST(number_format_options->create_data_property_or_throw(vm.names.signDisplay, PrimitiveString::create(vm, "never"sv)));
+    }
+
+    // 9. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
+    auto number_format = construct_number_format(vm, duration_format, number_format_options);
+
+    // 10. Let hoursParts be ! PartitionNumberPattern(nf, hoursValue).
+    auto hours_parts = partition_number_pattern(number_format, MathematicalValue { hours_value });
+
+    // 11. For each Record { [[Type]], [[Value]] } part of hoursParts, do
+    result.ensure_capacity(hours_parts.size());
+
+    for (auto& part : hours_parts) {
+        // a. Append the Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: "hour" } to result.
+        result.unchecked_append({ .type = part.type, .value = move(part.value), .unit = "hour"sv });
+    }
+
+    // 12. Return result.
+    return result;
+}
+
+// 1.1.10 FormatNumericMinutes ( durationFormat, minutesValue, hoursDisplayed, signDisplayed ), https://tc39.es/proposal-intl-duration-format/#sec-formatnumericminutes
+Vector<DurationFormatPart> format_numeric_minutes(VM& vm, DurationFormat const& duration_format, double minutes_value, bool hours_displayed, bool sign_displayed)
 {
     auto& realm = *vm.current_realm();
 
     // 1. Let result be a new empty List.
-    Vector<PatternPartition> result;
+    Vector<DurationFormatPart> result;
 
-    // 2. Let done be false.
-    bool done = false;
+    // 2. If hoursDisplayed is true, then
+    if (hours_displayed) {
+        // a. Let separator be durationFormat.[[DigitalFormat]].[[HoursMinutesSeparator]].
+        auto separator = duration_format.hours_minutes_separator();
 
-    // 3. While done is false, repeat for each row in Table 1 in order, except the header row:
-    for (size_t i = 0; !done && i < duration_instances_components.size(); ++i) {
-        auto const& duration_instances_component = duration_instances_components[i];
+        // b. Append the Record { [[Type]]: "literal", [[Value]]: separator, [[Unit]]: empty } to result.
+        result.append({ .type = "literal"sv, .value = move(separator), .unit = {} });
+    }
 
-        // a. Let styleSlot be the Style Slot value.
-        auto style_slot = duration_instances_component.get_style_slot;
+    // 3. Let minutesStyle be durationFormat.[[MinutesStyle]].
+    auto minutes_style = duration_format.minutes_style();
 
-        // b. Let displaySlot be the Display Slot value.
-        auto display_slot = duration_instances_component.get_display_slot;
+    // 4. Assert: minutesStyle is "numeric" or minutesStyle is "2-digit".
+    VERIFY(minutes_style == DurationFormat::ValueStyle::Numeric || minutes_style == DurationFormat::ValueStyle::TwoDigit);
 
-        // c. Let valueSlot be the Value Slot value.
-        auto value_slot = duration_instances_component.value_slot;
+    // 5. Let nfOpts be OrdinaryObjectCreate(null).
+    auto number_format_options = Object::create(realm, nullptr);
 
-        // d. Let unit be the Unit value.
-        auto unit = duration_instances_component.unit;
+    // 6. Let numberingSystem be durationFormat.[[NumberingSystem]].
+    auto const& numbering_system = duration_format.numbering_system();
 
-        // e. Let numberFormatUnit be the NumberFormat Unit value.
-        auto number_format_unit = duration_instances_component.number_format_unit;
+    // 7. Perform ! CreateDataPropertyOrThrow(nfOpts, "numberingSystem", numberingSystem).
+    MUST(number_format_options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, numbering_system)));
 
-        // f. Let style be durationFormat.[[<styleSlot>]].
-        auto style = (duration_format.*style_slot)();
+    // 8. If minutesStyle is "2-digit", then
+    if (minutes_style == DurationFormat::ValueStyle::TwoDigit) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumIntegerDigits", 2𝔽).
+        MUST(number_format_options->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value { 2 }));
+    }
 
-        // g. Let display be durationFormat.[[<displaySlot>]].
-        auto display = (duration_format.*display_slot)();
+    // 9. If signDisplayed is false, then
+    if (!sign_displayed) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "signDisplay", "never").
+        MUST(number_format_options->create_data_property_or_throw(vm.names.signDisplay, PrimitiveString::create(vm, "never"sv)));
+    }
 
-        // h. Let value be duration.[[<valueSlot>]].
-        auto value = duration.*value_slot;
+    // 10. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
+    auto number_format = construct_number_format(vm, duration_format, number_format_options);
 
-        // i. Let nfOpts be OrdinaryObjectCreate(null).
-        auto number_format_options = Object::create(realm, nullptr);
+    // 11. Let minutesParts be ! PartitionNumberPattern(nf, minutesValue).
+    auto minutes_parts = partition_number_pattern(number_format, MathematicalValue { minutes_value });
 
-        // j. If unit is "seconds", "milliseconds", or "microseconds", then
-        if (unit.is_one_of("seconds"sv, "milliseconds"sv, "microseconds"sv)) {
-            DurationFormat::ValueStyle next_style;
+    // 12. For each Record { [[Type]], [[Value]] } part of minutesParts, do
+    result.ensure_capacity(result.size() + minutes_parts.size());
 
-            // i. If unit is "seconds", then
-            if (unit == "seconds"sv) {
-                // 1. Let nextStyle be durationFormat.[[MillisecondsStyle]].
-                next_style = duration_format.milliseconds_style();
-            }
-            // ii. Else if unit is "milliseconds", then
-            else if (unit == "milliseconds"sv) {
-                // 1. Let nextStyle be durationFormat.[[MicrosecondsStyle]].
-                next_style = duration_format.microseconds_style();
-            }
-            // iii. Else,
-            else {
-                // 1. Let nextStyle be durationFormat.[[NanosecondsStyle]].
-                next_style = duration_format.nanoseconds_style();
-            }
+    for (auto& part : minutes_parts) {
+        // a. Append the Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: "minute" } to result.
+        result.unchecked_append({ .type = part.type, .value = move(part.value), .unit = "minute"sv });
+    }
 
-            // iv. If nextStyle is "numeric", then
-            if (next_style == DurationFormat::ValueStyle::Numeric) {
-                // 1. If unit is "seconds", then
-                if (unit == "seconds"sv) {
-                    // a. Set value to value + duration.[[Milliseconds]] / 10^3 + duration.[[Microseconds]] / 10^6 + duration.[[Nanoseconds]] / 10^9.
-                    value += duration.milliseconds / 1'000.0 + duration.microseconds / 1'000'000.0 + duration.nanoseconds / 1'000'000'000.0;
-                }
-                // 2. Else if unit is "milliseconds", then
-                else if (unit == "milliseconds"sv) {
-                    // a. Set value to value + duration.[[Microseconds]] / 10^3 + duration.[[Nanoseconds]] / 10^6.
-                    value += duration.microseconds / 1'000.0 + duration.nanoseconds / 1'000'000.0;
-                }
-                // 3. Else,
-                else {
-                    // a. Set value to value + duration.[[Nanoseconds]] / 10^3.
-                    value += duration.nanoseconds / 1'000.0;
-                }
+    // 13. Return result.
+    return result;
+}
 
-                // 4. Perform ! CreateDataPropertyOrThrow(nfOpts, "maximumFractionDigits", durationFormat.[[FractionalDigits]]).
-                MUST(number_format_options->create_data_property_or_throw(vm.names.maximumFractionDigits, duration_format.has_fractional_digits() ? Value(duration_format.fractional_digits()) : js_undefined()));
+// 1.1.11 FormatNumericSeconds ( durationFormat, secondsValue, minutesDisplayed, signDisplayed ), https://tc39.es/proposal-intl-duration-format/#sec-formatnumericseconds
+Vector<DurationFormatPart> format_numeric_seconds(VM& vm, DurationFormat const& duration_format, double seconds_value, bool minutes_displayed, bool sign_displayed)
+{
+    auto& realm = *vm.current_realm();
 
-                // 5. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumFractionDigits", durationFormat.[[FractionalDigits]]).
-                MUST(number_format_options->create_data_property_or_throw(vm.names.minimumFractionDigits, duration_format.has_fractional_digits() ? Value(duration_format.fractional_digits()) : js_undefined()));
+    // 1. Let secondsStyle be durationFormat.[[SecondsStyle]].
+    auto seconds_style = duration_format.seconds_style();
 
-                // 6. Set done to true.
-                done = true;
-            }
-        }
+    // 2. Assert: secondsStyle is "numeric" or secondsStyle is "2-digit".
+    VERIFY(seconds_style == DurationFormat::ValueStyle::Numeric || seconds_style == DurationFormat::ValueStyle::TwoDigit);
 
-        // k. If style is "2-digit", then
-        if (style == DurationFormat::ValueStyle::TwoDigit) {
-            // i. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumIntegerDigits", 2𝔽).
-            MUST(number_format_options->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(2)));
-        }
+    // 3. Let result be a new empty List.
+    Vector<DurationFormatPart> result;
+
+    // 4. If minutesDisplayed is true, then
+    if (minutes_displayed) {
+        // a. Let separator be durationFormat.[[DigitalFormat]].[[MinutesSecondsSeparator]].
+        auto separator = duration_format.minutes_seconds_separator();
 
-        // l. If value is not 0 or display is not "auto", then
-        if (value != 0.0 || display != DurationFormat::Display::Auto) {
-            // i. If style is "2-digit" or "numeric", then
-            if (style == DurationFormat::ValueStyle::TwoDigit || style == DurationFormat::ValueStyle::Numeric) {
-                // 1. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
-                auto* number_format = static_cast<NumberFormat*>(MUST(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), number_format_options)).ptr());
+        // b. Append the Record { [[Type]]: "literal", [[Value]]: separator, [[Unit]]: empty } to result.
+        result.append({ .type = "literal"sv, .value = move(separator), .unit = {} });
+    }
 
-                // 2. Let dataLocale be durationFormat.[[DataLocale]].
-                auto const& data_locale = duration_format.data_locale();
+    // 5. Let nfOpts be OrdinaryObjectCreate(null).
+    auto number_format_options = Object::create(realm, nullptr);
 
-                // 3. Let dataLocaleData be %DurationFormat%.[[LocaleData]].[[<dataLocale>]].
+    // 6. Let numberingSystem be durationFormat.[[NumberingSystem]].
+    auto const& numbering_system = duration_format.numbering_system();
 
-                // 4. Let num be ! FormatNumeric(nf, 𝔽(value)).
-                auto number = format_numeric(*number_format, MathematicalValue(value));
+    // 7. Perform ! CreateDataPropertyOrThrow(nfOpts, "numberingSystem", numberingSystem).
+    MUST(number_format_options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, numbering_system)));
 
-                // 5. Append the new Record { [[Type]]: unit, [[Value]]: num} to the end of result.
-                result.append({ unit, move(number) });
+    // 8. If secondsStyle is "2-digit", then
+    if (seconds_style == DurationFormat::ValueStyle::TwoDigit) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumIntegerDigits", 2𝔽).
+        MUST(number_format_options->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value { 2 }));
+    }
 
-                // 6. If unit is "hours" or "minutes", then
-                if (unit.is_one_of("hours"sv, "minutes"sv)) {
-                    double next_value = 0.0;
-                    DurationFormat::Display next_display;
+    // 9. If signDisplayed is false, then
+    if (!sign_displayed) {
+        // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "signDisplay", "never").
+        MUST(number_format_options->create_data_property_or_throw(vm.names.signDisplay, PrimitiveString::create(vm, "never"sv)));
+    }
 
-                    // a. If unit is "hours", then
-                    if (unit == "hours"sv) {
-                        // i. Let nextValue be duration.[[Minutes]].
-                        next_value = duration.minutes;
+    u8 maximum_fraction_digits = 0;
+    u8 minimum_fraction_digits = 0;
 
-                        // ii. Let nextDisplay be durationFormat.[[MinutesDisplay]].
-                        next_display = duration_format.minutes_display();
-                    }
-                    // b. Else,
-                    else {
-                        // i. Let nextValue be duration.[[Seconds]].
-                        next_value = duration.seconds;
+    // 11. If durationFormat.[[FractionalDigits]] is undefined, then
+    if (!duration_format.has_fractional_digits()) {
+        // a. Let maximumFractionDigits be 9𝔽.
+        maximum_fraction_digits = 9;
 
-                        // ii. Let nextDisplay be durationFormat.[[SecondsDisplay]].
-                        next_display = duration_format.seconds_display();
+        // b. Let minimumFractionDigits be +0𝔽.
+        minimum_fraction_digits = 0;
+    }
+    // 12. Else,
+    else {
+        // a. Let maximumFractionDigits be durationFormat.[[FractionalDigits]].
+        maximum_fraction_digits = duration_format.fractional_digits();
 
-                        // iii. If durationFormat.[[MillisecondsStyle]] is "numeric", then
-                        if (duration_format.milliseconds_style() == DurationFormat::ValueStyle::Numeric) {
-                            // i. Set nextValue to nextValue + duration.[[Milliseconds]] / 10^3 + duration.[[Microseconds]] / 10^6 + duration.[[Nanoseconds]] / 10^9.
-                            next_value += duration.milliseconds / 1'000.0 + duration.microseconds / 1'000'000.0 + duration.nanoseconds / 1'000'000'000.0;
-                        }
-                    }
+        // b. Let minimumFractionDigits be durationFormat.[[FractionalDigits]].
+        minimum_fraction_digits = duration_format.fractional_digits();
+    }
 
-                    // c. If nextValue is not 0 or nextDisplay is not "auto", then
-                    if (next_value != 0.0 || next_display != DurationFormat::Display::Auto) {
-                        // i. Let separator be dataLocaleData.[[formats]].[[digital]].[[separator]].
-                        auto separator = ::Locale::get_number_system_symbol(data_locale, duration_format.numbering_system(), ::Locale::NumericSymbol::TimeSeparator).value_or(":"sv);
+    // 13. Perform ! CreateDataPropertyOrThrow(nfOpts, "maximumFractionDigits", maximumFractionDigits).
+    MUST(number_format_options->create_data_property_or_throw(vm.names.maximumFractionDigits, Value { maximum_fraction_digits }));
 
-                        // ii. Append the new Record { [[Type]]: "literal", [[Value]]: separator} to the end of result.
-                        result.append({ "literal"sv, MUST(String::from_utf8(separator)) });
-                    }
-                }
-            }
-            // ii. Else,
-            else {
-                // 1. Perform ! CreateDataPropertyOrThrow(nfOpts, "style", "unit").
-                MUST(number_format_options->create_data_property_or_throw(vm.names.style, PrimitiveString::create(vm, "unit"_string)));
+    // 14. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumFractionDigits", minimumFractionDigits).
+    MUST(number_format_options->create_data_property_or_throw(vm.names.minimumFractionDigits, Value { minimum_fraction_digits }));
 
-                // 2. Perform ! CreateDataPropertyOrThrow(nfOpts, "unit", numberFormatUnit).
-                MUST(number_format_options->create_data_property_or_throw(vm.names.unit, PrimitiveString::create(vm, number_format_unit)));
+    // 15. Perform ! CreateDataPropertyOrThrow(nfOpts, "roundingMode", "trunc").
+    MUST(number_format_options->create_data_property_or_throw(vm.names.roundingMode, PrimitiveString::create(vm, "trunc"sv)));
 
-                // 3. Perform ! CreateDataPropertyOrThrow(nfOpts, "unitDisplay", style).
-                auto unicode_style = ::Locale::style_to_string(static_cast<::Locale::Style>(style));
-                MUST(number_format_options->create_data_property_or_throw(vm.names.unitDisplay, PrimitiveString::create(vm, unicode_style)));
+    // FIXME: We obviously have to create the NumberFormat object after its options are fully initialized.
+    //        https://github.com/tc39/proposal-intl-duration-format/pull/203
+    // 10. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
+    auto number_format = construct_number_format(vm, duration_format, number_format_options);
 
-                // 4. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
-                auto* number_format = static_cast<NumberFormat*>(MUST(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), number_format_options)).ptr());
+    // 16. Let secondsParts be ! PartitionNumberPattern(nf, secondsValue).
+    auto seconds_parts = partition_number_pattern(number_format, MathematicalValue { seconds_value });
 
-                // 5. Let parts be ! PartitionNumberPattern(nf, 𝔽(value)).
-                auto parts = partition_number_pattern(*number_format, MathematicalValue(value));
+    // 17. For each Record { [[Type]], [[Value]] } part of secondsParts, do
+    result.ensure_capacity(result.size() + seconds_parts.size());
 
-                // 6. Let concat be an empty String.
-                StringBuilder concat;
+    for (auto& part : seconds_parts) {
+        // a. Append the Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: "second" } to result.
+        result.unchecked_append({ .type = part.type, .value = move(part.value), .unit = "second"sv });
+    }
 
-                // 7. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do
-                for (auto const& part : parts) {
-                    // a. Set concat to the string-concatenation of concat and part.[[Value]].
-                    concat.append(part.value);
-                }
+    // 18. Return result.
+    return result;
+}
 
-                // 8. Append the new Record { [[Type]]: unit, [[Value]]: concat } to the end of result.
-                result.append({ unit, MUST(concat.to_string()) });
-            }
+// 1.1.12 FormatNumericUnits ( durationFormat, duration, firstNumericUnit, signDisplayed )
+Vector<DurationFormatPart> format_numeric_units(VM& vm, DurationFormat const& duration_format, Temporal::DurationRecord const& duration, StringView first_numeric_unit, bool sign_displayed)
+{
+    // 1. Assert: firstNumericUnit is "hours", "minutes", or "seconds".
+    VERIFY(first_numeric_unit.is_one_of("hours"sv, "minutes"sv, "seconds"sv));
+
+    // 2. Let numericPartsList be a new empty List.
+    Vector<DurationFormatPart> numeric_parts_list;
+
+    // 3. Let hoursValue be duration.[[Hours]].
+    auto hours_value = duration.hours;
+
+    // 4. Let hoursDisplay be durationFormat.[[HoursDisplay]].
+    auto hours_display = duration_format.hours_display();
+
+    // 5. Let minutesValue be duration.[[Minutes]].
+    auto minutes_value = duration.minutes;
+
+    // 6. Let minutesDisplay be durationFormat.[[MinutesDisplay]].
+    auto minutes_display = duration_format.minutes_display();
+
+    // 7. Let secondsValue be duration.[[Seconds]].
+    auto seconds_value = duration.seconds;
+
+    // 8. If duration.[[Milliseconds]] is not 0 or duration.[[Microseconds]] is not 0 or duration.[[Nanoseconds]] is not 0, then
+    if (duration.milliseconds != 0 || duration.microseconds != 0 || duration.nanoseconds != 0) {
+        // a. Set secondsValue to secondsValue + AddFractionalDigits(durationFormat, duration).
+        seconds_value += add_fractional_digits(duration_format, duration);
+    }
+
+    // 9. Let secondsDisplay be durationFormat.[[SecondsDisplay]].
+    auto seconds_display = duration_format.seconds_display();
+
+    // 10. Let hoursFormatted be false.
+    auto hours_formatted = false;
+
+    // 11. If firstNumericUnit is "hours", then
+    if (first_numeric_unit == "hours"sv) {
+        // a. If hoursValue is not 0 or hoursDisplay is not "auto", then
+        if (hours_value != 0 || hours_display != DurationFormat::Display::Auto) {
+            // i. Set hoursFormatted to true.
+            hours_formatted = true;
         }
     }
 
-    // 4. Let lfOpts be OrdinaryObjectCreate(null).
+    // 12. If secondsValue is not 0 or secondsDisplay is not "auto", then
+    //     a. Let secondsFormatted be true.
+    // 13. Else,
+    //     a. Let secondsFormatted be false.
+    auto seconds_formatted = seconds_value != 0 || seconds_display != DurationFormat::Display::Auto;
+
+    // 14. Let minutesFormatted be false.
+    auto minutes_formatted = false;
+
+    // 15. If firstNumericUnit is "hours" or firstNumericUnit is "minutes", then
+    if (first_numeric_unit.is_one_of("hours"sv, "minutes"sv)) {
+        // a. If hoursFormatted is true and secondsFormatted is true, then
+        if (hours_formatted && seconds_formatted) {
+            // i. Set minutesFormatted to true.
+            minutes_formatted = true;
+        }
+        // b. Else if minutesValue is not 0 or minutesDisplay is not "auto", then
+        else if (minutes_value != 0 || minutes_display != DurationFormat::Display::Auto) {
+            // i. Set minutesFormatted to true.
+            minutes_formatted = true;
+        }
+    }
+
+    // 16. If hoursFormatted is true, then
+    if (hours_formatted) {
+        // a. Append FormatNumericHours(durationFormat, hoursValue, signDisplayed) to numericPartsList.
+        numeric_parts_list.extend(format_numeric_hours(vm, duration_format, hours_value, sign_displayed));
+
+        // b. Set signDisplayed to false.
+        sign_displayed = hours_value < 0;
+    }
+
+    // 17. If minutesFormatted is true, then
+    if (minutes_formatted) {
+        // a. Append FormatNumericMinutes(durationFormat, minutesValue, hoursFormatted, signDisplayed) to numericPartsList.
+        numeric_parts_list.extend(format_numeric_minutes(vm, duration_format, minutes_value, hours_formatted, sign_displayed));
+
+        // b. Set signDisplayed to false.
+        sign_displayed = minutes_value < 0;
+    }
+
+    // 18. If secondsFormatted is true, then
+    if (seconds_formatted) {
+        // a. Append FormatNumericSeconds(durationFormat, secondsValue, minutesFormatted, signDisplayed) to numericPartsList.
+        numeric_parts_list.extend(format_numeric_seconds(vm, duration_format, seconds_value, minutes_formatted, sign_displayed));
+
+        // b. Set signDisplayed to false.
+        sign_displayed = seconds_value < 0;
+    }
+
+    // 19. Return numericPartsList.
+    return numeric_parts_list;
+}
+
+// 1.1.13 ListFormatParts ( durationFormat, partitionedPartsList ), https://tc39.es/proposal-intl-duration-format/#sec-listformatparts
+Vector<DurationFormatPart> list_format_parts(VM& vm, DurationFormat const& duration_format, Vector<Vector<DurationFormatPart>>& partitioned_parts_list)
+{
+    auto& realm = *vm.current_realm();
+
+    // 1. Let lfOpts be OrdinaryObjectCreate(null).
     auto list_format_options = Object::create(realm, nullptr);
 
-    // 5. Perform ! CreateDataPropertyOrThrow(lfOpts, "type", "unit").
-    MUST(list_format_options->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, "unit"_string)));
+    // 2. Perform ! CreateDataPropertyOrThrow(lfOpts, "type", "unit").
+    MUST(list_format_options->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, "unit"sv)));
 
-    // 6. Let listStyle be durationFormat.[[Style]].
+    // 3. Let listStyle be durationFormat.[[Style]].
     auto list_style = duration_format.style();
 
-    // 7. If listStyle is "digital", then
+    // 4. If listStyle is "digital", then
     if (list_style == DurationFormat::Style::Digital) {
         // a. Set listStyle to "short".
         list_style = DurationFormat::Style::Short;
     }
 
-    auto unicode_list_style = ::Locale::style_to_string(static_cast<::Locale::Style>(list_style));
+    // 5. Perform ! CreateDataPropertyOrThrow(lfOpts, "style", listStyle).
+    auto locale_list_style = ::Locale::style_to_string(static_cast<::Locale::Style>(list_style));
+    MUST(list_format_options->create_data_property_or_throw(vm.names.style, PrimitiveString::create(vm, locale_list_style)));
 
-    // 8. Perform ! CreateDataPropertyOrThrow(lfOpts, "style", listStyle).
-    MUST(list_format_options->create_data_property_or_throw(vm.names.style, PrimitiveString::create(vm, unicode_list_style)));
+    // 6. Let lf be ! Construct(%ListFormat%, « durationFormat.[[Locale]], lfOpts »).
+    auto list_format = construct_list_format(vm, duration_format, list_format_options);
 
-    // 9. Let lf be ! Construct(%ListFormat%, « durationFormat.[[Locale]], lfOpts »).
-    auto* list_format = static_cast<ListFormat*>(MUST(construct(vm, realm.intrinsics().intl_list_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), list_format_options)).ptr());
+    // 7. Let strings be a new empty List.
+    Vector<String> strings;
+    strings.ensure_capacity(partitioned_parts_list.size());
 
-    // FIXME: CreatePartsFromList expects a list of strings and creates a list of Pattern Partition records, but we already created a list of Pattern Partition records
-    //  so we try to hack something together from it that looks mostly right
-    Vector<String> string_result;
-    bool merge = false;
-    for (size_t i = 0; i < result.size(); ++i) {
-        auto const& part = result[i];
-        if (part.type == "literal") {
-            string_result.last() = MUST(String::formatted("{}{}", string_result.last(), part.value));
-            merge = true;
-            continue;
+    // 8. For each element parts of partitionedPartsList, do
+    for (auto const& parts : partitioned_parts_list) {
+        // a. Let string be the empty String.
+        StringBuilder string;
+
+        // b. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do
+        for (auto const& part : parts) {
+            // i. Set string to the string-concatenation of string and part.[[Value]].
+            string.append(part.value);
         }
-        if (merge) {
-            string_result.last() = MUST(String::formatted("{}{}", string_result.last(), part.value));
-            merge = false;
-            continue;
+
+        // c. Append string to strings.
+        strings.unchecked_append(MUST(string.to_string()));
+    }
+
+    // 9. Let formattedPartsList be CreatePartsFromList(lf, strings).
+    auto formatted_parts_list = create_parts_from_list(list_format, strings);
+
+    // 10. Let partitionedPartsIndex be 0.
+    size_t partitioned_parts_index = 0;
+
+    // 11. Let partitionedLength be the number of elements in partitionedPartsList.
+    auto partitioned_length = partitioned_parts_list.size();
+
+    // 12. Let flattenedPartsList be a new empty List.
+    Vector<DurationFormatPart> flattened_parts_list;
+
+    // 13. For each Record { [[Type]], [[Value]] } listPart in formattedPartsList, do
+    for (auto& list_part : formatted_parts_list) {
+        // a. If listPart.[[Type]] is "element", then
+        if (list_part.type == "element"sv) {
+            // i. Assert: partitionedPartsIndex < partitionedLength.
+            VERIFY(partitioned_parts_index < partitioned_length);
+
+            // ii. Let parts be partitionedPartsList[partitionedPartsIndex].
+            auto& parts = partitioned_parts_list[partitioned_parts_index];
+
+            // iii. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do
+            for (auto& part : parts) {
+                // 1. Append part to flattenedPartsList.
+                flattened_parts_list.append(move(part));
+            }
+
+            // iv. Set partitionedPartsIndex to partitionedPartsIndex + 1.
+            ++partitioned_parts_index;
+        }
+        // b. Else,
+        else {
+            // i. Assert: listPart.[[Type]] is "literal".
+            VERIFY(list_part.type == "literal"sv);
+
+            // ii. Append the Record { [[Type]]: "literal", [[Value]]: listPart.[[Value]], [[Unit]]: empty } to flattenedPartsList.
+            flattened_parts_list.append({ .type = "literal"sv, .value = move(list_part.value), .unit = {} });
         }
-        string_result.append(part.value);
     }
 
-    // 10. Set result to ! CreatePartsFromList(lf, result).
-    auto final_result = create_parts_from_list(*list_format, string_result);
+    // 14. Return flattenedPartsList.
+    return flattened_parts_list;
+}
+
+// 1.1.7 PartitionDurationFormatPattern ( durationFormat, duration ), https://tc39.es/proposal-intl-duration-format/#sec-partitiondurationformatpattern
+Vector<DurationFormatPart> partition_duration_format_pattern(VM& vm, DurationFormat const& duration_format, Temporal::DurationRecord const& duration)
+{
+    auto& realm = *vm.current_realm();
+
+    // 1. Let result be a new empty List.
+    Vector<Vector<DurationFormatPart>> result;
+
+    // 2. Let signDisplayed be true.
+    auto sign_displayed = true;
+
+    // 3. Let numericUnitFound be false.
+    auto numeric_unit_found = false;
+
+    // 4. While numericUnitFound is false, repeat for each row in Table 2 in table order, except the header row:
+    for (size_t i = 0; !numeric_unit_found && i < duration_instances_components.size(); ++i) {
+        auto const& duration_instances_component = duration_instances_components[i];
+
+        // a. Let value be the value of duration's field whose name is the Value Field value of the current row.
+        auto value = duration.*duration_instances_component.value_slot;
+
+        // b. Let style be the value of durationFormat's internal slot whose name is the Style Slot value of the current row.
+        auto style = (duration_format.*duration_instances_component.get_style_slot)();
+
+        // c. Let display be the value of durationFormat's internal slot whose name is the Display Slot value of the current row.
+        auto display = (duration_format.*duration_instances_component.get_display_slot)();
+
+        // d. Let unit be the Unit value of the current row.
+        auto unit = duration_instances_component.unit;
+
+        // e. If style is "numeric" or "2-digit", then
+        if (style == DurationFormat::ValueStyle::Numeric || style == DurationFormat::ValueStyle::TwoDigit) {
+            // i. Append FormatNumericUnits(durationFormat, duration, unit, signDisplayed) to result.
+            result.append(format_numeric_units(vm, duration_format, duration, unit, sign_displayed));
+
+            // ii. Set numericUnitFound to true.
+            numeric_unit_found = true;
+        }
+        // f. Else,
+        else {
+            // i. Let nfOpts be OrdinaryObjectCreate(null).
+            auto number_format_options = Object::create(realm, nullptr);
+
+            // ii. If unit is "seconds", "milliseconds", or "microseconds", then
+            if (unit.is_one_of("seconds"sv, "milliseconds"sv, "microseconds"sv)) {
+                // 1. If NextUnitFractional(durationFormat, unit) is true, then
+                if (next_unit_fractional(duration_format, unit)) {
+
+                    // a. Set value to value + AddFractionalDigits(durationFormat, duration).
+                    value += add_fractional_digits(duration_format, duration);
+
+                    u8 maximum_fraction_digits = 0;
+                    u8 minimum_fraction_digits = 0;
+
+                    // b. If durationFormat.[[FractionalDigits]] is undefined, then
+                    if (!duration_format.has_fractional_digits()) {
+                        // a. Let maximumFractionDigits be 9𝔽.
+                        maximum_fraction_digits = 9;
+
+                        // b. Let minimumFractionDigits be +0𝔽.
+                        minimum_fraction_digits = 0;
+                    }
+                    // c. Else,
+                    else {
+                        // a. Let maximumFractionDigits be durationFormat.[[FractionalDigits]].
+                        maximum_fraction_digits = duration_format.fractional_digits();
+
+                        // b. Let minimumFractionDigits be durationFormat.[[FractionalDigits]].
+                        minimum_fraction_digits = duration_format.fractional_digits();
+                    }
+
+                    // d. Perform ! CreateDataPropertyOrThrow(nfOpts, "maximumFractionDigits", maximumFractionDigits).
+                    MUST(number_format_options->create_data_property_or_throw(vm.names.maximumFractionDigits, Value { maximum_fraction_digits }));
+
+                    // e. Perform ! CreateDataPropertyOrThrow(nfOpts, "minimumFractionDigits", minimumFractionDigits).
+                    MUST(number_format_options->create_data_property_or_throw(vm.names.minimumFractionDigits, Value { minimum_fraction_digits }));
+
+                    // f. Perform ! CreateDataPropertyOrThrow(nfOpts, "roundingMode", "trunc").
+                    MUST(number_format_options->create_data_property_or_throw(vm.names.roundingMode, PrimitiveString::create(vm, "trunc"sv)));
+
+                    // g. Set numericUnitFound to true.
+                    numeric_unit_found = true;
+                }
+            }
+
+            // iii. If value is not 0 or display is not "auto", then
+            if (value != 0 || display != DurationFormat::Display::Auto) {
+                // 1. Let numberingSystem be durationFormat.[[NumberingSystem]].
+                auto const& numbering_system = duration_format.numbering_system();
+
+                // 2. Perform ! CreateDataPropertyOrThrow(nfOpts, "numberingSystem", numberingSystem).
+                MUST(number_format_options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, numbering_system)));
+
+                // 3. If signDisplayed is true, then
+                if (sign_displayed) {
+                    // a. Set signDisplayed to false.
+                    sign_displayed = false;
+
+                    // b. If value is 0 and DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]) is -1, then
+                    if (value == 0 && Temporal::duration_sign(duration.years, duration.months, duration.weeks, duration.days, duration.hours, duration.minutes, duration.seconds, duration.milliseconds, duration.microseconds, duration.nanoseconds) == -1) {
+                        // i. Set value to negative-zero.
+                        value = -0.0;
+                    }
+                }
+                // 4. Else,
+                else {
+                    // a. Perform ! CreateDataPropertyOrThrow(nfOpts, "signDisplay", "never").
+                    MUST(number_format_options->create_data_property_or_throw(vm.names.signDisplay, PrimitiveString::create(vm, "never"sv)));
+                }
+
+                // 5. Let numberFormatUnit be the NumberFormat Unit value of the current row.
+                auto number_format_unit = duration_instances_component.number_format_unit;
+
+                // 6. Perform ! CreateDataPropertyOrThrow(nfOpts, "style", "unit").
+                MUST(number_format_options->create_data_property_or_throw(vm.names.style, PrimitiveString::create(vm, "unit"sv)));
+
+                // 7. Perform ! CreateDataPropertyOrThrow(nfOpts, "unit", numberFormatUnit).
+                MUST(number_format_options->create_data_property_or_throw(vm.names.unit, PrimitiveString::create(vm, number_format_unit)));
+
+                // 8. Perform ! CreateDataPropertyOrThrow(nfOpts, "unitDisplay", style).
+                auto locale_style = ::Locale::style_to_string(static_cast<::Locale::Style>(style));
+                MUST(number_format_options->create_data_property_or_throw(vm.names.unitDisplay, PrimitiveString::create(vm, locale_style)));
+
+                // 9. Let nf be ! Construct(%NumberFormat%, « durationFormat.[[Locale]], nfOpts »).
+                auto number_format = construct_number_format(vm, duration_format, number_format_options);
+
+                // 10. Let parts be ! PartitionNumberPattern(nf, value).
+                auto parts = partition_number_pattern(number_format, MathematicalValue { value });
+
+                // 11. Let list be a new empty List.
+                Vector<DurationFormatPart> list;
+
+                // 12. For each Record { [[Type]], [[Value]] } part of parts, do
+                list.ensure_capacity(parts.size());
+
+                for (auto& part : parts) {
+                    // a. Append the Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: numberFormatUnit } to list.
+                    list.unchecked_append({ .type = part.type, .value = move(part.value), .unit = number_format_unit });
+                }
+
+                // 13. Append list to result.
+                result.append(list);
+            }
+        }
+    }
 
-    // 11. Return result.
-    return final_result;
+    // 5. Return ListFormatParts(durationFormat, result).
+    return list_format_parts(vm, duration_format, result);
 }
 
 }

+ 33 - 9
Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.h

@@ -12,7 +12,6 @@
 #include <LibJS/Runtime/Intl/AbstractOperations.h>
 #include <LibJS/Runtime/Object.h>
 #include <LibJS/Runtime/Temporal/Duration.h>
-#include <LibLocale/ListFormat.h>
 
 namespace JS::Intl {
 
@@ -33,7 +32,8 @@ public:
         Short,
         Narrow,
         Numeric,
-        TwoDigit
+        TwoDigit,
+        Fractional,
     };
     static_assert(to_underlying(ValueStyle::Long) == to_underlying(::Locale::Style::Long));
     static_assert(to_underlying(ValueStyle::Short) == to_underlying(::Locale::Style::Short));
@@ -62,6 +62,15 @@ public:
     void set_numbering_system(String numbering_system) { m_numbering_system = move(numbering_system); }
     String const& numbering_system() const { return m_numbering_system; }
 
+    void set_hours_minutes_separator(String hours_minutes_separator) { m_hours_minutes_separator = move(hours_minutes_separator); }
+    String const& hours_minutes_separator() const { return m_hours_minutes_separator; }
+
+    void set_minutes_seconds_separator(String minutes_seconds_separator) { m_minutes_seconds_separator = move(minutes_seconds_separator); }
+    String const& minutes_seconds_separator() const { return m_minutes_seconds_separator; }
+
+    void set_two_digit_hours(bool two_digit_hours) { m_two_digit_hours = two_digit_hours; }
+    bool two_digit_hours() const { return m_two_digit_hours; }
+
     void set_style(StringView style) { m_style = style_from_string(style); }
     Style style() const { return m_style; }
     StringView style_string() const { return style_to_string(m_style); }
@@ -162,9 +171,13 @@ private:
     static Display display_from_string(StringView display);
     static StringView display_to_string(Display);
 
-    String m_locale;                                      // [[Locale]]
-    String m_data_locale;                                 // [[DataLocale]]
-    String m_numbering_system;                            // [[NumberingSystem]]
+    String m_locale;                    // [[Locale]]
+    String m_data_locale;               // [[DataLocale]]
+    String m_numbering_system;          // [[NumberingSystem]]
+    String m_hours_minutes_separator;   // [[HourMinutesSeparator]]
+    String m_minutes_seconds_separator; // [[MinutesSecondsSeparator]]
+    bool m_two_digit_hours { false };   // [[TwoDigitHours]]
+
     Style m_style { Style::Long };                        // [[Style]]
     ValueStyle m_years_style { ValueStyle::Long };        // [[YearsStyle]]
     Display m_years_display { Display::Auto };            // [[YearsDisplay]]
@@ -223,10 +236,21 @@ struct DurationUnitOptions {
     String display;
 };
 
+struct DurationFormatPart {
+    StringView type;
+    String value;
+    StringView unit;
+};
+
 ThrowCompletionOr<Temporal::DurationRecord> to_duration_record(VM&, Value input);
-i8 duration_record_sign(Temporal::DurationRecord const&);
-bool is_valid_duration_record(Temporal::DurationRecord const&);
-ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(VM&, String const& unit, Object const& options, StringView base_style, ReadonlySpan<StringView> styles_list, StringView digital_base, StringView previous_style);
-Vector<::Locale::ListFormatPart> partition_duration_format_pattern(VM&, DurationFormat const&, Temporal::DurationRecord const& duration);
+ThrowCompletionOr<DurationUnitOptions> get_duration_unit_options(VM&, String const& unit, Object const& options, StringView base_style, ReadonlySpan<StringView> styles_list, StringView digital_base, StringView previous_style, bool two_digit_hours);
+double add_fractional_digits(DurationFormat const&, Temporal::DurationRecord const&);
+bool next_unit_fractional(DurationFormat const&, StringView unit);
+Vector<DurationFormatPart> format_numeric_hours(VM&, DurationFormat const&, double hours_value, bool sign_displayed);
+Vector<DurationFormatPart> format_numeric_minutes(VM&, DurationFormat const&, double minutes_value, bool hours_displayed, bool sign_displayed);
+Vector<DurationFormatPart> format_numeric_seconds(VM&, DurationFormat const&, double seconds_value, bool minutes_displayed, bool sign_displayed);
+Vector<DurationFormatPart> format_numeric_units(VM&, DurationFormat const&, Temporal::DurationRecord const&, StringView first_numeric_unit, bool sign_displayed);
+Vector<DurationFormatPart> list_format_parts(VM&, DurationFormat const&, Vector<Vector<DurationFormatPart>>& partitioned_parts_list);
+Vector<DurationFormatPart> partition_duration_format_pattern(VM&, DurationFormat const&, Temporal::DurationRecord const&);
 
 }

+ 40 - 21
Userland/Libraries/LibJS/Runtime/Intl/DurationFormatConstructor.cpp

@@ -1,6 +1,6 @@
 /*
  * Copyright (c) 2022, Idan Horowitz <idan.horowitz@serenityos.org>
- * Copyright (c) 2022-2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -11,6 +11,7 @@
 #include <LibJS/Runtime/Intl/AbstractOperations.h>
 #include <LibJS/Runtime/Intl/DurationFormat.h>
 #include <LibJS/Runtime/Intl/DurationFormatConstructor.h>
+#include <LibLocale/DurationFormat.h>
 
 namespace JS::Intl {
 
@@ -51,7 +52,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
     auto locales = vm.argument(0);
     auto options_value = vm.argument(1);
 
-    // 2. Let durationFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%DurationFormatPrototype%", « [[InitializedDurationFormat]], [[Locale]], [[DataLocale]], [[NumberingSystem]], [[Style]], [[YearsStyle]], [[YearsDisplay]], [[MonthsStyle]], [[MonthsDisplay]] , [[WeeksStyle]], [[WeeksDisplay]] , [[DaysStyle]], [[DaysDisplay]] , [[HoursStyle]], [[HoursDisplay]] , [[MinutesStyle]], [[MinutesDisplay]] , [[SecondsStyle]], [[SecondsDisplay]] , [[MillisecondsStyle]], [[MillisecondsDisplay]] , [[MicrosecondsStyle]], [[MicrosecondsDisplay]] , [[NanosecondsStyle]], [[NanosecondsDisplay]], [[FractionalDigits]] »).
+    // 2. Let durationFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%DurationFormatPrototype%", « [[InitializedDurationFormat]], [[Locale]], [[DataLocale]], [[NumberingSystem]], [[Style]], [[YearsStyle]], [[YearsDisplay]], [[MonthsStyle]], [[MonthsDisplay]], [[WeeksStyle]], [[WeeksDisplay]], [[DaysStyle]], [[DaysDisplay]], [[HoursStyle]], [[HoursDisplay]], [[MinutesStyle]], [[MinutesDisplay]], [[SecondsStyle]], [[SecondsDisplay]], [[MillisecondsStyle]], [[MillisecondsDisplay]], [[MicrosecondsStyle]], [[MicrosecondsDisplay]], [[NanosecondsStyle]], [[NanosecondsDisplay]], [[HoursMinutesSeparator]], [[MinutesSecondsSeparator]], [[FractionalDigits]], [[TwoDigitHours]] »).
     auto duration_format = TRY(ordinary_create_from_constructor<DurationFormat>(vm, new_target, &Intrinsics::intl_duration_format_prototype));
 
     // 3. Let requestedLocales be ? CanonicalizeLocaleList(locales).
@@ -63,7 +64,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
     // 5. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
     auto matcher = TRY(get_option(vm, *options, vm.names.localeMatcher, OptionType::String, { "lookup"sv, "best fit"sv }, "best fit"sv));
 
-    // 6. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
+    // 6. Let numberingSystem be ? GetOption(options, "numberingSystem", string, undefined, undefined).
     auto numbering_system = TRY(get_option(vm, *options, vm.names.numberingSystem, OptionType::String, {}, Empty {}));
 
     // 7. If numberingSystem is not undefined, then
@@ -76,7 +77,8 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
     // 8. Let opt be the Record { [[localeMatcher]]: matcher, [[nu]]: numberingSystem }.
     LocaleOptions opt {};
     opt.locale_matcher = matcher;
-    opt.nu = numbering_system.is_undefined() ? Optional<String>() : numbering_system.as_string().utf8_string();
+    if (!numbering_system.is_undefined())
+        opt.nu = numbering_system.as_string().utf8_string();
 
     // 9. Let r be ResolveLocale(%DurationFormat%.[[AvailableLocales]], requestedLocales, opt, %DurationFormat%.[[RelevantExtensionKeys]], %DurationFormat%.[[LocaleData]]).
     auto result = resolve_locale(requested_locales, opt, DurationFormat::relevant_extension_keys());
@@ -87,23 +89,40 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
     // 11. Set durationFormat.[[Locale]] to locale.
     duration_format->set_locale(move(locale));
 
-    // 12. Set durationFormat.[[NumberingSystem]] to r.[[nu]].
+    // 12. Set durationFormat.[[DataLocale]] to r.[[dataLocale]].
+    duration_format->set_data_locale(move(result.data_locale));
+
+    // 13. Let dataLocale be durationFormat.[[DataLocale]].
+    // 14. Let dataLocaleData be durationFormat.[[LocaleData]].[[<dataLocale>]].
+    // 15. Let digitalFormat be dataLocaleData.[[DigitalFormat]].
+    auto digital_format = ::Locale::digital_format(duration_format->data_locale());
+
+    // 16. Let twoDigitHours be digitalFormat.[[TwoDigitHours]].
+    // 17. Set durationFormat.[[TwoDigitHours]] to twoDigitHours.
+    duration_format->set_two_digit_hours(digital_format.uses_two_digit_hours);
+
+    // 18. Let hoursMinutesSeparator be digitalFormat.[[HoursMinutesSeparator]].
+    // 19. Set durationFormat.[[HoursMinutesSeparator]] to hoursMinutesSeparator.
+    duration_format->set_hours_minutes_separator(move(digital_format.hours_minutes_separator));
+
+    // 20. Let minutesSecondsSeparator be digitalFormat.[[MinutesSecondsSeparator]].
+    // 21. Set durationFormat.[[MinutesSecondsSeparator]] to minutesSecondsSeparator.
+    duration_format->set_minutes_seconds_separator(move(digital_format.minutes_seconds_separator));
+
+    // 22. Set durationFormat.[[NumberingSystem]] to r.[[nu]].
     if (result.nu.has_value())
         duration_format->set_numbering_system(result.nu.release_value());
 
-    // 13. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow", "digital" », "short").
+    // 23. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow", "digital" », "short").
     auto style = TRY(get_option(vm, *options, vm.names.style, OptionType::String, { "long"sv, "short"sv, "narrow"sv, "digital"sv }, "short"sv));
 
-    // 14. Set durationFormat.[[Style]] to style.
+    // 24. Set durationFormat.[[Style]] to style.
     duration_format->set_style(style.as_string().utf8_string_view());
 
-    // 15. Set durationFormat.[[DataLocale]] to r.[[dataLocale]].
-    duration_format->set_data_locale(move(result.data_locale));
-
-    // 16. Let prevStyle be the empty String.
+    // 25. Let prevStyle be the empty String.
     String previous_style {};
 
-    // 17. For each row of Table 1, except the header row, in table order, do
+    // 26. For each row of Table 3, except the header row, in table order, do
     for (auto const& duration_instances_component : duration_instances_components) {
         // a. Let styleSlot be the Style Slot value of the current row.
         auto style_slot = duration_instances_component.set_style_slot;
@@ -111,17 +130,17 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
         // b. Let displaySlot be the Display Slot value of the current row.
         auto display_slot = duration_instances_component.set_display_slot;
 
-        // c. Let unit be the Unit value.
+        // c. Let unit be the Unit value of the current row.
         auto unit = MUST(String::from_utf8(duration_instances_component.unit));
 
-        // d. Let valueList be the Values value.
+        // d. Let valueList be the Values value of the current row.
         auto value_list = duration_instances_component.values;
 
-        // e. Let digitalBase be the Digital Default value.
+        // e. Let digitalBase be the Digital Default value of the current row.
         auto digital_base = duration_instances_component.digital_default;
 
-        // f. Let unitOptions be ? GetDurationUnitOptions(unit, options, style, valueList, digitalBase, prevStyle).
-        auto unit_options = TRY(get_duration_unit_options(vm, unit, *options, style.as_string().utf8_string_view(), value_list, digital_base, previous_style));
+        // f. Let unitOptions be ? GetDurationUnitOptions(unit, options, style, valueList, digitalBase, prevStyle, twoDigitHours).
+        auto unit_options = TRY(get_duration_unit_options(vm, unit, *options, duration_format->style_string(), value_list, digital_base, previous_style, duration_format->two_digit_hours()));
 
         // g. Set the value of the styleSlot slot of durationFormat to unitOptions.[[Style]].
         (duration_format->*style_slot)(unit_options.style);
@@ -132,14 +151,14 @@ ThrowCompletionOr<NonnullGCPtr<Object>> DurationFormatConstructor::construct(Fun
         // i. If unit is one of "hours", "minutes", "seconds", "milliseconds", or "microseconds", then
         if (unit.is_one_of("hours"sv, "minutes"sv, "seconds"sv, "milliseconds"sv, "microseconds"sv)) {
             // i. Set prevStyle to unitOptions.[[Style]].
-            previous_style = unit_options.style;
+            previous_style = move(unit_options.style);
         }
     }
 
-    // 18. Set durationFormat.[[FractionalDigits]] to ? GetNumberOption(options, "fractionalDigits", 0, 9, 0).
-    duration_format->set_fractional_digits(Optional<u8>(TRY(get_number_option(vm, *options, vm.names.fractionalDigits, 0, 9, 0))));
+    // 27. Set durationFormat.[[FractionalDigits]] to ? GetNumberOption(options, "fractionalDigits", 0, 9, undefined).
+    duration_format->set_fractional_digits(Optional<u8>(TRY(get_number_option(vm, *options, vm.names.fractionalDigits, 0, 9, {}))));
 
-    // 19. Return durationFormat.
+    // 28. Return durationFormat.
     return duration_format;
 }
 

+ 58 - 36
Userland/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp

@@ -1,10 +1,11 @@
 /*
  * Copyright (c) 2022, Idan Horowitz <idan.horowitz@serenityos.org>
- * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <AK/Enumerate.h>
 #include <AK/StringBuilder.h>
 #include <LibJS/Runtime/Array.h>
 #include <LibJS/Runtime/GlobalObject.h>
@@ -81,9 +82,7 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
 
     // 6. Let n be 0.
     // 7. For each { [[Type]], [[Value]] } part in parts, do
-    for (size_t n = 0; n < parts.size(); ++n) {
-        auto const& part = parts[n];
-
+    for (auto [n, part] : enumerate(parts)) {
         // a. Let obj be OrdinaryObjectCreate(%ObjectPrototype%).
         auto object = Object::create(realm, realm.intrinsics().object_prototype());
 
@@ -91,12 +90,16 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format_to_parts)
         MUST(object->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, part.type)));
 
         // c. Perform ! CreateDataPropertyOrThrow(obj, "value", part.[[Value]]).
-        MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, part.value)));
+        MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, move(part.value))));
+
+        // d. If part.[[Unit]] is not empty, perform ! CreateDataPropertyOrThrow(obj, "unit", part.[[Unit]]).
+        if (!part.unit.is_empty())
+            MUST(object->create_data_property_or_throw(vm.names.unit, PrimitiveString::create(vm, part.unit)));
 
-        // d. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), obj).
+        // e. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), obj).
         MUST(result->create_data_property_or_throw(n, object));
 
-        // e. Increment n by 1.
+        // f. Increment n by 1.
     }
 
     // 8. Return result.
@@ -115,35 +118,54 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::resolved_options)
     // 3. Let options be OrdinaryObjectCreate(%Object.prototype%).
     auto options = Object::create(realm, realm.intrinsics().object_prototype());
 
-    // 4. For each row of Table 2, except the header row, in table order, do
-    //     a. Let p be the Property value of the current row.
-    //     b. Let v be the value of df's internal slot whose name is the Internal Slot value of the current row.
-    //     c. Assert: v is not undefined.
-    //     d. Perform ! CreateDataPropertyOrThrow(options, p, v).
-    MUST(options->create_data_property_or_throw(vm.names.locale, PrimitiveString::create(vm, duration_format->locale())));
-    MUST(options->create_data_property_or_throw(vm.names.style, PrimitiveString::create(vm, duration_format->style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.years, PrimitiveString::create(vm, duration_format->years_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.yearsDisplay, PrimitiveString::create(vm, duration_format->years_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.months, PrimitiveString::create(vm, duration_format->months_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.monthsDisplay, PrimitiveString::create(vm, duration_format->months_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.weeks, PrimitiveString::create(vm, duration_format->weeks_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.weeksDisplay, PrimitiveString::create(vm, duration_format->weeks_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.days, PrimitiveString::create(vm, duration_format->days_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.daysDisplay, PrimitiveString::create(vm, duration_format->days_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.hours, PrimitiveString::create(vm, duration_format->hours_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.hoursDisplay, PrimitiveString::create(vm, duration_format->hours_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.minutes, PrimitiveString::create(vm, duration_format->minutes_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.minutesDisplay, PrimitiveString::create(vm, duration_format->minutes_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.seconds, PrimitiveString::create(vm, duration_format->seconds_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.secondsDisplay, PrimitiveString::create(vm, duration_format->seconds_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.milliseconds, PrimitiveString::create(vm, duration_format->milliseconds_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.millisecondsDisplay, PrimitiveString::create(vm, duration_format->milliseconds_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.microseconds, PrimitiveString::create(vm, duration_format->microseconds_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.microsecondsDisplay, PrimitiveString::create(vm, duration_format->microseconds_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.nanoseconds, PrimitiveString::create(vm, duration_format->nanoseconds_style_string())));
-    MUST(options->create_data_property_or_throw(vm.names.nanosecondsDisplay, PrimitiveString::create(vm, duration_format->nanoseconds_display_string())));
-    MUST(options->create_data_property_or_throw(vm.names.fractionalDigits, duration_format->has_fractional_digits() ? Value(duration_format->fractional_digits()) : js_undefined()));
-    MUST(options->create_data_property_or_throw(vm.names.numberingSystem, PrimitiveString::create(vm, duration_format->numbering_system())));
+    // 4. For each row of Table 4, except the header row, in table order, do
+    auto create_option = [&](PropertyKey const& property, StringView value) {
+        // a. Let p be the Property value of the current row.
+        // b. Let v be the value of df's internal slot whose name is the Internal Slot value of the current row.
+
+        // c. If p is "fractionalDigits", then
+        //     i. If v is not undefined, set v to 𝔽(v).
+        // d. Else,
+        //     i. Assert: v is not undefined.
+
+        // e. If v is "fractional", then
+        if (value == "fractional"sv) {
+            // i. Assert: The Internal Slot value of the current row is [[MillisecondsStyle]], [[MicrosecondsStyle]], or [[NanosecondsStyle]].
+            // ii. Set v to "numeric".
+            value = "numeric"sv;
+        }
+
+        // f. If v is not undefined, then
+        //     i. Perform ! CreateDataPropertyOrThrow(options, p, v).
+        MUST(options->create_data_property_or_throw(property, PrimitiveString::create(vm, value)));
+    };
+
+    create_option(vm.names.locale, duration_format->locale());
+    create_option(vm.names.numberingSystem, duration_format->numbering_system());
+    create_option(vm.names.style, duration_format->style_string());
+    create_option(vm.names.years, duration_format->years_style_string());
+    create_option(vm.names.yearsDisplay, duration_format->years_display_string());
+    create_option(vm.names.months, duration_format->months_style_string());
+    create_option(vm.names.monthsDisplay, duration_format->months_display_string());
+    create_option(vm.names.weeks, duration_format->weeks_style_string());
+    create_option(vm.names.weeksDisplay, duration_format->weeks_display_string());
+    create_option(vm.names.days, duration_format->days_style_string());
+    create_option(vm.names.daysDisplay, duration_format->days_display_string());
+    create_option(vm.names.hours, duration_format->hours_style_string());
+    create_option(vm.names.hoursDisplay, duration_format->hours_display_string());
+    create_option(vm.names.minutes, duration_format->minutes_style_string());
+    create_option(vm.names.minutesDisplay, duration_format->minutes_display_string());
+    create_option(vm.names.seconds, duration_format->seconds_style_string());
+    create_option(vm.names.secondsDisplay, duration_format->seconds_display_string());
+    create_option(vm.names.milliseconds, duration_format->milliseconds_style_string());
+    create_option(vm.names.millisecondsDisplay, duration_format->milliseconds_display_string());
+    create_option(vm.names.microseconds, duration_format->microseconds_style_string());
+    create_option(vm.names.microsecondsDisplay, duration_format->microseconds_display_string());
+    create_option(vm.names.nanoseconds, duration_format->nanoseconds_style_string());
+    create_option(vm.names.nanosecondsDisplay, duration_format->nanoseconds_display_string());
+
+    if (duration_format->has_fractional_digits())
+        MUST(options->create_data_property_or_throw(vm.names.fractionalDigits, Value(duration_format->fractional_digits())));
 
     // 5. Return options.
     return options;

+ 24 - 2
Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.format.js

@@ -32,7 +32,7 @@ describe("correct behavior", () => {
             "1y 2m 3w 3d 4h 5m 6s 7ms 8μs 9ns"
         );
         expect(new Intl.DurationFormat("en", { style: "digital" }).format(duration)).toBe(
-            "1 yr, 2 mths, 3 wks, 3 days, 4:05:06"
+            "1 yr, 2 mths, 3 wks, 3 days, 4:05:06.007008009"
         );
         expect(
             new Intl.DurationFormat("en", {
@@ -52,7 +52,7 @@ describe("correct behavior", () => {
             "1 J, 2 M, 3 W, 3 T, 4 Std., 5 Min., 6 Sek., 7 ms, 8 μs und 9 ns"
         );
         expect(new Intl.DurationFormat("de", { style: "digital" }).format(duration)).toBe(
-            "1 J, 2 Mon., 3 Wo., 3 Tg. und 4:05:06"
+            "1 J, 2 Mon., 3 Wo., 3 Tg. und 4:05:06,007008009"
         );
         expect(
             new Intl.DurationFormat("de", {
@@ -88,6 +88,28 @@ describe("correct behavior", () => {
         expect(de.format(duration1)).toBe("1 J, 2 Mon., 3 Wo., 3 Tg. und 0:00:00");
         expect(de.format(duration2)).toBe("1 J, 2 Mon., 3 Wo., 3 Tg. und 4:05:06");
     });
+
+    test("locale with a time separator string other than ':'", () => {
+        const duration = {
+            hours: 1,
+            minutes: 2,
+            seconds: 3,
+        };
+
+        const da = new Intl.DurationFormat("da", { style: "digital" });
+        expect(da.format(duration)).toBe("1.02.03");
+    });
+
+    test("locale with preferred 2-digit hours", () => {
+        const duration = {
+            hours: 1,
+            minutes: 2,
+            seconds: 3,
+        };
+
+        const lt = new Intl.DurationFormat("lt", { style: "digital" });
+        expect(lt.format(duration)).toBe("01:02:03");
+    });
 });
 
 describe("errors", () => {

+ 317 - 108
Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.formatToParts.js

@@ -17,121 +17,225 @@ describe("correct behavior", () => {
             nanoseconds: 9,
         };
         expect(new Intl.DurationFormat().formatToParts(duration)).toEqual([
-            { type: "element", value: "1 yr" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "yr", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 mths" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "mths", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 wks" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "wks", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 days" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "days", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 hr" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "hr", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 min" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "min", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 sec" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "sec", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "9 ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("en").formatToParts(duration)).toEqual([
-            { type: "element", value: "1 yr" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "yr", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 mths" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "mths", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 wks" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "wks", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 days" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "days", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 hr" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "hr", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 min" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "min", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 sec" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "sec", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "9 ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("en", { style: "long" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1 year" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "year", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 months" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "months", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 weeks" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "weeks", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 days" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "days", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 hours" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "hours", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 minutes" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "minutes", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 seconds" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "seconds", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 milliseconds" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "milliseconds", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 microseconds" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "microseconds", unit: "microsecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "9 nanoseconds" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "nanoseconds", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("en", { style: "short" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1 yr" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "yr", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 mths" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "mths", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 wks" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "wks", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 days" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "days", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 hr" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "hr", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 min" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "min", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 sec" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "sec", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "9 ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("en", { style: "narrow" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1y" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "unit", value: "y", unit: "year" },
             { type: "literal", value: " " },
-            { type: "element", value: "2m" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "unit", value: "m", unit: "month" },
             { type: "literal", value: " " },
-            { type: "element", value: "3w" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "unit", value: "w", unit: "week" },
             { type: "literal", value: " " },
-            { type: "element", value: "3d" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "unit", value: "d", unit: "day" },
             { type: "literal", value: " " },
-            { type: "element", value: "4h" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "unit", value: "h", unit: "hour" },
             { type: "literal", value: " " },
-            { type: "element", value: "5m" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "unit", value: "m", unit: "minute" },
             { type: "literal", value: " " },
-            { type: "element", value: "6s" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "unit", value: "s", unit: "second" },
             { type: "literal", value: " " },
-            { type: "element", value: "7ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: " " },
-            { type: "element", value: "8μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: " " },
-            { type: "element", value: "9ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("en", { style: "digital" }).formatToParts(duration)).toEqual(
             [
-                { type: "element", value: "1 yr" },
+                { type: "integer", value: "1", unit: "year" },
+                { type: "literal", value: " ", unit: "year" },
+                { type: "unit", value: "yr", unit: "year" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "2 mths" },
+                { type: "integer", value: "2", unit: "month" },
+                { type: "literal", value: " ", unit: "month" },
+                { type: "unit", value: "mths", unit: "month" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "3 wks" },
+                { type: "integer", value: "3", unit: "week" },
+                { type: "literal", value: " ", unit: "week" },
+                { type: "unit", value: "wks", unit: "week" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "3 days" },
+                { type: "integer", value: "3", unit: "day" },
+                { type: "literal", value: " ", unit: "day" },
+                { type: "unit", value: "days", unit: "day" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "4:05:06" },
+                { type: "integer", value: "4", unit: "hour" },
+                { type: "literal", value: ":" },
+                { type: "integer", value: "05", unit: "minute" },
+                { type: "literal", value: ":" },
+                { type: "integer", value: "06", unit: "second" },
+                { type: "decimal", value: ".", unit: "second" },
+                { type: "fraction", value: "007008009", unit: "second" },
             ]
         );
         expect(
@@ -141,99 +245,184 @@ describe("correct behavior", () => {
                 fractionalDigits: 3,
             }).formatToParts(duration)
         ).toEqual([
-            { type: "element", value: "1y" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "unit", value: "y", unit: "year" },
             { type: "literal", value: " " },
-            { type: "element", value: "2m" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "unit", value: "m", unit: "month" },
             { type: "literal", value: " " },
-            { type: "element", value: "3w" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "unit", value: "w", unit: "week" },
             { type: "literal", value: " " },
-            { type: "element", value: "3d" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "unit", value: "d", unit: "day" },
             { type: "literal", value: " " },
-            { type: "element", value: "4h" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "unit", value: "h", unit: "hour" },
             { type: "literal", value: " " },
-            { type: "element", value: "5m" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "unit", value: "m", unit: "minute" },
             { type: "literal", value: " " },
-            { type: "element", value: "6s" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "unit", value: "s", unit: "second" },
             { type: "literal", value: " " },
-            { type: "element", value: "7ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: " " },
-            { type: "element", value: "8.009μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "decimal", value: ".", unit: "microsecond" },
+            { type: "fraction", value: "009", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
         ]);
 
         expect(new Intl.DurationFormat("de", { style: "long" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1 Jahr" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "Jahr", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 Monate" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "Monate", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 Wochen" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "Wochen", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 Tage" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "Tage", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 Stunden" },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "Stunden", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 Minuten" },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "Minuten", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 Sekunden" },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "Sekunden", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 Millisekunden" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "Millisekunden", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 Mikrosekunden" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "Mikrosekunden", unit: "microsecond" },
             { type: "literal", value: " und " },
-            { type: "element", value: "9 Nanosekunden" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "Nanosekunden", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("de", { style: "short" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1 J" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "J", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 Mon." },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "Mon.", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 Wo." },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "Wo.", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 Tg." },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "Tg.", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 Std." },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "Std.", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 Min." },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "Min.", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 Sek." },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "Sek.", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: " und " },
-            { type: "element", value: "9 ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("de", { style: "narrow" }).formatToParts(duration)).toEqual([
-            { type: "element", value: "1 J" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "J", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 M" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "M", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 W" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "W", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 T" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "T", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 Std." },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "Std.", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 Min." },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "Min.", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 Sek." },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "Sek.", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: ", " },
-            { type: "element", value: "8 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
             { type: "literal", value: " und " },
-            { type: "element", value: "9 ns" },
+            { type: "integer", value: "9", unit: "nanosecond" },
+            { type: "literal", value: " ", unit: "nanosecond" },
+            { type: "unit", value: "ns", unit: "nanosecond" },
         ]);
         expect(new Intl.DurationFormat("de", { style: "digital" }).formatToParts(duration)).toEqual(
             [
-                { type: "element", value: "1 J" },
+                { type: "integer", value: "1", unit: "year" },
+                { type: "literal", value: " ", unit: "year" },
+                { type: "unit", value: "J", unit: "year" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "2 Mon." },
+                { type: "integer", value: "2", unit: "month" },
+                { type: "literal", value: " ", unit: "month" },
+                { type: "unit", value: "Mon.", unit: "month" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "3 Wo." },
+                { type: "integer", value: "3", unit: "week" },
+                { type: "literal", value: " ", unit: "week" },
+                { type: "unit", value: "Wo.", unit: "week" },
                 { type: "literal", value: ", " },
-                { type: "element", value: "3 Tg." },
+                { type: "integer", value: "3", unit: "day" },
+                { type: "literal", value: " ", unit: "day" },
+                { type: "unit", value: "Tg.", unit: "day" },
                 { type: "literal", value: " und " },
-                { type: "element", value: "4:05:06" },
+                { type: "integer", value: "4", unit: "hour" },
+                { type: "literal", value: ":" },
+                { type: "integer", value: "05", unit: "minute" },
+                { type: "literal", value: ":" },
+                { type: "integer", value: "06", unit: "second" },
+                { type: "decimal", value: ",", unit: "second" },
+                { type: "fraction", value: "007008009", unit: "second" },
             ]
         );
         expect(
@@ -243,23 +432,43 @@ describe("correct behavior", () => {
                 fractionalDigits: 3,
             }).formatToParts(duration)
         ).toEqual([
-            { type: "element", value: "1 J" },
+            { type: "integer", value: "1", unit: "year" },
+            { type: "literal", value: " ", unit: "year" },
+            { type: "unit", value: "J", unit: "year" },
             { type: "literal", value: ", " },
-            { type: "element", value: "2 M" },
+            { type: "integer", value: "2", unit: "month" },
+            { type: "literal", value: " ", unit: "month" },
+            { type: "unit", value: "M", unit: "month" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 W" },
+            { type: "integer", value: "3", unit: "week" },
+            { type: "literal", value: " ", unit: "week" },
+            { type: "unit", value: "W", unit: "week" },
             { type: "literal", value: ", " },
-            { type: "element", value: "3 T" },
+            { type: "integer", value: "3", unit: "day" },
+            { type: "literal", value: " ", unit: "day" },
+            { type: "unit", value: "T", unit: "day" },
             { type: "literal", value: ", " },
-            { type: "element", value: "4 Std." },
+            { type: "integer", value: "4", unit: "hour" },
+            { type: "literal", value: " ", unit: "hour" },
+            { type: "unit", value: "Std.", unit: "hour" },
             { type: "literal", value: ", " },
-            { type: "element", value: "5 Min." },
+            { type: "integer", value: "5", unit: "minute" },
+            { type: "literal", value: " ", unit: "minute" },
+            { type: "unit", value: "Min.", unit: "minute" },
             { type: "literal", value: ", " },
-            { type: "element", value: "6 Sek." },
+            { type: "integer", value: "6", unit: "second" },
+            { type: "literal", value: " ", unit: "second" },
+            { type: "unit", value: "Sek.", unit: "second" },
             { type: "literal", value: ", " },
-            { type: "element", value: "7 ms" },
+            { type: "integer", value: "7", unit: "millisecond" },
+            { type: "literal", value: " ", unit: "millisecond" },
+            { type: "unit", value: "ms", unit: "millisecond" },
             { type: "literal", value: " und " },
-            { type: "element", value: "8,009 μs" },
+            { type: "integer", value: "8", unit: "microsecond" },
+            { type: "decimal", value: ",", unit: "microsecond" },
+            { type: "fraction", value: "009", unit: "microsecond" },
+            { type: "literal", value: " ", unit: "microsecond" },
+            { type: "unit", value: "μs", unit: "microsecond" },
         ]);
     });
 });

+ 1 - 1
Userland/Libraries/LibJS/Tests/builtins/Intl/DurationFormat/DurationFormat.prototype.resolvedOptions.js

@@ -284,7 +284,7 @@ describe("correct behavior", () => {
 
     test("fractionalDigits", () => {
         const en1 = new Intl.DurationFormat("en");
-        expect(en1.resolvedOptions().fractionalDigits).toBe(0);
+        expect(en1.resolvedOptions().fractionalDigits).toBeUndefined();
 
         [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(fractionalDigits => {
             const en2 = new Intl.DurationFormat("en", { fractionalDigits: fractionalDigits });