diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index f8976b6b06..ac33bcab6c 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -104,6 +104,8 @@ namespace JS { P(count) \ P(countReset) \ P(create) \ + P(dateFromFields) \ + P(day) \ P(days) \ P(debug) \ P(decodeURI) \ @@ -244,6 +246,8 @@ namespace JS { P(milliseconds) \ P(min) \ P(minutes) \ + P(month) \ + P(monthCode) \ P(months) \ P(multiline) \ P(name) \ @@ -367,6 +371,7 @@ namespace JS { P(weeks) \ P(with) \ P(writable) \ + P(year) \ P(years) struct CommonPropertyNames { diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 08d9e6e69a..39e1ce7799 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -172,9 +172,12 @@ M(TemporalInvalidDurationPropertyValue, "Invalid value for duration property '{}': must be an integer, got {}") \ M(TemporalInvalidEpochNanoseconds, "Invalid epoch nanoseconds value, must be in range -86400 * 10^17 to 86400 * 10^17") \ M(TemporalInvalidISODate, "Invalid ISO date") \ + M(TemporalInvalidMonthCode, "Invalid month code") \ M(TemporalInvalidPlainDate, "Invalid plain date") \ M(TemporalInvalidTime, "Invalid time") \ M(TemporalInvalidTimeZoneName, "Invalid time zone name") \ + M(TemporalMissingRequiredProperty, "Required property {} is missing or undefined") \ + M(TemporalPropertyMustBePositiveInteger, "Property must be a positive integer") \ M(ThisHasNotBeenInitialized, "|this| has not been initialized") \ M(ThisIsAlreadyInitialized, "|this| is already initialized") \ M(ToObjectNullOrUndefined, "ToObject on null or undefined") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index ea415d8619..4080da7961 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -116,6 +116,20 @@ Value get_option(GlobalObject& global_object, Object& options, String const& pro return value; } +// 13.6 ToTemporalOverflow ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaloverflow +Optional to_temporal_overflow(GlobalObject& global_object, Object& normalized_options) +{ + auto& vm = global_object.vm(); + + // 1. Return ? GetOption(normalizedOptions, "overflow", « String », « "constrain", "reject" », "constrain"). + auto option = get_option(global_object, normalized_options, "overflow", { OptionType::String }, { "constrain"sv, "reject"sv }, js_string(vm, "constrain")); + if (vm.exception()) + return {}; + + VERIFY(option.is_string()); + return option.as_string().string(); +} + // 13.8 ToTemporalRoundingMode ( normalizedOptions, fallback ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalroundingmode Optional to_temporal_rounding_mode(GlobalObject& global_object, Object& normalized_options, String const& fallback) { @@ -232,6 +246,12 @@ Optional to_smallest_temporal_unit(GlobalObject& global_object, Object& return smallest_unit; } +// 13.29 ConstrainToRange ( x, minimum, maximum ), https://tc39.es/proposal-temporal/#sec-temporal-constraintorange +double constrain_to_range(double x, double minimum, double maximum) +{ + return min(max(x, minimum), maximum); +} + // 13.32 RoundNumberToIncrement ( x, increment, roundingMode ) BigInt* round_number_to_increment(GlobalObject& global_object, BigInt const& x, u64 increment, String const& rounding_mode) { @@ -545,4 +565,93 @@ Optional parse_temporal_time_zone_string(GlobalObject& global_ return TemporalTimeZone { .z = false, .offset = offset, .name = name }; } +// 13.45 ToPositiveIntegerOrInfinity ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-topositiveintegerorinfinity +double to_positive_integer_or_infinity(GlobalObject& global_object, Value argument) +{ + auto& vm = global_object.vm(); + + // 1. Let integer be ? ToIntegerOrInfinity(argument). + auto integer = argument.to_integer_or_infinity(global_object); + if (vm.exception()) + return {}; + + // 2. If integer is -∞𝔽, then + if (Value(integer).is_negative_infinity()) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::TemporalPropertyMustBePositiveInteger); + return {}; + } + + // 3. If integer ≤ 0, then + if (integer <= 0) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::TemporalPropertyMustBePositiveInteger); + return {}; + } + + // 4. Return integer. + return integer; +} + +// 13.46 PrepareTemporalFields ( fields, fieldNames, requiredFields ), https://tc39.es/proposal-temporal/#sec-temporal-preparetemporalfields +Object* prepare_temporal_fields(GlobalObject& global_object, Object& fields, Vector field_names, Vector required_fields) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(fields) is Object. + + // 2. Let result be ! OrdinaryObjectCreate(%Object.prototype%). + auto* result = Object::create(global_object, global_object.object_prototype()); + VERIFY(result); + + // 3. For each value property of fieldNames, do + for (auto& property : field_names) { + // a. Let value be ? Get(fields, property). + auto value = fields.get(property); + if (vm.exception()) + return {}; + + // b. If value is undefined, then + if (value.is_undefined()) { + // i. If requiredFields contains property, then + if (required_fields.contains_slow(property)) { + // 1. Throw a TypeError exception. + vm.throw_exception(global_object, ErrorType::TemporalMissingRequiredProperty, property); + return {}; + } + // ii. If property is in the Property column of Table 13, then + // NOTE: The other properties in the table are automatically handled as their default value is undefined + if (property.is_one_of("hour", "minute", "second", "millisecond", "microsecond", "nanosecond")) { + // 1. Set value to the corresponding Default value of the same row. + value = Value(0); + } + } + // c. Else, + else { + // i. If property is in the Property column of Table 13 and there is a Conversion value in the same row, then + // 1. Let Conversion represent the abstract operation named by the Conversion value of the same row. + // 2. Set value to ? Conversion(value). + if (property.is_one_of("year", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond", "eraYear")) { + value = Value(value.to_integer_or_infinity(global_object)); + if (vm.exception()) + return {}; + } else if (property.is_one_of("month", "day")) { + value = Value(to_positive_integer_or_infinity(global_object, value)); + if (vm.exception()) + return {}; + } else if (property.is_one_of("monthCode", "offset", "era")) { + value = value.to_primitive_string(global_object); + if (vm.exception()) + return {}; + } + } + + // d. Perform ! CreateDataPropertyOrThrow(result, property, value). + result->create_data_property_or_throw(property, value); + } + + // 4. Return result. + return result; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index c0479427be..5b32fd5f83 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -46,6 +46,13 @@ struct TemporalInstant { Optional time_zone_offset; }; +struct TemporalDate { + i32 year; + i32 month; + i32 day; + Optional calendar; +}; + struct TemporalTimeZone { bool z; Optional offset; @@ -54,14 +61,18 @@ struct TemporalTimeZone { Object* get_options_object(GlobalObject&, Value options); Value get_option(GlobalObject&, Object& options, String const& property, Vector const& types, Vector const& values, Value fallback); +Optional to_temporal_overflow(GlobalObject&, Object& normalized_options); Optional to_temporal_rounding_mode(GlobalObject&, Object& normalized_options, String const& fallback); u64 to_temporal_rounding_increment(GlobalObject&, Object& normalized_options, Optional dividend, bool inclusive); Optional to_smallest_temporal_unit(GlobalObject&, Object& normalized_options, Vector const& disallowed_units, Optional fallback); +double constrain_to_range(double x, double minimum, double maximum); BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, String const& rounding_mode); Optional parse_iso_date_time(GlobalObject&, String const& iso_string); Optional parse_temporal_instant_string(GlobalObject&, String const& iso_string); Optional parse_temporal_calendar_string(GlobalObject&, String const& iso_string); Optional parse_temporal_duration_string(GlobalObject&, String const& iso_string); Optional parse_temporal_time_zone_string(GlobalObject&, String const& iso_string); +double to_positive_integer_or_infinity(GlobalObject&, Value argument); +Object* prepare_temporal_fields(GlobalObject&, Object& fields, Vector field_names, Vector required_fields); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 72270e198d..322552102d 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -193,4 +193,119 @@ i32 iso_days_in_month(i32 year, i32 month) return 28; } +// 12.1.36 BuildISOMonthCode ( month ), https://tc39.es/proposal-temporal/#sec-buildisomonthcode +String build_iso_month_code(i32 month) +{ + return String::formatted("M{:02}", month); +} + +// 12.1.37 ResolveISOMonth ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-resolveisomonth +double resolve_iso_month(GlobalObject& global_object, Object& fields) +{ + auto& vm = global_object.vm(); + + // 1. Let month be ? Get(fields, "month"). + auto month = fields.get(vm.names.month); + if (vm.exception()) + return {}; + + // 2. Let monthCode be ? Get(fields, "monthCode"). + auto month_code = fields.get(vm.names.monthCode); + if (vm.exception()) + return {}; + + // 3. If monthCode is undefined, then + if (month_code.is_undefined()) { + // a. If month is undefined, throw a TypeError exception. + if (month.is_undefined()) { + vm.throw_exception(global_object, ErrorType::TemporalMissingRequiredProperty, vm.names.month.as_string()); + return {}; + } + // b. Return month. + return month.as_double(); + } + + // 4. Assert: Type(monthCode) is String. + VERIFY(month_code.is_string()); + auto& month_code_string = month_code.as_string().string(); + // 5. Let monthLength be the length of monthCode. + auto month_length = month_code_string.length(); + // 6. If monthLength is not 3, throw a RangeError exception. + if (month_length != 3) { + vm.throw_exception(global_object, ErrorType::TemporalInvalidMonthCode); + return {}; + } + // 7. Let numberPart be the substring of monthCode from 1. + auto number_part = month_code_string.substring(1); + // 8. Set numberPart to ! ToIntegerOrInfinity(numberPart). + auto number_part_integer = Value(js_string(vm, move(number_part))).to_integer_or_infinity(global_object); + // 9. If numberPart < 1 or numberPart > 12, throw a RangeError exception. + if (number_part_integer < 1 || number_part_integer > 12) { + vm.throw_exception(global_object, ErrorType::TemporalInvalidMonthCode); + return {}; + } + // 10. If month is not undefined, and month ≠ numberPart, then + if (!month.is_undefined() && month.as_double() != number_part_integer) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::TemporalInvalidMonthCode); + return {}; + } + // 11. If ! SameValueNonNumeric(monthCode, ! BuildISOMonthCode(numberPart)) is false, then + if (month_code_string != build_iso_month_code(number_part_integer)) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::TemporalInvalidMonthCode); + return {}; + } + // 12. Return numberPart. + return number_part_integer; +} + +// 12.1.38 ISODateFromFields ( fields, options ), https://tc39.es/proposal-temporal/#sec-temporal-isodatefromfields +Optional iso_date_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, « "day", "month", "monthCode", "year" », «»). + auto* prepared_fields = prepare_temporal_fields(global_object, fields, { "day", "month", "monthCode", "year" }, {}); + 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(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 day be ? Get(fields, "day"). + auto day = prepared_fields->get(vm.names.day); + if (vm.exception()) + return {}; + + // 8. If day is undefined, throw a TypeError exception. + if (day.is_undefined()) { + vm.throw_exception(global_object, ErrorType::TemporalMissingRequiredProperty, vm.names.day.as_string()); + return {}; + } + + // 9. Return ? RegulateISODate(year, month, day, overflow). + return regulate_iso_date(global_object, year.as_double(), month, day.as_double(), *overflow); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h index d729fdba92..1bf37652f2 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include namespace JS::Temporal { @@ -36,5 +37,8 @@ Object* to_temporal_calendar(GlobalObject&, Value); Object* to_temporal_calendar_with_iso_default(GlobalObject&, Value); bool is_iso_leap_year(i32 year); i32 iso_days_in_month(i32 year, i32 month); +String build_iso_month_code(i32 month); +double resolve_iso_month(GlobalObject&, Object& fields); +Optional iso_date_from_fields(GlobalObject&, Object& fields, Object& options); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp index 822c6486d0..756f6ac7d4 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp @@ -5,8 +5,10 @@ */ #include +#include #include #include +#include namespace JS::Temporal { @@ -27,6 +29,7 @@ void CalendarPrototype::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_accessor(vm.names.id, id_getter, {}, Attribute::Configurable); + define_native_function(vm.names.dateFromFields, date_from_fields, 2, attr); define_native_function(vm.names.toString, to_string, 0, attr); define_native_function(vm.names.toJSON, to_json, 0, attr); } @@ -54,6 +57,40 @@ JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::id_getter) return js_string(vm, calendar.to_string(global_object)); } +// 12.4.4 Temporal.Calendar.prototype.dateFromFields ( fields, options ), https://tc39.es/proposal-temporal/#sec-temporal.calendar.prototype.datefromfields +// NOTE: This is the minimum dateFromFields implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::date_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(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 ? ISODateFromFields(fields, options). + auto result = iso_date_from_fields(global_object, fields.as_object(), *options); + if (vm.exception()) + return {}; + + // 7. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). + return create_temporal_date(global_object, result->year, result->month, result->day, *calendar); +} + // 12.4.23 Temporal.Calendar.prototype.toString ( ), https://tc39.es/proposal-temporal/#sec-temporal.calendar.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::to_string) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h index 80c96fc56b..714d1f9c51 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h @@ -20,6 +20,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(id_getter); + JS_DECLARE_NATIVE_FUNCTION(date_from_fields); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_json); }; diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index efe78699f8..1b59f6283b 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -66,6 +66,57 @@ PlainDate* create_temporal_date(GlobalObject& global_object, i32 iso_year, i32 i return object; } +// 3.5.4 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate +Optional regulate_iso_date(GlobalObject& global_object, double year, double month, double day, String const& overflow) +{ + auto& vm = global_object.vm(); + // 1. Assert: year, month, and day are integers. + VERIFY(year == trunc(year) && month == trunc(month) && day == trunc(day)); + // 2. Assert: overflow is either "constrain" or "reject". + // NOTE: Asserted by the VERIFY_NOT_REACHED at the end + + // 3. 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 IsValidISODate will immediately check that these values are valid ISO + // values (for years: -273975 - 273975, for months: 1 - 12, for days: 1 - 31) all of which are subsets of this check. + if (!AK::is_within_range(year) || !AK::is_within_range(month) || !AK::is_within_range(day)) { + vm.throw_exception(global_object, ErrorType::TemporalInvalidPlainDate); + return {}; + } + auto y = static_cast(year); + auto m = static_cast(month); + auto d = static_cast(day); + // a. If ! IsValidISODate(year, month, day) is false, throw a RangeError exception. + if (is_valid_iso_date(y, m, d)) { + vm.throw_exception(global_object, ErrorType::TemporalInvalidPlainDate); + return {}; + } + // b. Return the Record { [[Year]]: year, [[Month]]: month, [[Day]]: day }. + return TemporalDate { .year = y, .month = m, .day = d, .calendar = {} }; + } + // 4. If overflow is "constrain", then + else if (overflow == "constrain"sv) { + // IMPLEMENTATION DEFINED: This is an optimization that allows us to treat this double as normal integer from this point onwards. This + // does not change the exposed behaviour as the parent's call to CreateTemporalDate will immediately check that this value is a valid + // ISO value for years: -273975 - 273975, which is a subset of this check. + if (!AK::is_within_range(year)) { + vm.throw_exception(global_object, ErrorType::TemporalInvalidPlainDate); + return {}; + } + auto y = static_cast(year); + + // a. Set month to ! ConstrainToRange(month, 1, 12). + month = constrain_to_range(month, 1, 12); + // b. Set day to ! ConstrainToRange(day, 1, ! ISODaysInMonth(year, month)). + day = constrain_to_range(day, 1, iso_days_in_month(y, month)); + + // c. Return the Record { [[Year]]: year, [[Month]]: month, [[Day]]: day }. + return TemporalDate { .year = y, .month = static_cast(month), .day = static_cast(day), .calendar = {} }; + } + VERIFY_NOT_REACHED(); +} + // 3.5.5 IsValidISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate bool is_valid_iso_date(i32 year, i32 month, i32 day) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h index fde86d4bd3..1bbae1d0ec 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -6,6 +6,7 @@ #pragma once +#include #include namespace JS::Temporal { @@ -33,7 +34,8 @@ private: Object& m_calendar; // [[Calendar]] }; -PlainDate* create_temporal_date(GlobalObject&, i32 iso_year, i32 iso_month, i32 iso_day, Object& calendar, FunctionObject* new_target); +PlainDate* create_temporal_date(GlobalObject&, i32 iso_year, i32 iso_month, i32 iso_day, Object& calendar, FunctionObject* new_target = nullptr); +Optional regulate_iso_date(GlobalObject&, double year, double month, double day, String const& overflow); bool is_valid_iso_date(i32 year, i32 month, i32 day); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateFromFields.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateFromFields.js new file mode 100644 index 0000000000..552518d014 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateFromFields.js @@ -0,0 +1,11 @@ +describe("correct behavior", () => { + test("length is 2", () => { + expect(Temporal.Calendar.prototype.dateFromFields).toHaveLength(2); + }); + + test("basic functionality", () => { + const calendar = new Temporal.Calendar("iso8601"); + const date = calendar.dateFromFields({ year: 2000, month: 5, day: 2 }); + expect(date.calendar).toBe(calendar); + }); +});