diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 1a053abcc7e..9ef3a79c3d1 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -223,6 +223,7 @@ M(TemporalInvalidTime, "Invalid time") \ M(TemporalInvalidTimeZoneName, "Invalid time zone name") \ 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(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 c55f26f4a50..3a2477a3f2f 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -224,6 +224,18 @@ ThrowCompletionOr to_temporal_rounding_mode(GlobalObject& global_object, return option.as_string().string(); } +// 13.10 ToTemporalOffset ( normalizedOptions, fallback ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaloffset +ThrowCompletionOr to_temporal_offset(GlobalObject& global_object, Object const& normalized_options, String const& fallback) +{ + auto& vm = global_object.vm(); + + // 1. Return ? GetOption(normalizedOptions, "offset", « String », « "prefer", "use", "ignore", "reject" », fallback). + auto option = TRY(get_option(global_object, normalized_options, vm.names.offset, { OptionType::String }, { "prefer"sv, "use"sv, "ignore"sv, "reject"sv }, js_string(vm, fallback))); + + VERIFY(option.is_string()); + return option.as_string().string(); +} + // 13.11 ToShowCalendarOption ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-toshowcalendaroption ThrowCompletionOr to_show_calendar_option(GlobalObject& global_object, Object const& normalized_options) { @@ -950,6 +962,27 @@ ThrowCompletionOr parse_temporal_instant_string(GlobalObject& g return TemporalInstant { .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, .time_zone_offset = move(offset_string) }; } +// 13.36 ParseTemporalZonedDateTimeString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalzoneddatetimestring +ThrowCompletionOr parse_temporal_zoned_date_time_string(GlobalObject& global_object, String const& iso_string) +{ + // 1. Assert: Type(isoString) is String. + + // 2. If isoString does not satisfy the syntax of a TemporalZonedDateTimeString (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)); + + // 4. Let timeZoneResult be ? ParseTemporalTimeZoneString(isoString). + auto time_zone_result = TRY(parse_temporal_time_zone_string(global_object, iso_string)); + + // 5. 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]]: timeZoneResult.[[Z]], [[TimeZoneOffsetString]]: timeZoneResult.[[OffsetString]], [[TimeZoneName]]: timeZoneResult.[[Name]] }. + // NOTE: This returns the two structs together instead of separated to avoid a copy in ToTemporalZonedDateTime, as the spec tries to put the result of InterpretTemporalDateTimeFields and ParseTemporalZonedDateTimeString into the same `result` variable. + // InterpretTemporalDateTimeFields returns an ISODateTime, so the moved in `result` here is subsequently moved into ParseTemporalZonedDateTimeString's `result` variable. + return TemporalZonedDateTime { .date_time = move(result), .time_zone = move(time_zone_result) }; +} + // 13.37 ParseTemporalCalendarString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalcalendarstring ThrowCompletionOr parse_temporal_calendar_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 65c40d91e1b..241880cf459 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -78,6 +78,11 @@ struct TemporalYearMonth { Optional calendar = {}; }; +struct TemporalZonedDateTime { + ISODateTime date_time; + TemporalTimeZone time_zone; +}; + struct SecondsStringPrecision { Variant precision; String unit; @@ -92,6 +97,7 @@ ThrowCompletionOr> get_string_or_number_option(Globa ThrowCompletionOr to_temporal_overflow(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_temporal_disambiguation(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_temporal_rounding_mode(GlobalObject&, Object const& normalized_options, String const& fallback); +ThrowCompletionOr to_temporal_offset(GlobalObject&, Object const& normalized_options, String const& fallback); ThrowCompletionOr to_show_calendar_option(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_temporal_rounding_increment(GlobalObject&, Object const& normalized_options, Optional dividend, bool inclusive); ThrowCompletionOr to_temporal_date_time_rounding_increment(GlobalObject&, Object const& normalized_options, StringView smallest_unit); @@ -109,6 +115,7 @@ i64 round_number_to_increment(double, u64 increment, StringView rounding_mode); BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, StringView rounding_mode); ThrowCompletionOr parse_iso_date_time(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_instant_string(GlobalObject&, String const& iso_string); +ThrowCompletionOr parse_temporal_zoned_date_time_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_calendar_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_date_string(GlobalObject&, String const& iso_string); ThrowCompletionOr parse_temporal_date_time_string(GlobalObject&, String const& iso_string); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp index 74b0c733a8f..c317789a195 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -35,6 +36,220 @@ void ZonedDateTime::visit_edges(Cell::Visitor& visitor) visitor.visit(&m_calendar); } +// 6.5.1 InterpretISODateTimeOffset ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offsetBehaviour, offsetNanoseconds, timeZone, disambiguation, offsetOption, matchBehaviour ), https://tc39.es/proposal-temporal/#sec-temporal-interpretisodatetimeoffset +ThrowCompletionOr interpret_iso_date_time_offset(GlobalObject& global_object, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, OffsetBehavior offset_behavior, double offset_nanoseconds, Value time_zone, StringView disambiguation, StringView offset_option, MatchBehavior match_behavior) +{ + auto& vm = global_object.vm(); + + // 1. Assert: offsetNanoseconds is an integer. + VERIFY(trunc(offset_nanoseconds) == offset_nanoseconds); + + // 2. Let calendar be ! GetISO8601Calendar(). + auto* calendar = get_iso8601_calendar(global_object); + + // 3. Let dateTime be ? CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar). + auto* date_time = TRY(create_temporal_date_time(global_object, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, *calendar)); + + // 4. If offsetBehaviour is wall, or offsetOption is "ignore", then + if (offset_behavior == OffsetBehavior::Wall || offset_option == "ignore"sv) { + // a. Let instant be ? BuiltinTimeZoneGetInstantFor(timeZone, dateTime, disambiguation). + auto* instant = TRY(builtin_time_zone_get_instant_for(global_object, time_zone, *date_time, disambiguation)); + + // b. Return instant.[[Nanoseconds]]. + return &instant->nanoseconds(); + } + + // 5. If offsetBehaviour is exact, or offsetOption is "use", then + if (offset_behavior == OffsetBehavior::Exact || offset_option == "use"sv) { + // a. Let epochNanoseconds be ! GetEpochFromISOParts(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond). + auto* epoch_nanoseconds = get_epoch_from_iso_parts(global_object, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); + + // b. Return epochNanoseconds − offsetNanoseconds. + auto offset_nanoseconds_bigint = Crypto::SignedBigInteger::create_from((i64)offset_nanoseconds); + return js_bigint(vm, epoch_nanoseconds->big_integer().minus(offset_nanoseconds_bigint)); + } + + // 6. Assert: offsetBehaviour is option. + VERIFY(offset_behavior == OffsetBehavior::Option); + + // 7. Assert: offsetOption is "prefer" or "reject". + VERIFY(offset_option.is_one_of("prefer"sv, "reject"sv)); + + // 8. Let possibleInstants be ? GetPossibleInstantsFor(timeZone, dateTime). + auto possible_instants = TRY(get_possible_instants_for(global_object, time_zone, *date_time)); + + // 9. For each element candidate of possibleInstants, do + for (auto& candidate_value : possible_instants) { + // TODO: As per the comment in disambiguate_possible_instants, having a MarkedValueList would allow us to remove this cast. + auto& candidate = static_cast(candidate_value.as_object()); + + // a. Let candidateNanoseconds be ? GetOffsetNanosecondsFor(timeZone, candidate). + auto candidate_nanoseconds = TRY(get_offset_nanoseconds_for(global_object, time_zone, candidate)); + + // b. If candidateNanoseconds = offsetNanoseconds, then + if (candidate_nanoseconds == offset_nanoseconds) { + // i. Return candidate.[[Nanoseconds]]. + return &candidate.nanoseconds(); + } + + // c. If matchBehaviour is match minutes, then + if (match_behavior == MatchBehavior::MatchMinutes) { + // i. Let roundedCandidateNanoseconds be ! RoundNumberToIncrement(candidateNanoseconds, 60 × 10^9, "halfExpand"). + auto rounded_candidate_nanoseconds = round_number_to_increment(candidate_nanoseconds, 60000000000, "halfExpand"sv); + + // ii. If roundedCandidateNanoseconds = offsetNanoseconds, then + if (rounded_candidate_nanoseconds == offset_nanoseconds) { + // 1. Return candidate.[[Nanoseconds]]. + return &candidate.nanoseconds(); + } + } + } + + // 10. If offsetOption is "reject", throw a RangeError exception. + if (offset_option == "reject"sv) + return vm.throw_completion(global_object, ErrorType::TemporalInvalidZonedDateTimeOffset); + + // 11. Let instant be ? DisambiguatePossibleInstants(possibleInstants, timeZone, dateTime, disambiguation). + auto* instant = TRY(disambiguate_possible_instants(global_object, possible_instants, time_zone, *date_time, disambiguation)); + + // 12. Return instant.[[Nanoseconds]]. + return &instant->nanoseconds(); +} + +// 6.5.2 ToTemporalZonedDateTime ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalzoneddatetime +ThrowCompletionOr to_temporal_zoned_date_time(GlobalObject& global_object, Value item, Object* options) +{ + auto& vm = global_object.vm(); + + // 1. If options is not present, set options to ! OrdinaryObjectCreate(null). + if (!options) + options = Object::create(global_object, nullptr); + + // 2. Let offsetBehaviour be option. + auto offset_behavior = OffsetBehavior::Option; + + // 3. Let matchBehaviour be match exactly. + auto match_behavior = MatchBehavior::MatchExactly; + + Object* calendar = nullptr; + Object* time_zone = nullptr; + Optional offset_string; + ISODateTime result; + + // 4. If Type(item) is Object, then + if (item.is_object()) { + auto& item_object = item.as_object(); + + // a. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then + if (is(item_object)) { + // i. Return item. + return &static_cast(item_object); + } + + // b. Let calendar be ? GetTemporalCalendarWithISODefault(item). + calendar = TRY(get_temporal_calendar_with_iso_default(global_object, item_object)); + + // c. 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 })); + + // d. Append "timeZone" to fieldNames. + field_names.append("timeZone"); + + // e. Append "offset" to fieldNames. + field_names.append("offset"); + + // f. Let fields be ? PrepareTemporalFields(item, fieldNames, « "timeZone" »). + auto* fields = TRY(prepare_temporal_fields(global_object, item_object, field_names, { "timeZone"sv })); + + // g. Let timeZone be ? Get(fields, "timeZone"). + auto time_zone_value = TRY(fields->get(vm.names.timeZone)); + + // h. Set timeZone to ? ToTemporalTimeZone(timeZone). + time_zone = TRY(to_temporal_time_zone(global_object, time_zone_value)); + + // i. Let offsetString be ? Get(fields, "offset"). + auto offset_string_value = TRY(fields->get(vm.names.offset)); + + // j. If offsetString is undefined, then + if (offset_string_value.is_undefined()) { + // i. Set offsetBehaviour to wall. + offset_behavior = OffsetBehavior::Wall; + } + // k. Else, + else { + // i. Set offsetString to ? ToString(offsetString). + offset_string = TRY(offset_string_value.to_string(global_object)); + } + + // l. Let result be ? InterpretTemporalDateTimeFields(calendar, fields, options). + result = TRY(interpret_temporal_date_time_fields(global_object, *calendar, *fields, *options)); + } + // 5. Else, + else { + // a. Perform ? ToTemporalOverflow(options). + (void)TRY(to_temporal_overflow(global_object, *options)); + + // b. Let string be ? ToString(item). + auto string = TRY(item.to_string(global_object)); + + // c. Let result be ? ParseTemporalZonedDateTimeString(string). + auto parsed_result = TRY(parse_temporal_zoned_date_time_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 refers to `parsed_result`. + + // d. Assert: result.[[TimeZoneName]] is not undefined. + VERIFY(parsed_result.time_zone.name.has_value()); + + // e. Let offsetString be result.[[TimeZoneOffsetString]]. + offset_string = move(parsed_result.time_zone.offset); + + // 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.has_value()) { + // i. Set offsetBehaviour to wall. + offset_behavior = OffsetBehavior::Wall; + } + + // h. Let timeZone be ? CreateTemporalTimeZone(result.[[TimeZoneName]]). + time_zone = TRY(create_temporal_time_zone(global_object, *parsed_result.time_zone.name)); + + // i. Let calendar be ? ToTemporalCalendarWithISODefault(result.[[Calendar]]). + calendar = TRY(to_temporal_calendar_with_iso_default(global_object, js_string(vm, parsed_result.date_time.calendar.value()))); + + // j. Set matchBehaviour to match minutes. + match_behavior = MatchBehavior::MatchMinutes; + + // See NOTE above about why this is done. + result = move(parsed_result.date_time); + } + + // 6. Let offsetNanoseconds be 0. + double offset_nanoseconds = 0; + + // 7. If offsetBehaviour is option, then + if (offset_behavior == OffsetBehavior::Option) { + // a. Set offsetNanoseconds to ? ParseTimeZoneOffsetString(offsetString). + offset_nanoseconds = TRY(parse_time_zone_offset_string(global_object, *offset_string)); + } + + // 8. Let disambiguation be ? ToTemporalDisambiguation(options). + auto disambiguation = TRY(to_temporal_disambiguation(global_object, *options)); + + // 9. Let offsetOption be ? ToTemporalOffset(options, "reject"). + auto offset_option = TRY(to_temporal_offset(global_object, *options, "reject")); + + // 10. Let epochNanoseconds be ? InterpretISODateTimeOffset(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], offsetBehaviour, offsetNanoseconds, timeZone, disambiguation, offsetOption, 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_nanoseconds, time_zone, disambiguation, offset_option, match_behavior)); + + // 11. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar). + return MUST(create_temporal_zoned_date_time(global_object, *epoch_nanoseconds, *time_zone, *calendar)); +} + // 6.5.3 CreateTemporalZonedDateTime ( epochNanoseconds, timeZone, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalzoneddatetime ThrowCompletionOr create_temporal_zoned_date_time(GlobalObject& global_object, BigInt const& epoch_nanoseconds, Object& time_zone, Object& calendar, FunctionObject const* new_target) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h index 855793723da..ccc940411ba 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h @@ -40,6 +40,19 @@ struct NanosecondsToDaysResult { double day_length; }; +enum class OffsetBehavior { + Option, + Exact, + Wall, +}; + +enum class MatchBehavior { + MatchExactly, + MatchMinutes, +}; + +ThrowCompletionOr interpret_iso_date_time_offset(GlobalObject&, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, OffsetBehavior offset_behavior, double offset_nanoseconds, Value time_zone, StringView disambiguation, StringView offset_option, MatchBehavior match_behavior); +ThrowCompletionOr to_temporal_zoned_date_time(GlobalObject&, Value item, Object* options = nullptr); ThrowCompletionOr create_temporal_zoned_date_time(GlobalObject&, BigInt const& epoch_nanoseconds, Object& time_zone, Object& calendar, FunctionObject const* new_target = nullptr); ThrowCompletionOr add_zoned_date_time(GlobalObject&, BigInt const& epoch_nanoseconds, Value time_zone, Object& calendar, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, Object* options = nullptr); ThrowCompletionOr nanoseconds_to_days(GlobalObject&, BigInt const& nanoseconds, Value relative_to);