فهرست منبع

LibJS: Implement Temporal.PlainYearMonth.prototype.until/since

Timothy Flynn 8 ماه پیش
والد
کامیت
cb5d1b5086
26فایلهای تغییر یافته به همراه1586 افزوده شده و 1 حذف شده
  1. 1 0
      Libraries/LibJS/Forward.h
  2. 2 0
      Libraries/LibJS/Runtime/ErrorTypes.h
  3. 91 0
      Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp
  4. 15 0
      Libraries/LibJS/Runtime/Temporal/AbstractOperations.h
  5. 166 0
      Libraries/LibJS/Runtime/Temporal/Calendar.cpp
  6. 3 0
      Libraries/LibJS/Runtime/Temporal/Calendar.h
  7. 93 0
      Libraries/LibJS/Runtime/Temporal/DateEquations.cpp
  8. 2 0
      Libraries/LibJS/Runtime/Temporal/DateEquations.h
  9. 579 1
      Libraries/LibJS/Runtime/Temporal/Duration.cpp
  10. 22 0
      Libraries/LibJS/Runtime/Temporal/Duration.h
  11. 13 0
      Libraries/LibJS/Runtime/Temporal/Instant.cpp
  12. 2 0
      Libraries/LibJS/Runtime/Temporal/Instant.h
  13. 40 0
      Libraries/LibJS/Runtime/Temporal/PlainDate.cpp
  14. 2 0
      Libraries/LibJS/Runtime/Temporal/PlainDate.h
  15. 15 0
      Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp
  16. 1 0
      Libraries/LibJS/Runtime/Temporal/PlainDateTime.h
  17. 52 0
      Libraries/LibJS/Runtime/Temporal/PlainTime.cpp
  18. 2 0
      Libraries/LibJS/Runtime/Temporal/PlainTime.h
  19. 94 0
      Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp
  20. 8 0
      Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h
  21. 31 0
      Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp
  22. 2 0
      Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h
  23. 122 0
      Libraries/LibJS/Runtime/Temporal/TimeZone.cpp
  24. 12 0
      Libraries/LibJS/Runtime/Temporal/TimeZone.h
  25. 108 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js
  26. 108 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js

+ 1 - 0
Libraries/LibJS/Forward.h

@@ -286,6 +286,7 @@ struct DateDuration;
 struct InternalDuration;
 struct ISODate;
 struct ISODateTime;
+struct ISOYearMonth;
 struct ParseResult;
 struct PartialDuration;
 struct Time;

+ 2 - 0
Libraries/LibJS/Runtime/ErrorTypes.h

@@ -238,6 +238,8 @@
     M(TemporalAmbiguousMonthOfPlainMonthDay, "Accessing month of PlainMonthDay is ambiguous, use monthCode instead")                    \
     M(TemporalDifferentCalendars, "Cannot compare dates from two different calendars")                                                  \
     M(TemporalDifferentTimeZones, "Cannot compare dates from two different time zones")                                                 \
+    M(TemporalDisambiguatePossibleEpochNSRejectMoreThanOne, "Cannot disambiguate two or more possible epoch nanoseconds")               \
+    M(TemporalDisambiguatePossibleEpochNSRejectZero, "Cannot disambiguate zero possible epoch nanoseconds")                             \
     M(TemporalDisambiguatePossibleInstantsEarlierZero, "Cannot disambiguate zero possible instants with mode \"earlier\"")              \
     M(TemporalDisambiguatePossibleInstantsRejectMoreThanOne, "Cannot disambiguate two or more possible instants with mode \"reject\"")  \
     M(TemporalDisambiguatePossibleInstantsRejectZero, "Cannot disambiguate zero possible instants with mode \"reject\"")                \

+ 91 - 0
Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp

@@ -72,6 +72,19 @@ double epoch_days_to_epoch_ms(double day, double time)
     return day * JS::ms_per_day + time;
 }
 
+// 13.4 CheckISODaysRange ( isoDate ), https://tc39.es/proposal-temporal/#sec-checkisodaysrange
+ThrowCompletionOr<void> check_iso_days_range(VM& vm, ISODate const& iso_date)
+{
+    // 1. If abs(ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]])) > 10**8, then
+    if (fabs(iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day)) > 100'000'000) {
+        // a. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
+    }
+
+    // 2. Return unused.
+    return {};
+}
+
 // 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption
 ThrowCompletionOr<Overflow> get_temporal_overflow_option(VM& vm, Object const& options)
 {
@@ -86,6 +99,29 @@ ThrowCompletionOr<Overflow> get_temporal_overflow_option(VM& vm, Object const& o
     return Overflow::Reject;
 }
 
+// 13.8 NegateRoundingMode ( roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-negateroundingmode
+RoundingMode negate_rounding_mode(RoundingMode rounding_mode)
+{
+    // 1. If roundingMode is CEIL, return FLOOR.
+    if (rounding_mode == RoundingMode::Ceil)
+        return RoundingMode::Floor;
+
+    // 2. If roundingMode is FLOOR, return CEIL.
+    if (rounding_mode == RoundingMode::Floor)
+        return RoundingMode::Ceil;
+
+    // 3. If roundingMode is HALF-CEIL, return HALF-FLOOR.
+    if (rounding_mode == RoundingMode::HalfCeil)
+        return RoundingMode::HalfFloor;
+
+    // 4. If roundingMode is HALF-FLOOR, return HALF-CEIL.
+    if (rounding_mode == RoundingMode::HalfFloor)
+        return RoundingMode::HalfCeil;
+
+    // 5. Return roundingMode.
+    return rounding_mode;
+}
+
 // 13.10 GetTemporalShowCalendarNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowcalendarnameoption
 ThrowCompletionOr<ShowCalendar> get_temporal_show_calendar_name_option(VM& vm, Object const& options)
 {
@@ -1403,6 +1439,61 @@ CalendarFields iso_date_to_fields(StringView calendar, ISODate const& iso_date,
     return fields;
 }
 
+// 13.43 GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit ), https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings
+ThrowCompletionOr<DifferenceSettings> get_difference_settings(VM& vm, DurationOperation operation, Object const& options, UnitGroup unit_group, ReadonlySpan<Unit> disallowed_units, Unit fallback_smallest_unit, Unit smallest_largest_default_unit)
+{
+    // 1. NOTE: The following steps read options and perform independent validation in alphabetical order.
+
+    // 2. Let largestUnit be ? GetTemporalUnitValuedOption(options, "largestUnit", unitGroup, AUTO).
+    auto largest_unit = TRY(get_temporal_unit_valued_option(vm, options, vm.names.largestUnit, unit_group, Auto {}));
+
+    // 3. If disallowedUnits contains largestUnit, throw a RangeError exception.
+    if (auto* unit = largest_unit.get_pointer<Unit>(); unit && disallowed_units.contains_slow(*unit))
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(*unit), vm.names.largestUnit);
+
+    // 4. Let roundingIncrement be ? GetRoundingIncrementOption(options).
+    auto rounding_increment = TRY(get_rounding_increment_option(vm, options));
+
+    // 5. Let roundingMode be ? GetRoundingModeOption(options, TRUNC).
+    auto rounding_mode = TRY(get_rounding_mode_option(vm, options, RoundingMode::Trunc));
+
+    // 6. If operation is SINCE, then
+    if (operation == DurationOperation::Since) {
+        // a. Set roundingMode to NegateRoundingMode(roundingMode).
+        rounding_mode = negate_rounding_mode(rounding_mode);
+    }
+
+    // 7. Let smallestUnit be ? GetTemporalUnitValuedOption(options, "smallestUnit", unitGroup, fallbackSmallestUnit).
+    auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, options, vm.names.smallestUnit, unit_group, fallback_smallest_unit));
+    auto smallest_unit_value = smallest_unit.get<Unit>();
+
+    // 8. If disallowedUnits contains smallestUnit, throw a RangeError exception.
+    if (disallowed_units.contains_slow(smallest_unit_value))
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(smallest_unit_value), vm.names.smallestUnit);
+
+    // 9. Let defaultLargestUnit be LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit).
+    auto default_largest_unit = larger_of_two_temporal_units(smallest_largest_default_unit, smallest_unit.get<Unit>());
+
+    // 10. If largestUnit is AUTO, set largestUnit to defaultLargestUnit.
+    if (largest_unit.has<Auto>())
+        largest_unit = default_largest_unit;
+    auto largest_unit_value = largest_unit.get<Unit>();
+
+    // 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
+    if (larger_of_two_temporal_units(largest_unit_value, smallest_unit_value) != largest_unit_value)
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidUnitRange, temporal_unit_to_string(smallest_unit_value), temporal_unit_to_string(largest_unit_value));
+
+    // 12. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
+    auto maximum = maximum_temporal_duration_rounding_increment(smallest_unit_value);
+
+    // 13. If maximum is not UNSET, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
+    if (!maximum.has<Unset>())
+        TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get<u64>(), false));
+
+    // 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement,  }.
+    return DifferenceSettings { .smallest_unit = smallest_unit_value, .largest_unit = largest_unit_value, .rounding_mode = rounding_mode, .rounding_increment = rounding_increment };
+}
+
 // 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject
 ThrowCompletionOr<GC::Ref<Object>> get_options_object(VM& vm, Value options)
 {

+ 15 - 0
Libraries/LibJS/Runtime/Temporal/AbstractOperations.h

@@ -33,6 +33,11 @@ enum class DateType {
     YearMonth,
 };
 
+enum class DurationOperation {
+    Since,
+    Until,
+};
+
 enum class Overflow {
     Constrain,
     Reject,
@@ -148,9 +153,18 @@ struct ParsedISODateTime {
     Optional<String> calendar;
 };
 
+struct DifferenceSettings {
+    Unit smallest_unit;
+    Unit largest_unit;
+    RoundingMode rounding_mode;
+    u64 rounding_increment { 0 };
+};
+
 double iso_date_to_epoch_days(double year, double month, double date);
 double epoch_days_to_epoch_ms(double day, double time);
+ThrowCompletionOr<void> check_iso_days_range(VM&, ISODate const&);
 ThrowCompletionOr<Overflow> get_temporal_overflow_option(VM&, Object const& options);
+RoundingMode negate_rounding_mode(RoundingMode);
 ThrowCompletionOr<ShowCalendar> get_temporal_show_calendar_name_option(VM&, Object const& options);
 ThrowCompletionOr<void> validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive);
 ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM&, Object const& options);
@@ -177,6 +191,7 @@ ThrowCompletionOr<TimeZone> parse_temporal_time_zone_string(VM& vm, StringView t
 ThrowCompletionOr<String> to_month_code(VM&, Value argument);
 ThrowCompletionOr<String> to_offset_string(VM&, Value argument);
 CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType);
+ThrowCompletionOr<DifferenceSettings> get_difference_settings(VM&, DurationOperation, Object const& options, UnitGroup, ReadonlySpan<Unit> disallowed_units, Unit fallback_smallest_unit, Unit smallest_largest_default_unit);
 
 // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation
 template<typename... Args>

+ 166 - 0
Libraries/LibJS/Runtime/Temporal/Calendar.cpp

@@ -11,6 +11,7 @@
 #include <AK/QuickSort.h>
 #include <LibJS/Runtime/Temporal/Calendar.h>
 #include <LibJS/Runtime/Temporal/DateEquations.h>
+#include <LibJS/Runtime/Temporal/Duration.h>
 #include <LibJS/Runtime/Temporal/ISO8601.h>
 #include <LibJS/Runtime/Temporal/PlainDate.h>
 #include <LibJS/Runtime/Temporal/PlainMonthDay.h>
@@ -311,6 +312,154 @@ CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const&
     return merged;
 }
 
+// 12.2.6 CalendarDateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd
+ThrowCompletionOr<ISODate> calendar_date_add(VM& vm, StringView calendar, ISODate const& iso_date, DateDuration const& duration, Overflow overflow)
+{
+    ISODate result;
+
+    // 1. If calendar is "iso8601", then
+    // FIXME: Return an ISODate for an ISO8601 calendar for now.
+    if (true || calendar == "iso8601"sv) {
+        // a. Let intermediate be BalanceISOYearMonth(isoDate.[[Year]] + duration.[[Years]], isoDate.[[Month]] + duration.[[Months]]).
+        auto intermediate = balance_iso_year_month(static_cast<double>(iso_date.year) + duration.years, static_cast<double>(iso_date.month) + duration.months);
+
+        // b. Set intermediate to ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], isoDate.[[Day]], overflow).
+        auto intermediate_date = TRY(regulate_iso_date(vm, intermediate.year, intermediate.month, iso_date.day, overflow));
+
+        // c. Let d be intermediate.[[Day]] + duration.[[Days]] + 7 × duration.[[Weeks]].
+        auto day = intermediate_date.day + duration.days + (7 * duration.weeks);
+
+        // d. Let result be BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
+        result = balance_iso_date(intermediate_date.year, intermediate_date.month, day);
+    }
+    // 2. Else,
+    else {
+        // a. Let result be an implementation-defined ISO Date Record, or throw a RangeError exception, as described below.
+    }
+
+    // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
+    if (!iso_date_within_limits(result))
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
+
+    // 4. Return result.
+    return result;
+}
+
+// 12.2.7 CalendarDateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil
+DateDuration calendar_date_until(VM& vm, StringView calendar, ISODate const& one, ISODate const& two, Unit largest_unit)
+{
+    // 1. If calendar is "iso8601", then
+    if (calendar == "iso8601"sv) {
+        // a. Let sign be -CompareISODate(one, two).
+        auto sign = compare_iso_date(one, two);
+        sign *= -1;
+
+        // b. If sign = 0, return ZeroDateDuration().
+        if (sign == 0)
+            return zero_date_duration(vm);
+
+        // c. Let years be 0.
+        double years = 0;
+
+        // d. If largestUnit is YEAR, then
+        if (largest_unit == Unit::Year) {
+            // i. Let candidateYears be sign.
+            double candidate_years = sign;
+
+            // ii. Repeat, while ISODateSurpasses(sign, one.[[Year]] + candidateYears, one.[[Month]], one.[[Day]], two) is false,
+            while (!iso_date_surpasses(sign, static_cast<double>(one.year) + candidate_years, one.month, one.day, two)) {
+                // 1. Set years to candidateYears.
+                years = candidate_years;
+
+                // 2. Set candidateYears to candidateYears + sign.
+                candidate_years += sign;
+            }
+        }
+
+        // e. Let months be 0.
+        double months = 0;
+
+        // f. If largestUnit is YEAR or largestUnit is MONTH, then
+        if (largest_unit == Unit::Year || largest_unit == Unit::Month) {
+            // i. Let candidateMonths be sign.
+            double candidate_months = sign;
+
+            // ii. Let intermediate be BalanceISOYearMonth(one.[[Year]] + years, one.[[Month]] + candidateMonths).
+            auto intermediate = balance_iso_year_month(static_cast<double>(one.year) + years, static_cast<double>(one.month) + candidate_months);
+
+            // iii. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], one.[[Day]], two) is false,
+            while (!iso_date_surpasses(sign, intermediate.year, intermediate.month, one.day, two)) {
+                // 1. Set months to candidateMonths.
+                months = candidate_months;
+
+                // 2. Set candidateMonths to candidateMonths + sign.
+                candidate_months += sign;
+
+                // 3. Set intermediate to BalanceISOYearMonth(intermediate.[[Year]], intermediate.[[Month]] + sign).
+                intermediate = balance_iso_year_month(intermediate.year, static_cast<double>(intermediate.month) + sign);
+            }
+        }
+
+        // g. Set intermediate to BalanceISOYearMonth(one.[[Year]] + years, one.[[Month]] + months).
+        auto intermediate = balance_iso_year_month(static_cast<double>(one.year) + years, static_cast<double>(one.month) + months);
+
+        // h. Let constrained be ! RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], one.[[Day]], CONSTRAIN).
+        auto constrained = MUST(regulate_iso_date(vm, intermediate.year, intermediate.month, one.day, Overflow::Constrain));
+
+        // i. Let weeks be 0.
+        double weeks = 0;
+
+        // j. If largestUnit is WEEK, then
+        if (largest_unit == Unit::Week) {
+            // i. Let candidateWeeks be sign.
+            double candidate_weeks = sign;
+
+            // ii. Set intermediate to BalanceISODate(constrained.[[Year]], constrained.[[Month]], constrained.[[Day]] + 7 × candidateWeeks).
+            auto intermediate = balance_iso_date(constrained.year, constrained.month, static_cast<double>(constrained.day) + (7.0 * candidate_weeks));
+
+            // iii. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]], two) is false,
+            while (!iso_date_surpasses(sign, intermediate.year, intermediate.month, intermediate.day, two)) {
+                // 1. Set weeks to candidateWeeks.
+                weeks = candidate_weeks;
+
+                // 2. Set candidateWeeks to candidateWeeks + sign.
+                candidate_weeks += sign;
+
+                // 3. Set intermediate to BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]] + 7 × sign).
+                intermediate = balance_iso_date(intermediate.year, intermediate.month, static_cast<double>(intermediate.day) + (7.0 * sign));
+            }
+        }
+
+        // k. Let days be 0.
+        double days = 0;
+
+        // l. Let candidateDays be sign.
+        double candidate_days = sign;
+
+        // m. Set intermediate to BalanceISODate(constrained.[[Year]], constrained.[[Month]], constrained.[[Day]] + 7 × weeks + candidateDays).
+        auto intermediate_date = balance_iso_date(constrained.year, constrained.month, static_cast<double>(constrained.day) + (7.0 * weeks) + candidate_days);
+
+        // n. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]], two) is false,
+        while (!iso_date_surpasses(sign, intermediate_date.year, intermediate_date.month, intermediate_date.day, two)) {
+            // i. Set days to candidateDays.
+            days = candidate_days;
+
+            // ii. Set candidateDays to candidateDays + sign.
+            candidate_days += sign;
+
+            // iii. Set intermediate to BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]] + sign).
+            intermediate_date = balance_iso_date(intermediate_date.year, intermediate_date.month, static_cast<double>(intermediate_date.day) + sign);
+        }
+
+        // o. Return ! CreateDateDurationRecord(years, months, weeks, days).
+        return MUST(create_date_duration_record(vm, years, months, weeks, days));
+    }
+
+    // 2. Return an implementation-defined Date Duration Record as described above.
+    // FIXME: Return a DateDuration for an ISO8601 calendar for now.
+    return calendar_date_until(vm, "iso8601"sv, one, two, largest_unit);
+}
+
 // 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier
 ThrowCompletionOr<String> to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like)
 {
@@ -365,6 +514,23 @@ ThrowCompletionOr<String> get_temporal_calendar_identifier_with_iso_default(VM&
     return TRY(to_temporal_calendar_identifier(vm, calendar_like));
 }
 
+// 12.2.10 CalendarDateFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields
+ThrowCompletionOr<ISODate> calendar_date_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow)
+{
+    // 1. Perform ? CalendarResolveFields(calendar, fields, DATE).
+    TRY(calendar_resolve_fields(vm, calendar, fields, DateType::Date));
+
+    // 2. Let result be ? CalendarDateToISO(calendar, fields, overflow).
+    auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow));
+
+    // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
+    if (!iso_date_within_limits(result))
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
+
+    // 4. Return result.
+    return result;
+}
+
 // 12.2.11 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields
 ThrowCompletionOr<ISODate> calendar_year_month_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow)
 {

+ 3 - 0
Libraries/LibJS/Runtime/Temporal/Calendar.h

@@ -99,6 +99,7 @@ using CalendarFieldListOrPartial = Variant<Partial, CalendarFieldList>;
 ThrowCompletionOr<String> canonicalize_calendar(VM&, StringView id);
 Vector<String> const& available_calendars();
 ThrowCompletionOr<CalendarFields> prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names);
+ThrowCompletionOr<ISODate> calendar_date_from_fields(VM&, StringView calendar, CalendarFields, Overflow);
 ThrowCompletionOr<ISODate> calendar_year_month_from_fields(VM&, StringView calendar, CalendarFields, Overflow);
 ThrowCompletionOr<ISODate> calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow);
 String format_calendar_annotation(StringView id, ShowCalendar);
@@ -109,6 +110,8 @@ u16 iso_day_of_year(ISODate const&);
 u8 iso_day_of_week(ISODate const&);
 Vector<CalendarField> calendar_field_keys_present(CalendarFields const&);
 CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields);
+ThrowCompletionOr<ISODate> calendar_date_add(VM&, StringView calendar, ISODate const&, DateDuration const&, Overflow);
+DateDuration calendar_date_until(VM&, StringView calendar, ISODate const&, ISODate const&, Unit largest_unit);
 ThrowCompletionOr<String> to_temporal_calendar_identifier(VM&, Value temporal_calendar_like);
 ThrowCompletionOr<String> get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item);
 ThrowCompletionOr<ISODate> calendar_date_to_iso(VM&, StringView calendar, CalendarFields const&, Overflow);

+ 93 - 0
Libraries/LibJS/Runtime/Temporal/DateEquations.cpp

@@ -79,6 +79,52 @@ u16 epoch_time_to_day_in_year(double time)
     return static_cast<u16>(epoch_time_to_day_number(time) - epoch_day_number_for_year(epoch_time_to_epoch_year(time)));
 }
 
+// https://tc39.es/proposal-temporal/#eqn-epochtimetomonthinyear
+u8 epoch_time_to_month_in_year(double time)
+{
+    auto day_in_year = epoch_time_to_day_in_year(time);
+    auto in_leap_year = mathematical_in_leap_year(time);
+
+    // EpochTimeToMonthInYear(t)
+    //     = 0 if 0 ≤ EpochTimeToDayInYear(t) < 31
+    //     = 1 if 31 ≤ EpochTimeToDayInYear(t) < 59 + MathematicalInLeapYear(t)
+    //     = 2 if 59 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 90 + MathematicalInLeapYear(t)
+    //     = 3 if 90 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 120 + MathematicalInLeapYear(t)
+    //     = 4 if 120 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 151 + MathematicalInLeapYear(t)
+    //     = 5 if 151 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 181 + MathematicalInLeapYear(t)
+    //     = 6 if 181 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 212 + MathematicalInLeapYear(t)
+    //     = 7 if 212 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 243 + MathematicalInLeapYear(t)
+    //     = 8 if 243 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 273 + MathematicalInLeapYear(t)
+    //     = 9 if 273 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 304 + MathematicalInLeapYear(t)
+    //     = 10 if 304 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 334 + MathematicalInLeapYear(t)
+    //     = 11 if 334 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 365 + MathematicalInLeapYear(t)
+    if (day_in_year < 31)
+        return 0;
+    if (day_in_year >= 31 && day_in_year < 59 + in_leap_year)
+        return 1;
+    if (day_in_year >= 59 + in_leap_year && day_in_year < 90 + in_leap_year)
+        return 2;
+    if (day_in_year >= 90 + in_leap_year && day_in_year < 120 + in_leap_year)
+        return 3;
+    if (day_in_year >= 120 + in_leap_year && day_in_year < 151 + in_leap_year)
+        return 4;
+    if (day_in_year >= 151 + in_leap_year && day_in_year < 181 + in_leap_year)
+        return 5;
+    if (day_in_year >= 181 + in_leap_year && day_in_year < 212 + in_leap_year)
+        return 6;
+    if (day_in_year >= 212 + in_leap_year && day_in_year < 243 + in_leap_year)
+        return 7;
+    if (day_in_year >= 243 + in_leap_year && day_in_year < 273 + in_leap_year)
+        return 8;
+    if (day_in_year >= 273 + in_leap_year && day_in_year < 304 + in_leap_year)
+        return 9;
+    if (day_in_year >= 304 + in_leap_year && day_in_year < 334 + in_leap_year)
+        return 10;
+    if (day_in_year >= 334 + in_leap_year && day_in_year < 365 + in_leap_year)
+        return 11;
+    VERIFY_NOT_REACHED();
+}
+
 // https://tc39.es/proposal-temporal/#eqn-epochtimetoweekday
 u8 epoch_time_to_week_day(double time)
 {
@@ -86,4 +132,51 @@ u8 epoch_time_to_week_day(double time)
     return static_cast<u8>(modulo(epoch_time_to_day_number(time) + 4, 7.0));
 }
 
+// https://tc39.es/proposal-temporal/#eqn-epochtimetodate
+u8 epoch_time_to_date(double time)
+{
+    auto day_in_year = epoch_time_to_day_in_year(time);
+    auto month_in_year = epoch_time_to_month_in_year(time);
+    auto in_leap_year = mathematical_in_leap_year(time);
+
+    // EpochTimeToDate(t)
+    //     = EpochTimeToDayInYear(t) + 1 if EpochTimeToMonthInYear(t) = 0
+    //     = EpochTimeToDayInYear(t) - 30 if EpochTimeToMonthInYear(t) = 1
+    //     = EpochTimeToDayInYear(t) - 58 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 2
+    //     = EpochTimeToDayInYear(t) - 89 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 3
+    //     = EpochTimeToDayInYear(t) - 119 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 4
+    //     = EpochTimeToDayInYear(t) - 150 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 5
+    //     = EpochTimeToDayInYear(t) - 180 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 6
+    //     = EpochTimeToDayInYear(t) - 211 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 7
+    //     = EpochTimeToDayInYear(t) - 242 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 8
+    //     = EpochTimeToDayInYear(t) - 272 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 9
+    //     = EpochTimeToDayInYear(t) - 303 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 10
+    //     = EpochTimeToDayInYear(t) - 333 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 11
+    if (month_in_year == 0)
+        return day_in_year + 1;
+    if (month_in_year == 1)
+        return day_in_year - 30;
+    if (month_in_year == 2)
+        return day_in_year - 58 - in_leap_year;
+    if (month_in_year == 3)
+        return day_in_year - 89 - in_leap_year;
+    if (month_in_year == 4)
+        return day_in_year - 119 - in_leap_year;
+    if (month_in_year == 5)
+        return day_in_year - 150 - in_leap_year;
+    if (month_in_year == 6)
+        return day_in_year - 180 - in_leap_year;
+    if (month_in_year == 7)
+        return day_in_year - 211 - in_leap_year;
+    if (month_in_year == 8)
+        return day_in_year - 242 - in_leap_year;
+    if (month_in_year == 9)
+        return day_in_year - 272 - in_leap_year;
+    if (month_in_year == 10)
+        return day_in_year - 303 - in_leap_year;
+    if (month_in_year == 11)
+        return day_in_year - 333 - in_leap_year;
+    VERIFY_NOT_REACHED();
+}
+
 }

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/DateEquations.h

@@ -19,6 +19,8 @@ double epoch_day_number_for_year(double year);
 double epoch_time_for_year(double year);
 i32 epoch_time_to_epoch_year(double time);
 u16 epoch_time_to_day_in_year(double time);
+u8 epoch_time_to_month_in_year(double time);
 u8 epoch_time_to_week_day(double time);
+u8 epoch_time_to_date(double time);
 
 }

+ 579 - 1
Libraries/LibJS/Runtime/Temporal/Duration.cpp

@@ -9,13 +9,16 @@
 
 #include <AK/Math.h>
 #include <AK/NumericLimits.h>
-#include <LibCrypto/BigFraction/BigFraction.h>
 #include <LibJS/Runtime/AbstractOperations.h>
+#include <LibJS/Runtime/Date.h>
 #include <LibJS/Runtime/Intrinsics.h>
 #include <LibJS/Runtime/Realm.h>
+#include <LibJS/Runtime/Temporal/Calendar.h>
 #include <LibJS/Runtime/Temporal/Duration.h>
 #include <LibJS/Runtime/Temporal/DurationConstructor.h>
 #include <LibJS/Runtime/Temporal/Instant.h>
+#include <LibJS/Runtime/Temporal/PlainDateTime.h>
+#include <LibJS/Runtime/Temporal/TimeZone.h>
 #include <LibJS/Runtime/VM.h>
 #include <LibJS/Runtime/ValueInlines.h>
 #include <math.h>
@@ -294,6 +297,21 @@ ThrowCompletionOr<DateDuration> create_date_duration_record(VM& vm, double years
     return DateDuration { years, months, weeks, days };
 }
 
+// 7.5.10 AdjustDateDurationRecord ( dateDuration, days [ , weeks [ , months ] ] ), https://tc39.es/proposal-temporal/#sec-temporal-adjustdatedurationrecord
+ThrowCompletionOr<DateDuration> adjust_date_duration_record(VM& vm, DateDuration const& date_duration, double days, Optional<double> weeks, Optional<double> months)
+{
+    // 1. If weeks is not present, set weeks to dateDuration.[[Weeks]].
+    if (!weeks.has_value())
+        weeks = date_duration.weeks;
+
+    // 2. If months is not present, set months to dateDuration.[[Months]].
+    if (!months.has_value())
+        months = date_duration.months;
+
+    // 3. Return ? CreateDateDurationRecord(dateDuration.[[Years]], months, weeks, days).
+    return TRY(create_date_duration_record(vm, date_duration.years, *months, *weeks, days));
+}
+
 // 7.5.11 CombineDateAndTimeDuration ( dateDuration, timeDuration ), https://tc39.es/proposal-temporal/#sec-temporal-combinedateandtimeduration
 ThrowCompletionOr<InternalDuration> combine_date_and_time_duration(VM& vm, DateDuration date_duration, TimeDuration time_duration)
 {
@@ -418,6 +436,20 @@ i8 date_duration_sign(DateDuration const& date_duration)
     return 0;
 }
 
+// 7.5.15 InternalDurationSign ( internalDuration ), https://tc39.es/proposal-temporal/#sec-temporal-internaldurationsign
+i8 internal_duration_sign(InternalDuration const& internal_duration)
+{
+    // 1. Let dateSign be DateDurationSign(internalDuration.[[Date]]).
+    auto date_sign = date_duration_sign(internal_duration.date);
+
+    // 2. If dateSign ≠ 0, return dateSign.
+    if (date_sign != 0)
+        return date_sign;
+
+    // 3. Return TimeDurationSign(internalDuration.[[Time]]).
+    return time_duration_sign(internal_duration.time);
+}
+
 // 7.5.16 IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidduration
 bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds)
 {
@@ -687,6 +719,13 @@ ThrowCompletionOr<TimeDuration> add_24_hour_days_to_time_duration(VM& vm, TimeDu
     return result;
 }
 
+// 7.5.24 AddTimeDurationToEpochNanoseconds ( d, epochNs ), https://tc39.es/proposal-temporal/#sec-temporal-addtimedurationtoepochnanoseconds
+TimeDuration add_time_duration_to_epoch_nanoseconds(TimeDuration const& duration, TimeDuration const& epoch_nanoseconds)
+{
+    // 1. Return epochNs + ℤ(d).
+    return epoch_nanoseconds.plus(duration);
+}
+
 // 7.5.25 CompareTimeDuration ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-comparetimeduration
 i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two)
 {
@@ -702,6 +741,19 @@ i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two)
     return 0;
 }
 
+// 7.5.26 TimeDurationFromEpochNanosecondsDifference ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-timedurationfromepochnanosecondsdifference
+TimeDuration time_duration_from_epoch_nanoseconds_difference(TimeDuration const& one, TimeDuration const& two)
+{
+    // 1. Let result be ℝ(one) - ℝ(two).
+    auto result = one.minus(two);
+
+    // 2. Assert: abs(result) ≤ maxTimeDuration.
+    VERIFY(result.unsigned_value() <= MAX_TIME_DURATION.unsigned_value());
+
+    // 3. Return result.
+    return result;
+}
+
 // 7.5.27 RoundTimeDurationToIncrement ( d, increment, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtimedurationtoincrement
 ThrowCompletionOr<TimeDuration> round_time_duration_to_increment(VM& vm, TimeDuration const& duration, Crypto::UnsignedBigInteger const& increment, RoundingMode rounding_mode)
 {
@@ -756,6 +808,532 @@ double total_time_duration(TimeDuration const& time_duration, Unit unit)
     return result.to_double();
 }
 
+// 7.5.33 NudgeToCalendarUnit ( sign, duration, destEpochNs, isoDateTime, timeZone, calendar, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetocalendarunit
+ThrowCompletionOr<CalendarNudgeResult> nudge_to_calendar_unit(VM& vm, i8 sign, InternalDuration const& duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional<StringView> time_zone, StringView calendar, u64 increment, Unit unit, RoundingMode rounding_mode)
+{
+    DateDuration start_duration;
+    DateDuration end_duration;
+
+    double r1 = 0;
+    double r2 = 0;
+
+    // 1. If unit is YEAR, then
+    if (unit == Unit::Year) {
+        // a. Let years be RoundNumberToIncrement(duration.[[Date]].[[Years]], increment, TRUNC).
+        auto years = round_number_to_increment(duration.date.years, increment, RoundingMode::Trunc);
+
+        // b. Let r1 be years.
+        r1 = years;
+
+        // c. Let r2 be years + increment × sign.
+        r2 = years + static_cast<double>(increment) * sign;
+
+        // d. Let startDuration be ? CreateDateDurationRecord(r1, 0, 0, 0).
+        start_duration = TRY(create_date_duration_record(vm, r1, 0, 0, 0));
+
+        // e. Let endDuration be ? CreateDateDurationRecord(r2, 0, 0, 0).
+        end_duration = TRY(create_date_duration_record(vm, r2, 0, 0, 0));
+    }
+    // 2. Else if unit is MONTH, then
+    else if (unit == Unit::Month) {
+        // a. Let months be RoundNumberToIncrement(duration.[[Date]].[[Months]], increment, TRUNC).
+        auto months = round_number_to_increment(duration.date.months, increment, RoundingMode::Trunc);
+
+        // b. Let r1 be months.
+        r1 = months;
+
+        // c. Let r2 be months + increment × sign.
+        r2 = months + static_cast<double>(increment) * sign;
+
+        // d. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r1).
+        start_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, r1));
+
+        // e. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r2).
+        end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, r2));
+    }
+    // 3. Else if unit is WEEK, then
+    else if (unit == Unit::Week) {
+        // a. Let yearsMonths be ! AdjustDateDurationRecord(duration.[[Date]], 0, 0).
+        auto years_months = MUST(adjust_date_duration_record(vm, duration.date, 0, 0));
+
+        // b. Let weeksStart be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], yearsMonths, CONSTRAIN).
+        auto weeks_start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, years_months, Overflow::Constrain));
+
+        // c. Let weeksEnd be BalanceISODate(weeksStart.[[Year]], weeksStart.[[Month]], weeksStart.[[Day]] + duration.[[Date]].[[Days]]).
+        auto weeks_end = balance_iso_date(weeks_start.year, weeks_start.month, static_cast<double>(weeks_start.day) + duration.date.days);
+
+        // d. Let untilResult be CalendarDateUntil(calendar, weeksStart, weeksEnd, WEEK).
+        auto until_result = calendar_date_until(vm, calendar, weeks_start, weeks_end, Unit::Week);
+
+        // e. Let weeks be RoundNumberToIncrement(duration.[[Date]].[[Weeks]] + untilResult.[[Weeks]], increment, TRUNC).
+        auto weeks = round_number_to_increment(duration.date.weeks + until_result.weeks, increment, RoundingMode::Trunc);
+
+        // f. Let r1 be weeks.
+        r1 = weeks;
+
+        // g. Let r2 be weeks + increment × sign.
+        r2 = weeks + static_cast<double>(increment) * sign;
+
+        // h. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, r1).
+        start_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, r1));
+
+        // i. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, r2).
+        end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, r2));
+    }
+    // 4. Else,
+    else {
+        // a. Assert: unit is DAY.
+        VERIFY(unit == Unit::Day);
+
+        // b. Let days be RoundNumberToIncrement(duration.[[Date]].[[Days]], increment, TRUNC).
+        auto days = round_number_to_increment(duration.date.days, increment, RoundingMode::Trunc);
+
+        // c. Let r1 be days.
+        r1 = days;
+
+        // d. Let r2 be days + increment × sign.
+        r2 = days + static_cast<double>(increment) * sign;
+
+        // e. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], r1).
+        start_duration = TRY(adjust_date_duration_record(vm, duration.date, r1));
+
+        // f. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], r2).
+        end_duration = TRY(adjust_date_duration_record(vm, duration.date, r2));
+    }
+
+    // 5. Assert: If sign is 1, r1 ≥ 0 and r1 < r2.
+    if (sign == 1)
+        VERIFY(r1 >= 0 && r1 < r2);
+    // 6. Assert: If sign is -1, r1 ≤ 0 and r1 > r2.
+    else if (sign == -1)
+        VERIFY(r1 <= 0 && r1 > r2);
+
+    // 7. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], startDuration, CONSTRAIN).
+    auto start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, start_duration, Overflow::Constrain));
+
+    // 8. Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, CONSTRAIN).
+    auto end = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, end_duration, Overflow::Constrain));
+
+    // 9. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]).
+    auto start_date_time = combine_iso_date_and_time_record(start, iso_date_time.time);
+
+    // 10. Let endDateTime be CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]).
+    auto end_date_time = combine_iso_date_and_time_record(end, iso_date_time.time);
+
+    TimeDuration start_epoch_ns;
+    TimeDuration end_epoch_ns;
+
+    // 11. If timeZone is UNSET, then
+    if (!time_zone.has_value()) {
+        // a. Let startEpochNs be GetUTCEpochNanoseconds(startDateTime).
+        start_epoch_ns = get_utc_epoch_nanoseconds(start_date_time);
+
+        // b. Let endEpochNs be GetUTCEpochNanoseconds(endDateTime).
+        end_epoch_ns = get_utc_epoch_nanoseconds(end_date_time);
+    }
+    // 12. Else,
+    else {
+        // a. Let startEpochNs be ? GetEpochNanosecondsFor(timeZone, startDateTime, COMPATIBLE).
+        start_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, start_date_time, Disambiguation::Compatible));
+
+        // b. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE).
+        end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, end_date_time, Disambiguation::Compatible));
+    }
+
+    // 13. If sign is 1, then
+    if (sign == 1) {
+        // a. Assert: startEpochNs ≤ destEpochNs ≤ endEpochNs.
+        VERIFY(start_epoch_ns <= dest_epoch_ns);
+        VERIFY(dest_epoch_ns <= end_epoch_ns);
+    }
+    // 14. Else,
+    else {
+        // a. Assert: endEpochNs ≤ destEpochNs ≤ startEpochNs.
+        VERIFY(end_epoch_ns <= dest_epoch_ns);
+        VERIFY(dest_epoch_ns <= start_epoch_ns);
+    }
+
+    // 15. Assert: startEpochNs ≠ endEpochNs.
+    VERIFY(start_epoch_ns != end_epoch_ns);
+
+    // 16. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs).
+    auto progress_numerator = dest_epoch_ns.minus(start_epoch_ns);
+    auto progress_denominator = end_epoch_ns.minus(start_epoch_ns);
+    auto progress_equals_one = progress_numerator == progress_denominator;
+
+    // 17. Let total be r1 + progress × increment × sign.
+    auto total_numerator = progress_numerator.multiplied_by(Crypto::UnsignedBigInteger { increment });
+
+    if (sign == -1)
+        total_numerator.negate();
+    if (progress_denominator.is_negative())
+        total_numerator.negate();
+
+    auto total_mv = Crypto::BigFraction { Crypto::SignedBigInteger { r1 } } + Crypto::BigFraction { move(total_numerator), progress_denominator.unsigned_value() };
+    auto total = total_mv.to_double();
+
+    // 18. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic. This division can be
+    //     implemented as if expressing the denominator and numerator of total as two time durations, and performing one
+    //     division operation with a floating-point result.
+
+    // 19. Assert: 0 ≤ progress ≤ 1.
+
+    // 20. If sign < 0, let isNegative be NEGATIVE; else let isNegative be POSITIVE.
+    auto is_negative = sign < 0 ? Sign::Negative : Sign::Positive;
+
+    // 21. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
+    auto unsigned_rounding_mode = get_unsigned_rounding_mode(rounding_mode, is_negative);
+
+    double rounded_unit = 0;
+
+    // 22. If progress = 1, then
+    if (progress_equals_one) {
+        // a. Let roundedUnit be abs(r2).
+        rounded_unit = fabs(r2);
+    }
+    // 23. Else,
+    else {
+        // a. Assert: abs(r1) ≤ abs(total) < abs(r2).
+        VERIFY(fabs(r1) <= fabs(total));
+        VERIFY(fabs(total) <= fabs(r2));
+
+        // b. Let roundedUnit be ApplyUnsignedRoundingMode(abs(total), abs(r1), abs(r2), unsignedRoundingMode).
+        rounded_unit = apply_unsigned_rounding_mode(fabs(total), fabs(r1), fabs(r2), unsigned_rounding_mode);
+    }
+
+    auto did_expand_calendar_unit = false;
+    DateDuration result_duration;
+    TimeDuration nudged_epoch_ns;
+
+    // 24. If roundedUnit is abs(r2), then
+    if (rounded_unit == fabs(r2)) {
+        // a. Let didExpandCalendarUnit be true.
+        did_expand_calendar_unit = true;
+
+        // b. Let resultDuration be endDuration.
+        result_duration = end_duration;
+
+        // c. Let nudgedEpochNs be endEpochNs.
+        nudged_epoch_ns = move(end_epoch_ns);
+    }
+    // 25. Else,
+    else {
+        // a. Let didExpandCalendarUnit be false.
+        did_expand_calendar_unit = false;
+
+        // b. Let resultDuration be startDuration.
+        result_duration = start_duration;
+
+        // c. Let nudgedEpochNs be startEpochNs.
+        nudged_epoch_ns = move(start_epoch_ns);
+    }
+
+    // 26. Set resultDuration to ! CombineDateAndTimeDuration(resultDuration, 0).
+    auto result_date_and_time_duration = MUST(combine_date_and_time_duration(vm, result_duration, TimeDuration { 0 }));
+
+    // 27. Let nudgeResult be Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandCalendarUnit }.
+    DurationNudgeResult nudge_result { .duration = move(result_date_and_time_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_expand_calendar_unit };
+
+    // 28. Return the Record { [[NudgeResult]]: nudgeResult, [[Total]]: total }.
+    return CalendarNudgeResult { .nudge_result = move(nudge_result), .total = move(total_mv) };
+}
+
+// 7.5.34 NudgeToZonedTime ( sign, duration, isoDateTime, timeZone, calendar, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetozonedtime
+ThrowCompletionOr<DurationNudgeResult> nudge_to_zoned_time(VM& vm, i8 sign, InternalDuration const& duration, ISODateTime const& iso_date_time, StringView time_zone, StringView calendar, u64 increment, Unit unit, RoundingMode rounding_mode)
+{
+    // 1. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], duration.[[Date]], CONSTRAIN).
+    auto start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, duration.date, Overflow::Constrain));
+
+    // 2. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]).
+    auto start_date_time = combine_iso_date_and_time_record(start, iso_date_time.time);
+
+    // 3. Let endDate be BalanceISODate(start.[[Year]], start.[[Month]], start.[[Day]] + sign).
+    auto end_date = balance_iso_date(start.year, start.month, static_cast<double>(start.day) + sign);
+
+    // 4. Let endDateTime be CombineISODateAndTimeRecord(endDate, isoDateTime.[[Time]]).
+    auto end_date_time = combine_iso_date_and_time_record(end_date, iso_date_time.time);
+
+    // 5. Let startEpochNs be ? GetEpochNanosecondsFor(timeZone, startDateTime, COMPATIBLE).
+    auto start_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, time_zone, start_date_time, Disambiguation::Compatible));
+
+    // 6. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE).
+    auto end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, time_zone, end_date_time, Disambiguation::Compatible));
+
+    // 7. Let daySpan be TimeDurationFromEpochNanosecondsDifference(endEpochNs, startEpochNs).
+    auto day_span = time_duration_from_epoch_nanoseconds_difference(end_epoch_ns, start_epoch_ns);
+
+    // 8. Assert: TimeDurationSign(daySpan) = sign.
+    VERIFY(time_duration_sign(day_span) == sign);
+
+    // 9. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit.
+    auto const& unit_length = temporal_unit_length_in_nanoseconds(unit);
+
+    // 10. Let roundedTimeDuration be ? RoundTimeDurationToIncrement(duration.[[Time]], increment × unitLength, roundingMode).
+    auto unit_length_multiplied_by_increment = unit_length.multiplied_by(Crypto::UnsignedBigInteger { increment });
+    auto rounded_time_duration = TRY(round_time_duration_to_increment(vm, duration.time, unit_length_multiplied_by_increment, rounding_mode));
+
+    // 11. Let beyondDaySpan be ? AddTimeDuration(roundedTimeDuration, -daySpan).
+    day_span.negate();
+    auto beyond_day_span = TRY(add_time_duration(vm, rounded_time_duration, day_span));
+
+    auto did_round_beyond_day = false;
+    TimeDuration nudged_epoch_ns;
+    i8 day_delta = 0;
+
+    // 12. If TimeDurationSign(beyondDaySpan) ≠ -sign, then
+    if (time_duration_sign(beyond_day_span) != -sign) {
+        // a. Let didRoundBeyondDay be true.
+        did_round_beyond_day = true;
+
+        // b. Let dayDelta be sign.
+        day_delta = sign;
+
+        // c. Set roundedTimeDuration to ? RoundTimeDurationToIncrement(beyondDaySpan, increment × unitLength, roundingMode).
+        rounded_time_duration = TRY(round_time_duration_to_increment(vm, beyond_day_span, unit_length_multiplied_by_increment, rounding_mode));
+
+        // d. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(roundedTimeDuration, endEpochNs).
+        nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(rounded_time_duration, end_epoch_ns);
+    }
+    // 13. Else,
+    else {
+        // a. Let didRoundBeyondDay be false.
+        did_round_beyond_day = false;
+
+        // b. Let dayDelta be 0.
+        day_delta = 0;
+
+        // c. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(roundedTimeDuration, startEpochNs).
+        nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(rounded_time_duration, start_epoch_ns);
+    }
+
+    // 14. Let dateDuration be ? AdjustDateDurationRecord(duration.[[Date]], duration.[[Date]].[[Days]] + dayDelta).
+    auto date_duration = TRY(adjust_date_duration_record(vm, duration.date, duration.date.days + day_delta));
+
+    // 15. Let resultDuration be ? CombineDateAndTimeDuration(dateDuration, roundedTimeDuration).
+    auto result_duration = TRY(combine_date_and_time_duration(vm, date_duration, move(rounded_time_duration)));
+
+    // 16. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didRoundBeyondDay }.
+    return DurationNudgeResult { .duration = move(result_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_round_beyond_day };
+}
+
+// 7.5.35 NudgeToDayOrTime ( duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetodayortime
+ThrowCompletionOr<DurationNudgeResult> nudge_to_day_or_time(VM& vm, InternalDuration const& duration, TimeDuration const& dest_epoch_ns, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode rounding_mode)
+{
+    // 1. Let timeDuration be ! Add24HourDaysToTimeDuration(duration.[[Time]], duration.[[Date]].[[Days]]).
+    auto time_duration = MUST(add_24_hour_days_to_time_duration(vm, duration.time, duration.date.days));
+
+    // 2. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains smallestUnit.
+    auto const& unit_length = temporal_unit_length_in_nanoseconds(smallest_unit);
+
+    // 3. Let roundedTime be ? RoundTimeDurationToIncrement(timeDuration, unitLength × increment, roundingMode).
+    auto unit_length_multiplied_by_increment = unit_length.multiplied_by(Crypto::UnsignedBigInteger { increment });
+    auto rounded_time = TRY(round_time_duration_to_increment(vm, time_duration, unit_length_multiplied_by_increment, rounding_mode));
+
+    // 4. Let diffTime be ! AddTimeDuration(roundedTime, -timeDuration).
+    time_duration.negate();
+    auto diff_time = MUST(add_time_duration(vm, rounded_time, time_duration));
+    time_duration.negate();
+
+    // 5. Let wholeDays be truncate(TotalTimeDuration(timeDuration, DAY)).
+    auto whole_days = trunc(total_time_duration(time_duration, Unit::Day));
+
+    // 6. Let roundedWholeDays be truncate(TotalTimeDuration(roundedTime, DAY)).
+    auto rounded_whole_days = trunc(total_time_duration(rounded_time, Unit::Day));
+
+    // 7. Let dayDelta be roundedWholeDays - wholeDays.
+    auto day_delta = rounded_whole_days - whole_days;
+
+    // 8. If dayDelta < 0, let dayDeltaSign be -1; else if dayDelta > 0, let dayDeltaSign be 1; else let dayDeltaSign be 0.
+    auto day_delta_sign = day_delta < 0 ? -1 : (day_delta > 0 ? 1 : 0);
+
+    // 9. If dayDeltaSign = TimeDurationSign(timeDuration), let didExpandDays be true; else let didExpandDays be false.
+    auto did_expand_days = day_delta_sign == time_duration_sign(time_duration);
+
+    // 10. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(diffTime, destEpochNs).
+    auto nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(diff_time, dest_epoch_ns);
+
+    // 11. Let days be 0.
+    double days = 0;
+
+    // 12. Let remainder be roundedTime.
+    TimeDuration remainder;
+
+    // 13. If TemporalUnitCategory(largestUnit) is DATE, then
+    if (temporal_unit_category(largest_unit) == UnitCategory::Date) {
+        // a. Set days to roundedWholeDays.
+        days = rounded_whole_days;
+
+        // b. Set remainder to ! AddTimeDuration(roundedTime, TimeDurationFromComponents(-roundedWholeDays * HoursPerDay, 0, 0, 0, 0, 0)).
+        remainder = MUST(add_time_duration(vm, rounded_time, time_duration_from_components(-rounded_whole_days * JS::hours_per_day, 0, 0, 0, 0, 0)));
+    } else {
+        remainder = move(rounded_time);
+    }
+
+    // 14. Let dateDuration be ? AdjustDateDurationRecord(duration.[[Date]], days).
+    auto date_duration = TRY(adjust_date_duration_record(vm, duration.date, days));
+
+    // 15. Let resultDuration be ? CombineDateAndTimeDuration(dateDuration, remainder).
+    auto result_duration = TRY(combine_date_and_time_duration(vm, date_duration, move(remainder)));
+
+    // 16. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandDays }.
+    return DurationNudgeResult { .duration = move(result_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_expand_days };
+}
+
+// 7.5.36 BubbleRelativeDuration ( sign, duration, nudgedEpochNs, isoDateTime, timeZone, calendar, largestUnit, smallestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-bubblerelativeduration
+ThrowCompletionOr<InternalDuration> bubble_relative_duration(VM& vm, i8 sign, InternalDuration duration, TimeDuration const& nudged_epoch_ns, ISODateTime const& iso_date_time, Optional<StringView> time_zone, StringView calendar, Unit largest_unit, Unit smallest_unit)
+{
+    // 1. If smallestUnit is largestUnit, return duration.
+    if (smallest_unit == largest_unit)
+        return duration;
+
+    // 2. Let largestUnitIndex be the ordinal index of the row of Table 21 whose "Value" column contains largestUnit.
+    auto largest_unit_index = to_underlying(largest_unit);
+
+    // 3. Let smallestUnitIndex be the ordinal index of the row of Table 21 whose "Value" column contains smallestUnit.
+    auto smallest_unit_index = to_underlying(smallest_unit);
+
+    // 4. Let unitIndex be smallestUnitIndex - 1.
+    auto unit_index = smallest_unit_index - 1;
+
+    // 5. Let done be false.
+    auto done = false;
+
+    // 6. Repeat, while unitIndex ≥ largestUnitIndex and done is false,
+    while (unit_index >= largest_unit_index && !done) {
+        // a. Let unit be the value in the "Value" column of Table 21 in the row whose ordinal index is unitIndex.
+        auto unit = static_cast<Unit>(unit_index);
+
+        // b. If unit is not WEEK, or largestUnit is WEEK, then
+        if (unit != Unit::Week || largest_unit == Unit::Week) {
+            DateDuration end_duration;
+
+            // i. If unit is YEAR, then
+            if (unit == Unit::Year) {
+                // 1. Let years be duration.[[Date]].[[Years]] + sign.
+                auto years = duration.date.years + sign;
+
+                // 2. Let endDuration be ? CreateDateDurationRecord(years, 0, 0, 0).
+                end_duration = TRY(create_date_duration_record(vm, years, 0, 0, 0));
+            }
+            // ii. Else if unit is MONTH, then
+            else if (unit == Unit::Month) {
+                // 1. Let months be duration.[[Date]].[[Months]] + sign.
+                auto months = duration.date.months + sign;
+
+                // 2. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, months).
+                end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, months));
+            }
+            // iii. Else,
+            else {
+                // 1. Assert: unit is WEEK.
+                VERIFY(unit == Unit::Week);
+
+                // 2. Let weeks be duration.[[Date]].[[Weeks]] + sign.
+                auto weeks = duration.date.weeks + sign;
+
+                // 3. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, weeks).
+                end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, weeks));
+            }
+
+            // iv. Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, CONSTRAIN).
+            auto end = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, end_duration, Overflow::Constrain));
+
+            // v. Let endDateTime be CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]).
+            auto end_date_time = combine_iso_date_and_time_record(end, iso_date_time.time);
+
+            TimeDuration end_epoch_ns;
+
+            // vi. If timeZone is UNSET, then
+            if (!time_zone.has_value()) {
+                // 1. Let endEpochNs be GetUTCEpochNanoseconds(endDateTime).
+                end_epoch_ns = get_utc_epoch_nanoseconds(end_date_time);
+            }
+            // vii. Else,
+            else {
+                // 1. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE).
+                end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, end_date_time, Disambiguation::Compatible));
+            }
+
+            // viii. Let beyondEnd be nudgedEpochNs - endEpochNs.
+            auto beyond_end = nudged_epoch_ns.minus(end_epoch_ns);
+
+            // ix. If beyondEnd < 0, let beyondEndSign be -1; else if beyondEnd > 0, let beyondEndSign be 1; else let beyondEndSign be 0.
+            auto beyond_end_sign = beyond_end.is_negative() ? -1 : (beyond_end.is_positive() ? 1 : 0);
+
+            // x. If beyondEndSign ≠ -sign, then
+            if (beyond_end_sign != -sign) {
+                // 1. Set duration to ! CombineDateAndTimeDuration(endDuration, 0).
+                duration = MUST(combine_date_and_time_duration(vm, end_duration, TimeDuration { 0 }));
+            }
+            // xi. Else,
+            else {
+                // 1. Set done to true.
+                done = true;
+            }
+        }
+
+        // c. Set unitIndex to unitIndex - 1.
+        --unit_index;
+    }
+
+    // 7. Return duration.
+    return duration;
+}
+
+// 7.5.37 RoundRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone, calendar, largestUnit, increment, smallestUnit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundrelativeduration
+ThrowCompletionOr<InternalDuration> round_relative_duration(VM& vm, InternalDuration duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional<StringView> time_zone, StringView calendar, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode rounding_mode)
+{
+    // 1. Let irregularLengthUnit be false.
+    auto irregular_length_unit = false;
+
+    // 2. If IsCalendarUnit(smallestUnit) is true, set irregularLengthUnit to true.
+    if (is_calendar_unit(smallest_unit))
+        irregular_length_unit = true;
+
+    // 3. If timeZone is not UNSET and smallestUnit is DAY, set irregularLengthUnit to true.
+    if (time_zone.has_value() && smallest_unit == Unit::Day)
+        irregular_length_unit = true;
+
+    // 4. If InternalDurationSign(duration) < 0, let sign be -1; else let sign be 1.
+    i8 sign = internal_duration_sign(duration) < 0 ? -1 : 1;
+
+    DurationNudgeResult nudge_result;
+
+    // 5. If irregularLengthUnit is true, then
+    if (irregular_length_unit) {
+        // a. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode).
+        auto record = TRY(nudge_to_calendar_unit(vm, sign, duration, dest_epoch_ns, iso_date_time, time_zone, calendar, increment, smallest_unit, rounding_mode));
+
+        // b. Let nudgeResult be record.[[NudgeResult]].
+        nudge_result = move(record.nudge_result);
+    }
+    // 6. Else if timeZone is not UNSET, then
+    else if (time_zone.has_value()) {
+        // a. Let nudgeResult be ? NudgeToZonedTime(sign, duration, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode).
+        nudge_result = TRY(nudge_to_zoned_time(vm, sign, duration, iso_date_time, *time_zone, calendar, increment, smallest_unit, rounding_mode));
+    }
+    // 7. Else,
+    else {
+        // a. Let nudgeResult be ? NudgeToDayOrTime(duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode).
+        nudge_result = TRY(nudge_to_day_or_time(vm, duration, dest_epoch_ns, largest_unit, increment, smallest_unit, rounding_mode));
+    }
+
+    // 8. Set duration to nudgeResult.[[Duration]].
+    duration = move(nudge_result.duration);
+
+    // 9. If nudgeResult.[[DidExpandCalendarUnit]] is true and smallestUnit is not WEEK, then
+    if (nudge_result.did_expand_calendar_unit && smallest_unit != Unit::Week) {
+        // a. Let startUnit be LargerOfTwoTemporalUnits(smallestUnit, DAY).
+        auto start_unit = larger_of_two_temporal_units(smallest_unit, Unit::Day);
+
+        // b. Set duration to ? BubbleRelativeDuration(sign, duration, nudgeResult.[[NudgedEpochNs]], isoDateTime, timeZone, calendar, largestUnit, startUnit).
+        duration = TRY(bubble_relative_duration(vm, sign, move(duration), nudge_result.nudged_epoch_ns, iso_date_time, time_zone, calendar, largest_unit, start_unit));
+    }
+
+    // 10. Return duration.
+    return duration;
+}
+
 // 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring
 String temporal_duration_to_string(Duration const& duration, Precision precision)
 {

+ 22 - 0
Libraries/LibJS/Runtime/Temporal/Duration.h

@@ -9,6 +9,7 @@
 #pragma once
 
 #include <AK/Optional.h>
+#include <LibCrypto/BigFraction/BigFraction.h>
 #include <LibCrypto/BigInt/SignedBigInteger.h>
 #include <LibJS/Runtime/Completion.h>
 #include <LibJS/Runtime/Object.h>
@@ -100,15 +101,29 @@ struct InternalDuration {
     TimeDuration time;
 };
 
+// 7.5.32 Duration Nudge Result Records, https://tc39.es/proposal-temporal/#sec-temporal-duration-nudge-result-records
+struct DurationNudgeResult {
+    InternalDuration duration;
+    TimeDuration nudged_epoch_ns;
+    bool did_expand_calendar_unit { false };
+};
+
+struct CalendarNudgeResult {
+    DurationNudgeResult nudge_result;
+    Crypto::BigFraction total;
+};
+
 DateDuration zero_date_duration(VM&);
 InternalDuration to_internal_duration_record(VM&, Duration const&);
 InternalDuration to_internal_duration_record_with_24_hour_days(VM&, Duration const&);
 ThrowCompletionOr<GC::Ref<Duration>> temporal_duration_from_internal(VM&, InternalDuration const&, Unit largest_unit);
 ThrowCompletionOr<DateDuration> create_date_duration_record(VM&, double years, double months, double weeks, double days);
+ThrowCompletionOr<DateDuration> adjust_date_duration_record(VM&, DateDuration const&, double days, Optional<double> weeks = {}, Optional<double> months = {});
 ThrowCompletionOr<InternalDuration> combine_date_and_time_duration(VM&, DateDuration, TimeDuration);
 ThrowCompletionOr<GC::Ref<Duration>> to_temporal_duration(VM&, Value);
 i8 duration_sign(Duration const&);
 i8 date_duration_sign(DateDuration const&);
+i8 internal_duration_sign(InternalDuration const&);
 bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds);
 Unit default_temporal_largest_unit(Duration const&);
 ThrowCompletionOr<PartialDuration> to_temporal_partial_duration_record(VM&, Value temporal_duration_like);
@@ -117,10 +132,17 @@ GC::Ref<Duration> create_negated_temporal_duration(VM&, Duration const&);
 TimeDuration time_duration_from_components(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds);
 ThrowCompletionOr<TimeDuration> add_time_duration(VM&, TimeDuration const&, TimeDuration const&);
 ThrowCompletionOr<TimeDuration> add_24_hour_days_to_time_duration(VM&, TimeDuration const&, double days);
+TimeDuration add_time_duration_to_epoch_nanoseconds(TimeDuration const& duration, TimeDuration const& epoch_nanoseconds);
 i8 compare_time_duration(TimeDuration const&, TimeDuration const&);
+TimeDuration time_duration_from_epoch_nanoseconds_difference(TimeDuration const&, TimeDuration const&);
 ThrowCompletionOr<TimeDuration> round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode);
 i8 time_duration_sign(TimeDuration const&);
 ThrowCompletionOr<TimeDuration> round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode);
+ThrowCompletionOr<CalendarNudgeResult> nudge_to_calendar_unit(VM&, i8 sign, InternalDuration const&, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional<StringView> time_zone, StringView calendar, u64 increment, Unit, RoundingMode);
+ThrowCompletionOr<DurationNudgeResult> nudge_to_zoned_time(VM&, i8 sign, InternalDuration const&, ISODateTime const&, StringView time_zone, StringView calendar, u64 increment, Unit, RoundingMode);
+ThrowCompletionOr<DurationNudgeResult> nudge_to_day_or_time(VM&, InternalDuration const&, TimeDuration const& dest_epoch_ns, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode);
+ThrowCompletionOr<InternalDuration> bubble_relative_duration(VM&, i8 sign, InternalDuration, TimeDuration const& nudged_epoch_ns, ISODateTime const&, Optional<StringView> time_zone, StringView calendar, Unit largest_unit, Unit smallest_unit);
+ThrowCompletionOr<InternalDuration> round_relative_duration(VM&, InternalDuration, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional<StringView> time_zone, StringView calendar, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode);
 double total_time_duration(TimeDuration const&, Unit);
 String temporal_duration_to_string(Duration const&, Precision);
 ThrowCompletionOr<GC::Ref<Duration>> add_durations(VM&, ArithmeticOperation, Duration const&, Value);

+ 13 - 0
Libraries/LibJS/Runtime/Temporal/Instant.cpp

@@ -34,4 +34,17 @@ Crypto::UnsignedBigInteger const SECONDS_PER_MINUTE = 60_bigint;
 Crypto::UnsignedBigInteger const MINUTES_PER_HOUR = 60_bigint;
 Crypto::UnsignedBigInteger const HOURS_PER_DAY = 24_bigint;
 
+// 8.5.1 IsValidEpochNanoseconds ( epochNanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidepochnanoseconds
+bool is_valid_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds)
+{
+    // 1. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then
+    if (epoch_nanoseconds < NANOSECONDS_MIN_INSTANT || epoch_nanoseconds > NANOSECONDS_MAX_INSTANT) {
+        // a. Return false.
+        return false;
+    }
+
+    // 2. Return true.
+    return true;
+}
+
 }

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/Instant.h

@@ -36,4 +36,6 @@ extern Crypto::UnsignedBigInteger const SECONDS_PER_MINUTE;
 extern Crypto::UnsignedBigInteger const MINUTES_PER_HOUR;
 extern Crypto::UnsignedBigInteger const HOURS_PER_DAY;
 
+bool is_valid_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds);
+
 }

+ 40 - 0
Libraries/LibJS/Runtime/Temporal/PlainDate.cpp

@@ -9,6 +9,7 @@
 
 #include <AK/Checked.h>
 #include <LibJS/Runtime/Temporal/Calendar.h>
+#include <LibJS/Runtime/Temporal/DateEquations.h>
 #include <LibJS/Runtime/Temporal/PlainDate.h>
 #include <LibJS/Runtime/Temporal/PlainDateTime.h>
 #include <LibJS/Runtime/Temporal/PlainTime.h>
@@ -25,6 +26,32 @@ ISODate create_iso_date_record(double year, double month, double day)
     return { .year = static_cast<i32>(year), .month = static_cast<u8>(month), .day = static_cast<u8>(day) };
 }
 
+// 3.5.5 ISODateSurpasses ( sign, y1, m1, d1, isoDate2 ), https://tc39.es/proposal-temporal/#sec-temporal-isodatesurpasses
+bool iso_date_surpasses(i8 sign, double year1, double month1, double day1, ISODate iso_date2)
+{
+    // 1. If y1 ≠ isoDate2.[[Year]], then
+    if (year1 != iso_date2.year) {
+        // a. If sign × (y1 - isoDate2.[[Year]]) > 0, return true.
+        if (sign * (year1 - iso_date2.year) > 0)
+            return true;
+    }
+    // 2. Else if m1 ≠ isoDate2.[[Month]], then
+    else if (month1 != iso_date2.month) {
+        // a. If sign × (m1 - isoDate2.[[Month]]) > 0, return true.
+        if (sign * (month1 - iso_date2.month) > 0)
+            return true;
+    }
+    // 3. Else if d1 ≠ isoDate2.[[Day]], then
+    else if (day1 != iso_date2.day) {
+        // a. If sign × (d1 - isoDate2.[[Day]]) > 0, return true.
+        if (sign * (day1 - iso_date2.day) > 0)
+            return true;
+    }
+
+    // 4. Return false.
+    return false;
+}
+
 // 3.5.6 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate
 ThrowCompletionOr<ISODate> regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow)
 {
@@ -86,6 +113,19 @@ bool is_valid_iso_date(double year, double month, double day)
     return true;
 }
 
+// 3.5.8 BalanceISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisodate
+ISODate balance_iso_date(double year, double month, double day)
+{
+    // 1. Let epochDays be ISODateToEpochDays(year, month - 1, day).
+    auto epoch_days = iso_date_to_epoch_days(year, month - 1, day);
+
+    // 2. Let ms be EpochDaysToEpochMs(epochDays, 0).
+    auto ms = epoch_days_to_epoch_ms(epoch_days, 0);
+
+    // 3. Return CreateISODateRecord(EpochTimeToEpochYear(ms), EpochTimeToMonthInYear(ms) + 1, EpochTimeToDate(ms)).
+    return create_iso_date_record(epoch_time_to_epoch_year(ms), epoch_time_to_month_in_year(ms) + 1.0, epoch_time_to_date(ms));
+}
+
 // 3.5.9 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear
 String pad_iso_year(i32 year)
 {

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/PlainDate.h

@@ -23,8 +23,10 @@ struct ISODate {
 };
 
 ISODate create_iso_date_record(double year, double month, double day);
+bool iso_date_surpasses(i8 sign, double year1, double month1, double day1, ISODate iso_date2);
 ThrowCompletionOr<ISODate> regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow);
 bool is_valid_iso_date(double year, double month, double day);
+ISODate balance_iso_date(double year, double month, double day);
 String pad_iso_year(i32 year);
 bool iso_date_within_limits(ISODate);
 i8 compare_iso_date(ISODate, ISODate);

+ 15 - 0
Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp

@@ -8,7 +8,9 @@
 
 #include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/Temporal/Instant.h>
+#include <LibJS/Runtime/Temporal/PlainDate.h>
 #include <LibJS/Runtime/Temporal/PlainDateTime.h>
+#include <LibJS/Runtime/Temporal/PlainTime.h>
 
 namespace JS::Temporal {
 
@@ -52,4 +54,17 @@ bool iso_date_time_within_limits(ISODateTime iso_date_time)
     return true;
 }
 
+// 5.5.7 BalanceISODateTime ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisodatetime
+ISODateTime balance_iso_date_time(double year, double month, double day, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond)
+{
+    // 1. Let balancedTime be BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond).
+    auto balanced_time = balance_time(hour, minute, second, millisecond, microsecond, nanosecond);
+
+    // 2. Let balancedDate be BalanceISODate(year, month, day + balancedTime.[[Days]]).
+    auto balanced_date = balance_iso_date(year, month, day + balanced_time.days);
+
+    // 3. Return CombineISODateAndTimeRecord(balancedDate, balancedTime).
+    return combine_iso_date_and_time_record(balanced_date, balanced_time);
+}
+
 }

+ 1 - 0
Libraries/LibJS/Runtime/Temporal/PlainDateTime.h

@@ -21,5 +21,6 @@ struct ISODateTime {
 
 ISODateTime combine_iso_date_and_time_record(ISODate, Time);
 bool iso_date_time_within_limits(ISODateTime);
+ISODateTime balance_iso_date_time(double year, double month, double day, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond);
 
 }

+ 52 - 0
Libraries/LibJS/Runtime/Temporal/PlainTime.cpp

@@ -7,7 +7,9 @@
  */
 
 #include <AK/Assertions.h>
+#include <LibJS/Runtime/AbstractOperations.h>
 #include <LibJS/Runtime/Temporal/PlainTime.h>
+#include <math.h>
 
 namespace JS::Temporal {
 
@@ -30,6 +32,13 @@ Time create_time_record(double hour, double minute, double second, double millis
     };
 }
 
+// 4.5.3 MidnightTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-midnighttimerecord
+Time midnight_time_record()
+{
+    // 1. Return Time Record { [[Days]]: 0, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0  }.
+    return { .days = 0, .hour = 0, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 };
+}
+
 // 4.5.4 NoonTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord
 Time noon_time_record()
 {
@@ -80,4 +89,47 @@ bool is_valid_time(double hour, double minute, double second, double millisecond
     return true;
 }
 
+// 4.5.10 BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-balancetime
+Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond)
+{
+    // 1. Set microsecond to microsecond + floor(nanosecond / 1000).
+    microsecond += floor(nanosecond / 1000.0);
+
+    // 2. Set nanosecond to nanosecond modulo 1000.
+    nanosecond = modulo(nanosecond, 1000.0);
+
+    // 3. Set millisecond to millisecond + floor(microsecond / 1000).
+    millisecond += floor(microsecond / 1000.0);
+
+    // 4. Set microsecond to microsecond modulo 1000.
+    microsecond = modulo(microsecond, 1000.0);
+
+    // 5. Set second to second + floor(millisecond / 1000).
+    second += floor(millisecond / 1000.0);
+
+    // 6. Set millisecond to millisecond modulo 1000.
+    millisecond = modulo(millisecond, 1000.0);
+
+    // 7. Set minute to minute + floor(second / 60).
+    minute += floor(second / 60.0);
+
+    // 8. Set second to second modulo 60.
+    second = modulo(second, 60.0);
+
+    // 9. Set hour to hour + floor(minute / 60).
+    hour += floor(minute / 60.0);
+
+    // 10. Set minute to minute modulo 60.
+    minute = modulo(minute, 60.0);
+
+    // 11. Let deltaDays be floor(hour / 24).
+    auto delta_days = floor(hour / 24.0);
+
+    // 12. Set hour to hour modulo 24.
+    hour = modulo(hour, 24.0);
+
+    // 13. Return CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond, deltaDays).
+    return create_time_record(hour, minute, second, millisecond, microsecond, nanosecond, delta_days);
+}
+
 }

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/PlainTime.h

@@ -24,7 +24,9 @@ struct Time {
 };
 
 Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days = 0);
+Time midnight_time_record();
 Time noon_time_record();
 bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond);
+Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond);
 
 }

+ 94 - 0
Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp

@@ -9,6 +9,9 @@
 #include <LibJS/Runtime/Intrinsics.h>
 #include <LibJS/Runtime/Realm.h>
 #include <LibJS/Runtime/Temporal/Calendar.h>
+#include <LibJS/Runtime/Temporal/Duration.h>
+#include <LibJS/Runtime/Temporal/PlainDateTime.h>
+#include <LibJS/Runtime/Temporal/PlainTime.h>
 #include <LibJS/Runtime/Temporal/PlainYearMonth.h>
 #include <LibJS/Runtime/Temporal/PlainYearMonthConstructor.h>
 #include <LibJS/Runtime/VM.h>
@@ -127,6 +130,19 @@ bool iso_year_month_within_limits(ISODate iso_date)
     return true;
 }
 
+// 9.5.4 BalanceISOYearMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisoyearmonth
+ISOYearMonth balance_iso_year_month(double year, double month)
+{
+    // 1. Set year to year + floor((month - 1) / 12).
+    year += floor((month - 1.0) / 12.0);
+
+    // 2. Set month to ((month - 1) modulo 12) + 1.
+    month = modulo(month - 1, 12.0) + 1.0;
+
+    // 3. Return ISO Year-Month Record { [[Year]]: year, [[Month]]: month  }.
+    return { .year = static_cast<i32>(year), .month = static_cast<u8>(month) };
+}
+
 // 9.5.5 CreateTemporalYearMonth ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth
 ThrowCompletionOr<GC::Ref<PlainYearMonth>> create_temporal_year_month(VM& vm, ISODate iso_date, String calendar, GC::Ptr<FunctionObject> new_target)
 {
@@ -176,4 +192,82 @@ String temporal_year_month_to_string(PlainYearMonth const& year_month, ShowCalen
     return result;
 }
 
+// 9.5.7 DifferenceTemporalPlainYearMonth ( operation, yearMonth, other, options ), https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplainyearmonth
+ThrowCompletionOr<GC::Ref<Duration>> difference_temporal_plain_year_month(VM& vm, DurationOperation operation, PlainYearMonth const& year_month, Value other_value, Value options)
+{
+    // 1. Set other to ? ToTemporalYearMonth(other).
+    auto other = TRY(to_temporal_year_month(vm, other_value));
+
+    // 2. Let calendar be yearMonth.[[Calendar]].
+    auto const& calendar = year_month.calendar();
+
+    // 3. If CalendarEquals(calendar, other.[[Calendar]]) is false, throw a RangeError exception.
+    if (!calendar_equals(calendar, other->calendar()))
+        return vm.throw_completion<RangeError>(ErrorType::TemporalDifferentCalendars);
+
+    // 4. Let resolvedOptions be ? GetOptionsObject(options).
+    auto resolved_options = TRY(get_options_object(vm, options));
+
+    // 5. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, DATE, « WEEK, DAY », MONTH, YEAR).
+    auto settings = TRY(get_difference_settings(vm, operation, resolved_options, UnitGroup::Date, { { Unit::Week, Unit::Day } }, Unit::Month, Unit::Year));
+
+    // 6. If CompareISODate(yearMonth.[[ISODate]], other.[[ISODate]]) = 0, then
+    if (compare_iso_date(year_month.iso_date(), other->iso_date()) == 0) {
+        // a. Return ! CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0).
+        return MUST(create_temporal_duration(vm, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+    }
+
+    // 7. Let thisFields be ISODateToFields(calendar, yearMonth.[[ISODate]], YEAR-MONTH).
+    auto this_fields = iso_date_to_fields(calendar, year_month.iso_date(), DateType::YearMonth);
+
+    // 8. Set thisFields.[[Day]] to 1.
+    this_fields.day = 1;
+
+    // 9. Let thisDate be ? CalendarDateFromFields(calendar, thisFields, CONSTRAIN).
+    auto this_date = TRY(calendar_date_from_fields(vm, calendar, move(this_fields), Overflow::Constrain));
+
+    // 10. Let otherFields be ISODateToFields(calendar, other.[[ISODate]], YEAR-MONTH).
+    auto other_fields = iso_date_to_fields(calendar, other->iso_date(), DateType::YearMonth);
+
+    // 11. Set otherFields.[[Day]] to 1.
+    other_fields.day = 1;
+
+    // 12. Let otherDate be ? CalendarDateFromFields(calendar, otherFields, CONSTRAIN).
+    auto other_date = TRY(calendar_date_from_fields(vm, calendar, move(other_fields), Overflow::Constrain));
+
+    // 13. Let dateDifference be CalendarDateUntil(calendar, thisDate, otherDate, settings.[[LargestUnit]]).
+    auto date_difference = calendar_date_until(vm, calendar, this_date, other_date, settings.largest_unit);
+
+    // 14. Let yearsMonthsDifference be ! AdjustDateDurationRecord(dateDifference, 0, 0).
+    auto years_months_difference = MUST(adjust_date_duration_record(vm, date_difference, 0, 0));
+
+    // 15. Let duration be ! CombineDateAndTimeDuration(yearsMonthsDifference, 0).
+    auto duration = MUST(combine_date_and_time_duration(vm, years_months_difference, TimeDuration { 0 }));
+
+    // 16. If settings.[[SmallestUnit]] is not MONTH or settings.[[RoundingIncrement]] ≠ 1, then
+    if (settings.smallest_unit != Unit::Month || settings.rounding_increment != 1) {
+        // a. Let isoDateTime be CombineISODateAndTimeRecord(thisDate, MidnightTimeRecord()).
+        auto iso_date_time = combine_iso_date_and_time_record(this_date, midnight_time_record());
+
+        // b. Let isoDateTimeOther be CombineISODateAndTimeRecord(otherDate, MidnightTimeRecord()).
+        auto iso_date_time_other = combine_iso_date_and_time_record(other_date, midnight_time_record());
+
+        // c. Let destEpochNs be GetUTCEpochNanoseconds(isoDateTimeOther).
+        auto dest_epoch_ns = get_utc_epoch_nanoseconds(iso_date_time_other);
+
+        // d. Set duration to ? RoundRelativeDuration(duration, destEpochNs, isoDateTime, UNSET, calendar, settings.[[LargestUnit]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]).
+        duration = TRY(round_relative_duration(vm, move(duration), dest_epoch_ns, iso_date_time, {}, calendar, settings.largest_unit, settings.rounding_increment, settings.smallest_unit, settings.rounding_mode));
+    }
+
+    // 17. Let result be ? TemporalDurationFromInternal(duration, DAY).
+    auto result = TRY(temporal_duration_from_internal(vm, duration, Unit::Day));
+
+    // 18. If operation is SINCE, set result to CreateNegatedTemporalDuration(result).
+    if (operation == DurationOperation::Since)
+        result = create_negated_temporal_duration(vm, result);
+
+    // 19. Return result.
+    return result;
+}
+
 }

+ 8 - 0
Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h

@@ -32,9 +32,17 @@ private:
     String m_calendar;  // [[Calendar]]
 };
 
+// 9.5.1 ISO Year-Month Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-year-month-records
+struct ISOYearMonth {
+    i32 year { 0 };
+    u8 month { 0 };
+};
+
 ThrowCompletionOr<GC::Ref<PlainYearMonth>> to_temporal_year_month(VM&, Value item, Value options = js_undefined());
 bool iso_year_month_within_limits(ISODate);
+ISOYearMonth balance_iso_year_month(double year, double month);
 ThrowCompletionOr<GC::Ref<PlainYearMonth>> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr<FunctionObject> new_target = {});
 String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar);
+ThrowCompletionOr<GC::Ref<Duration>> difference_temporal_plain_year_month(VM&, DurationOperation, PlainYearMonth const&, Value other, Value options);
 
 }

+ 31 - 0
Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp

@@ -7,6 +7,7 @@
 
 #include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/Temporal/Calendar.h>
+#include <LibJS/Runtime/Temporal/Duration.h>
 #include <LibJS/Runtime/Temporal/PlainYearMonthPrototype.h>
 
 namespace JS::Temporal {
@@ -41,6 +42,8 @@ void PlainYearMonthPrototype::initialize(Realm& realm)
 
     u8 attr = Attribute::Writable | Attribute::Configurable;
     define_native_function(realm, vm.names.with, with, 1, attr);
+    define_native_function(realm, vm.names.until, until, 1, attr);
+    define_native_function(realm, vm.names.since, since, 1, attr);
     define_native_function(realm, vm.names.equals, equals, 1, attr);
     define_native_function(realm, vm.names.toString, to_string, 0, attr);
     define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
@@ -170,6 +173,34 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::with)
     return MUST(create_temporal_year_month(vm, iso_date, calendar));
 }
 
+// 9.3.16 Temporal.PlainYearMonth.prototype.until ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.until
+JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::until)
+{
+    auto other = vm.argument(0);
+    auto options = vm.argument(1);
+
+    // 1. Let yearMonth be the this value.
+    // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]).
+    auto year_month = TRY(typed_this_object(vm));
+
+    // 3. Return ? DifferenceTemporalPlainYearMonth(UNTIL, yearMonth, other, options).
+    return TRY(difference_temporal_plain_year_month(vm, DurationOperation::Until, year_month, other, options));
+}
+
+// 9.3.17 Temporal.PlainYearMonth.prototype.since ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.since
+JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::since)
+{
+    auto other = vm.argument(0);
+    auto options = vm.argument(1);
+
+    // 1. Let yearMonth be the this value.
+    // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]).
+    auto year_month = TRY(typed_this_object(vm));
+
+    // 3. Return ? DifferenceTemporalPlainYearMonth(SINCE, yearMonth, other, options).
+    return TRY(difference_temporal_plain_year_month(vm, DurationOperation::Since, year_month, other, options));
+}
+
 // 9.3.18 Temporal.PlainYearMonth.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.equals
 JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::equals)
 {

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h

@@ -34,6 +34,8 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(months_in_year_getter);
     JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter);
     JS_DECLARE_NATIVE_FUNCTION(with);
+    JS_DECLARE_NATIVE_FUNCTION(until);
+    JS_DECLARE_NATIVE_FUNCTION(since);
     JS_DECLARE_NATIVE_FUNCTION(equals);
     JS_DECLARE_NATIVE_FUNCTION(to_string);
     JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

+ 122 - 0
Libraries/LibJS/Runtime/Temporal/TimeZone.cpp

@@ -9,7 +9,10 @@
 #include <LibJS/Runtime/AbstractOperations.h>
 #include <LibJS/Runtime/Date.h>
 #include <LibJS/Runtime/Intl/AbstractOperations.h>
+#include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/Temporal/ISO8601.h>
+#include <LibJS/Runtime/Temporal/Instant.h>
+#include <LibJS/Runtime/Temporal/PlainDateTime.h>
 #include <LibJS/Runtime/Temporal/TimeZone.h>
 #include <LibJS/Runtime/VM.h>
 
@@ -70,6 +73,125 @@ ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM& vm, Value tempora
     return time_zone_identifier_record->identifier;
 }
 
+// 11.1.11 GetEpochNanosecondsFor ( timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor
+ThrowCompletionOr<Crypto::SignedBigInteger> get_epoch_nanoseconds_for(VM& vm, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation)
+{
+    // 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime).
+    auto possible_epoch_ns = TRY(get_possible_epoch_nanoseconds(vm, time_zone, iso_date_time));
+
+    // 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation).
+    return TRY(disambiguate_possible_epoch_nanoseconds(vm, move(possible_epoch_ns), time_zone, iso_date_time, disambiguation));
+}
+
+// 11.1.12 DisambiguatePossibleEpochNanoseconds ( possibleEpochNs, timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds
+ThrowCompletionOr<Crypto::SignedBigInteger> disambiguate_possible_epoch_nanoseconds(VM& vm, Vector<Crypto::SignedBigInteger> possible_epoch_ns, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation)
+{
+    // 1. Let n be possibleEpochNs's length.
+    auto n = possible_epoch_ns.size();
+
+    // 2. If n = 1, then
+    if (n == 1) {
+        // a. Return possibleEpochNs[0].
+        return move(possible_epoch_ns[0]);
+    }
+
+    // 3. If n ≠ 0, then
+    if (n != 0) {
+        // a. If disambiguation is EARLIER or COMPATIBLE, then
+        if (disambiguation == Disambiguation::Earlier || disambiguation == Disambiguation::Compatible) {
+            // i. Return possibleEpochNs[0].
+            return move(possible_epoch_ns[0]);
+        }
+
+        // b. If disambiguation is LATER, then
+        if (disambiguation == Disambiguation::Later) {
+            // i. Return possibleEpochNs[n - 1].
+            return move(possible_epoch_ns[n - 1]);
+        }
+
+        // c. Assert: disambiguation is REJECT.
+        VERIFY(disambiguation == Disambiguation::Reject);
+
+        // d. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(ErrorType::TemporalDisambiguatePossibleEpochNSRejectMoreThanOne);
+    }
+
+    // 4. Assert: n = 0.
+    VERIFY(n == 0);
+
+    // 5. If disambiguation is REJECT, then
+    if (disambiguation == Disambiguation::Reject) {
+        // a. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(ErrorType::TemporalDisambiguatePossibleEpochNSRejectZero);
+    }
+
+    // FIXME: GetNamedTimeZoneEpochNanoseconds currently does not produce zero instants.
+    (void)time_zone;
+    (void)iso_date_time;
+    TODO();
+}
+
+// 11.1.13 GetPossibleEpochNanoseconds ( timeZone, isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds
+ThrowCompletionOr<Vector<Crypto::SignedBigInteger>> get_possible_epoch_nanoseconds(VM& vm, StringView time_zone, ISODateTime const& iso_date_time)
+{
+    Vector<Crypto::SignedBigInteger> possible_epoch_nanoseconds;
+
+    // 1. Let parseResult be ! ParseTimeZoneIdentifier(timeZone).
+    auto parse_result = parse_time_zone_identifier(time_zone);
+
+    // 2. If parseResult.[[OffsetMinutes]] is not empty, then
+    if (parse_result.offset_minutes.has_value()) {
+        // a. Let balanced be BalanceISODateTime(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]], isoDateTime.[[ISODate]].[[Day]], isoDateTime.[[Time]].[[Hour]], isoDateTime.[[Time]].[[Minute]] - parseResult.[[OffsetMinutes]], isoDateTime.[[Time]].[[Second]], isoDateTime.[[Time]].[[Millisecond]], isoDateTime.[[Time]].[[Microsecond]], isoDateTime.[[Time]].[[Nanosecond]]).
+        auto balanced = balance_iso_date_time(
+            iso_date_time.iso_date.year,
+            iso_date_time.iso_date.month,
+            iso_date_time.iso_date.day,
+            iso_date_time.time.hour,
+            static_cast<double>(iso_date_time.time.minute) - static_cast<double>(*parse_result.offset_minutes),
+            iso_date_time.time.second,
+            iso_date_time.time.millisecond,
+            iso_date_time.time.microsecond,
+            iso_date_time.time.nanosecond);
+
+        // b. Perform ? CheckISODaysRange(balanced.[[ISODate]]).
+        TRY(check_iso_days_range(vm, balanced.iso_date));
+
+        // c. Let epochNanoseconds be GetUTCEpochNanoseconds(balanced).
+        auto epoch_nanoseconds = get_utc_epoch_nanoseconds(balanced);
+
+        // d. Let possibleEpochNanoseconds be « epochNanoseconds ».
+        possible_epoch_nanoseconds.append(move(epoch_nanoseconds));
+    }
+    // 3. Else,
+    else {
+        // a. Perform ? CheckISODaysRange(isoDateTime.[[ISODate]]).
+        TRY(check_iso_days_range(vm, iso_date_time.iso_date));
+
+        // b. Let possibleEpochNanoseconds be GetNamedTimeZoneEpochNanoseconds(parseResult.[[Name]], isoDateTime).
+        possible_epoch_nanoseconds = get_named_time_zone_epoch_nanoseconds(
+            *parse_result.name,
+            iso_date_time.iso_date.year,
+            iso_date_time.iso_date.month,
+            iso_date_time.iso_date.day,
+            iso_date_time.time.hour,
+            iso_date_time.time.minute,
+            iso_date_time.time.second,
+            iso_date_time.time.millisecond,
+            iso_date_time.time.microsecond,
+            iso_date_time.time.nanosecond);
+    }
+
+    // 4. For each value epochNanoseconds in possibleEpochNanoseconds, do
+    for (auto const& epoch_nanoseconds : possible_epoch_nanoseconds) {
+        // a. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception.
+        if (!is_valid_epoch_nanoseconds(epoch_nanoseconds))
+            return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidEpochNanoseconds);
+    }
+
+    // 5. Return possibleEpochNanoseconds.
+    return possible_epoch_nanoseconds;
+}
+
 // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
 ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM& vm, StringView identifier)
 {

+ 12 - 0
Libraries/LibJS/Runtime/Temporal/TimeZone.h

@@ -8,6 +8,8 @@
 #pragma once
 
 #include <AK/String.h>
+#include <AK/Vector.h>
+#include <LibCrypto/BigInt/SignedBigInteger.h>
 #include <LibJS/Runtime/Completion.h>
 #include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/Value.h>
@@ -19,8 +21,18 @@ struct TimeZone {
     Optional<i64> offset_minutes;
 };
 
+enum class Disambiguation {
+    Compatible,
+    Earlier,
+    Later,
+    Reject,
+};
+
 String format_offset_time_zone_identifier(i64 offset_minutes, Optional<TimeStyle> = {});
 ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like);
+ThrowCompletionOr<Crypto::SignedBigInteger> get_epoch_nanoseconds_for(VM&, StringView time_zone, ISODateTime const&, Disambiguation);
+ThrowCompletionOr<Crypto::SignedBigInteger> disambiguate_possible_epoch_nanoseconds(VM&, Vector<Crypto::SignedBigInteger> possible_epoch_ns, StringView time_zone, ISODateTime const&, Disambiguation);
+ThrowCompletionOr<Vector<Crypto::SignedBigInteger>> get_possible_epoch_nanoseconds(VM&, StringView time_zone, ISODateTime const&);
 ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM&, StringView identifier);
 TimeZone parse_time_zone_identifier(StringView identifier);
 TimeZone parse_time_zone_identifier(ParseResult const&);

+ 108 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js

@@ -0,0 +1,108 @@
+describe("correct behavior", () => {
+    test("length is 1", () => {
+        expect(Temporal.PlainYearMonth.prototype.since).toHaveLength(1);
+    });
+
+    test("basic functionality", () => {
+        const values = [
+            [[0, 1], [0, 1], "PT0S"],
+            [[2, 3], [1, 2], "P1Y1M"],
+            [[1, 2], [0, 1], "P1Y1M"],
+            [[0, 1], [1, 2], "-P1Y1M"],
+            [[0, 12], [0, 1], "P11M"],
+            [[0, 1], [0, 12], "-P11M"],
+        ];
+        for (const [args, argsOther, expected] of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(...args);
+            const other = new Temporal.PlainYearMonth(...argsOther);
+            expect(plainYearMonth.since(other).toString()).toBe(expected);
+        }
+    });
+
+    test("smallestUnit option", () => {
+        const plainYearMonth = new Temporal.PlainYearMonth(1, 2);
+        const other = new Temporal.PlainYearMonth(0, 1);
+        const values = [
+            ["year", "P1Y"],
+            ["month", "P1Y1M"],
+        ];
+        for (const [smallestUnit, expected] of values) {
+            expect(plainYearMonth.since(other, { smallestUnit }).toString()).toBe(expected);
+        }
+    });
+
+    test("largestUnit option", () => {
+        const plainYearMonth = new Temporal.PlainYearMonth(1, 2);
+        const other = new Temporal.PlainYearMonth(0, 1);
+        const values = [
+            ["year", "P1Y1M"],
+            ["month", "P13M"],
+        ];
+        for (const [largestUnit, expected] of values) {
+            expect(plainYearMonth.since(other, { largestUnit }).toString()).toBe(expected);
+        }
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainYearMonth object", () => {
+        expect(() => {
+            Temporal.PlainYearMonth.prototype.since.call("foo", {});
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth");
+    });
+
+    test("disallowed smallestUnit option values", () => {
+        const values = [
+            "week",
+            "day",
+            "hour",
+            "minute",
+            "second",
+            "millisecond",
+            "microsecond",
+            "nanosecond",
+        ];
+        for (const smallestUnit of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(1970, 1);
+            const other = new Temporal.PlainYearMonth(1970, 1);
+            expect(() => {
+                plainYearMonth.since(other, { smallestUnit });
+            }).toThrowWithMessage(
+                RangeError,
+                `${smallestUnit} is not a valid value for option smallestUnit`
+            );
+        }
+    });
+
+    test("disallowed largestUnit option values", () => {
+        const values = [
+            "week",
+            "day",
+            "hour",
+            "minute",
+            "second",
+            "millisecond",
+            "microsecond",
+            "nanosecond",
+        ];
+        for (const largestUnit of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(1970, 1);
+            const other = new Temporal.PlainYearMonth(1970, 1);
+            expect(() => {
+                plainYearMonth.since(other, { largestUnit });
+            }).toThrowWithMessage(
+                RangeError,
+                `${largestUnit} is not a valid value for option largestUnit`
+            );
+        }
+    });
+
+    test("cannot compare dates from different calendars", () => {
+        const plainYearMonthOne = new Temporal.PlainYearMonth(1970, 1, "iso8601");
+        const plainYearMonthTwo = new Temporal.PlainYearMonth(1970, 1, "gregory");
+
+        expect(() => {
+            plainYearMonthOne.since(plainYearMonthTwo);
+        }).toThrowWithMessage(RangeError, "Cannot compare dates from two different calendars");
+    });
+});

+ 108 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js

@@ -0,0 +1,108 @@
+describe("correct behavior", () => {
+    test("length is 1", () => {
+        expect(Temporal.PlainYearMonth.prototype.until).toHaveLength(1);
+    });
+
+    test("basic functionality", () => {
+        const values = [
+            [[0, 1], [0, 1], "PT0S"],
+            [[1, 2], [2, 3], "P1Y1M"],
+            [[0, 1], [1, 2], "P1Y1M"],
+            [[1, 2], [0, 1], "-P1Y1M"],
+            [[0, 1], [0, 12], "P11M"],
+            [[0, 12], [0, 1], "-P11M"],
+        ];
+        for (const [args, argsOther, expected] of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(...args);
+            const other = new Temporal.PlainYearMonth(...argsOther);
+            expect(plainYearMonth.until(other).toString()).toBe(expected);
+        }
+    });
+
+    test("smallestUnit option", () => {
+        const plainYearMonth = new Temporal.PlainYearMonth(0, 1);
+        const other = new Temporal.PlainYearMonth(1, 2);
+        const values = [
+            ["year", "P1Y"],
+            ["month", "P1Y1M"],
+        ];
+        for (const [smallestUnit, expected] of values) {
+            expect(plainYearMonth.until(other, { smallestUnit }).toString()).toBe(expected);
+        }
+    });
+
+    test("largestUnit option", () => {
+        const plainYearMonth = new Temporal.PlainYearMonth(0, 1);
+        const other = new Temporal.PlainYearMonth(1, 2);
+        const values = [
+            ["year", "P1Y1M"],
+            ["month", "P13M"],
+        ];
+        for (const [largestUnit, expected] of values) {
+            expect(plainYearMonth.until(other, { largestUnit }).toString()).toBe(expected);
+        }
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainYearMonth object", () => {
+        expect(() => {
+            Temporal.PlainYearMonth.prototype.until.call("foo", {});
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth");
+    });
+
+    test("disallowed smallestUnit option values", () => {
+        const values = [
+            "week",
+            "day",
+            "hour",
+            "minute",
+            "second",
+            "millisecond",
+            "microsecond",
+            "nanosecond",
+        ];
+        for (const smallestUnit of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(1970, 1);
+            const other = new Temporal.PlainYearMonth(1970, 1);
+            expect(() => {
+                plainYearMonth.until(other, { smallestUnit });
+            }).toThrowWithMessage(
+                RangeError,
+                `${smallestUnit} is not a valid value for option smallestUnit`
+            );
+        }
+    });
+
+    test("disallowed largestUnit option values", () => {
+        const values = [
+            "week",
+            "day",
+            "hour",
+            "minute",
+            "second",
+            "millisecond",
+            "microsecond",
+            "nanosecond",
+        ];
+        for (const largestUnit of values) {
+            const plainYearMonth = new Temporal.PlainYearMonth(1970, 1);
+            const other = new Temporal.PlainYearMonth(1970, 1);
+            expect(() => {
+                plainYearMonth.until(other, { largestUnit });
+            }).toThrowWithMessage(
+                RangeError,
+                `${largestUnit} is not a valid value for option largestUnit`
+            );
+        }
+    });
+
+    test("cannot compare dates from different calendars", () => {
+        const plainYearMonthOne = new Temporal.PlainYearMonth(1970, 1, "iso8601");
+        const plainYearMonthTwo = new Temporal.PlainYearMonth(1970, 1, "gregory");
+
+        expect(() => {
+            plainYearMonthOne.until(plainYearMonthTwo);
+        }).toThrowWithMessage(RangeError, "Cannot compare dates from two different calendars");
+    });
+});