diff options
author | Linus Groh <mail@linusgroh.de> | 2021-08-16 00:35:05 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-08-16 14:10:41 +0100 |
commit | ed9d37bd4049c4c178912d321183a8b146d5410f (patch) | |
tree | 76d8fecfd9119f3a0c460458b482733f5f1de798 | |
parent | c1c12497b56edbdbd02cb3f687a676f1c6a39cac (diff) | |
download | serenity-ed9d37bd4049c4c178912d321183a8b146d5410f.zip |
LibJS: Implement Temporal.Calendar.prototype.yearMonthFromFields()
10 files changed, 223 insertions, 14 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 7d74ae6959..1e1d5c3b13 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -420,6 +420,7 @@ namespace JS { P(withPlainDate) \ P(writable) \ P(year) \ + P(yearMonthFromFields) \ P(years) \ P(zonedDateTime) \ P(zonedDateTimeISO) diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index e3f4597060..13f291aee5 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org> + * Copyright (c) 2021, Linus Groh <linusg@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -714,6 +715,48 @@ Optional<ISODate> iso_date_from_fields(GlobalObject& global_object, Object& fiel return regulate_iso_date(global_object, year.as_double(), month, day.as_double(), *overflow); } +// 12.1.39 ISOYearMonthFromFields ( fields, options ), https://tc39.es/proposal-temporal/#sec-temporal-isoyearmonthfromfields +Optional<ISOYearMonth> iso_year_month_from_fields(GlobalObject& global_object, Object& fields, Object& options) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(fields) is Object. + + // 2. Let overflow be ? ToTemporalOverflow(options). + auto overflow = to_temporal_overflow(global_object, options); + if (vm.exception()) + return {}; + + // 3. Set fields to ? PrepareTemporalFields(fields, ยซ "month", "monthCode", "year" ยป, ยซยป). + auto* prepared_fields = prepare_temporal_fields(global_object, fields, { "month"sv, "monthCode"sv, "year"sv }, {}); + if (vm.exception()) + return {}; + + // 4. Let year be ? Get(fields, "year"). + auto year = prepared_fields->get(vm.names.year); + if (vm.exception()) + return {}; + + // 5. If year is undefined, throw a TypeError exception. + if (year.is_undefined()) { + vm.throw_exception<TypeError>(global_object, ErrorType::TemporalMissingRequiredProperty, vm.names.year.as_string()); + return {}; + } + + // 6. Let month be ? ResolveISOMonth(fields). + auto month = resolve_iso_month(global_object, *prepared_fields); + if (vm.exception()) + return {}; + + // 7. Let result be ? RegulateISOYearMonth(year, month, overflow). + auto result = regulate_iso_year_month(global_object, year.as_double(), month, *overflow); + if (vm.exception()) + return {}; + + // 8. Return the new Record { [[Year]]: result.[[Year]], [[Month]]: result.[[Month]], [[ReferenceISODay]]: 1 }. + return ISOYearMonth { .year = result->year, .month = result->month, .reference_iso_day = 1 }; +} + // 12.1.41 ISOYear ( temporalObject ), https://tc39.es/proposal-temporal/#sec-temporal-isoyear i32 iso_year(Object& temporal_object) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h index 653d37b175..162144f85c 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -9,6 +9,7 @@ #include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/Temporal/PlainDate.h> +#include <LibJS/Runtime/Temporal/PlainYearMonth.h> #include <LibJS/Runtime/Value.h> namespace JS::Temporal { @@ -59,6 +60,7 @@ u8 to_iso_week_of_year(i32 year, u8 month, u8 day); String build_iso_month_code(u8 month); double resolve_iso_month(GlobalObject&, Object& fields); Optional<ISODate> iso_date_from_fields(GlobalObject&, Object& fields, Object& options); +Optional<ISOYearMonth> iso_year_month_from_fields(GlobalObject&, Object& fields, Object& options); i32 iso_year(Object& temporal_object); u8 iso_month(Object& temporal_object); String iso_month_code(Object& temporal_object); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp index ac01702ae7..ecdd4ad969 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp @@ -34,6 +34,7 @@ void CalendarPrototype::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.dateFromFields, date_from_fields, 2, attr); + define_native_function(vm.names.yearMonthFromFields, year_month_from_fields, 2, attr); define_native_function(vm.names.year, year, 1, attr); define_native_function(vm.names.month, month, 1, attr); define_native_function(vm.names.monthCode, month_code, 1, attr); @@ -107,6 +108,40 @@ JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::date_from_fields) return create_temporal_date(global_object, result->year, result->month, result->day, *calendar); } +// 12.4.5 Temporal.Calendar.prototype.yearMonthFromFields ( fields, options ), https://tc39.es/proposal-temporal/#sec-temporal.calendar.prototype.yearmonthfromfields +// NOTE: This is the minimum yearMonthFromFields implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::year_month_from_fields) +{ + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + auto* calendar = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Assert: calendar.[[Identifier]] is "iso8601". + VERIFY(calendar->identifier() == "iso8601"sv); + + // 4. If Type(fields) is not Object, throw a TypeError exception. + auto fields = vm.argument(0); + if (!fields.is_object()) { + vm.throw_exception<TypeError>(global_object, ErrorType::NotAnObject, fields.to_string_without_side_effects()); + return {}; + } + + // 5. Set options to ? GetOptionsObject(options). + auto* options = get_options_object(global_object, vm.argument(1)); + if (vm.exception()) + return {}; + + // 6. Let result be ? ISOYearMonthFromFields(fields, options). + auto result = iso_year_month_from_fields(global_object, fields.as_object(), *options); + if (vm.exception()) + return {}; + + // 7. Return ? CreateTemporalYearMonth(result.[[Year]], result.[[Month]], calendar, result.[[ReferenceISODay]]). + return create_temporal_year_month(global_object, result->year, result->month, *calendar, result->reference_iso_day); +} + // 12.4.9 Temporal.Calendar.prototype.year ( temporalDateLike ), https://tc39.es/proposal-temporal/#sec-temporal.calendar.prototype.year // NOTE: This is the minimum year implementation for engines without ECMA-402. JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::year) diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h index 7cee764bba..c4d5a0d494 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h @@ -21,6 +21,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(id_getter); JS_DECLARE_NATIVE_FUNCTION(date_from_fields); + JS_DECLARE_NATIVE_FUNCTION(year_month_from_fields); JS_DECLARE_NATIVE_FUNCTION(year); JS_DECLARE_NATIVE_FUNCTION(month); JS_DECLARE_NATIVE_FUNCTION(month_code); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index 83d1a660fb..0b71e50244 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -237,18 +237,18 @@ bool is_valid_iso_date(i32 year, u8 month, u8 day) } // 3.5.6 BalanceISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisodate -ISODate balance_iso_date(i32 year, i32 month, i32 day) +ISODate balance_iso_date(double year_, double month_, double day) { // 1. Assert: year, month, and day are integers. // 2. Let balancedYearMonth be ! BalanceISOYearMonth(year, month). - auto balanced_year_month = balance_iso_year_month(year, month); + auto balanced_year_month = balance_iso_year_month(year_, month_); // 3. Set month to balancedYearMonth.[[Month]]. - month = balanced_year_month.month; + auto month = balanced_year_month.month; // 4. Set year to balancedYearMonth.[[Year]]. - year = balanced_year_month.year; + auto year = balanced_year_month.year; // 5. NOTE: To deal with negative numbers of days whose absolute value is greater than the number of days in a year, the following section subtracts years and adds days until the number of days is greater than โ366 or โ365. diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h index b0985fe52d..37489ee3b7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -44,7 +44,7 @@ PlainDate* create_temporal_date(GlobalObject&, i32 iso_year, u8 iso_month, u8 is PlainDate* to_temporal_date(GlobalObject&, Value item, Object* options = nullptr); Optional<ISODate> regulate_iso_date(GlobalObject&, double year, double month, double day, String const& overflow); bool is_valid_iso_date(i32 year, u8 month, u8 day); -ISODate balance_iso_date(i32 year, i32 month, i32 day); +ISODate balance_iso_date(double year, double month, double day); i8 compare_iso_date(i32 year1, u8 month1, u8 day1, i32 year2, u8 month2, u8 day2); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index 248aa43192..aa61eb9a4b 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -6,6 +6,7 @@ #include <LibJS/Runtime/AbstractOperations.h> #include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Temporal/AbstractOperations.h> #include <LibJS/Runtime/Temporal/PlainDate.h> #include <LibJS/Runtime/Temporal/PlainYearMonth.h> #include <LibJS/Runtime/Temporal/PlainYearMonthConstructor.h> @@ -28,19 +29,67 @@ void PlainYearMonth::visit_edges(Visitor& visitor) visitor.visit(&m_calendar); } -// 9.5.5 BalanceISOYearMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisoyearmonth -ISOYearMonth balance_iso_year_month(i32 year, i32 month) +// 9.5.2 RegulateISOYearMonth ( year, month, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisoyearmonth +Optional<ISOYearMonth> regulate_iso_year_month(GlobalObject& global_object, double year, double month, String const& overflow) { + auto& vm = global_object.vm(); + // 1. Assert: year and month are integers. + VERIFY(year == trunc(year) && month == trunc(month)); + + // 2. Assert: overflow is either "constrain" or "reject". + // NOTE: Asserted by the VERIFY_NOT_REACHED at the end + + // 3. If overflow is "constrain", then + if (overflow == "constrain"sv) { + // IMPLEMENTATION DEFINED: This is an optimization that allows us to treat `year` (a double) as normal integer from this point onwards. + // This does not change the exposed behaviour as the subsequent call to CreateTemporalYearMonth will check that its value is a valid ISO + // values (for years: -273975 - 273975) which is a subset of this check. + // If RegulateISOYearMonth is ever used outside ISOYearMonthFromFields, this may need to be changed. + if (!AK::is_within_range<i32>(year)) { + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidPlainYearMonth); + return {}; + } + + // a. Return ! ConstrainISOYearMonth(year, month). + return constrain_iso_year_month(year, month); + } - // 2. Set year to year + floor((month - 1) / 12). - year += (month - 1) / 12; + // 4. If overflow is "reject", then + if (overflow == "reject"sv) { + // IMPLEMENTATION DEFINED: This is an optimization that allows us to treat these doubles as normal integers from this point onwards. + // This does not change the exposed behaviour as the call to IsValidISOMonth and subsequent call to CreateTemporalDateTime will check + // that these values are valid ISO values (for years: -273975 - 273975, for months: 1 - 12) all of which are subsets of this check. + if (!AK::is_within_range<i32>(year) || !AK::is_within_range<u8>(month)) { + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidPlainYearMonth); + return {}; + } + + // a. If ! IsValidISOMonth(month) is false, throw a RangeError exception. + if (!is_valid_iso_month(month)) { + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidPlainYearMonth); + return {}; + } + + // b. Return the new Record { [[Year]]: year, [[Month]]: month }. + return ISOYearMonth { .year = static_cast<i32>(year), .month = static_cast<u8>(month), .reference_iso_day = 0 }; + } - // 3. Set month to (month โ 1) modulo 12 + 1. - month = (month - 1) % 12 + 1; + VERIFY_NOT_REACHED(); +} - // 4. Return the new Record { [[Year]]: year, [[Month]]: month }. - return ISOYearMonth { .year = year, .month = static_cast<u8>(month) }; +// 9.5.3 IsValidISOMonth ( month ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidisomonth +bool is_valid_iso_month(u8 month) +{ + // 1. Assert: month is an integer. + // 2. If month < 1 or month > 12, then + if (month < 1 || month > 12) { + // a.Return false. + return false; + } + + // 3. Return true. + return true; } // 9.5.4 ISOYearMonthWithinLimits ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isoyearmonthwithinlimits @@ -70,6 +119,36 @@ bool iso_year_month_within_limits(i32 year, u8 month) return true; } +// 9.5.5 BalanceISOYearMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisoyearmonth +ISOYearMonth balance_iso_year_month(double year, double month) +{ + // 1. Assert: year and month are integers. + VERIFY(year == trunc(year) && month == trunc(month)); + + // 2. Set year to year + floor((month - 1) / 12). + year += floor((month - 1) / 12); + + // 3. Set month to (month โ 1) modulo 12 + 1. + month = fmod(month - 1, 12) + 1; + + // 4. Return the new Record { [[Year]]: year, [[Month]]: month }. + return ISOYearMonth { .year = static_cast<i32>(year), .month = static_cast<u8>(month), .reference_iso_day = 0 }; +} + +// 9.5.6 ConstrainISOYearMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-constrainisoyearmonth +ISOYearMonth constrain_iso_year_month(double year, double month) +{ + // 1. Assert: year and month are integers. + VERIFY(year == trunc(year) && month == trunc(month)); + + // 2. Set month to ! ConstrainToRange(month, 1, 12). + month = constrain_to_range(month, 1, 12); + + // 3. Return the Record { [[Year]]: year, [[Month]]: month }. + // NOTE: `year` is known to be in the i32 range. + return ISOYearMonth { .year = static_cast<i32>(year), .month = static_cast<u8>(month), .reference_iso_day = 0 }; +} + // 9.5.7 CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth PlainYearMonth* create_temporal_year_month(GlobalObject& global_object, i32 iso_year, u8 iso_month, Object& calendar, u8 reference_iso_day, FunctionObject* new_target) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 12160fd011..063cb6ec25 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -36,10 +36,14 @@ private: struct ISOYearMonth { i32 year; u8 month; + u8 reference_iso_day; }; -ISOYearMonth balance_iso_year_month(i32 year, i32 month); +Optional<ISOYearMonth> regulate_iso_year_month(GlobalObject&, double year, double month, String const& overflow); +bool is_valid_iso_month(u8 month); bool iso_year_month_within_limits(i32 year, u8 month); +ISOYearMonth balance_iso_year_month(double year, double month); +ISOYearMonth constrain_iso_year_month(double year, double month); PlainYearMonth* create_temporal_year_month(GlobalObject&, i32 iso_year, u8 iso_month, Object& calendar, u8 reference_iso_day, FunctionObject* new_target = nullptr); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.yearMonthFromFields.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.yearMonthFromFields.js new file mode 100644 index 0000000000..058a137cd7 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.yearMonthFromFields.js @@ -0,0 +1,44 @@ +describe("correct behavior", () => { + test("length is 2", () => { + expect(Temporal.Calendar.prototype.yearMonthFromFields).toHaveLength(2); + }); + + test("basic functionality", () => { + const calendar = new Temporal.Calendar("iso8601"); + const plainYearMonth = calendar.yearMonthFromFields({ year: 2021, month: 7 }); + expect(plainYearMonth.calendar).toBe(calendar); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + }); + + test("with monthCode", () => { + const calendar = new Temporal.Calendar("iso8601"); + const plainYearMonth = calendar.yearMonthFromFields({ year: 2021, monthCode: "M07" }); + expect(plainYearMonth.calendar).toBe(calendar); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + }); +}); + +describe("errors", () => { + test("first argument must be an object", () => { + const calendar = new Temporal.Calendar("iso8601"); + expect(() => { + calendar.yearMonthFromFields(42); + }).toThrowWithMessage(TypeError, "42 is not an object"); + }); + + test("year field is required", () => { + const calendar = new Temporal.Calendar("iso8601"); + expect(() => { + calendar.yearMonthFromFields({ month: 7 }); + }).toThrowWithMessage(TypeError, "Required property year is missing or undefined"); + }); + + test("month or monthCode field is required", () => { + const calendar = new Temporal.Calendar("iso8601"); + expect(() => { + calendar.yearMonthFromFields({ year: 2021 }); + }).toThrowWithMessage(TypeError, "Required property month is missing or undefined"); + }); +}); |