diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 50e0f2800b..713272628d 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -360,6 +360,7 @@ namespace JS { P(reduceRight) \ P(region) \ P(reject) \ + P(relativeTo) \ P(repeat) \ P(resolve) \ P(resolvedOptions) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index a26a1cad63..22def7516d 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -225,6 +225,7 @@ M(TemporalInvalidUnitRange, "Invalid unit range, {} is larger than {}") \ M(TemporalInvalidZonedDateTimeOffset, "Invalid offset for the provided date and time in the current time zone") \ M(TemporalMissingOptionsObject, "Required options object is missing or undefined") \ + M(TemporalMissingStartingPoint, "A starting point is required for balancing {}") \ M(TemporalObjectMustHaveOneOf, "Object must have at least one of the following properties: {}") \ M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \ M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 1fd8b32f2e..48558bf445 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2021, Idan Horowitz * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -514,6 +515,152 @@ ThrowCompletionOr> to_smallest_temporal_unit(GlobalObject& glob return smallest_unit; } +// 13.21 ToRelativeTemporalObject ( options ), https://tc39.es/proposal-temporal/#sec-temporal-torelativetemporalobject +ThrowCompletionOr to_relative_temporal_object(GlobalObject& global_object, Object const& options) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(options) is Object. + + // 2. Let value be ? Get(options, "relativeTo"). + auto value = TRY(options.get(vm.names.relativeTo)); + + // 3. If value is undefined, then + if (value.is_undefined()) { + // a. Return value. + return value; + } + + // 4. Let offsetBehaviour be option. + auto offset_behavior = OffsetBehavior::Option; + + // 5. Let matchBehaviour be match exactly. + auto match_behavior = MatchBehavior::Exactly; + + ISODateTime result; + Value offset_string; + Value time_zone; + Object* calendar = nullptr; + + // 6. If Type(value) is Object, then + if (value.is_object()) { + auto& value_object = value.as_object(); + + // a. If value has either an [[InitializedTemporalDateTime]] or [[InitializedTemporalZonedDateTime]] internal slot, then + if (is(value_object) || is(value_object)) { + // i. Return value. + return value; + } + + // b. If value has an [[InitializedTemporalDate]] internal slot, then + if (is(value_object)) { + auto& plain_date_object = static_cast(value_object); + + // i. Return ? CreateTemporalDateTime(value.[[ISOYear]], value.[[ISOMonth]], value.[[ISODay]], 0, 0, 0, 0, 0, 0, value.[[Calendar]]). + return TRY(create_temporal_date_time(global_object, plain_date_object.iso_year(), plain_date_object.iso_month(), plain_date_object.iso_day(), 0, 0, 0, 0, 0, 0, plain_date_object.calendar())); + } + + // c. Let calendar be ? GetTemporalCalendarWithISODefault(value). + calendar = TRY(get_temporal_calendar_with_iso_default(global_object, value_object)); + + // d. Let fieldNames be ? CalendarFields(calendar, « "day", "hour", "microsecond", "millisecond", "minute", "month", "monthCode", "nanosecond", "second", "year" »). + auto field_names = TRY(calendar_fields(global_object, *calendar, { "day"sv, "hour"sv, "microsecond"sv, "millisecond"sv, "minute"sv, "month"sv, "monthCode"sv, "nanosecond"sv, "second"sv, "year"sv })); + + // e. Let fields be ? PrepareTemporalFields(value, fieldNames, «»). + auto* fields = TRY(prepare_temporal_fields(global_object, value_object, field_names, {})); + + // f. Let dateOptions be ! OrdinaryObjectCreate(null). + auto* date_options = Object::create(global_object, nullptr); + + // g. Perform ! CreateDataPropertyOrThrow(dateOptions, "overflow", "constrain"). + MUST(date_options->create_data_property_or_throw(vm.names.overflow, js_string(vm, "constrain"sv))); + + // h. Let result be ? InterpretTemporalDateTimeFields(calendar, fields, dateOptions). + result = TRY(interpret_temporal_date_time_fields(global_object, *calendar, *fields, *date_options)); + + // i. Let offsetString be ? Get(value, "offset"). + offset_string = TRY(value_object.get(vm.names.offset)); + + // j. Let timeZone be ? Get(value, "timeZone"). + time_zone = TRY(value_object.get(vm.names.timeZone)); + + // k. If offsetString is undefined, then + if (offset_string.is_undefined()) { + // i. Set offsetBehaviour to wall. + offset_behavior = OffsetBehavior::Wall; + } + } + // 7. Else, + else { + // a. Let string be ? ToString(value). + auto string = TRY(value.to_string(global_object)); + + // b. Let result be ? ParseTemporalRelativeToString(string). + auto parsed_result = TRY(parse_temporal_relative_to_string(global_object, string)); + + // NOTE: The ISODateTime struct inside `parsed_result` will be moved into `result` at the end of this path to avoid mismatching names. + // Thus, all remaining references to `result` in this path actually refer to `parsed_result`. + + // c. Let calendar be ? ToTemporalCalendarWithISODefault(result.[[Calendar]]). + calendar = TRY(to_temporal_calendar_with_iso_default(global_object, parsed_result.date_time.calendar.has_value() ? js_string(vm, *parsed_result.date_time.calendar) : js_undefined())); + + // d. Let offsetString be result.[[TimeZoneOffset]]. + offset_string = parsed_result.time_zone.offset.has_value() ? js_string(vm, *parsed_result.time_zone.offset) : js_undefined(); + + // e. Let timeZone be result.[[TimeZoneIANAName]]. + time_zone = parsed_result.time_zone.name.has_value() ? js_string(vm, *parsed_result.time_zone.name) : js_undefined(); + + // f. If result.[[TimeZoneZ]] is true, then + if (parsed_result.time_zone.z) { + // i. Set offsetBehaviour to exact. + offset_behavior = OffsetBehavior::Exact; + } + // g. Else if offsetString is undefined, then + else if (offset_string.is_undefined()) { + // i. Set offsetBehaviour to wall. + offset_behavior = OffsetBehavior::Wall; + } + + // h. Set matchBehaviour to match minutes. + match_behavior = MatchBehavior::Minutes; + + // See NOTE above about why this is done. + result = move(parsed_result.date_time); + } + + // 8. If timeZone is not undefined, then + if (!time_zone.is_undefined()) { + // a. Set timeZone to ? ToTemporalTimeZone(timeZone). + time_zone = TRY(to_temporal_time_zone(global_object, time_zone)); + + double offset_ns; + + // b. If offsetBehaviour is option, then + if (offset_behavior == OffsetBehavior::Option) { + // i. Set offsetString to ? ToString(offsetString). + // NOTE: offsetString is not used after this path, so we don't need to put this into the original offset_string which is of type JS::Value. + auto actual_offset_string = TRY(offset_string.to_string(global_object)); + + // ii. Let offsetNs be ? ParseTimeZoneOffsetString(offsetString). + offset_ns = TRY(parse_time_zone_offset_string(global_object, actual_offset_string)); + } + // c. Else, + else { + // i. Let offsetNs be 0. + offset_ns = 0; + } + + // d. Let epochNanoseconds be ? InterpretISODateTimeOffset(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], offsetBehaviour, offsetNs, timeZone, "compatible", "reject", matchBehaviour). + auto* epoch_nanoseconds = TRY(interpret_iso_date_time_offset(global_object, result.year, result.month, result.day, result.hour, result.minute, result.second, result.millisecond, result.microsecond, result.nanosecond, offset_behavior, offset_ns, time_zone, "compatible"sv, "reject"sv, match_behavior)); + + // e. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar). + return MUST(create_temporal_zoned_date_time(global_object, *epoch_nanoseconds, time_zone.as_object(), *calendar)); + } + + // 9. Return ? CreateTemporalDateTime(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], calendar). + return TRY(create_temporal_date_time(global_object, result.year, result.month, result.day, result.hour, result.minute, result.second, result.millisecond, result.microsecond, result.nanosecond, *calendar)); +} + // 13.22 ValidateTemporalUnitRange ( largestUnit, smallestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-validatetemporalunitrange ThrowCompletionOr validate_temporal_unit_range(GlobalObject& global_object, StringView largest_unit, StringView smallest_unit) { @@ -1114,6 +1261,39 @@ ThrowCompletionOr parse_temporal_month_day_string(GlobalObject return TemporalMonthDay { .year = year, .month = result.month, .day = result.day, .calendar = move(result.calendar) }; } +// 13.42 ParseTemporalRelativeToString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalrelativetostring +ThrowCompletionOr parse_temporal_relative_to_string(GlobalObject& global_object, String const& iso_string) +{ + // 1. Assert: Type(isoString) is String. + + // 2. If isoString does not satisfy the syntax of a TemporalRelativeToString (see 13.33), then + // a. Throw a RangeError exception. + // TODO + + // 3. Let result be ! ParseISODateTime(isoString). + auto result = MUST(parse_iso_date_time(global_object, iso_string)); + + bool z; + Optional offset; + Optional time_zone; + + // TODO: 4. If isoString satisfies the syntax of a TemporalZonedDateTimeString (see 13.33), then + // a. Let timeZoneResult be ! ParseTemporalTimeZoneString(isoString). + // b. Let z be timeZoneResult.[[Z]]. + // c. Let offset be timeZoneResult.[[Offset]]. + // d. Let timeZone be timeZoneResult.[[Name]]. + + // TODO: 5. Else, + // a. Let z be false. + z = false; + + // b. Let offset be undefined. (NOTE: It already is) + // c. Let timeZone be undefined. (NOTE: It already is) + + // 6. Return the Record { [[Year]]: result.[[Year]], [[Month]]: result.[[Month]], [[Day]]: result.[[Day]], [[Hour]]: result.[[Hour]], [[Minute]]: result.[[Minute]], [[Second]]: result.[[Second]], [[Millisecond]]: result.[[Millisecond]], [[Microsecond]]: result.[[Microsecond]], [[Nanosecond]]: result.[[Nanosecond]], [[Calendar]]: result.[[Calendar]], [[TimeZoneZ]]: z, [[TimeZoneOffset]]: offset, [[TimeZoneIANAName]]: timeZone }. + return TemporalZonedDateTime { .date_time = move(result), .time_zone = { .z = z, .offset = move(offset), .name = move(time_zone) } }; +} + // 13.43 ParseTemporalTimeString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimestring ThrowCompletionOr parse_temporal_time_string(GlobalObject& global_object, [[maybe_unused]] String const& iso_string) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index dbf941dabc..2b607bced4 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -113,6 +113,7 @@ ThrowCompletionOr to_temporal_date_time_rounding_increment(GlobalObject&, O ThrowCompletionOr to_seconds_string_precision(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_largest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, String const& fallback, Optional auto_value); ThrowCompletionOr> to_smallest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, Optional fallback); +ThrowCompletionOr to_relative_temporal_object(GlobalObject&, Object const& options); ThrowCompletionOr validate_temporal_unit_range(GlobalObject&, StringView largest_unit, StringView smallest_unit); String larger_of_two_temporal_units(StringView, StringView); ThrowCompletionOr merge_largest_unit_option(GlobalObject&, Object& options, String largest_unit); @@ -131,6 +132,7 @@ ThrowCompletionOr parse_temporal_date_string(GlobalObject&, String ThrowCompletionOr parse_temporal_date_time_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_duration_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_month_day_string(GlobalObject&, String const& iso_string); +ThrowCompletionOr parse_temporal_relative_to_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_time_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_time_zone_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_year_month_string(GlobalObject&, String const& iso_string); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 1a86899b31..1e40dffff7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -161,7 +161,7 @@ ThrowCompletionOr calendar_date_add(GlobalObject& global_object, Obj } // 12.1.8 CalendarDateUntil ( calendar, one, two, options [ , dateUntil ] ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil -ThrowCompletionOr calendar_date_until(GlobalObject& global_object, Object& calendar, PlainDate& one, PlainDate& two, Object& options, FunctionObject* date_until) +ThrowCompletionOr calendar_date_until(GlobalObject& global_object, Object& calendar, Value one, Value two, Object& options, FunctionObject* date_until) { auto& vm = global_object.vm(); @@ -172,7 +172,7 @@ ThrowCompletionOr calendar_date_until(GlobalObject& global_object, Ob date_until = TRY(Value(&calendar).get_method(global_object, vm.names.dateUntil)); // 3. Let duration be ? Call(dateUntil, calendar, « one, two, options »). - auto duration = TRY(call(global_object, date_until ?: js_undefined(), &calendar, &one, &two, &options)); + auto duration = TRY(call(global_object, date_until ?: js_undefined(), &calendar, one, two, &options)); // 4. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). auto* duration_object = TRY(duration.to_object(global_object)); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h index 5b2da3de5c..cd344acd1f 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -37,7 +37,7 @@ Calendar* get_iso8601_calendar(GlobalObject&); ThrowCompletionOr> calendar_fields(GlobalObject&, Object& calendar, Vector const& field_names); ThrowCompletionOr calendar_merge_fields(GlobalObject&, Object& calendar, Object& fields, Object& additional_fields); ThrowCompletionOr calendar_date_add(GlobalObject&, Object& calendar, Value date, Duration&, Object* options, FunctionObject* date_add = nullptr); -ThrowCompletionOr calendar_date_until(GlobalObject&, Object& calendar, PlainDate& one, PlainDate& two, Object& options, FunctionObject* date_until = nullptr); +ThrowCompletionOr calendar_date_until(GlobalObject&, Object& calendar, Value one, Value two, Object& options, FunctionObject* date_until = nullptr); ThrowCompletionOr calendar_year(GlobalObject&, Object& calendar, Object& date_like); ThrowCompletionOr calendar_month(GlobalObject&, Object& calendar, Object& date_like); ThrowCompletionOr calendar_month_code(GlobalObject&, Object& calendar, Object& date_like); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 7c2286c6fa..1eba7ec999 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -262,6 +262,34 @@ Duration* create_negated_temporal_duration(GlobalObject& global_object, Duration return MUST(create_temporal_duration(global_object, -duration.years(), -duration.months(), -duration.weeks(), -duration.days(), -duration.hours(), -duration.minutes(), -duration.seconds(), -duration.milliseconds(), -duration.microseconds(), -duration.nanoseconds())); } +// 7.5.9 CalculateOffsetShift ( relativeTo, y, mon, w, d, h, min, s, ms, mus, ns ), https://tc39.es/proposal-temporal/#sec-temporal-calculateoffsetshift +ThrowCompletionOr calculate_offset_shift(GlobalObject& global_object, Value relative_to_value, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) +{ + // 1. If Type(relativeTo) is not Object or relativeTo does not have an [[InitializedTemporalZonedDateTime]] internal slot, return 0. + if (!relative_to_value.is_object() || !is(relative_to_value.as_object())) + return 0.0; + + auto& relative_to = static_cast(relative_to_value.as_object()); + + // 2. Let instant be ! CreateTemporalInstant(relativeTo.[[Nanoseconds]]). + auto* instant = MUST(create_temporal_instant(global_object, relative_to.nanoseconds())); + + // 3. Let offsetBefore be ? GetOffsetNanosecondsFor(relativeTo.[[TimeZone]], instant). + auto offset_before = TRY(get_offset_nanoseconds_for(global_object, &relative_to.time_zone(), *instant)); + + // 4. Let after be ? AddZonedDateTime(relativeTo.[[Nanoseconds]], relativeTo.[[TimeZone]], relativeTo.[[Calendar]], y, mon, w, d, h, min, s, ms, mus, ns). + auto* after = TRY(add_zoned_date_time(global_object, relative_to.nanoseconds(), &relative_to.time_zone(), relative_to.calendar(), years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); + + // 5. Let instantAfter be ! CreateTemporalInstant(after). + auto* instant_after = MUST(create_temporal_instant(global_object, *after)); + + // 6. Let offsetAfter be ? GetOffsetNanosecondsFor(relativeTo.[[TimeZone]], instantAfter). + auto offset_after = TRY(get_offset_nanoseconds_for(global_object, &relative_to.time_zone(), *instant_after)); + + // 7. Return offsetAfter − offsetBefore. + return offset_after - offset_before; +} + // 7.5.10 TotalDurationNanoseconds ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, offsetShift ), https://tc39.es/proposal-temporal/#sec-temporal-totaldurationnanoseconds BigInt* total_duration_nanoseconds(GlobalObject& global_object, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, BigInt const& nanoseconds, double offset_shift) { @@ -441,6 +469,192 @@ ThrowCompletionOr balance_duration(GlobalObject& global_object return BalancedDuration { .days = days, .hours = hours * sign, .minutes = minutes * sign, .seconds = seconds * sign, .milliseconds = milliseconds * sign, .microseconds = microseconds * sign, .nanoseconds = result_nanoseconds * sign }; } +// 7.5.12 UnbalanceDurationRelative ( years, months, weeks, days, largestUnit, relativeTo ), https://tc39.es/proposal-temporal/#sec-temporal-unbalancedurationrelative +ThrowCompletionOr unbalance_duration_relative(GlobalObject& global_object, double years, double months, double weeks, double days, String const& largest_unit, Value relative_to) +{ + auto& vm = global_object.vm(); + + // 1. If largestUnit is "year", or years, months, weeks, and days are all 0, then + if (largest_unit == "year"sv || (years == 0 && months == 0 && weeks == 0 && days == 0)) { + // a. Return the Record { [[Years]]: years, [[Months]]: months, [[Weeks]]: weeks, [[Days]]: days }. + return UnbalancedDuration { .years = years, .months = months, .weeks = weeks, .days = days }; + } + + // 2. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). + auto sign = duration_sign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); + + // 3. Assert: sign ≠ 0. + VERIFY(sign != 0); + + // 4. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). + auto* one_year = MUST(create_temporal_duration(global_object, sign, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + // 5. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). + auto* one_month = MUST(create_temporal_duration(global_object, 0, sign, 0, 0, 0, 0, 0, 0, 0, 0)); + + // 6. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). + auto* one_week = MUST(create_temporal_duration(global_object, 0, 0, sign, 0, 0, 0, 0, 0, 0, 0)); + + Object* calendar; + + // 7. If relativeTo is not undefined, then + if (!relative_to.is_undefined()) { + // a. Set relativeTo to ? ToTemporalDateTime(relativeTo). + PlainDateTime* relative_to_plain_date_time = TRY(to_temporal_date_time(global_object, relative_to)); + relative_to = relative_to_plain_date_time; + + // b. Let calendar be relativeTo.[[Calendar]]. + calendar = &relative_to_plain_date_time->calendar(); + } + // 8. Else, + else { + // a. Let calendar be undefined. + calendar = nullptr; + } + + // 9. If largestUnit is "month", then + if (largest_unit == "month"sv) { + // a. If calendar is undefined, then + if (!calendar) { + // i. Throw a RangeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingStartingPoint, "months"); + } + + // b. Let dateAdd be ? GetMethod(calendar, "dateAdd"). + auto* date_add = TRY(Value(calendar).get_method(global_object, vm.names.dateAdd)); + + // c. Let dateUntil be ? GetMethod(calendar, "dateUntil"). + auto* date_until = TRY(Value(calendar).get_method(global_object, vm.names.dateUntil)); + + // d. Repeat, while years ≠ 0, + while (years != 0) { + // i. Let addOptions be ! OrdinaryObjectCreate(null). + auto* add_options = Object::create(global_object, nullptr); + + // ii. Let newRelativeTo be ? CalendarDateAdd(calendar, relativeTo, oneYear, addOptions, dateAdd). + auto* new_relative_to = TRY(calendar_date_add(global_object, *calendar, relative_to, *one_year, add_options, date_add)); + + // iii. Let untilOptions be ! OrdinaryObjectCreate(null). + auto* until_options = Object::create(global_object, nullptr); + + // iv. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). + MUST(until_options->create_data_property_or_throw(vm.names.largestUnit, js_string(vm, "month"sv))); + + // v. Let untilResult be ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). + auto* until_result = TRY(calendar_date_until(global_object, *calendar, relative_to, new_relative_to, *until_options, date_until)); + + // vi. Let oneYearMonths be untilResult.[[Months]]. + auto one_year_months = until_result->months(); + + // vii. Set relativeTo to newRelativeTo. + relative_to = new_relative_to; + + // viii. Set years to years − sign. + years -= sign; + + // ix. Set months to months + oneYearMonths. + months += one_year_months; + } + } + // 10. Else if largestUnit is "week", then + else if (largest_unit == "week"sv) { + // a. If calendar is undefined, then + if (!calendar) { + // i. Throw a RangeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingStartingPoint, "weeks"); + } + + // b. Repeat, while years ≠ 0, + while (years != 0) { + // i. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear). + auto move_result = TRY(move_relative_date(global_object, *calendar, verify_cast(relative_to.as_object()), *one_year)); + + // ii. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.relative_to.cell(); + + // iii. Set days to days + moveResult.[[Days]]. + days += move_result.days; + + // iv. Set years to years − sign. + years -= sign; + } + + // c. Repeat, while months ≠ 0, + while (months != 0) { + // i. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth). + auto move_result = TRY(move_relative_date(global_object, *calendar, verify_cast(relative_to.as_object()), *one_month)); + + // ii. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.relative_to.cell(); + + // iii. Set days to days + moveResult.[[Days]]. + days += move_result.days; + + // iv. Set months to months − sign. + months -= sign; + } + } + // 11. Else, + else { + // a. If any of years, months, and weeks are not zero, then + if (years != 0 || months != 0 || weeks != 0) { + // i. If calendar is undefined, then + if (!calendar) { + // i. Throw a RangeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingStartingPoint, "calendar units"); + } + + // ii. Repeat, while years ≠ 0, + while (years != 0) { + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear). + auto move_result = TRY(move_relative_date(global_object, *calendar, verify_cast(relative_to.as_object()), *one_year)); + + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.relative_to.cell(); + + // 3. Set days to days + moveResult.[[Days]]. + days += move_result.days; + + // 4. Set years to years − sign. + years -= sign; + } + + // iii. Repeat, while months ≠ 0, + while (months != 0) { + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth). + auto move_result = TRY(move_relative_date(global_object, *calendar, verify_cast(relative_to.as_object()), *one_month)); + + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.relative_to.cell(); + + // 3. Set days to days +moveResult.[[Days]]. + days += move_result.days; + + // 4. Set months to months − sign. + months -= sign; + } + + // iv. Repeat, while weeks ≠ 0, + while (weeks != 0) { + // 1. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneWeek). + auto move_result = TRY(move_relative_date(global_object, *calendar, verify_cast(relative_to.as_object()), *one_week)); + + // 2. Set relativeTo to moveResult.[[RelativeTo]]. + relative_to = move_result.relative_to.cell(); + + // 3. Set days to days + moveResult.[[Days]]. + days += move_result.days; + + // 4. Set weeks to weeks − sign. + weeks -= sign; + } + } + } + + // 12. Return the Record { [[Years]]: years, [[Months]]: months, [[Weeks]]: weeks, [[Days]]: days }. + return UnbalancedDuration { .years = years, .months = months, .weeks = weeks, .days = days }; +} + // 7.5.16 MoveRelativeDate ( calendar, relativeTo, duration ), https://tc39.es/proposal-temporal/#sec-temporal-moverelativedate ThrowCompletionOr move_relative_date(GlobalObject& global_object, Object& calendar, PlainDateTime& relative_to, Duration& duration) { @@ -615,7 +829,7 @@ ThrowCompletionOr round_duration(GlobalObject& global_object, d MUST(until_options->create_data_property_or_throw(vm.names.largestUnit, js_string(vm, "year"sv))); // p. Let timePassed be ? CalendarDateUntil(calendar, relativeTo, daysLater, untilOptions). - auto* time_passed = TRY(calendar_date_until(global_object, *calendar, *relative_to_date, *days_later, *until_options)); + auto* time_passed = TRY(calendar_date_until(global_object, *calendar, relative_to_date, days_later, *until_options)); // q. Let yearsPassed be timePassed.[[Years]]. auto years_passed = time_passed->years(); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h index 297f0bfefb..7843d7f5c0 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -109,6 +109,14 @@ struct RoundedDuration { double remainder; }; +// Used by UnbalanceDurationRelative to temporarily hold values +struct UnbalancedDuration { + double years; + double months; + double weeks; + double days; +}; + // Table 7: Properties of a TemporalDurationLike, https://tc39.es/proposal-temporal/#table-temporal-temporaldurationlike-properties template @@ -141,8 +149,10 @@ bool is_valid_duration(double years, double months, double weeks, double days, d ThrowCompletionOr to_partial_duration(GlobalObject&, Value temporal_duration_like); ThrowCompletionOr create_temporal_duration(GlobalObject&, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, FunctionObject const* new_target = nullptr); Duration* create_negated_temporal_duration(GlobalObject& global_object, Duration const& duration); +ThrowCompletionOr calculate_offset_shift(GlobalObject&, Value relative_to_value, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); BigInt* total_duration_nanoseconds(GlobalObject&, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, BigInt const& nanoseconds, double offset_shift); ThrowCompletionOr balance_duration(GlobalObject&, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, BigInt const& nanoseconds, String const& largest_unit, Object* relative_to = nullptr); +ThrowCompletionOr unbalance_duration_relative(GlobalObject&, double years, double months, double weeks, double days, String const& largest_unit, Value relative_to); ThrowCompletionOr move_relative_date(GlobalObject&, Object& calendar, PlainDateTime& relative_to, Duration& duration); ThrowCompletionOr move_relative_zoned_date_time(GlobalObject&, ZonedDateTime&, double years, double months, double weeks, double days); ThrowCompletionOr round_duration(GlobalObject&, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, u32 increment, StringView unit, StringView rounding_mode, Object* relative_to_object = nullptr); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp index cacdd6cf02..f8df847328 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -397,7 +397,7 @@ ThrowCompletionOr difference_iso_date_time(GlobalObject& globa auto* until_options = TRY(merge_largest_unit_option(global_object, *options, move(date_largest_unit))); // 12. Let dateDifference be ? CalendarDateUntil(calendar, date1, date2, untilOptions). - auto* date_difference = TRY(calendar_date_until(global_object, calendar, *date1, *date2, *until_options)); + auto* date_difference = TRY(calendar_date_until(global_object, calendar, date1, date2, *until_options)); // 13. Let balanceResult be ? BalanceDuration(dateDifference.[[Days]], timeDifference.[[Hours]], timeDifference.[[Minutes]], timeDifference.[[Seconds]], timeDifference.[[Milliseconds]], timeDifference.[[Microseconds]], timeDifference.[[Nanoseconds]], largestUnit). auto balance_result_ = TRY(balance_duration(global_object, date_difference->days(), time_difference.hours, time_difference.minutes, time_difference.seconds, time_difference.milliseconds, time_difference.microseconds, *js_bigint(vm, { (i32)time_difference.nanoseconds }), largest_unit));