diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /js/src/builtin/temporal/PlainDate.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | js/src/builtin/temporal/PlainDate.cpp | 2999 |
1 files changed, 2999 insertions, 0 deletions
diff --git a/js/src/builtin/temporal/PlainDate.cpp b/js/src/builtin/temporal/PlainDate.cpp new file mode 100644 index 0000000000..759456c9cc --- /dev/null +++ b/js/src/builtin/temporal/PlainDate.cpp @@ -0,0 +1,2999 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainDate.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" + +#include <algorithm> +#include <cmath> +#include <cstdlib> +#include <initializer_list> +#include <stdint.h> +#include <type_traits> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.h" +#include "builtin/temporal/TemporalParser.h" +#include "builtin/temporal/TemporalRoundingMode.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "builtin/temporal/TimeZone.h" +#include "builtin/temporal/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/Date.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" +#include "vm/PropertyInfo.h" +#include "vm/Realm.h" +#include "vm/Shape.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; +using namespace js::temporal; + +static inline bool IsPlainDate(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainDateObject>(); +} + +#ifdef DEBUG +/** + * IsValidISODate ( year, month, day ) + */ +template <typename T> +static bool IsValidISODate(T year, T month, T day) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 2. + if (month < 1 || month > 12) { + return false; + } + + // Step 3. + int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); + + // Step 4. + if (day < 1 || day > daysInMonth) { + return false; + } + + // Step 5. + return true; +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::IsValidISODate(const PlainDate& date) { + auto& [year, month, day] = date; + return ::IsValidISODate(year, month, day); +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::IsValidISODate(double year, double month, double day) { + return ::IsValidISODate(year, month, day); +} +#endif + +static void ReportInvalidDateValue(JSContext* cx, const char* name, int32_t min, + int32_t max, double num) { + Int32ToCStringBuf minCbuf; + const char* minStr = Int32ToCString(&minCbuf, min); + + Int32ToCStringBuf maxCbuf; + const char* maxStr = Int32ToCString(&maxCbuf, max); + + ToCStringBuf numCbuf; + const char* numStr = NumberToCString(&numCbuf, num); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID_VALUE, name, + minStr, maxStr, numStr); +} + +template <typename T> +static inline bool ThrowIfInvalidDateValue(JSContext* cx, const char* name, + int32_t min, int32_t max, T num) { + if (min <= num && num <= max) { + return true; + } + ReportInvalidDateValue(cx, name, min, max, num); + return false; +} + +/** + * IsValidISODate ( year, month, day ) + */ +template <typename T> +static bool ThrowIfInvalidISODate(JSContext* cx, T year, T month, T day) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 2. + if (!ThrowIfInvalidDateValue(cx, "month", 1, 12, month)) { + return false; + } + + // Step 3. + int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); + + // Step 4. + if (!ThrowIfInvalidDateValue(cx, "day", 1, daysInMonth, day)) { + return false; + } + + // Step 5. + return true; +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, const PlainDate& date) { + auto& [year, month, day] = date; + return ::ThrowIfInvalidISODate(cx, year, month, day); +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, double year, + double month, double day) { + return ::ThrowIfInvalidISODate(cx, year, month, day); +} + +/** + * RegulateISODate ( year, month, day, overflow ) + * + * With |overflow = "constrain"|. + */ +static PlainDate ConstrainISODate(const PlainDate& date) { + auto& [year, month, day] = date; + + // Step 1.a. + int32_t m = std::clamp(month, 1, 12); + + // Step 1.b. + int32_t daysInMonth = temporal::ISODaysInMonth(year, m); + + // Step 1.c. + int32_t d = std::clamp(day, 1, daysInMonth); + + // Step 1.d. + return {year, m, d}; +} + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool js::temporal::RegulateISODate(JSContext* cx, const PlainDate& date, + TemporalOverflow overflow, + PlainDate* result) { + // Step 1. + if (overflow == TemporalOverflow::Constrain) { + *result = ::ConstrainISODate(date); + return true; + } + + // Step 2.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 2.b. + if (!ThrowIfInvalidISODate(cx, date)) { + return false; + } + + // Step 2.b. (Inlined call to CreateISODateRecord.) + *result = date; + return true; +} + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool js::temporal::RegulateISODate(JSContext* cx, double year, double month, + double day, TemporalOverflow overflow, + RegulatedISODate* result) { + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 1. + if (overflow == TemporalOverflow::Constrain) { + // Step 1.a. + int32_t m = int32_t(std::clamp(month, 1.0, 12.0)); + + // Step 1.b. + double daysInMonth = double(ISODaysInMonth(year, m)); + + // Step 1.c. + int32_t d = int32_t(std::clamp(day, 1.0, daysInMonth)); + + // Step 1.d. + *result = {year, m, d}; + return true; + } + + // Step 2.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 2.b. + if (!ThrowIfInvalidISODate(cx, year, month, day)) { + return false; + } + + // Step 2.b. (Inlined call to CreateISODateRecord.) + *result = {year, int32_t(month), int32_t(day)}; + return true; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +static PlainDateObject* CreateTemporalDate(JSContext* cx, const CallArgs& args, + double isoYear, double isoMonth, + double isoDay, + Handle<CalendarValue> calendar) { + MOZ_ASSERT(IsInteger(isoYear)); + MOZ_ASSERT(IsInteger(isoMonth)); + MOZ_ASSERT(IsInteger(isoDay)); + + // Step 1. + if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(isoYear, isoMonth, isoDay)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return nullptr; + } + + // Steps 3-4. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainDate, + &proto)) { + return nullptr; + } + + auto* object = NewObjectWithClassProto<PlainDateObject>(cx, proto); + if (!object) { + return nullptr; + } + + // Step 5. + object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 8. + object->setFixedSlot(PlainDateObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 9. + return object; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +PlainDateObject* js::temporal::CreateTemporalDate( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar) { + auto& [isoYear, isoMonth, isoDay] = date; + + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(date)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return nullptr; + } + + // Steps 3-4. + auto* object = NewBuiltinClassInstance<PlainDateObject>(cx); + if (!object) { + return nullptr; + } + + // Step 5. + object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 8. + object->setFixedSlot(PlainDateObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 9. + return object; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +bool js::temporal::CreateTemporalDate( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar, + MutableHandle<PlainDateWithCalendar> result) { + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return false; + } + + // Step 2. + if (!ISODateTimeWithinLimits(date)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Steps 3-9. + result.set(PlainDateWithCalendar{date, calendar}); + return true; +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate( + JSContext* cx, Handle<JSObject*> item, Handle<PlainObject*> maybeOptions) { + // Step 1-2. (Not applicable in our implementation.) + + // Step 3.a. + if (item->canUnwrapAs<PlainDateObject>()) { + return item; + } + + // Step 3.b. + if (auto* zonedDateTime = item->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto epochInstant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + if (!timeZone.wrap(cx)) { + return nullptr; + } + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.b.i. + if (maybeOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeOptions, &ignored)) { + return nullptr; + } + } + + // Steps 3.b.ii-iv. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, epochInstant, &dateTime)) { + return nullptr; + } + + // Step 3.b.v. + return CreateTemporalDate(cx, dateTime.date, calendar); + } + + // Step 3.c. + if (auto* dateTime = item->maybeUnwrapIf<PlainDateTimeObject>()) { + auto date = ToPlainDate(dateTime); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.c.i. + if (maybeOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeOptions, &ignored)) { + return nullptr; + } + } + + // Step 3.c.ii. + return CreateTemporalDate(cx, date, calendar); + } + + // Step 3.d. + Rooted<CalendarValue> calendarValue(cx); + if (!GetTemporalCalendarWithISODefault(cx, item, &calendarValue)) { + return nullptr; + } + + // Step 3.e. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendar)) { + return nullptr; + } + + // Step 3.f. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return nullptr; + } + + // Step 3.g. + Rooted<PlainObject*> fields(cx, PrepareTemporalFields(cx, item, fieldNames)); + if (!fields) { + return nullptr; + } + + // Step 3.h. + if (maybeOptions) { + return temporal::CalendarDateFromFields(cx, calendar, fields, maybeOptions); + } + return temporal::CalendarDateFromFields(cx, calendar, fields); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate( + JSContext* cx, Handle<Value> item, Handle<JSObject*> maybeOptions) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return nullptr; + } + } + + // Step 3. + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + return ::ToTemporalDate(cx, itemObj, maybeResolvedOptions); + } + + // Step 4. + if (!item.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item, + nullptr, "not a string"); + return nullptr; + } + Rooted<JSString*> string(cx, item.toString()); + + // Step 5. + PlainDate result; + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalDateString(cx, string, &result, &calendarString)) { + return nullptr; + } + + // Step 6. + MOZ_ASSERT(IsValidISODate(result)); + + // Steps 7-10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return nullptr; + } + } + + // Step 11. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Step 12. + return CreateTemporalDate(cx, result, calendar); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate(JSContext* cx, + Handle<Value> item) { + return ::ToTemporalDate(cx, item, nullptr); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool js::temporal::ToTemporalDate(JSContext* cx, Handle<Value> item, + PlainDate* result) { + auto obj = ::ToTemporalDate(cx, item, nullptr); + if (!obj) { + return false; + } + + *result = ToPlainDate(&obj.unwrap()); + return true; +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool js::temporal::ToTemporalDate(JSContext* cx, Handle<Value> item, + MutableHandle<PlainDateWithCalendar> result) { + auto* obj = ::ToTemporalDate(cx, item, nullptr).unwrapOrNull(); + if (!obj) { + return false; + } + + auto date = ToPlainDate(obj); + Rooted<CalendarValue> calendar(cx, obj->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + result.set(PlainDateWithCalendar{date, calendar}); + return true; +} + +/** + * Mathematical Operations, "modulo" notation. + */ +static int32_t NonNegativeModulo(double x, int32_t y) { + MOZ_ASSERT(IsInteger(x)); + MOZ_ASSERT(y > 0); + + double r = std::fmod(x, y); + + int32_t result; + MOZ_ALWAYS_TRUE(mozilla::NumberEqualsInt32(r, &result)); + + return (result < 0) ? (result + y) : result; +} + +struct BalancedYearMonth final { + double year = 0; + int32_t month = 0; +}; + +/** + * BalanceISOYearMonth ( year, month ) + */ +static BalancedYearMonth BalanceISOYearMonth(double year, double month) { + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + + // Note: If either abs(year) or abs(month) is greater than 2^53 (the double + // integral precision limit), the additions resp. subtractions below are + // imprecise. This doesn't matter for us, because the single caller to this + // function (AddISODate) will throw an error for large values anyway. + + // Step 2. + year = year + std::floor((month - 1) / 12); + MOZ_ASSERT(IsInteger(year) || std::isinf(year)); + + // Step 3. + int32_t mon = NonNegativeModulo(month - 1, 12) + 1; + MOZ_ASSERT(1 <= mon && mon <= 12); + + // Step 4. + return {year, mon}; +} + +static bool CanBalanceISOYear(double year) { + // TODO: Export these values somewhere. + constexpr int32_t minYear = -271821; + constexpr int32_t maxYear = 275760; + + // If the year is below resp. above the min-/max-year, no value of |day| will + // make the resulting date valid. + return minYear <= year && year <= maxYear; +} + +static bool CanBalanceISODay(double day) { + // The maximum number of seconds from the epoch is 8.64 * 10^12. + constexpr int64_t maxInstantSeconds = 8'640'000'000'000; + + // In days that makes 10^8. + constexpr int64_t maxInstantDays = maxInstantSeconds / 60 / 60 / 24; + + // Multiply by two to take both directions into account and add twenty to + // account for the day number of the minimum date "-271821-02-20". + constexpr int64_t maximumDayDifference = 2 * maxInstantDays + 20; + + // When |day| is below |maximumDayDifference|, it can be represented as int32. + static_assert(maximumDayDifference <= INT32_MAX); + + // When the day difference exceeds the maximum valid day difference, the + // overall result won't be a valid date. Detect this early so we don't have to + // struggle with floating point precision issues in BalanceISODate. + // + // This also means BalanceISODate, step 1 doesn't apply to our implementation. + return std::abs(day) <= maximumDayDifference; +} + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate js::temporal::BalanceISODateNew(int32_t year, int32_t month, + int32_t day) { + MOZ_ASSERT(1 <= month && month <= 12); + + // Steps 1-3. + int64_t ms = MakeDate(year, month, day); + + // FIXME: spec issue - |ms| can be non-finite + // https://github.com/tc39/proposal-temporal/issues/2315 + + // TODO: This approach isn't efficient, because MonthFromTime and DayFromTime + // both recompute YearFromTime. + + // Step 4. + return {int32_t(JS::YearFromTime(ms)), int32_t(JS::MonthFromTime(ms) + 1), + int32_t(JS::DayFromTime(ms))}; +} + +/** + * BalanceISODate ( year, month, day ) + */ +bool js::temporal::BalanceISODate(JSContext* cx, int32_t year, int32_t month, + int64_t day, PlainDate* result) { + if (!CanBalanceISODay(day)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + *result = BalanceISODate(year, month, int32_t(day)); + return true; +} + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate js::temporal::BalanceISODate(int32_t year, int32_t month, + int32_t day) { + // Check no inputs can lead to floating point precision issues below. This + // also ensures all loops can finish in reasonable time, so we don't need to + // worry about interrupts here. And it ensures there won't be overflows when + // using int32_t values. + MOZ_ASSERT(CanBalanceISOYear(year)); + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(CanBalanceISODay(day)); + + // TODO: BalanceISODate now works using MakeDate + // TODO: Can't use JS::MakeDate, because it expects valid month/day values. + // https://github.com/tc39/proposal-temporal/issues/2315 + + // Step 1. (Not applicable in our implementation.) + + // Steps 3-4. (Not applicable in our implementation.) + + constexpr int32_t daysInNonLeapYear = 365; + + // Skip steps 5-11 for the common case when abs(day) doesn't exceed 365. + if (std::abs(day) > daysInNonLeapYear) { + // Step 5. (Note) + + // Steps 6-7. + int32_t testYear = month > 2 ? year : year - 1; + + // Step 8. + while (day < -ISODaysInYear(testYear)) { + // Step 8.a. + day += ISODaysInYear(testYear); + + // Step 8.b. + year -= 1; + + // Step 8.c. + testYear -= 1; + } + + // Step 9. (Note) + + // Step 10. + testYear += 1; + + // Step 11. + while (day > ISODaysInYear(testYear)) { + // Step 11.a. + day -= ISODaysInYear(testYear); + + // Step 11.b. + year += 1; + + // Step 11.c. + testYear += 1; + } + } + + // Step 12. (Note) + + // Step 13. + while (day < 1) { + // Steps 13.a-b. (Inlined call to BalanceISOYearMonth.) + if (--month == 0) { + month = 12; + year -= 1; + } + + // Step 13.d + day += ISODaysInMonth(year, month); + } + + // Step 14. (Note) + + // Step 15. + while (day > ISODaysInMonth(year, month)) { + // Step 15.a. + day -= ISODaysInMonth(year, month); + + // Steps 15.b-d. (Inlined call to BalanceISOYearMonth.) + if (++month == 13) { + month = 1; + year += 1; + } + } + + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(1 <= day && day <= 31); + + // Step 16. + return {year, month, day}; +} + +/** + * AddISODate ( year, month, day, years, months, weeks, days, overflow ) + */ +bool js::temporal::AddISODate(JSContext* cx, const PlainDate& date, + const Duration& duration, + TemporalOverflow overflow, PlainDate* result) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + // TODO: Not quite sure if this holds for all callers. But if it does hold, + // then we can directly reject any numbers which can't be represented with + // int32_t. That in turn avoids the precision loss issue noted in + // BalanceISODate. + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 1. + MOZ_ASSERT(IsInteger(duration.years)); + MOZ_ASSERT(IsInteger(duration.months)); + MOZ_ASSERT(IsInteger(duration.weeks)); + MOZ_ASSERT(IsInteger(duration.days)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + auto yearMonth = BalanceISOYearMonth(date.year + duration.years, + date.month + duration.months); + MOZ_ASSERT(IsInteger(yearMonth.year) || std::isinf(yearMonth.year)); + MOZ_ASSERT(1 <= yearMonth.month && yearMonth.month <= 12); + + // FIXME: spec issue? + // new Temporal.PlainDate(2021, 5, 31).subtract({months:1, days:1}).toString() + // returns "2021-04-29", but "2021-04-30" seems more likely expected. + // Note: "2021-04-29" agrees with java.time, though. + // + // Example where this creates inconsistent results: + // + // clang-format off + // + // js> Temporal.PlainDate.from("2021-05-31").since("2021-04-30", {largestUnit:"months"}).toString() + // "P1M1D" + // js> Temporal.PlainDate.from("2021-05-31").subtract("P1M1D").toString() + // "2021-04-29" + // + // clang-format on + // + // Later: This now returns "P1M" instead "P1M1D", so the results are at least + // consistent. Let's add a test case for this behaviour. + // + // Revisit when <https://github.com/tc39/proposal-temporal/issues/2535> has + // been addressed. + + // |yearMonth.year| can only exceed the valid years range when called from + // `Temporal.Calendar.prototype.dateAdd`. And because `dateAdd` uses the + // result of AddISODate to create a new Temporal.PlainDate, we can directly + // throw an error if the result isn't within the valid date-time limits. This + // in turn allows to work on integer values and we don't have to worry about + // imprecise double value computations. + if (!CanBalanceISOYear(yearMonth.year)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Step 4. + PlainDate regulated; + if (!RegulateISODate(cx, {int32_t(yearMonth.year), yearMonth.month, date.day}, + overflow, ®ulated)) { + return false; + } + + // NB: BalanceISODate will reject too large days, so we don't have to worry + // about imprecise number arithmetic here. + + // Steps 5-6. + double d = regulated.day + (duration.days + duration.weeks * 7); + + // Just as with |yearMonth.year|, also directly throw an error if the |days| + // value is too large. + if (!CanBalanceISODay(d)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Step 7. + auto balanced = BalanceISODate(regulated.year, regulated.month, int32_t(d)); + MOZ_ASSERT(IsValidISODate(balanced)); + + *result = balanced; + return true; +} + +struct YearMonthDuration { + int32_t years = 0; + int32_t months = 0; +}; + +/** + * AddISODate ( year, month, day, years, months, weeks, days, overflow ) + * + * With |overflow = "constrain"|. + */ +static PlainDate AddISODate(const PlainDate& date, + const YearMonthDuration& duration) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + MOZ_ASSERT_IF(duration.years < 0, duration.months <= 0); + MOZ_ASSERT_IF(duration.years > 0, duration.months >= 0); + + // TODO: Export these values somewhere. + [[maybe_unused]] constexpr int32_t minYear = -271821; + [[maybe_unused]] constexpr int32_t maxYear = 275760; + + MOZ_ASSERT(std::abs(duration.years) <= (maxYear - minYear), + "years doesn't exceed the maximum duration between valid years"); + MOZ_ASSERT(std::abs(duration.months) <= 12, + "months duration is at most one year"); + + // Steps 1-2. (Not applicable) + + // Step 3. (Inlined BalanceISOYearMonth) + int32_t year = date.year + duration.years; + int32_t month = date.month + duration.months; + MOZ_ASSERT(-11 <= month && month <= 24); + + if (month > 12) { + month -= 12; + year += 1; + } else if (month <= 0) { + month += 12; + year -= 1; + } + + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(CanBalanceISOYear(year)); + + // Steps 4-7. + return ::ConstrainISODate({year, month, date.day}); +} + +static bool HasYearsMonthsOrWeeks(const Duration& duration) { + return duration.years != 0 || duration.months != 0 || duration.weeks != 0; +} + +static bool AddDate(JSContext* cx, const PlainDate& date, + const Duration& duration, Handle<JSObject*> maybeOptions, + PlainDate* result) { + MOZ_ASSERT(!HasYearsMonthsOrWeeks(duration)); + + // Steps 1-3. (Not applicable) + + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return false; + } + } + + // Step 5. + TimeDuration daysDuration; + if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &daysDuration)) { + return false; + } + + // Step 6. + return AddISODate(cx, date, {0, 0, 0, daysDuration.days}, overflow, result); +} + +static bool AddDate(JSContext* cx, Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, Handle<JSObject*> maybeOptions, + PlainDate* result) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return false; + } + return ::AddDate(cx, ToPlainDate(unwrappedDate), duration, maybeOptions, + result); +} + +static PlainDateObject* AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, + Handle<JSObject*> maybeOptions) { + // Steps 1-3. (Not applicable) + + // Steps 4-6. + PlainDate resultDate; + if (!::AddDate(cx, date, duration, maybeOptions, &resultDate)) { + return nullptr; + } + + // Step 7. + return CreateTemporalDate(cx, resultDate, calendar.receiver()); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration, + Handle<JSObject*> options) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, duration, options); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, options); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, duration); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, nullptr); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> durationObj) { + auto* unwrappedDuration = durationObj.unwrap(cx); + if (!unwrappedDuration) { + return nullptr; + } + auto duration = ToDuration(unwrappedDuration); + + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, durationObj); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, nullptr); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> durationObj, Handle<JSObject*> options) { + auto* unwrappedDuration = durationObj.unwrap(cx); + if (!unwrappedDuration) { + return nullptr; + } + auto duration = ToDuration(unwrappedDuration); + + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, durationObj, options); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, options); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool js::temporal::AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + Handle<JSObject*> options, PlainDate* result) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, duration, options, + result); + } + + // Steps 4-7. + return ::AddDate(cx, date, duration, options, result); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool js::temporal::AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, PlainDate* result) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, duration, result); + } + + // Steps 4-7. + return ::AddDate(cx, date, duration, nullptr, result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + Handle<PlainObject*> options, + Duration* result) { + auto* unwrappedOne = one.unwrap(cx); + if (!unwrappedOne) { + return false; + } + auto oneDate = ToPlainDate(unwrappedOne); + + auto* unwrappedTwo = two.unwrap(cx); + if (!unwrappedTwo) { + return false; + } + auto twoDate = ToPlainDate(unwrappedTwo); + + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + MOZ_ASSERT(options->staticPrototype() == nullptr); + + // Step 4. + MOZ_ASSERT(options->containsPure(cx->names().largestUnit)); + + // Step 5. + if (oneDate == twoDate) { + *result = {}; + return true; + } + + // Step 6. + Rooted<JS::Value> largestUnit(cx); + if (!GetProperty(cx, options, options, cx->names().largestUnit, + &largestUnit)) { + return false; + } + + if (largestUnit.isString()) { + bool isDay; + if (!EqualStrings(cx, largestUnit.toString(), cx->names().day, &isDay)) { + return false; + } + + if (isDay) { + // Step 6.a. + int32_t days = DaysUntil(oneDate, twoDate); + + // Step 6.b. + *result = {0, 0, 0, double(days)}; + return true; + } + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, options, result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, Duration* result) { + auto* unwrappedOne = one.unwrap(cx); + if (!unwrappedOne) { + return false; + } + auto oneDate = ToPlainDate(unwrappedOne); + + auto* unwrappedTwo = two.unwrap(cx); + if (!unwrappedTwo) { + return false; + } + auto twoDate = ToPlainDate(unwrappedTwo); + + // Steps 1-4. (Not applicable in our implementation.) + + // Step 5. + if (oneDate == twoDate) { + *result = {}; + return true; + } + + // Step 6. + if (largestUnit == TemporalUnit::Day) { + // Step 6.a. + int32_t days = DaysUntil(oneDate, twoDate); + + // Step 6.b. + *result = {0, 0, 0, double(days)}; + return true; + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, largestUnit, result); +} + +/** + * CompareISODate ( y1, m1, d1, y2, m2, d2 ) + */ +int32_t js::temporal::CompareISODate(const PlainDate& one, + const PlainDate& two) { + // Steps 1-2. + if (one.year != two.year) { + return one.year < two.year ? -1 : 1; + } + + // Steps 3-4. + if (one.month != two.month) { + return one.month < two.month ? -1 : 1; + } + + // Steps 5-6. + if (one.day != two.day) { + return one.day < two.day ? -1 : 1; + } + + // Step 7. + return 0; +} + +/** + * CreateDateDurationRecord ( years, months, weeks, days ) + */ +static DateDuration CreateDateDurationRecord(int32_t years, int32_t months, + int32_t weeks, int32_t days) { + MOZ_ASSERT(IsValidDuration( + {double(years), double(months), double(weeks), double(days)})); + return {double(years), double(months), double(weeks), double(days)}; +} + +/** + * DifferenceISODate ( y1, m1, d1, y2, m2, d2, largestUnit ) + */ +DateDuration js::temporal::DifferenceISODate(const PlainDate& start, + const PlainDate& end, + TemporalUnit largestUnit) { + // Steps 1-2. + MOZ_ASSERT(IsValidISODate(start)); + MOZ_ASSERT(IsValidISODate(end)); + + // Both inputs are also within the date-time limits. + MOZ_ASSERT(ISODateTimeWithinLimits(start)); + MOZ_ASSERT(ISODateTimeWithinLimits(end)); + + // Because both inputs are valid dates, we don't need to worry about integer + // overflow in any of the computations below. + + MOZ_ASSERT(TemporalUnit::Year <= largestUnit && + largestUnit <= TemporalUnit::Day); + + // Step 3. + if (largestUnit == TemporalUnit::Year || largestUnit == TemporalUnit::Month) { + // Step 3.a. + int32_t sign = -CompareISODate(start, end); + + // Step 3.b. + if (sign == 0) { + return CreateDateDurationRecord(0, 0, 0, 0); + } + + // FIXME: spec issue - results can be ambiguous, is this intentional? + // https://github.com/tc39/proposal-temporal/issues/2535 + // + // clang-format off + // js> var end = new Temporal.PlainDate(1970, 2, 28) + // js> var start = new Temporal.PlainDate(1970, 1, 28) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 29) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 30) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 31) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // + // Compare to java.time.temporal + // + // jshell> import java.time.LocalDate + // jshell> var end = LocalDate.of(1970, 2, 28) + // end ==> 1970-02-28 + // jshell> var start = LocalDate.of(1970, 1, 28) + // start ==> 1970-01-28 + // jshell> start.until(end) + // $27 ==> P1M + // jshell> var start = LocalDate.of(1970, 1, 29) + // start ==> 1970-01-29 + // jshell> start.until(end) + // $29 ==> P30D + // jshell> var start = LocalDate.of(1970, 1, 30) + // start ==> 1970-01-30 + // jshell> start.until(end) + // $31 ==> P29D + // jshell> var start = LocalDate.of(1970, 1, 31) + // start ==> 1970-01-31 + // jshell> start.until(end) + // $33 ==> P28D + // + // Also compare to: + // + // js> var end = new Temporal.PlainDate(1970, 2, 27) + // js> var start = new Temporal.PlainDate(1970, 1, 27) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 28) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P30D" + // js> var start = new Temporal.PlainDate(1970, 1, 29) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P29D" + // + // clang-format on + + // Steps 3.c-d. (Not applicable in our implementation.) + + // FIXME: spec issue - consistently use either |end.[[Year]]| or |y2|. + + // Step 3.e. + int32_t years = end.year - start.year; + + // TODO: We could inline this, because the AddISODate call is just a more + // complicated way to perform: + // mid = ConstrainISODate(end.year, start.month, start.day) + // + // The remaining computations can probably simplified similarily. + + // Step 3.f. + auto mid = ::AddISODate(start, {years, 0}); + + // Step 3.g. + int32_t midSign = -CompareISODate(mid, end); + + // Step 3.h. + if (midSign == 0) { + // Step 3.h.i. + if (largestUnit == TemporalUnit::Year) { + return CreateDateDurationRecord(years, 0, 0, 0); + } + + // Step 3.h.ii. + return CreateDateDurationRecord(0, years * 12, 0, 0); + } + + // Step 3.i. + int32_t months = end.month - start.month; + + // Step 3.j. + if (midSign != sign) { + // Step 3.j.i. + years -= sign; + + // Step 3.j.ii. + months += sign * 12; + } + + // Step 3.k. + mid = ::AddISODate(start, {years, months}); + + // Step 3.l. + midSign = -CompareISODate(mid, end); + + // Step 3.m. + if (midSign == 0) { + // Step 3.m.i. + if (largestUnit == TemporalUnit::Year) { + return CreateDateDurationRecord(years, months, 0, 0); + } + + // Step 3.m.ii. + return CreateDateDurationRecord(0, months + years * 12, 0, 0); + } + + // Step 3.n. + if (midSign != sign) { + // Step 3.n.i. + months -= sign; + + // Step 3.n.ii. + mid = ::AddISODate(start, {years, months}); + } + + // Steps 3.o-q. + int32_t days; + if (mid.month == end.month) { + MOZ_ASSERT(mid.year == end.year); + + days = end.day - mid.day; + } else if (sign < 0) { + days = -mid.day - (ISODaysInMonth(end.year, end.month) - end.day); + } else { + days = end.day + (ISODaysInMonth(mid.year, mid.month) - mid.day); + } + + // Step 3.r. + if (largestUnit == TemporalUnit::Month) { + // Step 3.r.i. + months += years * 12; + + // Step 3.r.ii. + years = 0; + } + + // Step 3.s. + return CreateDateDurationRecord(years, months, 0, days); + } + + // Step 4.a. + MOZ_ASSERT(largestUnit == TemporalUnit::Week || + largestUnit == TemporalUnit::Day); + + // Step 4.b. + int32_t epochDaysStart = MakeDay(start); + + // Step 4.c. + int32_t epochDaysEnd = MakeDay(end); + + // Step 4.d. + int32_t days = epochDaysEnd - epochDaysStart; + + // Step 4.e. + int32_t weeks = 0; + + // Step 4.f. + if (largestUnit == TemporalUnit::Week) { + // Step 4.f.i + weeks = days / 7; + + // Step 4.f.ii. + days = days % 7; + } + + // Step 4.g. + return CreateDateDurationRecord(0, 0, weeks, days); +} + +/** + * DifferenceTemporalPlainDate ( operation, temporalDate, other, options ) + */ +static bool DifferenceTemporalPlainDate(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 1. (Not applicable in our implementation) + + // Step 2. + auto wrappedOther = ::ToTemporalDate(cx, args.get(0)); + if (!wrappedOther) { + return false; + } + auto* unwrappedOther = &wrappedOther.unwrap(); + auto otherDate = ToPlainDate(unwrappedOther); + + Rooted<Wrapped<PlainDateObject*>> other(cx, wrappedOther); + Rooted<CalendarValue> otherCalendar(cx, unwrappedOther->calendar()); + if (!otherCalendar.wrap(cx)) { + return false; + } + + // Step 3. + if (!CalendarEqualsOrThrow(cx, calendarValue, otherCalendar)) { + return false; + } + + // Steps 4-5. + DifferenceSettings settings; + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 4. + resolvedOptions = SnapshotOwnProperties(cx, options); + if (!resolvedOptions) { + return false; + } + + // Step 5. + if (!GetDifferenceSettings(cx, operation, resolvedOptions, + TemporalUnitGroup::Date, TemporalUnit::Day, + TemporalUnit::Day, &settings)) { + return false; + } + } else { + // Steps 4-5. + settings = { + TemporalUnit::Day, + TemporalUnit::Day, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 6. + if (ToPlainDate(temporalDate) == otherDate) { + auto* obj = CreateTemporalDuration(cx, {}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 7. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Steps 8-9. + Duration duration; + if (resolvedOptions) { + // Step 8. + Rooted<Value> largestUnitValue( + cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); + if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, + largestUnitValue)) { + return false; + } + + // Step 9. + Duration result; + if (!DifferenceDate(cx, calendar, temporalDate, other, resolvedOptions, + &result)) { + return false; + } + duration = result.date(); + } else { + // Steps 8-9. + Duration result; + if (!DifferenceDate(cx, calendar, temporalDate, other, settings.largestUnit, + &result)) { + return false; + } + duration = result.date(); + } + + // Step 10. + bool roundingGranularityIsNoop = settings.smallestUnit == TemporalUnit::Day && + settings.roundingIncrement == Increment{1}; + + // Step 11. + if (!roundingGranularityIsNoop) { + // Steps 11.a-b. + Duration roundResult; + if (!temporal::RoundDuration(cx, duration.date(), + settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + temporalDate, calendar, &roundResult)) { + return false; + } + + // Step 11.c. + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, roundResult.date(), settings.largestUnit, settings.smallestUnit, + temporalDate, calendar, &balanceResult)) { + return false; + } + duration = balanceResult.toDuration(); + } + + // Step 12. + if (operation == TemporalDifference::Since) { + duration = duration.negate(); + } + + auto* obj = CreateTemporalDuration(cx, duration.date()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate ( isoYear, isoMonth, isoDay [ , calendarLike ] ) + */ +static bool PlainDateConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainDate")) { + return false; + } + + // Step 2. + double isoYear; + if (!ToIntegerWithTruncation(cx, args.get(0), "year", &isoYear)) { + return false; + } + + // Step 3. + double isoMonth; + if (!ToIntegerWithTruncation(cx, args.get(1), "month", &isoMonth)) { + return false; + } + + // Step 4. + double isoDay; + if (!ToIntegerWithTruncation(cx, args.get(2), "day", &isoDay)) { + return false; + } + + // Step 5. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(3), &calendar)) { + return false; + } + + // Step 6. + auto* temporalDate = + CreateTemporalDate(cx, args, isoYear, isoMonth, isoDay, calendar); + if (!temporalDate) { + return false; + } + + args.rval().setObject(*temporalDate); + return true; +} + +/** + * Temporal.PlainDate.from ( item [ , options ] ) + */ +static bool PlainDate_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + if (auto* temporalDate = item->maybeUnwrapIf<PlainDateObject>()) { + auto date = ToPlainDate(temporalDate); + + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Step 2.a. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, options, &ignored)) { + return false; + } + } + + // Step 2.b. + auto* result = CreateTemporalDate(cx, date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + } + + // Step 3. + auto result = ToTemporalDate(cx, args.get(0), options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.compare ( one, two ) + */ +static bool PlainDate_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + PlainDate one; + if (!ToTemporalDate(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + PlainDate two; + if (!ToTemporalDate(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareISODate(one, two)); + return true; +} + +/** + * get Temporal.PlainDate.prototype.calendarId + */ +static bool PlainDate_calendarId(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.PlainDate.prototype.calendarId + */ +static bool PlainDate_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_calendarId>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.year + */ +static bool PlainDate_year(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.year + */ +static bool PlainDate_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_year>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.month + */ +static bool PlainDate_month(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonth(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.month + */ +static bool PlainDate_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_month>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.monthCode + */ +static bool PlainDate_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonthCode(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.monthCode + */ +static bool PlainDate_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_monthCode>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.day + */ +static bool PlainDate_day(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDay(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.day + */ +static bool PlainDate_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_day>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.dayOfWeek + */ +static bool PlainDate_dayOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDayOfWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.dayOfWeek + */ +static bool PlainDate_dayOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_dayOfWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.dayOfYear + */ +static bool PlainDate_dayOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDayOfYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.dayOfYear + */ +static bool PlainDate_dayOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_dayOfYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.weekOfYear + */ +static bool PlainDate_weekOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarWeekOfYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.weekOfYear + */ +static bool PlainDate_weekOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_weekOfYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.yearOfWeek + */ +static bool PlainDate_yearOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarYearOfWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.yearOfWeek + */ +static bool PlainDate_yearOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_yearOfWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInWeek + */ +static bool PlainDate_daysInWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInWeek + */ +static bool PlainDate_daysInWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInMonth + */ +static bool PlainDate_daysInMonth(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInMonth(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInMonth + */ +static bool PlainDate_daysInMonth(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInMonth>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInYear + */ +static bool PlainDate_daysInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInYear + */ +static bool PlainDate_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.monthsInYear + */ +static bool PlainDate_monthsInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonthsInYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.monthsInYear + */ +static bool PlainDate_monthsInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_monthsInYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.inLeapYear + */ +static bool PlainDate_inLeapYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarInLeapYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.inLeapYear + */ +static bool PlainDate_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_inLeapYear>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toPlainYearMonth ( ) + */ +static bool PlainDate_toPlainYearMonth(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Step 6. + auto obj = CalendarYearMonthFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainYearMonth ( ) + */ +static bool PlainDate_toPlainYearMonth(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainYearMonth>(cx, + args); +} + +/** + * Temporal.PlainDate.prototype.toPlainMonthDay ( ) + */ +static bool PlainDate_toPlainMonthDay(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Example for the optimisation described in TemporalFields.cpp + + // Optimization for built-in objects. + do { + // Step 4. + static constexpr std::initializer_list<CalendarField> fieldNames = { + CalendarField::Day, CalendarField::MonthCode}; + + // Step 5. + if (calendarValue.isObject()) { + Rooted<JSObject*> calendarObj(cx, calendarValue.toObject()); + if (!calendarObj->is<CalendarObject>()) { + break; + } + auto builtinCalendar = calendarObj.as<CalendarObject>(); + + // Step 5. + if (!IsBuiltinAccess(cx, builtinCalendar, fieldNames)) { + break; + } + } + if (!IsBuiltinAccess(cx, temporalDate, fieldNames)) { + break; + } + + // Step 6. + auto date = ToPlainDate(temporalDate); + auto result = PlainDate{1972 /* referenceISOYear */, date.month, date.day}; + + auto* obj = CreateTemporalMonthDay(cx, result, calendarValue); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } while (false); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::MonthCode}, + &fieldNames)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Steps 6-7. + auto obj = CalendarMonthDayFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainMonthDay ( ) + */ +static bool PlainDate_toPlainMonthDay(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainMonthDay>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toPlainDateTime ( [ temporalTime ] ) + */ +static bool PlainDate_toPlainDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Default initialize the time component to all zero. + PlainDateTime dateTime = {ToPlainDate(temporalDate), {}}; + + // Step 4. (Reordered) + if (args.hasDefined(0)) { + if (!ToTemporalTime(cx, args[0], &dateTime.time)) { + return false; + } + } + + // Steps 3 and 5. + auto* obj = CreateTemporalDateTime(cx, dateTime, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainDateTime ( [ temporalTime ] ) + */ +static bool PlainDate_toPlainDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainDateTime>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.getISOFields ( ) + */ +static bool PlainDate_getISOFields(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + auto calendar = temporalDate->calendar(); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().calendar), calendar.toValue())) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), Int32Value(date.day))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(date.month))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(date.year))) { + return false; + } + + // Step 8. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.getISOFields ( ) + */ +static bool PlainDate_getISOFields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_getISOFields>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.getCalendar ( ) + */ +static bool PlainDate_getCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.getCalendar ( ) + */ +static bool PlainDate_getCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_getCalendar>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_add(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Rooted<Wrapped<DurationObject*>> duration( + cx, ToTemporalDuration(cx, args.get(0))); + if (!duration) { + return false; + } + + // Step 4. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "add", args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 5. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 6. + auto result = AddDate(cx, calendar, temporalDate, duration, options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_add>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.subtract ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_subtract(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Duration duration; + if (!ToTemporalDuration(cx, args.get(0), &duration)) { + return false; + } + + // Step 4. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "subtract", args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 5. + auto negatedDuration = duration.negate(); + + // Step 6. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 7. + auto result = + temporal::AddDate(cx, calendar, temporalDate, negatedDuration, options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.subtract ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_subtract>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ) + */ +static bool PlainDate_with(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + Rooted<JSObject*> temporalDateLike( + cx, RequireObjectArg(cx, "temporalDateLike", "with", args.get(0))); + if (!temporalDateLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalDateLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Step 9. + Rooted<PlainObject*> partialDate( + cx, PreparePartialTemporalFields(cx, temporalDateLike, fieldNames)); + if (!partialDate) { + return false; + } + + // Step 10. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialDate)); + if (!mergedFields) { + return false; + } + + // Step 11. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames); + if (!fields) { + return false; + } + + // Step 12. + auto result = + temporal::CalendarDateFromFields(cx, calendar, fields, resolvedOptions); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ) + */ +static bool PlainDate_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_with>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.withCalendar ( calendar ) + */ +static bool PlainDate_withCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + + // Step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // Step 4. + auto* result = CreateTemporalDate(cx, date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.withCalendar ( calendar ) + */ +static bool PlainDate_withCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_withCalendar>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.until ( other [ , options ] ) + */ +static bool PlainDate_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDate(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.PlainDate.prototype.until ( other [ , options ] ) + */ +static bool PlainDate_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_until>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.since ( other [ , options ] ) + */ +static bool PlainDate_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDate(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.PlainDate.prototype.since ( other [ , options ] ) + */ +static bool PlainDate_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_since>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.equals ( other ) + */ +static bool PlainDate_equals(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + Rooted<PlainDateWithCalendar> other(cx); + if (!ToTemporalDate(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-7. + bool equals = date == other.date(); + if (equals && !CalendarEquals(cx, calendar, other.calendar(), &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.PlainDate.prototype.equals ( other ) + */ +static bool PlainDate_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_equals>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toZonedDateTime ( item ) + * + * The |item| argument represents either a time zone or an options object. The + * following cases are supported: + * - |item| is a `Temporal.TimeZone` object. + * - |item| is a user-defined time zone object. + * - |item| is an options object with `timeZone` and `plainTime` properties. + * - |item| is a time zone identifier string. + * + * User-defined time zone objects are distinguished from options objects by the + * `timeZone` property, i.e. if a `timeZone` property is present, the object is + * treated as an options object, otherwise an object is treated as a + * user-defined time zone. + */ +static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Steps 3-4 + Rooted<TimeZoneValue> timeZone(cx); + Rooted<Value> temporalTime(cx); + if (args.get(0).isObject()) { + Rooted<JSObject*> item(cx, &args[0].toObject()); + + // Steps 3.a-b. + if (item->canUnwrapAs<TimeZoneObject>()) { + // Step 3.a.i. + timeZone.set(TimeZoneValue(item)); + + // Step 3.a.ii. + temporalTime.setUndefined(); + } else { + // Step 3.b.i. + Rooted<Value> timeZoneLike(cx); + if (!GetProperty(cx, item, item, cx->names().timeZone, &timeZoneLike)) { + return false; + } + + // Steps 3.b.ii-iii. + if (timeZoneLike.isUndefined()) { + // Step 3.b.ii.1. + if (!ToTemporalTimeZone(cx, args[0], &timeZone)) { + return false; + } + + // Step 3.b.ii.2. + temporalTime.setUndefined(); + } else { + // Step 3.b.iii.1. + if (!ToTemporalTimeZone(cx, timeZoneLike, &timeZone)) { + return false; + } + + // Step 3.b.iii.2. + if (!GetProperty(cx, item, item, cx->names().plainTime, + &temporalTime)) { + return false; + } + } + } + } else { + // Step 4.a. + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // Step 4.b. + temporalTime.setUndefined(); + } + + // Step 6.a. + PlainTime time = {}; + if (!temporalTime.isUndefined()) { + if (!ToTemporalTime(cx, temporalTime, &time)) { + return false; + } + } + + // Steps 5.a and 6.b + Rooted<PlainDateTimeWithCalendar> temporalDateTime(cx); + if (!CreateTemporalDateTime(cx, {date, time}, calendar, &temporalDateTime)) { + return false; + } + + // Steps 7-8. + Instant instant; + if (!GetInstantFor(cx, timeZone, temporalDateTime, + TemporalDisambiguation::Compatible, &instant)) { + return false; + } + + // Step 9. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.toZonedDateTime ( item ) + */ +static bool PlainDate_toZonedDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toZonedDateTime>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toString ( [ options ] ) + */ +static bool PlainDate_toString(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + auto showCalendar = CalendarOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Step 4. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + } + + // Step 5. + JSString* str = TemporalDateToString(cx, temporalDate, showCalendar); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toString ( [ options ] ) + */ +static bool PlainDate_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toString>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDate_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + JSString* str = TemporalDateToString(cx, temporalDate, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDate_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toLocaleString>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toJSON ( ) + */ +static bool PlainDate_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + JSString* str = TemporalDateToString(cx, temporalDate, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toJSON ( ) + */ +static bool PlainDate_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toJSON>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.valueOf ( ) + */ +static bool PlainDate_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainDate", "primitive type"); + return false; +} + +const JSClass PlainDateObject::class_ = { + "Temporal.PlainDate", + JSCLASS_HAS_RESERVED_SLOTS(PlainDateObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainDate), + JS_NULL_CLASS_OPS, + &PlainDateObject::classSpec_, +}; + +const JSClass& PlainDateObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainDate_methods[] = { + JS_FN("from", PlainDate_from, 1, 0), + JS_FN("compare", PlainDate_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainDate_prototype_methods[] = { + JS_FN("toPlainMonthDay", PlainDate_toPlainMonthDay, 0, 0), + JS_FN("toPlainYearMonth", PlainDate_toPlainYearMonth, 0, 0), + JS_FN("toPlainDateTime", PlainDate_toPlainDateTime, 0, 0), + JS_FN("getISOFields", PlainDate_getISOFields, 0, 0), + JS_FN("getCalendar", PlainDate_getCalendar, 0, 0), + JS_FN("add", PlainDate_add, 1, 0), + JS_FN("subtract", PlainDate_subtract, 1, 0), + JS_FN("with", PlainDate_with, 1, 0), + JS_FN("withCalendar", PlainDate_withCalendar, 1, 0), + JS_FN("until", PlainDate_until, 1, 0), + JS_FN("since", PlainDate_since, 1, 0), + JS_FN("equals", PlainDate_equals, 1, 0), + JS_FN("toZonedDateTime", PlainDate_toZonedDateTime, 1, 0), + JS_FN("toString", PlainDate_toString, 0, 0), + JS_FN("toLocaleString", PlainDate_toLocaleString, 0, 0), + JS_FN("toJSON", PlainDate_toJSON, 0, 0), + JS_FN("valueOf", PlainDate_valueOf, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainDate_prototype_properties[] = { + JS_PSG("calendarId", PlainDate_calendarId, 0), + JS_PSG("year", PlainDate_year, 0), + JS_PSG("month", PlainDate_month, 0), + JS_PSG("monthCode", PlainDate_monthCode, 0), + JS_PSG("day", PlainDate_day, 0), + JS_PSG("dayOfWeek", PlainDate_dayOfWeek, 0), + JS_PSG("dayOfYear", PlainDate_dayOfYear, 0), + JS_PSG("weekOfYear", PlainDate_weekOfYear, 0), + JS_PSG("yearOfWeek", PlainDate_yearOfWeek, 0), + JS_PSG("daysInWeek", PlainDate_daysInWeek, 0), + JS_PSG("daysInMonth", PlainDate_daysInMonth, 0), + JS_PSG("daysInYear", PlainDate_daysInYear, 0), + JS_PSG("monthsInYear", PlainDate_monthsInYear, 0), + JS_PSG("inLeapYear", PlainDate_inLeapYear, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainDate", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainDateObject::classSpec_ = { + GenericCreateConstructor<PlainDateConstructor, 3, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainDateObject>, + PlainDate_methods, + nullptr, + PlainDate_prototype_methods, + PlainDate_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; + +struct PlainDateNameAndNative final { + PropertyName* name; + JSNative native; +}; + +static PlainDateNameAndNative GetPlainDateNameAndNative( + JSContext* cx, CalendarField fieldName) { + switch (fieldName) { + case CalendarField::Year: + return {cx->names().year, PlainDate_year}; + case CalendarField::Month: + return {cx->names().month, PlainDate_month}; + case CalendarField::MonthCode: + return {cx->names().monthCode, PlainDate_monthCode}; + case CalendarField::Day: + return {cx->names().day, PlainDate_day}; + } + MOZ_CRASH("invalid temporal field name"); +} + +bool js::temporal::IsBuiltinAccess( + JSContext* cx, Handle<PlainDateObject*> date, + std::initializer_list<CalendarField> fieldNames) { + // Don't optimize when the object has any own properties which may shadow the + // built-in methods. + if (date->shape()->propMapLength() > 0) { + return false; + } + + JSObject* proto = cx->global()->maybeGetPrototype(JSProto_PlainDate); + + // Don't attempt to optimize when the class isn't yet initialized. + if (!proto) { + return false; + } + + // Don't optimize when the prototype isn't the built-in prototype. + if (date->staticPrototype() != proto) { + return false; + } + + auto* nproto = &proto->as<NativeObject>(); + for (auto fieldName : fieldNames) { + auto [name, native] = GetPlainDateNameAndNative(cx, fieldName); + auto prop = nproto->lookupPure(name); + + // Return if the property isn't a data property. + if (!prop || !prop->isDataProperty()) { + return false; + } + + // Return if the property isn't the initial method. + if (!IsNativeFunction(nproto->getSlot(prop->slot()), native)) { + return false; + } + } + + // Success! The access can be optimized. + return true; +} |