From 99adb54391d6afa0ee600c0c6f512f5bd5cb4f39 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 10 Oct 2021 22:46:10 +0100 Subject: LibJS: Implement Temporal.Calendar.prototype.dateUntil() --- .../Libraries/LibJS/Runtime/CommonPropertyNames.h | 1 + .../LibJS/Runtime/Temporal/CalendarPrototype.cpp | 33 ++++ .../LibJS/Runtime/Temporal/CalendarPrototype.h | 1 + .../Libraries/LibJS/Runtime/Temporal/PlainDate.cpp | 185 +++++++++++++++++++++ .../Libraries/LibJS/Runtime/Temporal/PlainDate.h | 8 + .../Calendar/Calendar.prototype.dateUntil.js | 79 +++++++++ 6 files changed, 307 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateUntil.js (limited to 'Userland/Libraries') diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index f3abe5bbd0..be62d21cdf 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -117,6 +117,7 @@ namespace JS { P(currencySign) \ P(dateAdd) \ P(dateFromFields) \ + P(dateUntil) \ P(day) \ P(dayOfWeek) \ P(dayOfYear) \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp index 9cd2d314e8..204162e55f 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.cpp @@ -41,6 +41,7 @@ void CalendarPrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.yearMonthFromFields, year_month_from_fields, 1, attr); define_native_function(vm.names.monthDayFromFields, month_day_from_fields, 1, attr); define_native_function(vm.names.dateAdd, date_add, 2, attr); + define_native_function(vm.names.dateUntil, date_until, 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); @@ -199,6 +200,38 @@ JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::date_add) return TRY_OR_DISCARD(create_temporal_date(global_object, result.year, result.month, result.day, *calendar)); } +// 12.4.8 Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.calendar.prototype.dateuntil +// NOTE: This is the minimum dateUntil implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(CalendarPrototype::date_until) +{ + // 1. Let calendar be the this value. + // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). + auto* calendar = typed_this_object(global_object); + if (vm.exception()) + return {}; + + // 3. Assert: calendar.[[Identifier]] is "iso8601". + VERIFY(calendar->identifier() == "iso8601"sv); + + // 4. Set one to ? ToTemporalDate(one). + auto* one = TRY_OR_DISCARD(to_temporal_date(global_object, vm.argument(0))); + + // 5. Set two to ? ToTemporalDate(two). + auto* two = TRY_OR_DISCARD(to_temporal_date(global_object, vm.argument(1))); + + // 6. Set options to ? GetOptionsObject(options). + auto* options = TRY_OR_DISCARD(get_options_object(global_object, vm.argument(2))); + + // 7. Let largestUnit be ? ToLargestTemporalUnit(options, « "hour", "minute", "second", "millisecond", "microsecond", "nanosecond" », "auto", "day"). + auto largest_unit = TRY_OR_DISCARD(to_largest_temporal_unit(global_object, *options, { "hour"sv, "minute"sv, "second"sv, "millisecond"sv, "microsecond"sv, "nanosecond"sv }, "auto"sv, "day"sv)); + + // 8. Let result be ! DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit). + auto result = difference_iso_date(global_object, one->iso_year(), one->iso_month(), one->iso_day(), two->iso_year(), two->iso_month(), two->iso_day(), largest_unit); + + // 9. Return ? CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0). + return TRY_OR_DISCARD(create_temporal_duration(global_object, result.years, result.months, result.weeks, result.days, 0, 0, 0, 0, 0, 0)); +} + // 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 ef08bc2c7b..4c32e4a7d6 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/CalendarPrototype.h @@ -25,6 +25,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(year_month_from_fields); JS_DECLARE_NATIVE_FUNCTION(month_day_from_fields); JS_DECLARE_NATIVE_FUNCTION(date_add); + JS_DECLARE_NATIVE_FUNCTION(date_until); 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 493ad6c750..21aa82fe5e 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -142,6 +142,191 @@ ThrowCompletionOr to_temporal_date(GlobalObject& global_object, Valu return create_temporal_date(global_object, result.year, result.month, result.day, *calendar); } +// 3.5.3 DifferenceISODate ( y1, m1, d1, y2, m2, d2, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-differenceisodate +DifferenceISODateResult difference_iso_date(GlobalObject& global_object, i32 year1, u8 month1, u8 day1, i32 year2, u8 month2, u8 day2, StringView largest_unit) +{ + // 1. Assert: largestUnit is one of "year", "month", "week", or "day". + VERIFY(largest_unit.is_one_of("year"sv, "month"sv, "week"sv, "day"sv)); + + // 2. If largestUnit is "year" or "month", then + if (largest_unit.is_one_of("year"sv, "month"sv)) { + // a. Let sign be -(! CompareISODate(y1, m1, d1, y2, m2, d2)). + auto sign = -compare_iso_date(year1, month1, day1, year2, month2, day2); + + // b. If sign is 0, return the Record { [[Years]]: 0, [[Months]]: 0, [[Weeks]]: 0, [[Days]]: 0 }. + if (sign == 0) + return { .years = 0, .months = 0, .weeks = 0, .days = 0 }; + + // c. Let start be the Record { [[Year]]: y1, [[Month]]: m1, [[Day]]: d1 }. + auto start = ISODate { .year = year1, .month = month1, .day = day1 }; + + // d. Let end be the Record { [[Year]]: y2, [[Month]]: m2, [[Day]]: d2 }. + auto end = ISODate { .year = year2, .month = month2, .day = day2 }; + + // e. Let years be end.[[Year]] − start.[[Year]]. + double years = end.year - start.year; + + // f. Let mid be ! AddISODate(y1, m1, d1, years, 0, 0, 0, "constrain"). + auto mid = MUST(add_iso_date(global_object, year1, month1, day1, years, 0, 0, 0, "constrain"sv)); + + // g. Let midSign be -(! CompareISODate(mid.[[Year]], mid.[[Month]], mid.[[Day]], y2, m2, d2)). + auto mid_sign = -compare_iso_date(mid.year, mid.month, mid.day, year2, month2, day2); + + // h. If midSign is 0, then + if (mid_sign == 0) { + // i. If largestUnit is "year", return the Record { [[Years]]: years, [[Months]]: 0, [[Weeks]]: 0, [[Days]]: 0 }. + if (largest_unit == "year"sv) + return { .years = years, .months = 0, .weeks = 0, .days = 0 }; + + // ii. Return the Record { [[Years]]: 0, [[Months]]: years × 12, [[Weeks]]: 0, [[Days]]: 0 }. + return { .years = 0, .months = years * 12, .weeks = 0, .days = 0 }; + } + + // i. Let months be end.[[Month]] − start.[[Month]]. + double months = end.month - start.month; + + // j. If midSign is not equal to sign, then + if (mid_sign != sign) { + // i. Set years to years - sign. + years -= sign; + + // ii. Set months to months + sign × 12. + months += sign * 12; + } + + // k. Set mid to ! AddISODate(y1, m1, d1, years, months, 0, 0, "constrain"). + mid = MUST(add_iso_date(global_object, year1, month1, day1, years, months, 0, 0, "constrain"sv)); + + // l. Set midSign to -(! CompareISODate(mid.[[Year]], mid.[[Month]], mid.[[Day]], y2, m2, d2)). + mid_sign = -compare_iso_date(mid.year, mid.month, mid.day, year2, month2, day2); + + // m. If midSign is 0, then + if (mid_sign == 0) { + // i. If largestUnit is "year", return the Record { [[Years]]: years, [[Months]]: months, [[Weeks]]: 0, [[Days]]: 0 }. + if (largest_unit == "year"sv) + return { .years = years, .months = months, .weeks = 0, .days = 0 }; + + // ii. Return the Record { [[Years]]: 0, [[Months]]: months + years × 12, [[Weeks]]: 0, [[Days]]: 0 }. + return { .years = 0, .months = months + years * 12, .weeks = 0, .days = 0 }; + } + + // n. If midSign is not equal to sign, then + if (mid_sign != sign) { + // i. Set months to months - sign. + months -= sign; + + // ii. If months is equal to -sign, then + if (months == -sign) { + // 1. Set years to years - sign. + years -= sign; + + // 2. Set months to 11 × sign. + months = 11 * sign; + } + + // iii. Set mid to ! AddISODate(y1, m1, d1, years, months, 0, 0, "constrain"). + mid = MUST(add_iso_date(global_object, year1, month1, day1, years, months, 0, 0, "constrain"sv)); + + // FIXME: This is not used (spec issue, see https://github.com/tc39/proposal-temporal/issues/1483). + // iv. Set midSign to -(! CompareISODate(mid.[[Year]], mid.[[Month]], mid.[[Day]], y2, m2, d2)). + mid_sign = -compare_iso_date(mid.year, mid.month, mid.day, year2, month2, day2); + } + + // o. Let days be 0. + double days = 0; + + // p. If mid.[[Month]] = end.[[Month]], then + if (mid.month == end.month) { + // i. Assert: mid.[[Year]] = end.[[Year]]. + VERIFY(mid.year == end.year); + + // ii. Set days to end.[[Day]] - mid.[[Day]]. + days = end.day - mid.day; + } + // q. Else if sign < 0, set days to -mid.[[Day]] - (! ISODaysInMonth(end.[[Year]], end.[[Month]]) - end.[[Day]]). + else if (sign < 0) { + days = -mid.day - (iso_days_in_month(end.year, end.month) - end.day); + } + // r. Else, set days to end.[[Day]] + (! ISODaysInMonth(mid.[[Year]], mid.[[Month]]) - mid.[[Day]]). + else { + days = end.day + (iso_days_in_month(mid.year, mid.month) - mid.day); + } + + // s. If largestUnit is "month", then + if (largest_unit == "month"sv) { + // i. Set months to months + years × 12. + months += years * 12; + + // ii. Set years to 0. + years = 0; + } + + // t. Return the Record { [[Years]]: years, [[Months]]: months, [[Weeks]]: 0, [[Days]]: days }. + return { .years = years, .months = months, .weeks = 0, .days = days }; + } + // 3. If largestUnit is "day" or "week", then + else { + ISODate smaller; + ISODate greater; + i8 sign; + + // a. If ! CompareISODate(y1, m1, d1, y2, m2, d2) < 0, then + if (compare_iso_date(year1, month1, day1, year2, month2, day2) < 0) { + // i. Let smaller be the Record { [[Year]]: y1, [[Month]]: m1, [[Day]]: d1 }. + smaller = { .year = year1, .month = month1, .day = day1 }; + + // ii. Let greater be the Record { [[Year]]: y2, [[Month]]: m2, [[Day]]: d2 }. + greater = { .year = year2, .month = month2, .day = day2 }; + + // iii. Let sign be 1. + sign = 1; + } + // b. Else, + else { + // i. Let smaller be the Record { [[Year]]: y2, [[Month]]: m2, [[Day]]: d2 }. + smaller = { .year = year2, .month = month2, .day = day2 }; + + // ii. Let greater be the Record { [[Year]]: y1, [[Month]]: m1, [[Day]]: d1 }. + greater = { .year = year1, .month = month1, .day = day1 }; + + // iii. Let sign be −1. + sign = -1; + } + + // c. Let days be ! ToISODayOfYear(greater.[[Year]], greater.[[Month]], greater.[[Day]]) − ! ToISODayOfYear(smaller.[[Year]], smaller.[[Month]], smaller.[[Day]]). + double days = to_iso_day_of_year(greater.year, greater.month, greater.day) - to_iso_day_of_year(smaller.year, smaller.month, smaller.day); + + // d. Let year be smaller.[[Year]]. + auto year = smaller.year; + + // e. Repeat, while year < greater.[[Year]], + while (year < greater.year) { + // i. Set days to days + ! ISODaysInYear(year). + days += iso_days_in_year(year); + + // ii. Set year to year + 1. + year++; + } + + // f. Let weeks be 0. + double weeks = 0; + + // g. If largestUnit is "week", then + if (largest_unit == "week"sv) { + // i. Set weeks to floor(days / 7). + weeks = floor(days / 7); + + // ii. Set days to days modulo 7. + days = fmod(days, 7); + } + + // h. Return the Record { [[Years]]: 0, [[Months]]: 0, [[Weeks]]: weeks × sign, [[Days]]: days × sign }. + // NOTE: We set weeks and days conditionally to avoid negative zero for 0 * -1. + return { .years = 0, .months = 0, .weeks = (weeks != 0) ? weeks * sign : 0, .days = (days != 0) ? days * sign : 0 }; + } + VERIFY_NOT_REACHED(); +} + // 3.5.4 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate ThrowCompletionOr regulate_iso_date(GlobalObject& global_object, double year, double month, double day, StringView overflow) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 4d89285e8b..bbce1b366f 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -41,8 +41,16 @@ struct ISODate { u8 day; }; +struct DifferenceISODateResult { + double years; + double months; + double weeks; + double days; +}; + ThrowCompletionOr create_temporal_date(GlobalObject&, i32 iso_year, u8 iso_month, u8 iso_day, Object& calendar, FunctionObject const* new_target = nullptr); ThrowCompletionOr to_temporal_date(GlobalObject&, Value item, Object* options = nullptr); +DifferenceISODateResult difference_iso_date(GlobalObject&, i32 year1, u8 month1, u8 day1, i32 year2, u8 month2, u8 day2, StringView largest_unit); ThrowCompletionOr regulate_iso_date(GlobalObject&, double year, double month, double day, StringView overflow); bool is_valid_iso_date(i32 year, u8 month, u8 day); ISODate balance_iso_date(double year, double month, double day); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateUntil.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateUntil.js new file mode 100644 index 0000000000..6758371906 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Calendar/Calendar.prototype.dateUntil.js @@ -0,0 +1,79 @@ +describe("correct behavior", () => { + test("length is 2", () => { + expect(Temporal.Calendar.prototype.dateUntil).toHaveLength(2); + }); + + test("basic functionality", () => { + const calendar = new Temporal.Calendar("iso8601"); + const one = new Temporal.PlainDate(2021, 7, 6); + const two = new Temporal.PlainDate(2021, 10, 10); + + const oneToTwo = calendar.dateUntil(one, two); + expect(oneToTwo.years).toBe(0); + expect(oneToTwo.months).toBe(0); + expect(oneToTwo.weeks).toBe(0); + expect(oneToTwo.days).toBe(96); + expect(oneToTwo.hours).toBe(0); + expect(oneToTwo.minutes).toBe(0); + expect(oneToTwo.seconds).toBe(0); + expect(oneToTwo.milliseconds).toBe(0); + expect(oneToTwo.microseconds).toBe(0); + expect(oneToTwo.nanoseconds).toBe(0); + + const twoToOne = calendar.dateUntil(two, one); + expect(twoToOne.years).toBe(0); + expect(twoToOne.months).toBe(0); + expect(twoToOne.weeks).toBe(0); + expect(twoToOne.days).toBe(-96); + expect(twoToOne.hours).toBe(0); + expect(twoToOne.minutes).toBe(0); + expect(twoToOne.seconds).toBe(0); + expect(twoToOne.milliseconds).toBe(0); + expect(twoToOne.microseconds).toBe(0); + expect(twoToOne.nanoseconds).toBe(0); + }); + + test("largestUnit option", () => { + const calendar = new Temporal.Calendar("iso8601"); + const one = new Temporal.PlainDate(1970, 1, 1); + const two = new Temporal.PlainDate(2021, 7, 6); + + const values = [ + ["years", 51, 6, 0, 5], + ["months", 0, 618, 0, 5], + ["weeks", 0, 0, 2687, 5], + ["days", 0, 0, 0, 18814], + ]; + for (const [largestUnit, years, months, weeks, days] of values) { + const duration = calendar.dateUntil(one, two, { largestUnit }); + expect(duration.years).toBe(years); + expect(duration.months).toBe(months); + expect(duration.weeks).toBe(weeks); + expect(duration.days).toBe(days); + expect(duration.hours).toBe(0); + expect(duration.minutes).toBe(0); + expect(duration.seconds).toBe(0); + expect(duration.milliseconds).toBe(0); + expect(duration.microseconds).toBe(0); + expect(duration.nanoseconds).toBe(0); + } + }); +}); + +describe("errors", () => { + test("forbidden largestUnit option values", () => { + const calendar = new Temporal.Calendar("iso8601"); + const one = new Temporal.PlainDate(1970, 1, 1); + const two = new Temporal.PlainDate(2021, 7, 6); + + const values = ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"]; + for (const largestUnit of values) { + expect(() => { + calendar.dateUntil(one, two, { largestUnit }); + }).toThrowWithMessage( + RangeError, + `${largestUnit} is not a valid value for option largestUnit` + ); + } + }); +}); -- cgit v1.2.3