diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 766ae9fd521..af73e54a3fe 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -286,6 +286,7 @@ struct DateDuration; struct InternalDuration; struct ISODate; struct ISODateTime; +struct ISOYearMonth; struct ParseResult; struct PartialDuration; struct Time; diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index e5301f763f1..59850358e2c 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/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\"") \ diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 63434bf6454..aee307c1d8c 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/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 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(ErrorType::TemporalInvalidISODate); + } + + // 2. Return unused. + return {}; +} + // 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& options) { @@ -86,6 +99,29 @@ ThrowCompletionOr 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 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 get_difference_settings(VM& vm, DurationOperation operation, Object const& options, UnitGroup unit_group, ReadonlySpan 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 && disallowed_units.contains_slow(*unit)) + return vm.throw_completion(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(); + + // 8. If disallowedUnits contains smallestUnit, throw a RangeError exception. + if (disallowed_units.contains_slow(smallest_unit_value)) + return vm.throw_completion(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()); + + // 10. If largestUnit is AUTO, set largestUnit to defaultLargestUnit. + if (largest_unit.has()) + largest_unit = default_largest_unit; + auto largest_unit_value = largest_unit.get(); + + // 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(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()) + TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get(), 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> get_options_object(VM& vm, Value options) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index f31a269e55f..a91f95f80f3 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/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 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 check_iso_days_range(VM&, ISODate const&); ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); +RoundingMode negate_rounding_mode(RoundingMode); ThrowCompletionOr get_temporal_show_calendar_name_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); @@ -177,6 +191,7 @@ ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView t ThrowCompletionOr to_month_code(VM&, Value argument); ThrowCompletionOr to_offset_string(VM&, Value argument); CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType); +ThrowCompletionOr get_difference_settings(VM&, DurationOperation, Object const& options, UnitGroup, ReadonlySpan disallowed_units, Unit fallback_smallest_unit, Unit smallest_largest_default_unit); // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation template diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 7ff0dc06897..1113546c76e 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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 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(iso_date.year) + duration.years, static_cast(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(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(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(one.year) + years, static_cast(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(intermediate.month) + sign); + } + } + + // g. Set intermediate to BalanceISOYearMonth(one.[[Year]] + years, one.[[Month]] + months). + auto intermediate = balance_iso_year_month(static_cast(one.year) + years, static_cast(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(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(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(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(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 to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) { @@ -365,6 +514,23 @@ ThrowCompletionOr 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 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(ErrorType::TemporalInvalidISODate); + + // 4. Return result. + return result; +} + // 12.2.11 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields ThrowCompletionOr calendar_year_month_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index df2dce55fee..0c820b0c643 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -99,6 +99,7 @@ using CalendarFieldListOrPartial = Variant; ThrowCompletionOr canonicalize_calendar(VM&, StringView id); Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); +ThrowCompletionOr calendar_date_from_fields(VM&, StringView calendar, CalendarFields, Overflow); ThrowCompletionOr calendar_year_month_from_fields(VM&, StringView calendar, CalendarFields, Overflow); ThrowCompletionOr 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 calendar_field_keys_present(CalendarFields const&); CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields); +ThrowCompletionOr 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 to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); ThrowCompletionOr calendar_date_to_iso(VM&, StringView calendar, CalendarFields const&, Overflow); diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp index fc499e183f0..473add52b51 100644 --- a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp @@ -79,6 +79,52 @@ u16 epoch_time_to_day_in_year(double time) return static_cast(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(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(); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.h b/Libraries/LibJS/Runtime/Temporal/DateEquations.h index 0dcd6033014..659691830fd 100644 --- a/Libraries/LibJS/Runtime/Temporal/DateEquations.h +++ b/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); } diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 31f6d28a54e..03b35d95fff 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -9,13 +9,16 @@ #include #include -#include #include +#include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -294,6 +297,21 @@ ThrowCompletionOr 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 adjust_date_duration_record(VM& vm, DateDuration const& date_duration, double days, Optional weeks, Optional 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 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 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 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 nudge_to_calendar_unit(VM& vm, i8 sign, InternalDuration const& duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional 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(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(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(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(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(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 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(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 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 bubble_relative_duration(VM& vm, i8 sign, InternalDuration duration, TimeDuration const& nudged_epoch_ns, ISODateTime const& iso_date_time, Optional 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_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 round_relative_duration(VM& vm, InternalDuration duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional 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) { diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index 22d967d5155..ab3696e08aa 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -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> temporal_duration_from_internal(VM&, InternalDuration const&, Unit largest_unit); ThrowCompletionOr create_date_duration_record(VM&, double years, double months, double weeks, double days); +ThrowCompletionOr adjust_date_duration_record(VM&, DateDuration const&, double days, Optional weeks = {}, Optional months = {}); ThrowCompletionOr combine_date_and_time_duration(VM&, DateDuration, TimeDuration); ThrowCompletionOr> 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 to_temporal_partial_duration_record(VM&, Value temporal_duration_like); @@ -117,10 +132,17 @@ GC::Ref 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 add_time_duration(VM&, TimeDuration const&, TimeDuration const&); ThrowCompletionOr 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 round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); i8 time_duration_sign(TimeDuration const&); ThrowCompletionOr round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_calendar_unit(VM&, i8 sign, InternalDuration const&, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional time_zone, StringView calendar, u64 increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_zoned_time(VM&, i8 sign, InternalDuration const&, ISODateTime const&, StringView time_zone, StringView calendar, u64 increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_day_or_time(VM&, InternalDuration const&, TimeDuration const& dest_epoch_ns, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode); +ThrowCompletionOr bubble_relative_duration(VM&, i8 sign, InternalDuration, TimeDuration const& nudged_epoch_ns, ISODateTime const&, Optional time_zone, StringView calendar, Unit largest_unit, Unit smallest_unit); +ThrowCompletionOr round_relative_duration(VM&, InternalDuration, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional 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> add_durations(VM&, ArithmeticOperation, Duration const&, Value); diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.cpp b/Libraries/LibJS/Runtime/Temporal/Instant.cpp index 5a23ec90287..80c31945668 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.cpp +++ b/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; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.h b/Libraries/LibJS/Runtime/Temporal/Instant.h index 9c90733e5c4..fc61ea048d4 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.h +++ b/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); + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index cb87ee86f79..09d3ef01f27 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,32 @@ ISODate create_iso_date_record(double year, double month, double day) return { .year = static_cast(year), .month = static_cast(month), .day = static_cast(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 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) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 7a235c9d7fa..f4bb8067022 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/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 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); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp index 84c21661e02..6e2467fa83f 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include 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); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h index 31260cca3ae..cea62f83237 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h +++ b/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); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index 85f6a39a3b0..6cc5fcff5c9 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -7,7 +7,9 @@ */ #include +#include #include +#include 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); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h index 40eefc37a11..eec0c0cb2a6 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/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); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index f737d92a949..292089d9175 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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(year), .month = static_cast(month) }; +} + // 9.5.5 CreateTemporalYearMonth ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth ThrowCompletionOr> create_temporal_year_month(VM& vm, ISODate iso_date, String calendar, GC::Ptr 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> 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(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; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 20b876d244a..0e51648e4d9 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/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> 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> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar); +ThrowCompletionOr> difference_temporal_plain_year_month(VM&, DurationOperation, PlainYearMonth const&, Value other, Value options); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index b3d5c7421c8..893186b3869 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -7,6 +7,7 @@ #include #include +#include #include 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) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 737412ea623..8f8215fe47d 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/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); diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index 31bc53655af..d511218dd35 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -9,7 +9,10 @@ #include #include #include +#include #include +#include +#include #include #include @@ -70,6 +73,125 @@ ThrowCompletionOr 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 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 disambiguate_possible_epoch_nanoseconds(VM& vm, Vector 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(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(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> get_possible_epoch_nanoseconds(VM& vm, StringView time_zone, ISODateTime const& iso_date_time) +{ + Vector 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(iso_date_time.time.minute) - static_cast(*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(ErrorType::TemporalInvalidEpochNanoseconds); + } + + // 5. Return possibleEpochNanoseconds. + return possible_epoch_nanoseconds; +} + // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier ThrowCompletionOr parse_time_zone_identifier(VM& vm, StringView identifier) { diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.h b/Libraries/LibJS/Runtime/Temporal/TimeZone.h index 97972ab0c88..711cf1e7758 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.h +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include #include #include @@ -19,8 +21,18 @@ struct TimeZone { Optional offset_minutes; }; +enum class Disambiguation { + Compatible, + Earlier, + Later, + Reject, +}; + String format_offset_time_zone_identifier(i64 offset_minutes, Optional = {}); ThrowCompletionOr to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like); +ThrowCompletionOr get_epoch_nanoseconds_for(VM&, StringView time_zone, ISODateTime const&, Disambiguation); +ThrowCompletionOr disambiguate_possible_epoch_nanoseconds(VM&, Vector possible_epoch_ns, StringView time_zone, ISODateTime const&, Disambiguation); +ThrowCompletionOr> get_possible_epoch_nanoseconds(VM&, StringView time_zone, ISODateTime const&); ThrowCompletionOr parse_time_zone_identifier(VM&, StringView identifier); TimeZone parse_time_zone_identifier(StringView identifier); TimeZone parse_time_zone_identifier(ParseResult const&); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js new file mode 100644 index 00000000000..6cab7d59f24 --- /dev/null +++ b/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"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js new file mode 100644 index 00000000000..f05fa44c4e8 --- /dev/null +++ b/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"); + }); +});