Calendar.cpp (127902B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "builtin/temporal/Calendar.h" 8 9 #include "mozilla/Assertions.h" 10 #include "mozilla/Attributes.h" 11 #include "mozilla/Casting.h" 12 #include "mozilla/CheckedInt.h" 13 #include "mozilla/EnumSet.h" 14 #include "mozilla/FloatingPoint.h" 15 #include "mozilla/intl/Locale.h" 16 #include "mozilla/MathAlgorithms.h" 17 #include "mozilla/Maybe.h" 18 #include "mozilla/Result.h" 19 #include "mozilla/ResultVariant.h" 20 #include "mozilla/Span.h" 21 #include "mozilla/TextUtils.h" 22 #include "mozilla/UniquePtr.h" 23 24 #include <algorithm> 25 #include <array> 26 #include <cmath> 27 #include <stddef.h> 28 #include <stdint.h> 29 30 #include "diplomat_runtime.hpp" 31 #include "jsnum.h" 32 #include "jstypes.h" 33 #include "NamespaceImports.h" 34 35 #include "builtin/temporal/CalendarFields.h" 36 #include "builtin/temporal/Duration.h" 37 #include "builtin/temporal/Era.h" 38 #include "builtin/temporal/MonthCode.h" 39 #include "builtin/temporal/PlainDate.h" 40 #include "builtin/temporal/PlainDateTime.h" 41 #include "builtin/temporal/PlainMonthDay.h" 42 #include "builtin/temporal/PlainTime.h" 43 #include "builtin/temporal/PlainYearMonth.h" 44 #include "builtin/temporal/Temporal.h" 45 #include "builtin/temporal/TemporalParser.h" 46 #include "builtin/temporal/TemporalRoundingMode.h" 47 #include "builtin/temporal/TemporalTypes.h" 48 #include "builtin/temporal/TemporalUnit.h" 49 #include "builtin/temporal/ZonedDateTime.h" 50 #include "gc/Barrier.h" 51 #include "gc/GCEnum.h" 52 #include "icu4x/Calendar.hpp" 53 #include "icu4x/Date.hpp" 54 #include "icu4x/IsoDate.hpp" 55 #include "js/AllocPolicy.h" 56 #include "js/ErrorReport.h" 57 #include "js/friend/ErrorMessages.h" 58 #include "js/Printer.h" 59 #include "js/RootingAPI.h" 60 #include "js/TracingAPI.h" 61 #include "js/Value.h" 62 #include "js/Vector.h" 63 #include "util/Text.h" 64 #include "vm/BytecodeUtil.h" 65 #include "vm/Compartment.h" 66 #include "vm/JSAtomState.h" 67 #include "vm/JSContext.h" 68 #include "vm/StringType.h" 69 70 #include "vm/Compartment-inl.h" 71 #include "vm/JSContext-inl.h" 72 #include "vm/JSObject-inl.h" 73 #include "vm/ObjectOperations-inl.h" 74 75 // diplomat_simple_write isn't defined in C++ headers, but we have to use it to 76 // avoid memory allocation. 77 // (https://github.com/rust-diplomat/diplomat/issues/866) 78 namespace diplomat::capi { 79 extern "C" DiplomatWrite diplomat_simple_write(char* buf, size_t buf_size); 80 } 81 82 using namespace js; 83 using namespace js::temporal; 84 85 void js::temporal::CalendarValue::trace(JSTracer* trc) { 86 TraceRoot(trc, &value_, "CalendarValue::value"); 87 } 88 89 bool js::temporal::WrapCalendarValue(JSContext* cx, 90 MutableHandle<JS::Value> calendar) { 91 MOZ_ASSERT(calendar.isInt32()); 92 return cx->compartment()->wrap(cx, calendar); 93 } 94 95 /** 96 * IsISOLeapYear ( year ) 97 */ 98 static constexpr bool IsISOLeapYear(int32_t year) { 99 // Steps 1-5. 100 return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); 101 } 102 103 /** 104 * ISODaysInYear ( year ) 105 */ 106 static int32_t ISODaysInYear(int32_t year) { 107 // Steps 1-3. 108 return IsISOLeapYear(year) ? 366 : 365; 109 } 110 111 /** 112 * ISODaysInMonth ( year, month ) 113 */ 114 static constexpr int32_t ISODaysInMonth(int32_t year, int32_t month) { 115 MOZ_ASSERT(1 <= month && month <= 12); 116 117 constexpr uint8_t daysInMonth[2][13] = { 118 {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, 119 {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; 120 121 // Steps 1-4. 122 return daysInMonth[IsISOLeapYear(year)][month]; 123 } 124 125 /** 126 * ISODaysInMonth ( year, month ) 127 */ 128 int32_t js::temporal::ISODaysInMonth(int32_t year, int32_t month) { 129 return ::ISODaysInMonth(year, month); 130 } 131 132 /** 133 * 21.4.1.6 Week Day 134 * 135 * Compute the week day from |day| without first expanding |day| into a full 136 * date through |MakeDate(day, 0)|: 137 * 138 * WeekDay(MakeDate(day, 0)) 139 * = WeekDay(day × msPerDay + 0) 140 * = WeekDay(day × msPerDay) 141 * = 𝔽(ℝ(Day(day × msPerDay) + 4𝔽) modulo 7) 142 * = 𝔽(ℝ(𝔽(floor(ℝ((day × msPerDay) / msPerDay))) + 4𝔽) modulo 7) 143 * = 𝔽(ℝ(𝔽(floor(ℝ(day))) + 4𝔽) modulo 7) 144 * = 𝔽(ℝ(𝔽(day) + 4𝔽) modulo 7) 145 */ 146 static int32_t WeekDay(int32_t day) { 147 int32_t result = (day + 4) % 7; 148 if (result < 0) { 149 result += 7; 150 } 151 return result; 152 } 153 154 /** 155 * ISODayOfWeek ( isoDate ) 156 */ 157 static int32_t ISODayOfWeek(const ISODate& isoDate) { 158 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 159 160 // Step 1. 161 int32_t day = MakeDay(isoDate); 162 163 // Step 2. 164 int32_t dayOfWeek = WeekDay(day); 165 166 // Steps 3-4. 167 return dayOfWeek != 0 ? dayOfWeek : 7; 168 } 169 170 static constexpr auto FirstDayOfMonth(int32_t year) { 171 // The following array contains the day of year for the first day of each 172 // month, where index 0 is January, and day 0 is January 1. 173 std::array<int32_t, 13> days = {}; 174 for (int32_t month = 1; month <= 12; ++month) { 175 days[month] = days[month - 1] + ::ISODaysInMonth(year, month); 176 } 177 return days; 178 } 179 180 /** 181 * ISODayOfYear ( isoDate ) 182 */ 183 static int32_t ISODayOfYear(const ISODate& isoDate) { 184 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 185 186 const auto& [year, month, day] = isoDate; 187 188 // First day of month arrays for non-leap and leap years. 189 constexpr decltype(FirstDayOfMonth(0)) firstDayOfMonth[2] = { 190 FirstDayOfMonth(1), FirstDayOfMonth(0)}; 191 192 // Steps 1-2. 193 // 194 // Instead of first computing the date and then using DayWithinYear to map the 195 // date to the day within the year, directly lookup the first day of the month 196 // and then add the additional days. 197 return firstDayOfMonth[IsISOLeapYear(year)][month - 1] + day; 198 } 199 200 static int32_t FloorDiv(int32_t dividend, int32_t divisor) { 201 MOZ_ASSERT(divisor > 0); 202 203 int32_t quotient = dividend / divisor; 204 int32_t remainder = dividend % divisor; 205 if (remainder < 0) { 206 quotient -= 1; 207 } 208 return quotient; 209 } 210 211 /** 212 * 21.4.1.3 Year Number, DayFromYear 213 */ 214 static int32_t DayFromYear(int32_t year) { 215 return 365 * (year - 1970) + FloorDiv(year - 1969, 4) - 216 FloorDiv(year - 1901, 100) + FloorDiv(year - 1601, 400); 217 } 218 219 /** 220 * 21.4.1.11 MakeTime ( hour, min, sec, ms ) 221 */ 222 static int64_t MakeTime(const Time& time) { 223 MOZ_ASSERT(IsValidTime(time)); 224 225 // Step 1 (Not applicable). 226 227 // Step 2. 228 int64_t h = time.hour; 229 230 // Step 3. 231 int64_t m = time.minute; 232 233 // Step 4. 234 int64_t s = time.second; 235 236 // Step 5. 237 int64_t milli = time.millisecond; 238 239 // Steps 6-7. 240 return h * ToMilliseconds(TemporalUnit::Hour) + 241 m * ToMilliseconds(TemporalUnit::Minute) + 242 s * ToMilliseconds(TemporalUnit::Second) + milli; 243 } 244 245 /** 246 * 21.4.1.12 MakeDay ( year, month, date ) 247 */ 248 int32_t js::temporal::MakeDay(const ISODate& date) { 249 MOZ_ASSERT(ISODateWithinLimits(date)); 250 251 return DayFromYear(date.year) + ISODayOfYear(date) - 1; 252 } 253 254 /** 255 * 21.4.1.13 MakeDate ( day, time ) 256 */ 257 int64_t js::temporal::MakeDate(const ISODateTime& dateTime) { 258 MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); 259 260 // Step 1 (Not applicable). 261 262 // Steps 2-3. 263 int64_t tv = MakeDay(dateTime.date) * ToMilliseconds(TemporalUnit::Day) + 264 MakeTime(dateTime.time); 265 266 // Step 4. 267 return tv; 268 } 269 270 struct YearWeek final { 271 int32_t year = 0; 272 int32_t week = 0; 273 }; 274 275 /** 276 * ISOWeekOfYear ( isoDate ) 277 */ 278 static YearWeek ISOWeekOfYear(const ISODate& isoDate) { 279 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 280 281 // Step 1. 282 int32_t year = isoDate.year; 283 284 // Step 2-7. (Not applicable in our implementation.) 285 286 // Steps 8-9. 287 int32_t dayOfYear = ISODayOfYear(isoDate); 288 int32_t dayOfWeek = ISODayOfWeek(isoDate); 289 290 // Step 10. 291 int32_t week = (10 + dayOfYear - dayOfWeek) / 7; 292 MOZ_ASSERT(0 <= week && week <= 53); 293 294 // An ISO year has 53 weeks if the year starts on a Thursday or if it's a 295 // leap year which starts on a Wednesday. 296 auto isLongYear = [](int32_t year) { 297 int32_t startOfYear = ISODayOfWeek({year, 1, 1}); 298 return startOfYear == 4 || (startOfYear == 3 && IsISOLeapYear(year)); 299 }; 300 301 // Step 11. 302 // 303 // Part of last year's last week, which is either week 52 or week 53. 304 if (week == 0) { 305 return {year - 1, 52 + int32_t(isLongYear(year - 1))}; 306 } 307 308 // Step 12. 309 // 310 // Part of next year's first week if the current year isn't a long year. 311 if (week == 53 && !isLongYear(year)) { 312 return {year + 1, 1}; 313 } 314 315 // Step 13. 316 return {year, week}; 317 } 318 319 /** 320 * ToTemporalCalendarIdentifier ( calendarSlotValue ) 321 */ 322 std::string_view js::temporal::CalendarIdentifier(CalendarId calendarId) { 323 switch (calendarId) { 324 case CalendarId::ISO8601: 325 return "iso8601"; 326 case CalendarId::Buddhist: 327 return "buddhist"; 328 case CalendarId::Chinese: 329 return "chinese"; 330 case CalendarId::Coptic: 331 return "coptic"; 332 case CalendarId::Dangi: 333 return "dangi"; 334 case CalendarId::Ethiopian: 335 return "ethiopic"; 336 case CalendarId::EthiopianAmeteAlem: 337 return "ethioaa"; 338 case CalendarId::Gregorian: 339 return "gregory"; 340 case CalendarId::Hebrew: 341 return "hebrew"; 342 case CalendarId::Indian: 343 return "indian"; 344 case CalendarId::IslamicCivil: 345 return "islamic-civil"; 346 case CalendarId::IslamicTabular: 347 return "islamic-tbla"; 348 case CalendarId::IslamicUmmAlQura: 349 return "islamic-umalqura"; 350 case CalendarId::Japanese: 351 return "japanese"; 352 case CalendarId::Persian: 353 return "persian"; 354 case CalendarId::ROC: 355 return "roc"; 356 } 357 MOZ_CRASH("invalid calendar id"); 358 } 359 360 class MOZ_STACK_CLASS AsciiLowerCaseChars final { 361 static constexpr size_t InlineCapacity = 24; 362 363 Vector<char, InlineCapacity> chars_; 364 365 public: 366 explicit AsciiLowerCaseChars(JSContext* cx) : chars_(cx) {} 367 368 operator mozilla::Span<const char>() const { 369 return mozilla::Span<const char>{chars_}; 370 } 371 372 [[nodiscard]] bool init(JSLinearString* str) { 373 MOZ_ASSERT(StringIsAscii(str)); 374 375 if (!chars_.resize(str->length())) { 376 return false; 377 } 378 379 CopyChars(reinterpret_cast<JS::Latin1Char*>(chars_.begin()), *str); 380 381 mozilla::intl::AsciiToLowerCase(chars_.begin(), chars_.length(), 382 chars_.begin()); 383 384 return true; 385 } 386 }; 387 388 /** 389 * CanonicalizeCalendar ( id ) 390 */ 391 bool js::temporal::CanonicalizeCalendar(JSContext* cx, Handle<JSString*> id, 392 MutableHandle<CalendarValue> result) { 393 Rooted<JSLinearString*> linear(cx, id->ensureLinear(cx)); 394 if (!linear) { 395 return false; 396 } 397 398 // Steps 1-3. 399 do { 400 if (!StringIsAscii(linear) || linear->empty()) { 401 break; 402 } 403 404 AsciiLowerCaseChars lowerCaseChars(cx); 405 if (!lowerCaseChars.init(linear)) { 406 return false; 407 } 408 mozilla::Span<const char> id = lowerCaseChars; 409 410 // Reject invalid types before trying to resolve aliases. 411 if (mozilla::intl::LocaleParser::CanParseUnicodeExtensionType(id).isErr()) { 412 break; 413 } 414 415 // Resolve calendar aliases. 416 static constexpr auto key = mozilla::MakeStringSpan("ca"); 417 if (const char* replacement = 418 mozilla::intl::Locale::ReplaceUnicodeExtensionType(key, id)) { 419 id = mozilla::MakeStringSpan(replacement); 420 } 421 422 // Step 1. 423 static constexpr auto& calendars = AvailableCalendars(); 424 425 // Steps 2-3. 426 for (auto identifier : calendars) { 427 if (id == mozilla::Span{CalendarIdentifier(identifier)}) { 428 result.set(CalendarValue(identifier)); 429 return true; 430 } 431 } 432 } while (false); 433 434 if (auto chars = QuoteString(cx, linear)) { 435 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 436 JSMSG_TEMPORAL_CALENDAR_INVALID_ID, chars.get()); 437 } 438 return false; 439 } 440 441 template <typename T, typename... Ts> 442 static bool ToTemporalCalendar(JSContext* cx, Handle<JSObject*> object, 443 MutableHandle<CalendarValue> result) { 444 if (auto* unwrapped = object->maybeUnwrapIf<T>()) { 445 result.set(unwrapped->calendar()); 446 return result.wrap(cx); 447 } 448 449 if constexpr (sizeof...(Ts) > 0) { 450 return ToTemporalCalendar<Ts...>(cx, object, result); 451 } 452 453 result.set(CalendarValue()); 454 return true; 455 } 456 457 /** 458 * ToTemporalCalendarSlotValue ( temporalCalendarLike ) 459 */ 460 bool js::temporal::ToTemporalCalendar(JSContext* cx, 461 Handle<Value> temporalCalendarLike, 462 MutableHandle<CalendarValue> result) { 463 // Step 1. 464 if (temporalCalendarLike.isObject()) { 465 Rooted<JSObject*> obj(cx, &temporalCalendarLike.toObject()); 466 467 // Step 1.a. 468 Rooted<CalendarValue> calendar(cx); 469 if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject, 470 PlainMonthDayObject, PlainYearMonthObject, 471 ZonedDateTimeObject>(cx, obj, &calendar)) { 472 return false; 473 } 474 if (calendar) { 475 result.set(calendar); 476 return true; 477 } 478 } 479 480 // Step 2. 481 if (!temporalCalendarLike.isString()) { 482 ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, 483 temporalCalendarLike, nullptr, "not a string"); 484 return false; 485 } 486 Rooted<JSString*> str(cx, temporalCalendarLike.toString()); 487 488 // Step 3. 489 Rooted<JSLinearString*> id(cx, ParseTemporalCalendarString(cx, str)); 490 if (!id) { 491 return false; 492 } 493 494 // Step 4. 495 return CanonicalizeCalendar(cx, id, result); 496 } 497 498 /** 499 * GetTemporalCalendarSlotValueWithISODefault ( item ) 500 */ 501 bool js::temporal::GetTemporalCalendarWithISODefault( 502 JSContext* cx, Handle<JSObject*> item, 503 MutableHandle<CalendarValue> result) { 504 // Step 1. 505 Rooted<CalendarValue> calendar(cx); 506 if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject, 507 PlainMonthDayObject, PlainYearMonthObject, 508 ZonedDateTimeObject>(cx, item, &calendar)) { 509 return false; 510 } 511 if (calendar) { 512 result.set(calendar); 513 return true; 514 } 515 516 // Step 2. 517 Rooted<Value> calendarValue(cx); 518 if (!GetProperty(cx, item, item, cx->names().calendar, &calendarValue)) { 519 return false; 520 } 521 522 // Step 3. 523 if (calendarValue.isUndefined()) { 524 result.set(CalendarValue(CalendarId::ISO8601)); 525 return true; 526 } 527 528 // Step 4. 529 return ToTemporalCalendar(cx, calendarValue, result); 530 } 531 532 static inline int32_t OrdinalMonth(const icu4x::capi::Date* date) { 533 int32_t month = icu4x::capi::icu4x_Date_ordinal_month_mv1(date); 534 MOZ_ASSERT(month > 0); 535 return month; 536 } 537 538 static inline int32_t DayOfMonth(const icu4x::capi::Date* date) { 539 int32_t dayOfMonth = icu4x::capi::icu4x_Date_day_of_month_mv1(date); 540 MOZ_ASSERT(dayOfMonth > 0); 541 return dayOfMonth; 542 } 543 544 static inline int32_t DayOfYear(const icu4x::capi::Date* date) { 545 int32_t dayOfYear = icu4x::capi::icu4x_Date_day_of_year_mv1(date); 546 MOZ_ASSERT(dayOfYear > 0); 547 return dayOfYear; 548 } 549 550 static inline int32_t DaysInMonth(const icu4x::capi::Date* date) { 551 int32_t daysInMonth = icu4x::capi::icu4x_Date_days_in_month_mv1(date); 552 MOZ_ASSERT(daysInMonth > 0); 553 return daysInMonth; 554 } 555 556 static inline int32_t DaysInYear(const icu4x::capi::Date* date) { 557 int32_t daysInYear = icu4x::capi::icu4x_Date_days_in_year_mv1(date); 558 MOZ_ASSERT(daysInYear > 0); 559 return daysInYear; 560 } 561 562 static inline int32_t MonthsInYear(const icu4x::capi::Date* date) { 563 int32_t monthsInYear = icu4x::capi::icu4x_Date_months_in_year_mv1(date); 564 MOZ_ASSERT(monthsInYear > 0); 565 return monthsInYear; 566 } 567 568 static auto ToAnyCalendarKind(CalendarId id) { 569 switch (id) { 570 case CalendarId::ISO8601: 571 return icu4x::capi::CalendarKind_Iso; 572 case CalendarId::Buddhist: 573 return icu4x::capi::CalendarKind_Buddhist; 574 case CalendarId::Chinese: 575 return icu4x::capi::CalendarKind_Chinese; 576 case CalendarId::Coptic: 577 return icu4x::capi::CalendarKind_Coptic; 578 case CalendarId::Dangi: 579 return icu4x::capi::CalendarKind_Dangi; 580 case CalendarId::Ethiopian: 581 return icu4x::capi::CalendarKind_Ethiopian; 582 case CalendarId::EthiopianAmeteAlem: 583 return icu4x::capi::CalendarKind_EthiopianAmeteAlem; 584 case CalendarId::Gregorian: 585 return icu4x::capi::CalendarKind_Gregorian; 586 case CalendarId::Hebrew: 587 return icu4x::capi::CalendarKind_Hebrew; 588 case CalendarId::Indian: 589 return icu4x::capi::CalendarKind_Indian; 590 case CalendarId::IslamicCivil: 591 return icu4x::capi::CalendarKind_HijriTabularTypeIIFriday; 592 case CalendarId::IslamicTabular: 593 return icu4x::capi::CalendarKind_HijriTabularTypeIIThursday; 594 case CalendarId::IslamicUmmAlQura: 595 return icu4x::capi::CalendarKind_HijriUmmAlQura; 596 case CalendarId::Japanese: 597 return icu4x::capi::CalendarKind_Japanese; 598 case CalendarId::Persian: 599 return icu4x::capi::CalendarKind_Persian; 600 case CalendarId::ROC: 601 return icu4x::capi::CalendarKind_Roc; 602 } 603 MOZ_CRASH("invalid calendar id"); 604 } 605 606 class ICU4XCalendarDeleter { 607 public: 608 void operator()(icu4x::capi::Calendar* ptr) { 609 icu4x::capi::icu4x_Calendar_destroy_mv1(ptr); 610 } 611 }; 612 613 using UniqueICU4XCalendar = 614 mozilla::UniquePtr<icu4x::capi::Calendar, ICU4XCalendarDeleter>; 615 616 static UniqueICU4XCalendar CreateICU4XCalendar(CalendarId id) { 617 auto* result = icu4x::capi::icu4x_Calendar_create_mv1(ToAnyCalendarKind(id)); 618 MOZ_ASSERT(result, "unexpected null-pointer result"); 619 return UniqueICU4XCalendar{result}; 620 } 621 622 static uint32_t MaximumISOYear(CalendarId calendarId) { 623 switch (calendarId) { 624 case CalendarId::ISO8601: 625 case CalendarId::Buddhist: 626 case CalendarId::Coptic: 627 case CalendarId::Ethiopian: 628 case CalendarId::EthiopianAmeteAlem: 629 case CalendarId::Gregorian: 630 case CalendarId::Hebrew: 631 case CalendarId::Indian: 632 case CalendarId::IslamicCivil: 633 case CalendarId::IslamicTabular: 634 case CalendarId::Japanese: 635 case CalendarId::Persian: 636 case CalendarId::ROC: { 637 // Passing values near INT32_{MIN,MAX} triggers ICU4X assertions, so we 638 // have to handle large input years early. 639 return 300'000; 640 } 641 642 case CalendarId::Chinese: 643 case CalendarId::Dangi: { 644 // Lower limit for these calendars to avoid running into ICU4X assertions. 645 // 646 // https://github.com/unicode-org/icu4x/issues/4917 647 return 10'000; 648 } 649 650 case CalendarId::IslamicUmmAlQura: { 651 // Lower limit for these calendars to avoid running into ICU4X assertions. 652 // 653 // https://github.com/unicode-org/icu4x/issues/4917 654 return 5'000; 655 } 656 } 657 MOZ_CRASH("invalid calendar"); 658 } 659 660 static uint32_t MaximumCalendarYear(CalendarId calendarId) { 661 switch (calendarId) { 662 case CalendarId::ISO8601: 663 case CalendarId::Buddhist: 664 case CalendarId::Coptic: 665 case CalendarId::Ethiopian: 666 case CalendarId::EthiopianAmeteAlem: 667 case CalendarId::Gregorian: 668 case CalendarId::Hebrew: 669 case CalendarId::Indian: 670 case CalendarId::IslamicCivil: 671 case CalendarId::IslamicTabular: 672 case CalendarId::Japanese: 673 case CalendarId::Persian: 674 case CalendarId::ROC: { 675 // Passing values near INT32_{MIN,MAX} triggers ICU4X assertions, so we 676 // have to handle large input years early. 677 return 300'000; 678 } 679 680 case CalendarId::Chinese: 681 case CalendarId::Dangi: { 682 // Lower limit for these calendars to avoid running into ICU4X assertions. 683 // 684 // https://github.com/unicode-org/icu4x/issues/4917 685 return 10'000; 686 } 687 688 case CalendarId::IslamicUmmAlQura: { 689 // Lower limit for these calendars to avoid running into ICU4X assertions. 690 // 691 // https://github.com/unicode-org/icu4x/issues/4917 692 return 5'000; 693 } 694 } 695 MOZ_CRASH("invalid calendar"); 696 } 697 698 static void ReportCalendarFieldOverflow(JSContext* cx, const char* name, 699 double num) { 700 ToCStringBuf numCbuf; 701 const char* numStr = NumberToCString(&numCbuf, num); 702 703 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 704 JSMSG_TEMPORAL_CALENDAR_OVERFLOW_FIELD, name, 705 numStr); 706 } 707 708 class ICU4XDateDeleter { 709 public: 710 void operator()(icu4x::capi::Date* ptr) { 711 icu4x::capi::icu4x_Date_destroy_mv1(ptr); 712 } 713 }; 714 715 using UniqueICU4XDate = mozilla::UniquePtr<icu4x::capi::Date, ICU4XDateDeleter>; 716 717 static UniqueICU4XDate CreateICU4XDate(JSContext* cx, const ISODate& date, 718 CalendarId calendarId, 719 const icu4x::capi::Calendar* calendar) { 720 if (mozilla::Abs(date.year) > MaximumISOYear(calendarId)) { 721 ReportCalendarFieldOverflow(cx, "year", date.year); 722 return nullptr; 723 } 724 725 auto result = icu4x::capi::icu4x_Date_from_iso_in_calendar_mv1( 726 date.year, date.month, date.day, calendar); 727 if (!result.is_ok) { 728 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 729 JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR); 730 return nullptr; 731 } 732 return UniqueICU4XDate{result.ok}; 733 } 734 735 class ICU4XIsoDateDeleter { 736 public: 737 void operator()(icu4x::capi::IsoDate* ptr) { 738 icu4x::capi::icu4x_IsoDate_destroy_mv1(ptr); 739 } 740 }; 741 742 using UniqueICU4XIsoDate = 743 mozilla::UniquePtr<icu4x::capi::IsoDate, ICU4XIsoDateDeleter>; 744 745 static constexpr size_t EraNameMaxLength() { 746 size_t length = 0; 747 for (auto calendar : AvailableCalendars()) { 748 for (auto era : CalendarEras(calendar)) { 749 for (auto name : CalendarEraNames(calendar, era)) { 750 length = std::max(length, name.length()); 751 } 752 } 753 } 754 return length; 755 } 756 757 /** 758 * CanonicalizeEraInCalendar ( calendar, era ) 759 */ 760 static mozilla::Maybe<EraCode> CanonicalizeEraInCalendar( 761 CalendarId calendar, JSLinearString* string) { 762 MOZ_ASSERT(CalendarSupportsEra(calendar)); 763 764 // Note: Assigning MaxLength to EraNameMaxLength() breaks the CDT indexer. 765 constexpr size_t MaxLength = 8; 766 static_assert(MaxLength >= EraNameMaxLength(), 767 "Storage size is at least as large as the largest known era"); 768 769 if (string->length() > MaxLength || !StringIsAscii(string)) { 770 return mozilla::Nothing(); 771 } 772 773 char chars[MaxLength] = {}; 774 CopyChars(reinterpret_cast<JS::Latin1Char*>(chars), *string); 775 776 auto stringView = std::string_view{chars, string->length()}; 777 778 for (auto era : CalendarEras(calendar)) { 779 for (auto name : CalendarEraNames(calendar, era)) { 780 if (name == stringView) { 781 return mozilla::Some(era); 782 } 783 } 784 } 785 return mozilla::Nothing(); 786 } 787 788 static constexpr std::string_view IcuEraName(CalendarId calendar, EraCode era) { 789 switch (calendar) { 790 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Iso.html#era-codes 791 case CalendarId::ISO8601: { 792 MOZ_ASSERT(era == EraCode::Standard); 793 return "default"; 794 } 795 796 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Buddhist.html#era-codes 797 case CalendarId::Buddhist: { 798 MOZ_ASSERT(era == EraCode::Standard); 799 return "be"; 800 } 801 802 // https://docs.rs/icu/latest/icu/calendar/cal/east_asian_traditional/struct.EastAsianTraditional.html#year-and-era-codes 803 case CalendarId::Chinese: { 804 MOZ_ASSERT(era == EraCode::Standard); 805 return ""; 806 } 807 808 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Coptic.html#era-codes 809 case CalendarId::Coptic: { 810 MOZ_ASSERT(era == EraCode::Standard); 811 return "am"; 812 } 813 814 // https://docs.rs/icu/latest/icu/calendar/cal/east_asian_traditional/struct.EastAsianTraditional.html#year-and-era-codes 815 case CalendarId::Dangi: { 816 MOZ_ASSERT(era == EraCode::Standard); 817 return ""; 818 } 819 820 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Ethiopian.html#era-codes 821 case CalendarId::Ethiopian: { 822 MOZ_ASSERT(era == EraCode::Standard); 823 return "am"; 824 } 825 826 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Ethiopian.html#era-codes 827 case CalendarId::EthiopianAmeteAlem: { 828 MOZ_ASSERT(era == EraCode::Standard); 829 return "aa"; 830 } 831 832 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Gregorian.html#era-codes 833 case CalendarId::Gregorian: { 834 MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse); 835 return era == EraCode::Standard ? "ce" : "bce"; 836 } 837 838 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Hebrew.html#era-codes 839 case CalendarId::Hebrew: { 840 MOZ_ASSERT(era == EraCode::Standard); 841 return "am"; 842 } 843 844 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Indian.html#era-codes 845 case CalendarId::Indian: { 846 MOZ_ASSERT(era == EraCode::Standard); 847 return "shaka"; 848 } 849 850 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Hijri.html#era-codes 851 case CalendarId::IslamicCivil: 852 case CalendarId::IslamicTabular: 853 case CalendarId::IslamicUmmAlQura: { 854 MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse); 855 return era == EraCode::Standard ? "ah" : "bh"; 856 } 857 858 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Persian.html#era-codes 859 case CalendarId::Persian: { 860 MOZ_ASSERT(era == EraCode::Standard); 861 return "ap"; 862 } 863 864 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Japanese.html#era-codes 865 case CalendarId::Japanese: { 866 switch (era) { 867 case EraCode::Standard: 868 return "ce"; 869 case EraCode::Inverse: 870 return "bce"; 871 case EraCode::Meiji: 872 return "meiji"; 873 case EraCode::Taisho: 874 return "taisho"; 875 case EraCode::Showa: 876 return "showa"; 877 case EraCode::Heisei: 878 return "heisei"; 879 case EraCode::Reiwa: 880 return "reiwa"; 881 } 882 break; 883 } 884 885 // https://docs.rs/icu/latest/icu/calendar/cal/struct.Roc.html#era-codes 886 case CalendarId::ROC: { 887 MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse); 888 return era == EraCode::Standard ? "roc" : "broc"; 889 } 890 } 891 MOZ_CRASH("invalid era"); 892 } 893 894 enum class CalendarError { 895 // Catch-all kind for all other error types. 896 Generic, 897 898 // https://docs.rs/icu/latest/icu/calendar/enum.DateError.html#variant.Range 899 OutOfRange, 900 901 // https://docs.rs/icu/latest/icu/calendar/enum.DateError.html#variant.UnknownEra 902 UnknownEra, 903 904 // https://docs.rs/icu/latest/icu/calendar/enum.DateError.html#variant.UnknownMonthCode 905 UnknownMonthCode, 906 }; 907 908 #ifdef DEBUG 909 static auto CalendarErasAsEnumSet(CalendarId calendarId) { 910 // `mozilla::EnumSet<EraCode>(CalendarEras(calendarId))` doesn't work in old 911 // GCC versions, so add all era codes manually to the enum set. 912 mozilla::EnumSet<EraCode> eras{}; 913 for (auto era : CalendarEras(calendarId)) { 914 eras += era; 915 } 916 return eras; 917 } 918 #endif 919 920 static mozilla::Result<UniqueICU4XDate, CalendarError> CreateDateFromCodes( 921 CalendarId calendarId, const icu4x::capi::Calendar* calendar, 922 EraYear eraYear, MonthCode monthCode, int32_t day) { 923 MOZ_ASSERT(calendarId != CalendarId::ISO8601); 924 MOZ_ASSERT(icu4x::capi::icu4x_Calendar_kind_mv1(calendar) == 925 ToAnyCalendarKind(calendarId)); 926 MOZ_ASSERT(CalendarErasAsEnumSet(calendarId).contains(eraYear.era)); 927 MOZ_ASSERT_IF(CalendarEraHasInverse(calendarId), eraYear.year > 0); 928 MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId)); 929 MOZ_ASSERT(IsValidMonthCodeForCalendar(calendarId, monthCode)); 930 MOZ_ASSERT(day > 0); 931 MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second); 932 933 auto era = IcuEraName(calendarId, eraYear.era); 934 auto monthCodeView = std::string_view{monthCode}; 935 auto date = icu4x::capi::icu4x_Date_from_codes_in_calendar_mv1( 936 diplomat::capi::DiplomatStringView{era.data(), era.length()}, 937 eraYear.year, 938 diplomat::capi::DiplomatStringView{monthCodeView.data(), 939 monthCodeView.length()}, 940 day, calendar); 941 if (date.is_ok) { 942 return UniqueICU4XDate{date.ok}; 943 } 944 945 // Map possible calendar errors. 946 // 947 // Calendar error codes which can't happen for `create_from_codes_in_calendar` 948 // are mapped to `CalendarError::Generic`. 949 switch (date.err) { 950 case icu4x::capi::CalendarError_OutOfRange: 951 return mozilla::Err(CalendarError::OutOfRange); 952 case icu4x::capi::CalendarError_UnknownEra: 953 return mozilla::Err(CalendarError::UnknownEra); 954 case icu4x::capi::CalendarError_UnknownMonthCode: 955 return mozilla::Err(CalendarError::UnknownMonthCode); 956 default: 957 return mozilla::Err(CalendarError::Generic); 958 } 959 } 960 961 /** 962 * Return the first year (gannen) of a Japanese era. 963 */ 964 static bool FirstYearOfJapaneseEra(JSContext* cx, CalendarId calendarId, 965 const icu4x::capi::Calendar* calendar, 966 EraCode era, int32_t* result) { 967 MOZ_ASSERT(calendarId == CalendarId::Japanese); 968 MOZ_ASSERT(IsJapaneseEraName(era)); 969 970 // All supported Japanese eras last at least one year, so December 31 is 971 // guaranteed to be in the first year of the era. 972 auto dateResult = 973 CreateDateFromCodes(calendarId, calendar, {era, 1}, MonthCode{12}, 31); 974 if (dateResult.isErr()) { 975 MOZ_ASSERT(dateResult.inspectErr() == CalendarError::Generic, 976 "unexpected non-generic calendar error"); 977 978 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 979 JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR); 980 return false; 981 } 982 983 auto date = dateResult.unwrap(); 984 UniqueICU4XIsoDate isoDate{icu4x::capi::icu4x_Date_to_iso_mv1(date.get())}; 985 MOZ_ASSERT(isoDate, "unexpected null-pointer result"); 986 987 int32_t isoYear = icu4x::capi::icu4x_IsoDate_year_mv1(isoDate.get()); 988 MOZ_ASSERT(isoYear > 0, "unexpected era start before 1 CE"); 989 990 *result = isoYear; 991 return true; 992 } 993 994 /** 995 * Return the equivalent common era year for a Japanese era year. 996 */ 997 static bool JapaneseEraYearToCommonEraYear( 998 JSContext* cx, CalendarId calendarId, const icu4x::capi::Calendar* calendar, 999 EraYear eraYear, EraYear* result) { 1000 int32_t firstYearOfEra; 1001 if (!FirstYearOfJapaneseEra(cx, calendarId, calendar, eraYear.era, 1002 &firstYearOfEra)) { 1003 return false; 1004 } 1005 1006 // Map non-positive era years to years before the first era year: 1007 // 1008 // 1 Reiwa = 2019 CE 1009 // 0 Reiwa -> 2018 CE 1010 // -1 Reiwa -> 2017 CE 1011 // etc. 1012 // 1013 // Map too large era years to the next era: 1014 // 1015 // Heisei 31 = 2019 CE 1016 // Heisei 32 -> 2020 CE 1017 // ... 1018 1019 int32_t year = (firstYearOfEra - 1) + eraYear.year; 1020 if (year > 0) { 1021 *result = {EraCode::Standard, year}; 1022 return true; 1023 } 1024 *result = {EraCode::Inverse, int32_t(mozilla::Abs(year) + 1)}; 1025 return true; 1026 } 1027 1028 static constexpr int32_t ethiopianYearsFromCreationToIncarnation = 5500; 1029 1030 static int32_t FromAmeteAlemToAmeteMihret(int32_t year) { 1031 // Subtract the number of years from creation to incarnation to anchor 1032 // at the date of incarnation. 1033 return year - ethiopianYearsFromCreationToIncarnation; 1034 } 1035 1036 static int32_t FromAmeteMihretToAmeteAlem(int32_t year) { 1037 // Add the number of years from creation to incarnation to anchor at the date 1038 // of creation. 1039 return year + ethiopianYearsFromCreationToIncarnation; 1040 } 1041 1042 /** 1043 * ConstrainMonthCode ( calendar, arithmeticYear, monthCode, overflow ) 1044 */ 1045 static bool ConstrainMonthCode(JSContext* cx, CalendarId calendar, 1046 MonthCode monthCode, TemporalOverflow overflow, 1047 MonthCode* result) { 1048 // Step 1. 1049 MOZ_ASSERT(IsValidMonthCodeForCalendar(calendar, monthCode)); 1050 1051 // Steps 2 and 4. 1052 MOZ_ASSERT(CalendarHasLeapMonths(calendar)); 1053 MOZ_ASSERT(monthCode.isLeapMonth()); 1054 1055 // Step 3. 1056 if (overflow == TemporalOverflow::Reject) { 1057 // Ensure the month code is null-terminated. 1058 char code[5] = {}; 1059 auto monthCodeView = std::string_view{monthCode}; 1060 monthCodeView.copy(code, monthCodeView.length()); 1061 1062 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 1063 JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, code); 1064 return false; 1065 } 1066 1067 // Steps 5-6. 1068 bool skipBackward = 1069 calendar == CalendarId::Chinese || calendar == CalendarId::Dangi; 1070 1071 // Step 7. 1072 if (skipBackward) { 1073 // Step 7.a. 1074 *result = MonthCode{monthCode.ordinal()}; 1075 return true; 1076 } 1077 1078 // Step 8.a 1079 MOZ_ASSERT(calendar == CalendarId::Hebrew); 1080 MOZ_ASSERT(monthCode.code() == MonthCode::Code::M05L); 1081 1082 // Step 8.b 1083 *result = MonthCode{6}; 1084 return true; 1085 } 1086 1087 static UniqueICU4XDate CreateDateFromCodes( 1088 JSContext* cx, CalendarId calendarId, const icu4x::capi::Calendar* calendar, 1089 EraYear eraYear, MonthCode monthCode, int32_t day, 1090 TemporalOverflow overflow) { 1091 MOZ_ASSERT(IsValidMonthCodeForCalendar(calendarId, monthCode)); 1092 MOZ_ASSERT(day > 0); 1093 MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second); 1094 1095 // Constrain day to the maximum possible day for the input month. 1096 // 1097 // Special cases like February 29 in leap years of the Gregorian calendar are 1098 // handled below. 1099 int32_t daysInMonth = CalendarDaysInMonth(calendarId, monthCode).second; 1100 if (overflow == TemporalOverflow::Constrain) { 1101 day = std::min(day, daysInMonth); 1102 } else { 1103 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 1104 1105 if (day > daysInMonth) { 1106 ReportCalendarFieldOverflow(cx, "day", day); 1107 return nullptr; 1108 } 1109 } 1110 1111 // ICU4X doesn't support large dates, so we have to handle this case early. 1112 if (mozilla::Abs(eraYear.year) > MaximumCalendarYear(calendarId)) { 1113 ReportCalendarFieldOverflow(cx, "year", eraYear.year); 1114 return nullptr; 1115 } 1116 1117 // ICU4X requires to switch from Amete Mihret to Amete Alem calendar when the 1118 // year is non-positive. 1119 // 1120 // https://unicode-org.atlassian.net/browse/CLDR-18739 1121 if (calendarId == CalendarId::Ethiopian && eraYear.year <= 0) { 1122 auto cal = CreateICU4XCalendar(CalendarId::EthiopianAmeteAlem); 1123 return CreateDateFromCodes( 1124 cx, CalendarId::EthiopianAmeteAlem, cal.get(), 1125 {EraCode::Standard, FromAmeteMihretToAmeteAlem(eraYear.year)}, 1126 monthCode, day, overflow); 1127 } 1128 1129 auto result = 1130 CreateDateFromCodes(calendarId, calendar, eraYear, monthCode, day); 1131 if (result.isOk()) { 1132 return result.unwrap(); 1133 } 1134 1135 switch (result.inspectErr()) { 1136 case CalendarError::UnknownMonthCode: { 1137 // We've asserted above that |monthCode| is valid for this calendar, so 1138 // any unknown month code must be for a leap month which doesn't happen in 1139 // the current year. 1140 MonthCode constrained; 1141 if (!ConstrainMonthCode(cx, calendarId, monthCode, overflow, 1142 &constrained)) { 1143 return nullptr; 1144 } 1145 MOZ_ASSERT(!constrained.isLeapMonth()); 1146 1147 // Retry as non-leap month when we're allowed to constrain. 1148 return CreateDateFromCodes(cx, calendarId, calendar, eraYear, constrained, 1149 day, overflow); 1150 } 1151 1152 case CalendarError::OutOfRange: { 1153 // ICU4X throws an out-of-range error if: 1154 // 1. Dates are before/after the requested named Japanese era. 1155 // 2. month > monthsInYear(year), or 1156 // 3. days > daysInMonthOf(year, month). 1157 1158 // If a named Japanese era is used, this can be an error for either case 1 1159 // or case 3. Handle a possible case 1 error first by mapping the era year 1160 // to a common era year and then re-try creating the date. 1161 if (calendarId == CalendarId::Japanese && 1162 IsJapaneseEraName(eraYear.era)) { 1163 EraYear commonEraYear; 1164 if (!JapaneseEraYearToCommonEraYear(cx, calendarId, calendar, eraYear, 1165 &commonEraYear)) { 1166 return nullptr; 1167 } 1168 return CreateDateFromCodes(cx, calendarId, calendar, commonEraYear, 1169 monthCode, day, overflow); 1170 } 1171 1172 // Case 2 can't happen for month-codes, so it doesn't apply here. 1173 // Case 3 can only happen when |day| is larger than the minimum number 1174 // of days in the month. 1175 MOZ_ASSERT(day > CalendarDaysInMonth(calendarId, monthCode).first); 1176 1177 if (overflow == TemporalOverflow::Reject) { 1178 ReportCalendarFieldOverflow(cx, "day", day); 1179 return nullptr; 1180 } 1181 1182 auto firstDayOfMonth = CreateDateFromCodes( 1183 cx, calendarId, calendar, eraYear, monthCode, 1, overflow); 1184 if (!firstDayOfMonth) { 1185 return nullptr; 1186 } 1187 1188 int32_t daysInMonth = DaysInMonth(firstDayOfMonth.get()); 1189 MOZ_ASSERT(day > daysInMonth); 1190 return CreateDateFromCodes(cx, calendarId, calendar, eraYear, monthCode, 1191 daysInMonth, overflow); 1192 } 1193 1194 case CalendarError::UnknownEra: 1195 MOZ_ASSERT(false, "unexpected calendar error"); 1196 break; 1197 1198 case CalendarError::Generic: 1199 break; 1200 } 1201 1202 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 1203 JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR); 1204 return nullptr; 1205 } 1206 1207 static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendarId, 1208 const icu4x::capi::Calendar* calendar, 1209 EraYear eraYear, int32_t month, 1210 int32_t day, TemporalOverflow overflow) { 1211 MOZ_ASSERT(calendarId != CalendarId::ISO8601); 1212 MOZ_ASSERT(month > 0); 1213 MOZ_ASSERT(day > 0); 1214 MOZ_ASSERT(month <= CalendarMonthsPerYear(calendarId)); 1215 MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second); 1216 1217 switch (calendarId) { 1218 case CalendarId::ISO8601: 1219 case CalendarId::Buddhist: 1220 case CalendarId::Coptic: 1221 case CalendarId::Ethiopian: 1222 case CalendarId::EthiopianAmeteAlem: 1223 case CalendarId::Gregorian: 1224 case CalendarId::Indian: 1225 case CalendarId::IslamicCivil: 1226 case CalendarId::IslamicTabular: 1227 case CalendarId::IslamicUmmAlQura: 1228 case CalendarId::Japanese: 1229 case CalendarId::Persian: 1230 case CalendarId::ROC: { 1231 MOZ_ASSERT(!CalendarHasLeapMonths(calendarId)); 1232 1233 // Use the month-code corresponding to the ordinal month number for 1234 // calendar systems without leap months. 1235 auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1236 MonthCode{month}, day, overflow); 1237 if (!date) { 1238 return nullptr; 1239 } 1240 MOZ_ASSERT_IF(!CalendarHasMidYearEras(calendarId), 1241 OrdinalMonth(date.get()) == month); 1242 return date; 1243 } 1244 1245 case CalendarId::Dangi: 1246 case CalendarId::Chinese: { 1247 static_assert(CalendarHasLeapMonths(CalendarId::Chinese)); 1248 static_assert(CalendarMonthsPerYear(CalendarId::Chinese) == 13); 1249 static_assert(CalendarHasLeapMonths(CalendarId::Dangi)); 1250 static_assert(CalendarMonthsPerYear(CalendarId::Dangi) == 13); 1251 1252 MOZ_ASSERT(1 <= month && month <= 13); 1253 1254 // Create date with month number replaced by month-code. 1255 auto monthCode = MonthCode{std::min(month, 12)}; 1256 auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1257 monthCode, day, overflow); 1258 if (!date) { 1259 return nullptr; 1260 } 1261 1262 // If the ordinal month of |date| matches the input month, no additional 1263 // changes are necessary and we can directly return |date|. 1264 int32_t ordinal = OrdinalMonth(date.get()); 1265 if (ordinal == month) { 1266 return date; 1267 } 1268 1269 // Otherwise we need to handle three cases: 1270 // 1. The input year contains a leap month and we need to adjust the 1271 // month-code. 1272 // 2. The thirteenth month of a year without leap months was requested. 1273 // 3. The thirteenth month of a year with leap months was requested. 1274 if (ordinal > month) { 1275 MOZ_ASSERT(1 < month && month <= 12); 1276 1277 // This case can only happen in leap years. 1278 MOZ_ASSERT(MonthsInYear(date.get()) == 13); 1279 1280 // Leap months can occur after any month in the Chinese calendar. 1281 // 1282 // Example when the fourth month is a leap month between M03 and M04. 1283 // 1284 // Month code: M01 M02 M03 M03L M04 M05 M06 ... 1285 // Ordinal month: 1 2 3 4 5 6 7 1286 1287 // The month can be off by exactly one. 1288 MOZ_ASSERT((ordinal - month) == 1); 1289 1290 // First try the case when the previous month isn't a leap month. This 1291 // case can only occur when |month > 2|, because otherwise we know that 1292 // "M01L" is the correct answer. 1293 if (month > 2) { 1294 auto previousMonthCode = MonthCode{month - 1}; 1295 date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1296 previousMonthCode, day, overflow); 1297 if (!date) { 1298 return nullptr; 1299 } 1300 1301 int32_t ordinal = OrdinalMonth(date.get()); 1302 if (ordinal == month) { 1303 return date; 1304 } 1305 } 1306 1307 // Fall-through when the previous month is a leap month. 1308 } else { 1309 MOZ_ASSERT(month == 13); 1310 MOZ_ASSERT(ordinal == 12); 1311 1312 // Years with leap months contain thirteen months. 1313 if (MonthsInYear(date.get()) != 13) { 1314 if (overflow == TemporalOverflow::Reject) { 1315 ReportCalendarFieldOverflow(cx, "month", month); 1316 return nullptr; 1317 } 1318 return date; 1319 } 1320 1321 // Fall-through to return leap month "M12L" at the end of the year. 1322 } 1323 1324 // Finally handle the case when the previous month is a leap month. 1325 auto leapMonthCode = MonthCode{month - 1, /* isLeapMonth= */ true}; 1326 date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1327 leapMonthCode, day, overflow); 1328 if (!date) { 1329 return nullptr; 1330 } 1331 MOZ_ASSERT(OrdinalMonth(date.get()) == month, "unexpected ordinal month"); 1332 return date; 1333 } 1334 1335 case CalendarId::Hebrew: { 1336 static_assert(CalendarHasLeapMonths(CalendarId::Hebrew)); 1337 static_assert(CalendarMonthsPerYear(CalendarId::Hebrew) == 13); 1338 1339 MOZ_ASSERT(1 <= month && month <= 13); 1340 1341 // Constrain |day| when overflow is "reject" to avoid rejecting too large 1342 // day values in CreateDateFromCodes. 1343 // 1344 // For example when month = 10 and day = 30 and the input year is a leap 1345 // year. We first try month code "M10", but since "M10" can have at most 1346 // 29 days, we need to constrain the days value before calling 1347 // CreateDateFromCodes. 1348 int32_t constrainedDay = day; 1349 if (overflow == TemporalOverflow::Reject) { 1350 constexpr auto daysInMonth = CalendarDaysInMonth(CalendarId::Hebrew); 1351 if (day > daysInMonth.first && day <= daysInMonth.second) { 1352 constrainedDay = daysInMonth.first; 1353 } 1354 } 1355 1356 // Create date with month number replaced by month-code. 1357 auto monthCode = MonthCode{std::min(month, 12)}; 1358 auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1359 monthCode, constrainedDay, overflow); 1360 if (!date) { 1361 return nullptr; 1362 } 1363 1364 // If the ordinal month of |date| matches the input month, no additional 1365 // changes are necessary and we can directly return |date|. 1366 int32_t ordinal = OrdinalMonth(date.get()); 1367 if (ordinal == month) { 1368 // If |day| was constrained, check if the actual input days value 1369 // exceeds the number of days in the resolved month. 1370 if (constrainedDay < day) { 1371 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 1372 1373 if (day > CalendarDaysInMonth(calendarId, monthCode).second) { 1374 ReportCalendarFieldOverflow(cx, "day", day); 1375 return nullptr; 1376 } 1377 return CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1378 monthCode, day, overflow); 1379 } 1380 return date; 1381 } 1382 1383 // Otherwise we need to handle two cases: 1384 // 1. The input year contains a leap month and we need to adjust the 1385 // month-code. 1386 // 2. The thirteenth month of a year without leap months was requested. 1387 if (ordinal > month) { 1388 MOZ_ASSERT(1 < month && month <= 12); 1389 1390 // This case can only happen in leap years. 1391 MOZ_ASSERT(MonthsInYear(date.get()) == 13); 1392 1393 // Leap months can occur between M05 and M06 in the Hebrew calendar. 1394 // 1395 // Month code: M01 M02 M03 M04 M05 M05L M06 ... 1396 // Ordinal month: 1 2 3 4 5 6 7 1397 1398 // The month can be off by exactly one. 1399 MOZ_ASSERT((ordinal - month) == 1); 1400 } else { 1401 MOZ_ASSERT(month == 13); 1402 MOZ_ASSERT(ordinal == 12); 1403 1404 if (overflow == TemporalOverflow::Reject) { 1405 ReportCalendarFieldOverflow(cx, "month", month); 1406 return nullptr; 1407 } 1408 return date; 1409 } 1410 1411 // The previous month is the leap month Adar I iff |month| is six. 1412 bool isLeapMonth = month == 6; 1413 auto previousMonthCode = MonthCode{month - 1, isLeapMonth}; 1414 date = CreateDateFromCodes(cx, calendarId, calendar, eraYear, 1415 previousMonthCode, day, overflow); 1416 if (!date) { 1417 return nullptr; 1418 } 1419 MOZ_ASSERT(OrdinalMonth(date.get()) == month, "unexpected ordinal month"); 1420 return date; 1421 } 1422 } 1423 MOZ_CRASH("invalid calendar id"); 1424 } 1425 1426 static constexpr size_t ICUEraNameMaxLength() { 1427 size_t length = 0; 1428 for (auto calendar : AvailableCalendars()) { 1429 for (auto era : CalendarEras(calendar)) { 1430 auto name = IcuEraName(calendar, era); 1431 length = std::max(length, name.length()); 1432 } 1433 } 1434 return length; 1435 } 1436 1437 class EraName { 1438 // Note: Assigning MaxLength to ICUEraNameMaxLength() breaks the CDT indexer. 1439 static constexpr size_t MaxLength = 7; 1440 1441 // Disable tautological-value-range-compare to avoid a bogus Clang warning. 1442 // See bug 1956918 and bug 1936626. 1443 #ifdef __clang__ 1444 # pragma clang diagnostic push 1445 # pragma clang diagnostic ignored "-Wtautological-value-range-compare" 1446 #endif 1447 1448 static_assert(MaxLength >= ICUEraNameMaxLength(), 1449 "Storage size is at least as large as the largest known era"); 1450 1451 #ifdef __clang__ 1452 # pragma clang diagnostic pop 1453 #endif 1454 1455 // Storage for the largest known era string and the terminating NUL-character. 1456 char buf[MaxLength + 1] = {}; 1457 size_t length = 0; 1458 1459 public: 1460 explicit EraName(const icu4x::capi::Date* date) { 1461 auto writable = diplomat::capi::diplomat_simple_write(buf, std::size(buf)); 1462 1463 icu4x::capi::icu4x_Date_era_mv1(date, &writable); 1464 MOZ_ASSERT(writable.buf == buf, "unexpected buffer relocation"); 1465 1466 length = writable.len; 1467 } 1468 1469 bool operator==(std::string_view sv) const { 1470 return std::string_view{buf, length} == sv; 1471 } 1472 1473 bool operator!=(std::string_view sv) const { return !(*this == sv); } 1474 }; 1475 1476 /** 1477 * Retrieve the era code from |date| and then map the returned ICU4X era code to 1478 * the corresponding |EraCode| member. 1479 */ 1480 static bool CalendarDateEra(JSContext* cx, CalendarId calendar, 1481 const icu4x::capi::Date* date, EraCode* result) { 1482 MOZ_ASSERT(calendar != CalendarId::ISO8601); 1483 1484 auto eraName = EraName(date); 1485 1486 // Map from era name to era code. 1487 for (auto era : CalendarEras(calendar)) { 1488 if (eraName == IcuEraName(calendar, era)) { 1489 *result = era; 1490 return true; 1491 } 1492 } 1493 1494 // Invalid/Unknown era name. 1495 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 1496 JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR); 1497 return false; 1498 } 1499 1500 /** 1501 * Return the extended (non-era) year from |date|. 1502 */ 1503 static int32_t CalendarDateYear(CalendarId calendar, 1504 const icu4x::capi::Date* date) { 1505 MOZ_ASSERT(calendar != CalendarId::ISO8601); 1506 1507 switch (calendar) { 1508 case CalendarId::ISO8601: 1509 case CalendarId::Buddhist: 1510 case CalendarId::Coptic: 1511 case CalendarId::EthiopianAmeteAlem: 1512 case CalendarId::Hebrew: 1513 case CalendarId::Indian: 1514 case CalendarId::Persian: 1515 case CalendarId::Gregorian: 1516 case CalendarId::IslamicCivil: 1517 case CalendarId::IslamicTabular: 1518 case CalendarId::IslamicUmmAlQura: 1519 case CalendarId::Japanese: { 1520 return icu4x::capi::icu4x_Date_extended_year_mv1(date); 1521 } 1522 1523 case CalendarId::Chinese: 1524 case CalendarId::Dangi: { 1525 // Return the related ISO year for Chinese/Dangi. 1526 return icu4x::capi::icu4x_Date_era_year_or_related_iso_mv1(date); 1527 } 1528 1529 case CalendarId::Ethiopian: { 1530 // ICU4X implements the current CLDR rules for Ethopian (Amete Mihret) 1531 // calendar eras. It's unclear if CLDR reflects modern use of the 1532 // calendar, therefore we map all years to a single era, anchored at the 1533 // date of incarnation. 1534 // 1535 // https://unicode-org.atlassian.net/browse/CLDR-18739 1536 1537 int32_t year = icu4x::capi::icu4x_Date_extended_year_mv1(date); 1538 1539 auto eraName = EraName(date); 1540 MOZ_ASSERT( 1541 eraName == IcuEraName(CalendarId::Ethiopian, EraCode::Standard) || 1542 eraName == 1543 IcuEraName(CalendarId::EthiopianAmeteAlem, EraCode::Standard)); 1544 1545 // Workaround for <https://github.com/unicode-org/icu4x/issues/6719>. 1546 if (eraName == 1547 IcuEraName(CalendarId::EthiopianAmeteAlem, EraCode::Standard)) { 1548 year = FromAmeteAlemToAmeteMihret(year); 1549 } 1550 1551 return year; 1552 } 1553 1554 case CalendarId::ROC: { 1555 static_assert(CalendarEras(CalendarId::ROC).size() == 2); 1556 1557 // ICU4X returns the related ISO year for the extended year, but we want 1558 // to anchor the extended year at 1 ROC instead. 1559 // 1560 // https://github.com/unicode-org/icu4x/issues/6720 1561 1562 int32_t year = icu4x::capi::icu4x_Date_era_year_or_related_iso_mv1(date); 1563 MOZ_ASSERT(year > 0, "era years are strictly positive in ICU4X"); 1564 1565 auto eraName = EraName(date); 1566 MOZ_ASSERT(eraName == IcuEraName(CalendarId::ROC, EraCode::Standard) || 1567 eraName == IcuEraName(CalendarId::ROC, EraCode::Inverse)); 1568 1569 // Map from era year to extended year. Examples: 1570 // 1571 // ---------------------------- 1572 // | Era Year | Extended Year | 1573 // | 2 ROC | 2 | 1574 // | 1 ROC | 1 | 1575 // | 1 BROC | 0 | 1576 // | 2 BROC | -1 | 1577 // ---------------------------- 1578 if (eraName == IcuEraName(CalendarId::ROC, EraCode::Inverse)) { 1579 year = -(year - 1); 1580 } 1581 1582 return year; 1583 } 1584 } 1585 MOZ_CRASH("invalid calendar id"); 1586 } 1587 1588 /** 1589 * Retrieve the month code from |date| and then map the returned ICU4X month 1590 * code to the corresponding |MonthCode| member. 1591 */ 1592 static MonthCode CalendarDateMonthCode(CalendarId calendar, 1593 const icu4x::capi::Date* date) { 1594 MOZ_ASSERT(calendar != CalendarId::ISO8601); 1595 1596 // Valid month codes are "M01".."M13" and "M01L".."M12L". 1597 constexpr size_t MaxLength = 1598 std::string_view{MonthCode::maxLeapMonth()}.length(); 1599 static_assert( 1600 MaxLength > std::string_view{MonthCode::maxNonLeapMonth()}.length(), 1601 "string representation of max-leap month is larger"); 1602 1603 // Storage for the largest valid month code and the terminating NUL-character. 1604 char buf[MaxLength + 1] = {}; 1605 auto writable = diplomat::capi::diplomat_simple_write(buf, std::size(buf)); 1606 1607 icu4x::capi::icu4x_Date_month_code_mv1(date, &writable); 1608 MOZ_ASSERT(writable.buf == buf, "unexpected buffer relocation"); 1609 1610 auto view = std::string_view{writable.buf, writable.len}; 1611 1612 MOZ_ASSERT(view.length() >= 3); 1613 MOZ_ASSERT(view[0] == 'M'); 1614 MOZ_ASSERT(mozilla::IsAsciiDigit(view[1])); 1615 MOZ_ASSERT(mozilla::IsAsciiDigit(view[2])); 1616 MOZ_ASSERT_IF(view.length() > 3, view[3] == 'L'); 1617 1618 int32_t ordinal = 1619 AsciiDigitToNumber(view[1]) * 10 + AsciiDigitToNumber(view[2]); 1620 bool isLeapMonth = view.length() > 3; 1621 auto monthCode = MonthCode{ordinal, isLeapMonth}; 1622 1623 // The month code must be valid for this calendar. 1624 MOZ_ASSERT(IsValidMonthCodeForCalendar(calendar, monthCode)); 1625 1626 return monthCode; 1627 } 1628 1629 class MonthCodeString { 1630 // Zero-terminated month code string. 1631 char str_[4 + 1]; 1632 1633 public: 1634 explicit MonthCodeString(MonthCodeField field) { 1635 str_[0] = 'M'; 1636 str_[1] = char('0' + (field.ordinal() / 10)); 1637 str_[2] = char('0' + (field.ordinal() % 10)); 1638 str_[3] = field.isLeapMonth() ? 'L' : '\0'; 1639 str_[4] = '\0'; 1640 } 1641 1642 const char* toCString() const { return str_; } 1643 }; 1644 1645 /** 1646 * CalendarResolveFields ( calendar, fields, type ) 1647 */ 1648 static bool ISOCalendarResolveMonth(JSContext* cx, 1649 Handle<CalendarFields> fields, 1650 double* result) { 1651 double month = fields.month(); 1652 MOZ_ASSERT_IF(fields.has(CalendarField::Month), 1653 IsInteger(month) && month > 0); 1654 1655 // CalendarResolveFields, steps 1.e. 1656 if (!fields.has(CalendarField::MonthCode)) { 1657 MOZ_ASSERT(fields.has(CalendarField::Month)); 1658 1659 *result = month; 1660 return true; 1661 } 1662 1663 auto monthCode = fields.monthCode(); 1664 1665 // CalendarResolveFields, steps 1.f-k. 1666 int32_t ordinal = monthCode.ordinal(); 1667 if (ordinal < 1 || ordinal > 12 || monthCode.isLeapMonth()) { 1668 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 1669 JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, 1670 MonthCodeString{monthCode}.toCString()); 1671 return false; 1672 } 1673 1674 // CalendarResolveFields, steps 1.l-m. 1675 if (fields.has(CalendarField::Month) && month != ordinal) { 1676 ToCStringBuf cbuf; 1677 const char* monthStr = NumberToCString(&cbuf, month); 1678 1679 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 1680 JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_MONTHCODE, 1681 MonthCodeString{monthCode}.toCString(), monthStr); 1682 return false; 1683 } 1684 1685 // CalendarResolveFields, steps 1.n. 1686 *result = ordinal; 1687 return true; 1688 } 1689 1690 struct EraYears { 1691 // Year starting from the calendar epoch. 1692 mozilla::Maybe<EraYear> fromEpoch; 1693 1694 // Year starting from a specific calendar era. 1695 mozilla::Maybe<EraYear> fromEra; 1696 }; 1697 1698 static bool CalendarEraYear(JSContext* cx, CalendarId calendarId, 1699 EraYear eraYear, EraYear* result) { 1700 MOZ_ASSERT(CalendarSupportsEra(calendarId)); 1701 MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId)); 1702 1703 if (eraYear.year > 0 || !CalendarEraHasInverse(calendarId)) { 1704 *result = eraYear; 1705 return true; 1706 } 1707 1708 switch (eraYear.era) { 1709 case EraCode::Standard: { 1710 // Map non-positive era years as follows: 1711 // 1712 // 0 CE -> 1 BCE 1713 // -1 CE -> 2 BCE 1714 // etc. 1715 *result = {EraCode::Inverse, int32_t(mozilla::Abs(eraYear.year) + 1)}; 1716 return true; 1717 } 1718 1719 case EraCode::Inverse: { 1720 // Map non-positive era years as follows: 1721 // 1722 // 0 BCE -> 1 CE 1723 // -1 BCE -> 2 CE 1724 // etc. 1725 *result = {EraCode::Standard, int32_t(mozilla::Abs(eraYear.year) + 1)}; 1726 return true; 1727 } 1728 1729 case EraCode::Meiji: 1730 case EraCode::Taisho: 1731 case EraCode::Showa: 1732 case EraCode::Heisei: 1733 case EraCode::Reiwa: { 1734 MOZ_ASSERT(calendarId == CalendarId::Japanese); 1735 1736 auto cal = CreateICU4XCalendar(calendarId); 1737 return JapaneseEraYearToCommonEraYear(cx, calendarId, cal.get(), eraYear, 1738 result); 1739 } 1740 } 1741 MOZ_CRASH("invalid era id"); 1742 } 1743 1744 /** 1745 * CalendarResolveFields ( calendar, fields, type ) 1746 * CalendarDateToISO ( calendar, fields, overflow ) 1747 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ) 1748 * 1749 * Extract `year` and `eraYear` from |fields| and perform some initial 1750 * validation to ensure the values are valid for the requested calendar. 1751 */ 1752 static bool CalendarFieldYear(JSContext* cx, CalendarId calendar, 1753 Handle<CalendarFields> fields, EraYears* result) { 1754 MOZ_ASSERT(fields.has(CalendarField::Year) || 1755 fields.has(CalendarField::EraYear)); 1756 1757 // |eraYear| is to be ignored when not relevant for |calendar| per 1758 // CalendarResolveFields. 1759 bool supportsEra = 1760 fields.has(CalendarField::Era) && CalendarSupportsEra(calendar); 1761 MOZ_ASSERT_IF(fields.has(CalendarField::Era), CalendarSupportsEra(calendar)); 1762 1763 // Case 1: |year| field is present. 1764 mozilla::Maybe<EraYear> fromEpoch; 1765 if (fields.has(CalendarField::Year)) { 1766 double year = fields.year(); 1767 MOZ_ASSERT(IsInteger(year)); 1768 1769 int32_t intYear; 1770 if (!mozilla::NumberEqualsInt32(year, &intYear) || 1771 mozilla::Abs(intYear) > MaximumCalendarYear(calendar)) { 1772 ReportCalendarFieldOverflow(cx, "year", year); 1773 return false; 1774 } 1775 1776 fromEpoch = mozilla::Some(CalendarEraYear(calendar, intYear)); 1777 } else { 1778 MOZ_ASSERT(supportsEra); 1779 } 1780 1781 // Case 2: |era| and |eraYear| fields are present and relevant for |calendar|. 1782 mozilla::Maybe<EraYear> fromEra; 1783 if (supportsEra) { 1784 MOZ_ASSERT(fields.has(CalendarField::Era)); 1785 MOZ_ASSERT(fields.has(CalendarField::EraYear)); 1786 1787 auto era = fields.era(); 1788 MOZ_ASSERT(era); 1789 1790 double eraYear = fields.eraYear(); 1791 MOZ_ASSERT(IsInteger(eraYear)); 1792 1793 auto* linearEra = era->ensureLinear(cx); 1794 if (!linearEra) { 1795 return false; 1796 } 1797 1798 // Ensure the requested era is valid for |calendar|. 1799 auto eraCode = CanonicalizeEraInCalendar(calendar, linearEra); 1800 if (!eraCode) { 1801 if (auto code = QuoteString(cx, era)) { 1802 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 1803 JSMSG_TEMPORAL_CALENDAR_INVALID_ERA, 1804 code.get()); 1805 } 1806 return false; 1807 } 1808 1809 int32_t intEraYear; 1810 if (!mozilla::NumberEqualsInt32(eraYear, &intEraYear) || 1811 mozilla::Abs(intEraYear) > MaximumCalendarYear(calendar)) { 1812 ReportCalendarFieldOverflow(cx, "eraYear", eraYear); 1813 return false; 1814 } 1815 1816 EraYear eraAndYear; 1817 if (!CalendarEraYear(cx, calendar, {*eraCode, intEraYear}, &eraAndYear)) { 1818 return false; 1819 } 1820 fromEra = mozilla::Some(eraAndYear); 1821 } 1822 1823 *result = {fromEpoch, fromEra}; 1824 return true; 1825 } 1826 1827 struct Month { 1828 // Month code. 1829 MonthCode code; 1830 1831 // Ordinal month number. 1832 int32_t ordinal = 0; 1833 }; 1834 1835 /** 1836 * NonISOCalendarDateToISO ( calendar, fields, overflow ) 1837 * NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow ) 1838 * 1839 * Extract `month` and `monthCode` from |fields| and perform some initial 1840 * validation to ensure the values are valid for the requested calendar. 1841 */ 1842 static bool CalendarFieldMonth(JSContext* cx, CalendarId calendar, 1843 Handle<CalendarFields> fields, 1844 TemporalOverflow overflow, Month* result) { 1845 MOZ_ASSERT(fields.has(CalendarField::Month) || 1846 fields.has(CalendarField::MonthCode)); 1847 1848 // Case 1: |month| field is present. 1849 int32_t intMonth = 0; 1850 if (fields.has(CalendarField::Month)) { 1851 double month = fields.month(); 1852 MOZ_ASSERT(IsInteger(month) && month > 0); 1853 1854 if (!mozilla::NumberEqualsInt32(month, &intMonth)) { 1855 intMonth = 0; 1856 } 1857 1858 const int32_t monthsPerYear = CalendarMonthsPerYear(calendar); 1859 if (intMonth < 1 || intMonth > monthsPerYear) { 1860 if (overflow == TemporalOverflow::Reject) { 1861 ReportCalendarFieldOverflow(cx, "month", month); 1862 return false; 1863 } 1864 MOZ_ASSERT(overflow == TemporalOverflow::Constrain); 1865 1866 intMonth = monthsPerYear; 1867 } 1868 1869 MOZ_ASSERT(intMonth > 0); 1870 } 1871 1872 // Case 2: |monthCode| field is present. 1873 MonthCode fromMonthCode; 1874 if (fields.has(CalendarField::MonthCode)) { 1875 auto monthCode = fields.monthCode(); 1876 int32_t ordinal = monthCode.ordinal(); 1877 bool isLeapMonth = monthCode.isLeapMonth(); 1878 1879 constexpr int32_t minMonth = MonthCode{1}.ordinal(); 1880 constexpr int32_t maxNonLeapMonth = MonthCode::maxNonLeapMonth().ordinal(); 1881 constexpr int32_t maxLeapMonth = MonthCode::maxLeapMonth().ordinal(); 1882 1883 // Minimum month number is 1. Maximum month is 12 (or 13 when the calendar 1884 // uses epagomenal months). 1885 const int32_t maxMonth = isLeapMonth ? maxLeapMonth : maxNonLeapMonth; 1886 if (minMonth <= ordinal && ordinal <= maxMonth) { 1887 fromMonthCode = MonthCode{ordinal, isLeapMonth}; 1888 } 1889 1890 // Ensure the month code is valid for this calendar. 1891 if (!IsValidMonthCodeForCalendar(calendar, fromMonthCode)) { 1892 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 1893 JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, 1894 MonthCodeString{monthCode}.toCString()); 1895 return false; 1896 } 1897 } 1898 1899 *result = {fromMonthCode, intMonth}; 1900 return true; 1901 } 1902 1903 /** 1904 * CalendarResolveFields ( calendar, fields, type ) 1905 * CalendarDateToISO ( calendar, fields, overflow ) 1906 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ) 1907 * 1908 * Extract `day` from |fields| and perform some initial validation to ensure the 1909 * value is valid for the requested calendar. 1910 */ 1911 static bool CalendarFieldDay(JSContext* cx, CalendarId calendar, 1912 Handle<CalendarFields> fields, 1913 TemporalOverflow overflow, int32_t* result) { 1914 MOZ_ASSERT(fields.has(CalendarField::Day)); 1915 1916 double day = fields.day(); 1917 MOZ_ASSERT(IsInteger(day) && day > 0); 1918 1919 int32_t intDay; 1920 if (!mozilla::NumberEqualsInt32(day, &intDay)) { 1921 intDay = 0; 1922 } 1923 1924 // Constrain to a valid day value in this calendar. 1925 int32_t daysPerMonth = CalendarDaysInMonth(calendar).second; 1926 if (intDay < 1 || intDay > daysPerMonth) { 1927 if (overflow == TemporalOverflow::Reject) { 1928 ReportCalendarFieldOverflow(cx, "day", day); 1929 return false; 1930 } 1931 MOZ_ASSERT(overflow == TemporalOverflow::Constrain); 1932 1933 intDay = daysPerMonth; 1934 } 1935 1936 *result = intDay; 1937 return true; 1938 } 1939 1940 /** 1941 * CalendarResolveFields ( calendar, fields, type ) 1942 * 1943 * > The operation throws a TypeError exception if the properties of fields are 1944 * > internally inconsistent within the calendar [...]. For example: 1945 * > 1946 * > [...] The values for "era" and "eraYear" do not together identify the same 1947 * > year as the value for "year". 1948 */ 1949 static bool CalendarFieldEraYearMatchesYear(JSContext* cx, CalendarId calendar, 1950 Handle<CalendarFields> fields, 1951 const icu4x::capi::Date* date) { 1952 MOZ_ASSERT(fields.has(CalendarField::EraYear)); 1953 MOZ_ASSERT(fields.has(CalendarField::Year)); 1954 1955 double year = fields.year(); 1956 MOZ_ASSERT(IsInteger(year)); 1957 1958 int32_t intYear; 1959 MOZ_ALWAYS_TRUE(mozilla::NumberEqualsInt32(year, &intYear)); 1960 1961 int32_t yearFromEraYear = CalendarDateYear(calendar, date); 1962 1963 // The user requested year must match the actual (extended/epoch) year. 1964 if (intYear != yearFromEraYear) { 1965 ToCStringBuf yearCbuf; 1966 const char* yearStr = NumberToCString(&yearCbuf, intYear); 1967 1968 ToCStringBuf fromEraCbuf; 1969 const char* fromEraStr = NumberToCString(&fromEraCbuf, yearFromEraYear); 1970 1971 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 1972 JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_YEAR, 1973 yearStr, fromEraStr); 1974 return false; 1975 } 1976 return true; 1977 } 1978 1979 /** 1980 * CalendarResolveFields ( calendar, fields, type ) 1981 * 1982 * > The operation throws a TypeError exception if the properties of fields are 1983 * > internally inconsistent within the calendar [...]. For example: 1984 * > 1985 * > If "month" and "monthCode" in the calendar [...] do not identify the same 1986 * > month. 1987 */ 1988 static bool CalendarFieldMonthCodeMatchesMonth(JSContext* cx, 1989 Handle<CalendarFields> fields, 1990 const icu4x::capi::Date* date, 1991 int32_t month) { 1992 int32_t ordinal = OrdinalMonth(date); 1993 1994 // The user requested month must match the actual ordinal month. 1995 if (month != ordinal) { 1996 ToCStringBuf cbuf; 1997 const char* monthStr = NumberToCString(&cbuf, fields.month()); 1998 1999 JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, 2000 JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_MONTHCODE, 2001 MonthCodeString{fields.monthCode()}.toCString(), 2002 monthStr); 2003 return false; 2004 } 2005 return true; 2006 } 2007 2008 static ISODate ToISODate(const icu4x::capi::Date* date) { 2009 UniqueICU4XIsoDate isoDate{icu4x::capi::icu4x_Date_to_iso_mv1(date)}; 2010 MOZ_ASSERT(isoDate, "unexpected null-pointer result"); 2011 2012 int32_t isoYear = icu4x::capi::icu4x_IsoDate_year_mv1(isoDate.get()); 2013 2014 int32_t isoMonth = icu4x::capi::icu4x_IsoDate_month_mv1(isoDate.get()); 2015 MOZ_ASSERT(1 <= isoMonth && isoMonth <= 12); 2016 2017 int32_t isoDay = icu4x::capi::icu4x_IsoDate_day_of_month_mv1(isoDate.get()); 2018 MOZ_ASSERT(1 <= isoDay && isoDay <= ::ISODaysInMonth(isoYear, isoMonth)); 2019 2020 return {isoYear, isoMonth, isoDay}; 2021 } 2022 2023 static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendar, 2024 const icu4x::capi::Calendar* cal, 2025 const EraYears& eraYears, 2026 const Month& month, int32_t day, 2027 Handle<CalendarFields> fields, 2028 TemporalOverflow overflow) { 2029 // Use |eraYear| if present, so we can more easily check for consistent 2030 // |year| and |eraYear| fields. 2031 auto eraYear = eraYears.fromEra ? *eraYears.fromEra : *eraYears.fromEpoch; 2032 2033 UniqueICU4XDate date; 2034 if (month.code != MonthCode{}) { 2035 date = CreateDateFromCodes(cx, calendar, cal, eraYear, month.code, day, 2036 overflow); 2037 } else { 2038 date = CreateDateFrom(cx, calendar, cal, eraYear, month.ordinal, day, 2039 overflow); 2040 } 2041 if (!date) { 2042 return nullptr; 2043 } 2044 2045 // |year| and |eraYear| must be consistent. 2046 if (eraYears.fromEpoch && eraYears.fromEra) { 2047 if (!CalendarFieldEraYearMatchesYear(cx, calendar, fields, date.get())) { 2048 return nullptr; 2049 } 2050 } 2051 2052 // |month| and |monthCode| must be consistent. 2053 if (month.code != MonthCode{} && month.ordinal > 0) { 2054 if (!CalendarFieldMonthCodeMatchesMonth(cx, fields, date.get(), 2055 month.ordinal)) { 2056 return nullptr; 2057 } 2058 } 2059 2060 return date; 2061 } 2062 2063 /** 2064 * RegulateISODate ( year, month, day, overflow ) 2065 */ 2066 static bool RegulateISODate(JSContext* cx, int32_t year, double month, 2067 double day, TemporalOverflow overflow, 2068 ISODate* result) { 2069 MOZ_ASSERT(IsInteger(month)); 2070 MOZ_ASSERT(IsInteger(day)); 2071 2072 // Step 1. 2073 if (overflow == TemporalOverflow::Constrain) { 2074 // Step 1.a. 2075 int32_t m = int32_t(std::clamp(month, 1.0, 12.0)); 2076 2077 // Step 1.b. 2078 double daysInMonth = double(::ISODaysInMonth(year, m)); 2079 2080 // Step 1.c. 2081 int32_t d = int32_t(std::clamp(day, 1.0, daysInMonth)); 2082 2083 // Step 3. (Inlined call to CreateISODateRecord.) 2084 *result = {year, m, d}; 2085 return true; 2086 } 2087 2088 // Step 2.a. 2089 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 2090 2091 // Step 2.b. 2092 if (!ThrowIfInvalidISODate(cx, year, month, day)) { 2093 return false; 2094 } 2095 2096 // Step 3. (Inlined call to CreateISODateRecord.) 2097 *result = {year, int32_t(month), int32_t(day)}; 2098 return true; 2099 } 2100 2101 /** 2102 * NonISOCalendarDateToISO ( calendar, fields, overflow ) 2103 */ 2104 static bool NonISOCalendarDateToISO(JSContext* cx, CalendarId calendar, 2105 Handle<CalendarFields> fields, 2106 TemporalOverflow overflow, 2107 ISODate* result) { 2108 EraYears eraYears; 2109 if (!CalendarFieldYear(cx, calendar, fields, &eraYears)) { 2110 return false; 2111 } 2112 2113 Month month; 2114 if (!CalendarFieldMonth(cx, calendar, fields, overflow, &month)) { 2115 return false; 2116 } 2117 2118 int32_t day; 2119 if (!CalendarFieldDay(cx, calendar, fields, overflow, &day)) { 2120 return false; 2121 } 2122 2123 auto cal = CreateICU4XCalendar(calendar); 2124 auto date = CreateDateFrom(cx, calendar, cal.get(), eraYears, month, day, 2125 fields, overflow); 2126 if (!date) { 2127 return false; 2128 } 2129 2130 *result = ToISODate(date.get()); 2131 return true; 2132 } 2133 2134 /** 2135 * CalendarDateToISO ( calendar, fields, overflow ) 2136 */ 2137 static bool CalendarDateToISO(JSContext* cx, CalendarId calendar, 2138 Handle<CalendarFields> fields, 2139 TemporalOverflow overflow, ISODate* result) { 2140 // Step 1. 2141 if (calendar == CalendarId::ISO8601) { 2142 // Step 1.a. 2143 MOZ_ASSERT(fields.has(CalendarField::Year)); 2144 MOZ_ASSERT(fields.has(CalendarField::Month) || 2145 fields.has(CalendarField::MonthCode)); 2146 MOZ_ASSERT(fields.has(CalendarField::Day)); 2147 2148 // Remaining steps from CalendarResolveFields to resolve the month. 2149 double month; 2150 if (!ISOCalendarResolveMonth(cx, fields, &month)) { 2151 return false; 2152 } 2153 2154 int32_t intYear; 2155 if (!mozilla::NumberEqualsInt32(fields.year(), &intYear)) { 2156 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 2157 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 2158 return false; 2159 } 2160 2161 // Step 1.b. 2162 return RegulateISODate(cx, intYear, month, fields.day(), overflow, result); 2163 } 2164 2165 // Step 2. 2166 return NonISOCalendarDateToISO(cx, calendar, fields, overflow, result); 2167 } 2168 2169 /** 2170 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ) 2171 */ 2172 static bool NonISOMonthDayToISOReferenceDate(JSContext* cx, CalendarId calendar, 2173 icu4x::capi::Calendar* cal, 2174 ISODate startISODate, 2175 ISODate endISODate, 2176 MonthCode monthCode, int32_t day, 2177 UniqueICU4XDate& resultDate) { 2178 MOZ_ASSERT(startISODate != endISODate); 2179 2180 int32_t direction = startISODate > endISODate ? -1 : 1; 2181 2182 auto fromIsoDate = CreateICU4XDate(cx, startISODate, calendar, cal); 2183 if (!fromIsoDate) { 2184 return false; 2185 } 2186 2187 auto toIsoDate = CreateICU4XDate(cx, endISODate, calendar, cal); 2188 if (!toIsoDate) { 2189 return false; 2190 } 2191 2192 // Find the calendar year for the ISO start date. 2193 int32_t calendarYear = CalendarDateYear(calendar, fromIsoDate.get()); 2194 2195 // Find the calendar year for the ISO end date. 2196 int32_t toCalendarYear = CalendarDateYear(calendar, toIsoDate.get()); 2197 2198 while (direction < 0 ? calendarYear >= toCalendarYear 2199 : calendarYear <= toCalendarYear) { 2200 // This loop can run for a long time. 2201 if (!CheckForInterrupt(cx)) { 2202 return false; 2203 } 2204 2205 auto candidateYear = CalendarEraYear(calendar, calendarYear); 2206 2207 auto result = 2208 CreateDateFromCodes(calendar, cal, candidateYear, monthCode, day); 2209 if (result.isOk()) { 2210 auto isoDate = ToISODate(result.inspect().get()); 2211 2212 // Make sure the resolved date is before |startISODate|. 2213 if (direction < 0 ? isoDate > startISODate : isoDate < startISODate) { 2214 calendarYear += direction; 2215 continue; 2216 } 2217 2218 // Stop searching if |endISODate| was reached. 2219 if (direction < 0 ? isoDate < endISODate : isoDate > endISODate) { 2220 resultDate = nullptr; 2221 return true; 2222 } 2223 2224 resultDate = result.unwrap(); 2225 return true; 2226 } 2227 2228 switch (result.inspectErr()) { 2229 case CalendarError::UnknownMonthCode: { 2230 MOZ_ASSERT(CalendarHasLeapMonths(calendar)); 2231 MOZ_ASSERT(monthCode.isLeapMonth()); 2232 2233 // Try the next candidate year if the requested leap month doesn't 2234 // occur in the current year. 2235 calendarYear += direction; 2236 continue; 2237 } 2238 2239 case CalendarError::OutOfRange: { 2240 // ICU4X throws an out-of-range error when: 2241 // 1. month > monthsInYear(year), or 2242 // 2. days > daysInMonthOf(year, month). 2243 // 2244 // Case 1 can't happen for month-codes, so it doesn't apply here. 2245 // Case 2 can only happen when |day| is larger than the minimum number 2246 // of days in the month. 2247 MOZ_ASSERT(day > CalendarDaysInMonth(calendar, monthCode).first); 2248 2249 // Try next candidate year to find an earlier year which can fulfill 2250 // the input request. 2251 calendarYear += direction; 2252 continue; 2253 } 2254 2255 case CalendarError::UnknownEra: 2256 MOZ_ASSERT(false, "unexpected calendar error"); 2257 break; 2258 2259 case CalendarError::Generic: 2260 break; 2261 } 2262 2263 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 2264 JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR); 2265 return false; 2266 } 2267 2268 resultDate = nullptr; 2269 return true; 2270 } 2271 2272 /** 2273 * NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow ) 2274 */ 2275 static bool NonISOMonthDayToISOReferenceDate(JSContext* cx, CalendarId calendar, 2276 Handle<CalendarFields> fields, 2277 TemporalOverflow overflow, 2278 ISODate* result) { 2279 EraYears eraYears; 2280 if (fields.has(CalendarField::Year) || fields.has(CalendarField::EraYear)) { 2281 if (!CalendarFieldYear(cx, calendar, fields, &eraYears)) { 2282 return false; 2283 } 2284 } else { 2285 MOZ_ASSERT(fields.has(CalendarField::MonthCode)); 2286 } 2287 2288 Month month; 2289 if (!CalendarFieldMonth(cx, calendar, fields, overflow, &month)) { 2290 return false; 2291 } 2292 2293 int32_t day; 2294 if (!CalendarFieldDay(cx, calendar, fields, overflow, &day)) { 2295 return false; 2296 } 2297 2298 auto cal = CreateICU4XCalendar(calendar); 2299 2300 // We first have to compute the month-code if it wasn't provided to us. 2301 auto monthCode = month.code; 2302 if (fields.has(CalendarField::Year) || fields.has(CalendarField::EraYear)) { 2303 auto date = CreateDateFrom(cx, calendar, cal.get(), eraYears, month, day, 2304 fields, overflow); 2305 if (!date) { 2306 return false; 2307 } 2308 2309 // This operation throws a RangeError if the ISO 8601 year corresponding to 2310 // `fields.[[Year]]` is outside the valid limits. 2311 auto isoDate = ToISODate(date.get()); 2312 if (!ISODateWithinLimits(isoDate)) { 2313 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 2314 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 2315 return false; 2316 } 2317 2318 if (!fields.has(CalendarField::MonthCode)) { 2319 monthCode = CalendarDateMonthCode(calendar, date.get()); 2320 } 2321 MOZ_ASSERT(monthCode != MonthCode{}); 2322 2323 if (overflow == TemporalOverflow::Constrain) { 2324 // Call into ICU4X if `day` exceeds the minimum number of days. 2325 int32_t minDaysInMonth = CalendarDaysInMonth(calendar, monthCode).first; 2326 if (day > minDaysInMonth) { 2327 day = DayOfMonth(date.get()); 2328 } 2329 } else { 2330 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 2331 MOZ_ASSERT(day == DayOfMonth(date.get())); 2332 } 2333 } else { 2334 MOZ_ASSERT(monthCode != MonthCode{}); 2335 2336 // Constrain `day` to maximum possible day of the input month. 2337 int32_t maxDaysInMonth = CalendarDaysInMonth(calendar, monthCode).second; 2338 if (overflow == TemporalOverflow::Constrain) { 2339 day = std::min(day, maxDaysInMonth); 2340 } else { 2341 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 2342 2343 if (day > maxDaysInMonth) { 2344 ReportCalendarFieldOverflow(cx, "day", day); 2345 return false; 2346 } 2347 } 2348 } 2349 2350 constexpr ISODate candidates[][2] = { 2351 // The reference date is the latest ISO 8601 date corresponding to the 2352 // calendar date that is between January 1, 1900 and December 31, 1972 2353 // inclusive. 2354 {ISODate{1972, 12, 31}, ISODate{1900, 1, 1}}, 2355 2356 // If there is no such date, it is the earliest ISO 8601 date 2357 // corresponding to the calendar date between January 1, 1973 and 2358 // December 31, 2035. 2359 {ISODate{1973, 1, 1}, ISODate{2035, 12, 31}}, 2360 2361 // If there is still no such date, it is the latest ISO 8601 date 2362 // corresponding to the calendar date on or before December 31, 1899. 2363 // 2364 // Year -8000 is sufficient to find all possible month-days, even for 2365 // rare cases like `{calendar: "chinese", monthCode: "M09L", day: 30}`. 2366 {ISODate{1899, 12, 31}, ISODate{-8000, 1, 1}}, 2367 }; 2368 2369 UniqueICU4XDate date; 2370 for (auto& [start, end] : candidates) { 2371 if (!NonISOMonthDayToISOReferenceDate(cx, calendar, cal.get(), start, end, 2372 monthCode, day, date)) { 2373 return false; 2374 } 2375 if (date) { 2376 break; 2377 } 2378 } 2379 2380 // We shouldn't end up here with |maxIterations == 10'000|, but just in case 2381 // still handle this case and report an error. 2382 if (!date) { 2383 ReportCalendarFieldOverflow(cx, "day", day); 2384 return false; 2385 } 2386 2387 *result = ToISODate(date.get()); 2388 return true; 2389 } 2390 2391 /** 2392 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ) 2393 */ 2394 static bool CalendarMonthDayToISOReferenceDate(JSContext* cx, 2395 CalendarId calendar, 2396 Handle<CalendarFields> fields, 2397 TemporalOverflow overflow, 2398 ISODate* result) { 2399 // Step 1. 2400 if (calendar == CalendarId::ISO8601) { 2401 // Step 1.a. 2402 MOZ_ASSERT(fields.has(CalendarField::Month) || 2403 fields.has(CalendarField::MonthCode)); 2404 MOZ_ASSERT(fields.has(CalendarField::Day)); 2405 2406 // Remaining steps from CalendarResolveFields to resolve the month. 2407 double month; 2408 if (!ISOCalendarResolveMonth(cx, fields, &month)) { 2409 return false; 2410 } 2411 2412 // Step 1.b. 2413 int32_t referenceISOYear = 1972; 2414 2415 // Step 1.c. 2416 double year = 2417 !fields.has(CalendarField::Year) ? referenceISOYear : fields.year(); 2418 2419 int32_t intYear; 2420 if (!mozilla::NumberEqualsInt32(year, &intYear)) { 2421 // Calendar cycles repeat every 400 years in the Gregorian calendar. 2422 intYear = int32_t(std::fmod(year, 400)); 2423 } 2424 2425 // Step 1.d. 2426 ISODate regulated; 2427 if (!RegulateISODate(cx, intYear, month, fields.day(), overflow, 2428 ®ulated)) { 2429 return false; 2430 } 2431 2432 // Step 1.e. 2433 *result = {referenceISOYear, regulated.month, regulated.day}; 2434 return true; 2435 } 2436 2437 // Step 2. 2438 return NonISOMonthDayToISOReferenceDate(cx, calendar, fields, overflow, 2439 result); 2440 } 2441 2442 enum class FieldType { Date, YearMonth, MonthDay }; 2443 2444 /** 2445 * NonISOResolveFields ( calendar, fields, type ) 2446 */ 2447 static bool NonISOResolveFields(JSContext* cx, CalendarId calendar, 2448 Handle<CalendarFields> fields, FieldType type) { 2449 // Date and Month-Day require |day| to be present. 2450 bool requireDay = type == FieldType::Date || type == FieldType::MonthDay; 2451 2452 // Date and Year-Month require |year| (or |eraYear|) to be present. 2453 // Month-Day requires |year| (or |eraYear|) if |monthCode| is absent. 2454 // Month-Day requires |year| (or |eraYear|) if |month| is present, even if 2455 // |monthCode| is also present. 2456 bool requireYear = type == FieldType::Date || type == FieldType::YearMonth || 2457 !fields.has(CalendarField::MonthCode) || 2458 fields.has(CalendarField::Month); 2459 2460 // Determine if any calendar fields are missing. 2461 const char* missingField = nullptr; 2462 if (!fields.has(CalendarField::MonthCode) && 2463 !fields.has(CalendarField::Month)) { 2464 // |monthCode| or |month| must be present. 2465 missingField = "monthCode"; 2466 } else if (requireDay && !fields.has(CalendarField::Day)) { 2467 missingField = "day"; 2468 } else if (!CalendarSupportsEra(calendar)) { 2469 if (requireYear && !fields.has(CalendarField::Year)) { 2470 missingField = "year"; 2471 } 2472 } else { 2473 if (fields.has(CalendarField::Era) != fields.has(CalendarField::EraYear)) { 2474 // |era| and |eraYear| must either both be present or both absent. 2475 missingField = fields.has(CalendarField::Era) ? "eraYear" : "era"; 2476 } else if (requireYear && !fields.has(CalendarField::EraYear) && 2477 !fields.has(CalendarField::Year)) { 2478 missingField = "eraYear"; 2479 } 2480 } 2481 2482 if (missingField) { 2483 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 2484 JSMSG_TEMPORAL_CALENDAR_MISSING_FIELD, 2485 missingField); 2486 return false; 2487 } 2488 2489 return true; 2490 } 2491 2492 /** 2493 * CalendarResolveFields ( calendar, fields, type ) 2494 */ 2495 static bool CalendarResolveFields(JSContext* cx, CalendarId calendar, 2496 Handle<CalendarFields> fields, 2497 FieldType type) { 2498 // Step 1. 2499 if (calendar == CalendarId::ISO8601) { 2500 // Steps 1.a-e. 2501 const char* missingField = nullptr; 2502 if ((type == FieldType::Date || type == FieldType::YearMonth) && 2503 !fields.has(CalendarField::Year)) { 2504 missingField = "year"; 2505 } else if ((type == FieldType::Date || type == FieldType::MonthDay) && 2506 !fields.has(CalendarField::Day)) { 2507 missingField = "day"; 2508 } else if (!fields.has(CalendarField::MonthCode) && 2509 !fields.has(CalendarField::Month)) { 2510 missingField = "month"; 2511 } 2512 2513 if (missingField) { 2514 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 2515 JSMSG_TEMPORAL_CALENDAR_MISSING_FIELD, 2516 missingField); 2517 return false; 2518 } 2519 2520 // Steps 1.f-n. (Handled in ISOCalendarResolveMonth.) 2521 2522 return true; 2523 } 2524 2525 // Step 2. 2526 return NonISOResolveFields(cx, calendar, fields, type); 2527 } 2528 2529 /** 2530 * CalendarISOToDate ( calendar, isoDate ) 2531 * NonISOCalendarISOToDate ( calendar, isoDate ) 2532 * CalendarDateEra ( calendar, date ) 2533 * 2534 * Return the Calendar Date Record's [[Era]] field. 2535 */ 2536 bool js::temporal::CalendarEra(JSContext* cx, Handle<CalendarValue> calendar, 2537 const ISODate& date, 2538 MutableHandle<Value> result) { 2539 auto calendarId = calendar.identifier(); 2540 2541 // Step 1. 2542 if (calendarId == CalendarId::ISO8601) { 2543 result.setUndefined(); 2544 return true; 2545 } 2546 2547 // Step 2. 2548 if (!CalendarSupportsEra(calendarId)) { 2549 result.setUndefined(); 2550 return true; 2551 } 2552 2553 auto era = EraCode::Standard; 2554 2555 // Call into ICU4X if the calendar has more than one era. 2556 auto eras = CalendarEras(calendarId); 2557 if (eras.size() > 1) { 2558 auto cal = CreateICU4XCalendar(calendarId); 2559 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2560 if (!dt) { 2561 return false; 2562 } 2563 2564 if (!CalendarDateEra(cx, calendarId, dt.get(), &era)) { 2565 return false; 2566 } 2567 } else { 2568 MOZ_ASSERT(*eras.begin() == EraCode::Standard, 2569 "single era calendars use only the standard era"); 2570 } 2571 2572 auto* str = NewStringCopy<CanGC>(cx, CalendarEraName(calendarId, era)); 2573 if (!str) { 2574 return false; 2575 } 2576 2577 result.setString(str); 2578 return true; 2579 } 2580 2581 /** 2582 * CalendarISOToDate ( calendar, isoDate ) 2583 * NonISOCalendarISOToDate ( calendar, isoDate ) 2584 * CalendarDateEraYear ( calendar, date ) 2585 * 2586 * Return the Calendar Date Record's [[EraYear]] field. 2587 */ 2588 bool js::temporal::CalendarEraYear(JSContext* cx, 2589 Handle<CalendarValue> calendar, 2590 const ISODate& date, 2591 MutableHandle<Value> result) { 2592 auto calendarId = calendar.identifier(); 2593 2594 // Step 1. 2595 if (calendarId == CalendarId::ISO8601) { 2596 result.setUndefined(); 2597 return true; 2598 } 2599 2600 // Step 2. 2601 if (!CalendarSupportsEra(calendarId)) { 2602 result.setUndefined(); 2603 return true; 2604 } 2605 2606 auto eras = CalendarEras(calendarId); 2607 if (eras.size() == 1) { 2608 // Return the calendar year for calendars with a single era. 2609 return CalendarYear(cx, calendar, date, result); 2610 } 2611 MOZ_ASSERT(eras.size() > 1); 2612 2613 auto cal = CreateICU4XCalendar(calendarId); 2614 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2615 if (!dt) { 2616 return false; 2617 } 2618 2619 int32_t year = icu4x::capi::icu4x_Date_era_year_or_related_iso_mv1(dt.get()); 2620 result.setInt32(year); 2621 return true; 2622 } 2623 2624 /** 2625 * CalendarISOToDate ( calendar, isoDate ) 2626 * NonISOCalendarISOToDate ( calendar, isoDate ) 2627 * CalendarDateArithmeticYear ( calendar, date ) 2628 * 2629 * Return the Calendar Date Record's [[Year]] field. 2630 */ 2631 bool js::temporal::CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, 2632 const ISODate& date, 2633 MutableHandle<Value> result) { 2634 auto calendarId = calendar.identifier(); 2635 2636 // Step 1. 2637 if (calendarId == CalendarId::ISO8601) { 2638 result.setInt32(date.year); 2639 return true; 2640 } 2641 2642 // Step 2. 2643 auto cal = CreateICU4XCalendar(calendarId); 2644 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2645 if (!dt) { 2646 return false; 2647 } 2648 2649 int32_t year = CalendarDateYear(calendarId, dt.get()); 2650 result.setInt32(year); 2651 return true; 2652 } 2653 2654 /** 2655 * CalendarISOToDate ( calendar, isoDate ) 2656 * NonISOCalendarISOToDate ( calendar, isoDate ) 2657 * 2658 * Return the Calendar Date Record's [[Month]] field. 2659 */ 2660 bool js::temporal::CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, 2661 const ISODate& date, 2662 MutableHandle<Value> result) { 2663 auto calendarId = calendar.identifier(); 2664 2665 // Step 1. 2666 if (calendarId == CalendarId::ISO8601) { 2667 result.setInt32(date.month); 2668 return true; 2669 } 2670 2671 // Step 2. 2672 auto cal = CreateICU4XCalendar(calendarId); 2673 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2674 if (!dt) { 2675 return false; 2676 } 2677 2678 int32_t month = OrdinalMonth(dt.get()); 2679 result.setInt32(month); 2680 return true; 2681 } 2682 2683 /** 2684 * CalendarISOToDate ( calendar, isoDate ) 2685 * NonISOCalendarISOToDate ( calendar, isoDate ) 2686 * 2687 * Return the Calendar Date Record's [[MonthCode]] field. 2688 */ 2689 bool js::temporal::CalendarMonthCode(JSContext* cx, 2690 Handle<CalendarValue> calendar, 2691 const ISODate& date, 2692 MutableHandle<Value> result) { 2693 auto calendarId = calendar.identifier(); 2694 2695 // Step 1. 2696 if (calendarId == CalendarId::ISO8601) { 2697 // Steps 1.a-b. 2698 auto monthCode = MonthCode{date.month}; 2699 JSString* str = NewStringCopy<CanGC>(cx, std::string_view{monthCode}); 2700 if (!str) { 2701 return false; 2702 } 2703 2704 result.setString(str); 2705 return true; 2706 } 2707 2708 // Step 2. 2709 auto cal = CreateICU4XCalendar(calendarId); 2710 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2711 if (!dt) { 2712 return false; 2713 } 2714 2715 auto monthCode = CalendarDateMonthCode(calendarId, dt.get()); 2716 auto* str = NewStringCopy<CanGC>(cx, std::string_view{monthCode}); 2717 if (!str) { 2718 return false; 2719 } 2720 2721 result.setString(str); 2722 return true; 2723 } 2724 2725 /** 2726 * CalendarISOToDate ( calendar, isoDate ) 2727 * NonISOCalendarISOToDate ( calendar, isoDate ) 2728 * 2729 * Return the Calendar Date Record's [[Day]] field. 2730 */ 2731 bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarValue> calendar, 2732 const ISODate& date, 2733 MutableHandle<Value> result) { 2734 auto calendarId = calendar.identifier(); 2735 2736 // Step 1. 2737 if (calendarId == CalendarId::ISO8601) { 2738 result.setInt32(date.day); 2739 return true; 2740 } 2741 2742 // Step 2. 2743 auto cal = CreateICU4XCalendar(calendarId); 2744 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2745 if (!dt) { 2746 return false; 2747 } 2748 2749 int32_t day = DayOfMonth(dt.get()); 2750 result.setInt32(day); 2751 return true; 2752 } 2753 2754 /** 2755 * CalendarISOToDate ( calendar, isoDate ) 2756 * NonISOCalendarISOToDate ( calendar, isoDate ) 2757 * 2758 * Return the Calendar Date Record's [[DayOfWeek]] field. 2759 */ 2760 bool js::temporal::CalendarDayOfWeek(JSContext* cx, 2761 Handle<CalendarValue> calendar, 2762 const ISODate& date, 2763 MutableHandle<Value> result) { 2764 auto calendarId = calendar.identifier(); 2765 2766 // Step 1. 2767 if (calendarId == CalendarId::ISO8601) { 2768 result.setInt32(ISODayOfWeek(date)); 2769 return true; 2770 } 2771 2772 // Step 2. 2773 auto cal = CreateICU4XCalendar(calendarId); 2774 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2775 if (!dt) { 2776 return false; 2777 } 2778 2779 // Week day codes are correctly ordered. 2780 static_assert(icu4x::capi::Weekday_Monday == 1); 2781 static_assert(icu4x::capi::Weekday_Tuesday == 2); 2782 static_assert(icu4x::capi::Weekday_Wednesday == 3); 2783 static_assert(icu4x::capi::Weekday_Thursday == 4); 2784 static_assert(icu4x::capi::Weekday_Friday == 5); 2785 static_assert(icu4x::capi::Weekday_Saturday == 6); 2786 static_assert(icu4x::capi::Weekday_Sunday == 7); 2787 2788 icu4x::capi::Weekday day = icu4x::capi::icu4x_Date_day_of_week_mv1(dt.get()); 2789 result.setInt32(static_cast<int32_t>(day)); 2790 return true; 2791 } 2792 2793 /** 2794 * CalendarISOToDate ( calendar, isoDate ) 2795 * NonISOCalendarISOToDate ( calendar, isoDate ) 2796 * 2797 * Return the Calendar Date Record's [[DayOfYear]] field. 2798 */ 2799 bool js::temporal::CalendarDayOfYear(JSContext* cx, 2800 Handle<CalendarValue> calendar, 2801 const ISODate& date, 2802 MutableHandle<Value> result) { 2803 auto calendarId = calendar.identifier(); 2804 2805 // Step 1. 2806 if (calendarId == CalendarId::ISO8601) { 2807 result.setInt32(ISODayOfYear(date)); 2808 return true; 2809 } 2810 2811 // Step 2. 2812 auto cal = CreateICU4XCalendar(calendarId); 2813 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2814 if (!dt) { 2815 return false; 2816 } 2817 2818 int32_t day = DayOfYear(dt.get()); 2819 result.setInt32(day); 2820 return true; 2821 } 2822 2823 /** 2824 * CalendarISOToDate ( calendar, isoDate ) 2825 * NonISOCalendarISOToDate ( calendar, isoDate ) 2826 * 2827 * Return the Calendar Date Record's [[WeekOfYear]].[[Week]] field. 2828 */ 2829 bool js::temporal::CalendarWeekOfYear(JSContext* cx, 2830 Handle<CalendarValue> calendar, 2831 const ISODate& date, 2832 MutableHandle<Value> result) { 2833 auto calendarId = calendar.identifier(); 2834 2835 // Step 1. 2836 if (calendarId == CalendarId::ISO8601) { 2837 result.setInt32(ISOWeekOfYear(date).week); 2838 return true; 2839 } 2840 2841 // Step 2. 2842 // 2843 // Non-Gregorian calendars don't get week-of-year support for now. 2844 // 2845 // https://github.com/tc39/proposal-temporal/issues/3096 2846 // https://github.com/tc39/proposal-intl-era-monthcode/issues/15 2847 result.setUndefined(); 2848 return true; 2849 } 2850 2851 /** 2852 * CalendarISOToDate ( calendar, isoDate ) 2853 * NonISOCalendarISOToDate ( calendar, isoDate ) 2854 * 2855 * Return the Calendar Date Record's [[WeekOfYear]].[[Year]] field. 2856 */ 2857 bool js::temporal::CalendarYearOfWeek(JSContext* cx, 2858 Handle<CalendarValue> calendar, 2859 const ISODate& date, 2860 MutableHandle<Value> result) { 2861 auto calendarId = calendar.identifier(); 2862 2863 // Step 1. 2864 if (calendarId == CalendarId::ISO8601) { 2865 result.setInt32(ISOWeekOfYear(date).year); 2866 return true; 2867 } 2868 2869 // Step 2. 2870 // 2871 // Non-ISO8601 calendars don't get year-of-week support for now. 2872 // 2873 // https://github.com/tc39/proposal-temporal/issues/3096 2874 // https://github.com/tc39/proposal-intl-era-monthcode/issues/15 2875 result.setUndefined(); 2876 return true; 2877 } 2878 2879 /** 2880 * CalendarISOToDate ( calendar, isoDate ) 2881 * NonISOCalendarISOToDate ( calendar, isoDate ) 2882 * 2883 * Return the Calendar Date Record's [[DaysInWeek]] field. 2884 */ 2885 bool js::temporal::CalendarDaysInWeek(JSContext* cx, 2886 Handle<CalendarValue> calendar, 2887 const ISODate& date, 2888 MutableHandle<Value> result) { 2889 // All supported ICU4X calendars use a 7-day week and so does the ISO 8601 2890 // calendar. 2891 // 2892 // This function isn't supported through the ICU4X FFI, so we have to 2893 // hardcode the result. 2894 2895 // Step 1-2. 2896 result.setInt32(7); 2897 return true; 2898 } 2899 2900 /** 2901 * CalendarISOToDate ( calendar, isoDate ) 2902 * NonISOCalendarISOToDate ( calendar, isoDate ) 2903 * 2904 * Return the Calendar Date Record's [[DaysInMonth]] field. 2905 */ 2906 bool js::temporal::CalendarDaysInMonth(JSContext* cx, 2907 Handle<CalendarValue> calendar, 2908 const ISODate& date, 2909 MutableHandle<Value> result) { 2910 auto calendarId = calendar.identifier(); 2911 2912 // Step 1. 2913 if (calendarId == CalendarId::ISO8601) { 2914 result.setInt32(::ISODaysInMonth(date.year, date.month)); 2915 return true; 2916 } 2917 2918 // Step 2. 2919 auto cal = CreateICU4XCalendar(calendarId); 2920 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2921 if (!dt) { 2922 return false; 2923 } 2924 2925 int32_t days = DaysInMonth(dt.get()); 2926 result.setInt32(days); 2927 return true; 2928 } 2929 2930 /** 2931 * CalendarISOToDate ( calendar, isoDate ) 2932 * NonISOCalendarISOToDate ( calendar, isoDate ) 2933 * 2934 * Return the Calendar Date Record's [[DaysInYear]] field. 2935 */ 2936 bool js::temporal::CalendarDaysInYear(JSContext* cx, 2937 Handle<CalendarValue> calendar, 2938 const ISODate& date, 2939 MutableHandle<Value> result) { 2940 auto calendarId = calendar.identifier(); 2941 2942 // Step 1. 2943 if (calendarId == CalendarId::ISO8601) { 2944 result.setInt32(ISODaysInYear(date.year)); 2945 return true; 2946 } 2947 2948 // Step 2. 2949 auto cal = CreateICU4XCalendar(calendarId); 2950 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2951 if (!dt) { 2952 return false; 2953 } 2954 2955 int32_t days = DaysInYear(dt.get()); 2956 result.setInt32(days); 2957 return true; 2958 } 2959 2960 /** 2961 * CalendarISOToDate ( calendar, isoDate ) 2962 * NonISOCalendarISOToDate ( calendar, isoDate ) 2963 * 2964 * Return the Calendar Date Record's [[MonthsInYear]] field. 2965 */ 2966 bool js::temporal::CalendarMonthsInYear(JSContext* cx, 2967 Handle<CalendarValue> calendar, 2968 const ISODate& date, 2969 MutableHandle<Value> result) { 2970 auto calendarId = calendar.identifier(); 2971 2972 // Step 1. 2973 if (calendarId == CalendarId::ISO8601) { 2974 result.setInt32(12); 2975 return true; 2976 } 2977 2978 // Step 2 2979 auto cal = CreateICU4XCalendar(calendarId); 2980 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 2981 if (!dt) { 2982 return false; 2983 } 2984 2985 int32_t months = MonthsInYear(dt.get()); 2986 result.setInt32(months); 2987 return true; 2988 } 2989 2990 /** 2991 * CalendarISOToDate ( calendar, isoDate ) 2992 * NonISOCalendarISOToDate ( calendar, isoDate ) 2993 * 2994 * Return the Calendar Date Record's [[InLeapYear]] field. 2995 */ 2996 bool js::temporal::CalendarInLeapYear(JSContext* cx, 2997 Handle<CalendarValue> calendar, 2998 const ISODate& date, 2999 MutableHandle<Value> result) { 3000 auto calendarId = calendar.identifier(); 3001 3002 // Step 1. 3003 if (calendarId == CalendarId::ISO8601) { 3004 result.setBoolean(IsISOLeapYear(date.year)); 3005 return true; 3006 } 3007 3008 // Step 2. 3009 3010 // FIXME: Not supported in ICU4X. 3011 // 3012 // https://github.com/unicode-org/icu4x/issues/5654 3013 3014 auto cal = CreateICU4XCalendar(calendarId); 3015 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 3016 if (!dt) { 3017 return false; 3018 } 3019 3020 bool inLeapYear = false; 3021 switch (calendarId) { 3022 case CalendarId::ISO8601: 3023 case CalendarId::Buddhist: 3024 case CalendarId::Gregorian: 3025 case CalendarId::Japanese: 3026 case CalendarId::Coptic: 3027 case CalendarId::Ethiopian: 3028 case CalendarId::EthiopianAmeteAlem: 3029 case CalendarId::Indian: 3030 case CalendarId::Persian: 3031 case CalendarId::ROC: { 3032 MOZ_ASSERT(!CalendarHasLeapMonths(calendarId)); 3033 3034 // Solar calendars have either 365 or 366 days per year. 3035 int32_t days = DaysInYear(dt.get()); 3036 MOZ_ASSERT(days == 365 || days == 366); 3037 3038 // Leap years have 366 days. 3039 inLeapYear = days == 366; 3040 break; 3041 } 3042 3043 case CalendarId::IslamicCivil: 3044 case CalendarId::IslamicTabular: 3045 case CalendarId::IslamicUmmAlQura: { 3046 MOZ_ASSERT(!CalendarHasLeapMonths(calendarId)); 3047 3048 // Lunar Islamic calendars have either 354 or 355 days per year. 3049 // 3050 // Allow 353 days to workaround 3051 // <https://github.com/unicode-org/icu4x/issues/4930>. 3052 int32_t days = DaysInYear(dt.get()); 3053 MOZ_ASSERT(days == 353 || days == 354 || days == 355); 3054 3055 // Leap years have 355 days. 3056 inLeapYear = days == 355; 3057 break; 3058 } 3059 3060 case CalendarId::Chinese: 3061 case CalendarId::Dangi: 3062 case CalendarId::Hebrew: { 3063 MOZ_ASSERT(CalendarHasLeapMonths(calendarId)); 3064 3065 // Calendars with separate leap months have either 12 or 13 months per 3066 // year. 3067 int32_t months = MonthsInYear(dt.get()); 3068 MOZ_ASSERT(months == 12 || months == 13); 3069 3070 // Leap years have 13 months. 3071 inLeapYear = months == 13; 3072 break; 3073 } 3074 } 3075 3076 result.setBoolean(inLeapYear); 3077 return true; 3078 } 3079 3080 enum class DateFieldType { Date, YearMonth, MonthDay }; 3081 3082 /** 3083 * ISODateToFields ( calendar, isoDate, type ) 3084 */ 3085 static bool ISODateToFields(JSContext* cx, Handle<CalendarValue> calendar, 3086 const ISODate& date, DateFieldType type, 3087 MutableHandle<CalendarFields> result) { 3088 auto calendarId = calendar.identifier(); 3089 3090 // Step 1. 3091 result.set(CalendarFields{}); 3092 3093 // Steps 2-6. (Optimization for the ISO 8601 calendar.) 3094 if (calendarId == CalendarId::ISO8601) { 3095 // Step 2. (Not applicable in our implementation.) 3096 3097 // Step 3. 3098 result.setMonthCode(MonthCode{date.month}); 3099 3100 // Step 4. 3101 if (type == DateFieldType::MonthDay || type == DateFieldType::Date) { 3102 result.setDay(date.day); 3103 } 3104 3105 // Step 5. 3106 if (type == DateFieldType::YearMonth || type == DateFieldType::Date) { 3107 result.setYear(date.year); 3108 } 3109 3110 // Step 6. 3111 return true; 3112 } 3113 3114 // Step 2. 3115 auto cal = CreateICU4XCalendar(calendarId); 3116 auto dt = CreateICU4XDate(cx, date, calendarId, cal.get()); 3117 if (!dt) { 3118 return false; 3119 } 3120 3121 // Step 3. 3122 auto monthCode = CalendarDateMonthCode(calendarId, dt.get()); 3123 result.setMonthCode(monthCode); 3124 3125 // Step 4. 3126 if (type == DateFieldType::MonthDay || type == DateFieldType::Date) { 3127 int32_t day = DayOfMonth(dt.get()); 3128 result.setDay(day); 3129 } 3130 3131 // Step 5. 3132 if (type == DateFieldType::YearMonth || type == DateFieldType::Date) { 3133 int32_t year = CalendarDateYear(calendarId, dt.get()); 3134 result.setYear(year); 3135 } 3136 3137 // Step 6. 3138 return true; 3139 } 3140 3141 /** 3142 * ISODateToFields ( calendar, isoDate, type ) 3143 */ 3144 bool js::temporal::ISODateToFields(JSContext* cx, Handle<PlainDate> date, 3145 MutableHandle<CalendarFields> result) { 3146 return ISODateToFields(cx, date.calendar(), date, DateFieldType::Date, 3147 result); 3148 } 3149 3150 /** 3151 * ISODateToFields ( calendar, isoDate, type ) 3152 */ 3153 bool js::temporal::ISODateToFields(JSContext* cx, 3154 Handle<PlainDateTime> dateTime, 3155 MutableHandle<CalendarFields> result) { 3156 return ISODateToFields(cx, dateTime.calendar(), dateTime.date(), 3157 DateFieldType::Date, result); 3158 } 3159 3160 /** 3161 * ISODateToFields ( calendar, isoDate, type ) 3162 */ 3163 bool js::temporal::ISODateToFields(JSContext* cx, 3164 Handle<PlainMonthDay> monthDay, 3165 MutableHandle<CalendarFields> result) { 3166 return ISODateToFields(cx, monthDay.calendar(), monthDay.date(), 3167 DateFieldType::MonthDay, result); 3168 } 3169 3170 /** 3171 * ISODateToFields ( calendar, isoDate, type ) 3172 */ 3173 bool js::temporal::ISODateToFields(JSContext* cx, 3174 Handle<PlainYearMonth> yearMonth, 3175 MutableHandle<CalendarFields> result) { 3176 return ISODateToFields(cx, yearMonth.calendar(), yearMonth.date(), 3177 DateFieldType::YearMonth, result); 3178 } 3179 3180 /** 3181 * CalendarDateFromFields ( calendar, fields, overflow ) 3182 */ 3183 bool js::temporal::CalendarDateFromFields(JSContext* cx, 3184 Handle<CalendarValue> calendar, 3185 Handle<CalendarFields> fields, 3186 TemporalOverflow overflow, 3187 MutableHandle<PlainDate> result) { 3188 auto calendarId = calendar.identifier(); 3189 3190 // Step 1. 3191 if (!CalendarResolveFields(cx, calendarId, fields, FieldType::Date)) { 3192 return false; 3193 } 3194 3195 // Step 2. 3196 ISODate date; 3197 if (!CalendarDateToISO(cx, calendarId, fields, overflow, &date)) { 3198 return false; 3199 } 3200 3201 // Steps 3-4. 3202 return CreateTemporalDate(cx, date, calendar, result); 3203 } 3204 3205 /** 3206 * CalendarYearMonthFromFields ( calendar, fields, overflow ) 3207 */ 3208 bool js::temporal::CalendarYearMonthFromFields( 3209 JSContext* cx, Handle<CalendarValue> calendar, 3210 Handle<CalendarFields> fields, TemporalOverflow overflow, 3211 MutableHandle<PlainYearMonth> result) { 3212 auto calendarId = calendar.identifier(); 3213 3214 // Step 2. 3215 if (!CalendarResolveFields(cx, calendarId, fields, FieldType::YearMonth)) { 3216 return false; 3217 } 3218 3219 // Step 1. (Reordered) 3220 Rooted<CalendarFields> resolvedFields(cx, CalendarFields{fields}); 3221 resolvedFields.setDay(1); 3222 3223 // Step 3. 3224 ISODate date; 3225 if (!CalendarDateToISO(cx, calendarId, resolvedFields, overflow, &date)) { 3226 return false; 3227 } 3228 3229 // Steps 4-5. 3230 return CreateTemporalYearMonth(cx, date, calendar, result); 3231 } 3232 3233 /** 3234 * CalendarMonthDayFromFields ( calendar, fields, overflow ) 3235 */ 3236 bool js::temporal::CalendarMonthDayFromFields( 3237 JSContext* cx, Handle<CalendarValue> calendar, 3238 Handle<CalendarFields> fields, TemporalOverflow overflow, 3239 MutableHandle<PlainMonthDay> result) { 3240 auto calendarId = calendar.identifier(); 3241 3242 // Step 1. 3243 if (!CalendarResolveFields(cx, calendarId, fields, FieldType::MonthDay)) { 3244 return false; 3245 } 3246 3247 // Step 2. 3248 ISODate date; 3249 if (!CalendarMonthDayToISOReferenceDate(cx, calendarId, fields, overflow, 3250 &date)) { 3251 return false; 3252 } 3253 3254 // Step 3-4. 3255 return CreateTemporalMonthDay(cx, date, calendar, result); 3256 } 3257 3258 /** 3259 * Mathematical Operations, "modulo" notation. 3260 */ 3261 static int32_t NonNegativeModulo(int64_t x, int32_t y) { 3262 MOZ_ASSERT(y > 0); 3263 3264 int32_t result = mozilla::AssertedCast<int32_t>(x % y); 3265 return (result < 0) ? (result + y) : result; 3266 } 3267 3268 /** 3269 * RegulateISODate ( year, month, day, overflow ) 3270 * 3271 * With |overflow = "constrain"|. 3272 */ 3273 static ISODate ConstrainISODate(const ISODate& date) { 3274 const auto& [year, month, day] = date; 3275 3276 // Step 1.a. 3277 int32_t m = std::clamp(month, 1, 12); 3278 3279 // Step 1.b. 3280 int32_t daysInMonth = ::ISODaysInMonth(year, m); 3281 3282 // Step 1.c. 3283 int32_t d = std::clamp(day, 1, daysInMonth); 3284 3285 // Step 3. 3286 return {year, m, d}; 3287 } 3288 3289 /** 3290 * RegulateISODate ( year, month, day, overflow ) 3291 */ 3292 static bool RegulateISODate(JSContext* cx, const ISODate& date, 3293 TemporalOverflow overflow, ISODate* result) { 3294 // Step 1. 3295 if (overflow == TemporalOverflow::Constrain) { 3296 // Steps 1.a-c and 3. 3297 *result = ConstrainISODate(date); 3298 return true; 3299 } 3300 3301 // Step 2.a. 3302 MOZ_ASSERT(overflow == TemporalOverflow::Reject); 3303 3304 // Step 2.b. 3305 if (!ThrowIfInvalidISODate(cx, date)) { 3306 return false; 3307 } 3308 3309 // Step 3. (Inlined call to CreateISODateRecord.) 3310 *result = date; 3311 return true; 3312 } 3313 3314 struct BalancedYearMonth final { 3315 int64_t year = 0; 3316 int32_t month = 0; 3317 }; 3318 3319 /** 3320 * BalanceISOYearMonth ( year, month ) 3321 */ 3322 static BalancedYearMonth BalanceISOYearMonth(int64_t year, int64_t month) { 3323 MOZ_ASSERT(std::abs(year) < (int64_t(1) << 33), 3324 "year is the addition of plain-date year with duration years"); 3325 MOZ_ASSERT(std::abs(month) < (int64_t(1) << 33), 3326 "month is the addition of plain-date month with duration months"); 3327 3328 // Step 1. (Not applicable in our implementation.) 3329 3330 // Step 2. 3331 int64_t balancedYear = year + temporal::FloorDiv(month - 1, 12); 3332 3333 // Step 3. 3334 int32_t balancedMonth = NonNegativeModulo(month - 1, 12) + 1; 3335 MOZ_ASSERT(1 <= balancedMonth && balancedMonth <= 12); 3336 3337 // Step 4. 3338 return {balancedYear, balancedMonth}; 3339 } 3340 3341 static BalancedYearMonth BalanceYearMonth(int64_t year, int64_t month, 3342 int32_t monthsPerYear) { 3343 MOZ_ASSERT(std::abs(year) < (int64_t(1) << 33), 3344 "year is the addition of plain-date year with duration years"); 3345 MOZ_ASSERT(std::abs(month) < (int64_t(1) << 33), 3346 "month is the addition of plain-date month with duration months"); 3347 3348 int64_t balancedYear = year + temporal::FloorDiv(month - 1, monthsPerYear); 3349 3350 int32_t balancedMonth = NonNegativeModulo(month - 1, monthsPerYear) + 1; 3351 MOZ_ASSERT(1 <= balancedMonth && balancedMonth <= monthsPerYear); 3352 3353 return {balancedYear, balancedMonth}; 3354 } 3355 3356 /** 3357 * CalendarDateAdd ( calendar, isoDate, duration, overflow ) 3358 */ 3359 static bool AddISODate(JSContext* cx, const ISODate& isoDate, 3360 const DateDuration& duration, TemporalOverflow overflow, 3361 ISODate* result) { 3362 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 3363 MOZ_ASSERT(IsValidDuration(duration)); 3364 3365 // Step 1.a. 3366 auto yearMonth = BalanceISOYearMonth(isoDate.year + duration.years, 3367 isoDate.month + duration.months); 3368 MOZ_ASSERT(1 <= yearMonth.month && yearMonth.month <= 12); 3369 3370 auto balancedYear = mozilla::CheckedInt<int32_t>(yearMonth.year); 3371 if (!balancedYear.isValid()) { 3372 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3373 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3374 return false; 3375 } 3376 3377 // Step 1.b. 3378 ISODate regulated; 3379 if (!RegulateISODate(cx, {balancedYear.value(), yearMonth.month, isoDate.day}, 3380 overflow, ®ulated)) { 3381 return false; 3382 } 3383 if (!ISODateWithinLimits(regulated)) { 3384 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3385 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3386 return false; 3387 } 3388 3389 // Step 1.c. 3390 int64_t days = duration.days + duration.weeks * 7; 3391 3392 // Step 1.d. 3393 ISODate balanced; 3394 if (!BalanceISODate(cx, regulated, days, &balanced)) { 3395 return false; 3396 } 3397 MOZ_ASSERT(IsValidISODate(balanced)); 3398 3399 *result = balanced; 3400 return true; 3401 } 3402 3403 struct CalendarDate { 3404 int32_t year = 0; 3405 MonthCode monthCode = {}; 3406 int32_t day = 0; 3407 }; 3408 3409 struct CalendarDateWithOrdinalMonth { 3410 int32_t year = 0; 3411 int32_t month = 0; 3412 int32_t day = 0; 3413 }; 3414 3415 /** 3416 * CompareISODate adjusted for calendar dates. 3417 */ 3418 static int32_t CompareCalendarDate(const CalendarDate& one, 3419 const CalendarDate& two) { 3420 if (one.year != two.year) { 3421 return one.year < two.year ? -1 : 1; 3422 } 3423 if (one.monthCode != two.monthCode) { 3424 return one.monthCode < two.monthCode ? -1 : 1; 3425 } 3426 if (one.day != two.day) { 3427 return one.day < two.day ? -1 : 1; 3428 } 3429 return 0; 3430 } 3431 3432 /** 3433 * CompareISODate adjusted for calendar dates. 3434 */ 3435 static int32_t CompareCalendarDate(const CalendarDateWithOrdinalMonth& one, 3436 const CalendarDateWithOrdinalMonth& two) { 3437 return CompareISODate(ISODate{one.year, one.month, one.day}, 3438 ISODate{two.year, two.month, two.day}); 3439 } 3440 3441 static CalendarDate ToCalendarDate(CalendarId calendarId, 3442 const icu4x::capi::Date* dt) { 3443 int32_t year = CalendarDateYear(calendarId, dt); 3444 auto monthCode = CalendarDateMonthCode(calendarId, dt); 3445 int32_t day = DayOfMonth(dt); 3446 3447 return {year, monthCode, day}; 3448 } 3449 3450 static CalendarDateWithOrdinalMonth ToCalendarDateWithOrdinalMonth( 3451 CalendarId calendarId, const icu4x::capi::Date* dt) { 3452 MOZ_ASSERT(!CalendarHasLeapMonths(calendarId)); 3453 3454 int32_t year = CalendarDateYear(calendarId, dt); 3455 int32_t month = OrdinalMonth(dt); 3456 int32_t day = DayOfMonth(dt); 3457 3458 return {year, month, day}; 3459 } 3460 3461 static bool AddYearMonthDuration( 3462 JSContext* cx, CalendarId calendarId, 3463 const CalendarDateWithOrdinalMonth& calendarDate, 3464 const DateDuration& duration, CalendarDate* result) { 3465 MOZ_ASSERT(!CalendarHasLeapMonths(calendarId)); 3466 MOZ_ASSERT(IsValidDuration(duration)); 3467 3468 auto [year, month, day] = calendarDate; 3469 3470 // Months per year are fixed, so we can directly compute the final number of 3471 // years. 3472 auto yearMonth = 3473 BalanceYearMonth(year + duration.years, month + duration.months, 3474 CalendarMonthsPerYear(calendarId)); 3475 3476 auto balancedYear = mozilla::CheckedInt<int32_t>(yearMonth.year); 3477 if (!balancedYear.isValid()) { 3478 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3479 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3480 return false; 3481 } 3482 3483 *result = {balancedYear.value(), MonthCode{yearMonth.month}, day}; 3484 return true; 3485 } 3486 3487 static bool AddYearMonthDuration(JSContext* cx, CalendarId calendarId, 3488 const icu4x::capi::Calendar* calendar, 3489 const CalendarDate& calendarDate, 3490 const DateDuration& duration, 3491 CalendarDate* result) { 3492 MOZ_ASSERT(CalendarHasLeapMonths(calendarId)); 3493 MOZ_ASSERT(IsValidDuration(duration)); 3494 3495 auto [year, monthCode, day] = calendarDate; 3496 3497 // Add all duration years. 3498 auto durationYear = mozilla::CheckedInt<int32_t>(year) + duration.years; 3499 if (!durationYear.isValid()) { 3500 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3501 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3502 return false; 3503 } 3504 year = durationYear.value(); 3505 3506 // Months per year are variable, so we have construct a new date for each 3507 // year to balance the years and months. 3508 int64_t months = duration.months; 3509 if (months != 0) { 3510 auto eraYear = CalendarEraYear(calendarId, year); 3511 auto firstDayOfMonth = 3512 CreateDateFromCodes(cx, calendarId, calendar, eraYear, monthCode, 1, 3513 TemporalOverflow::Constrain); 3514 if (!firstDayOfMonth) { 3515 return false; 3516 } 3517 3518 if (months > 0) { 3519 while (true) { 3520 // Check if adding |months| is still in the current year. 3521 int32_t month = OrdinalMonth(firstDayOfMonth.get()); 3522 int32_t monthsInYear = MonthsInYear(firstDayOfMonth.get()); 3523 if (month + months <= monthsInYear) { 3524 break; 3525 } 3526 3527 // We've crossed a year boundary. Increase |year| and adjust |months|. 3528 year += 1; 3529 months -= (monthsInYear - month + 1); 3530 3531 // Restart the loop with the first month of the next year. 3532 eraYear = CalendarEraYear(calendarId, year); 3533 firstDayOfMonth = CreateDateFrom(cx, calendarId, calendar, eraYear, 1, 3534 1, TemporalOverflow::Constrain); 3535 if (!firstDayOfMonth) { 3536 return false; 3537 } 3538 } 3539 } else { 3540 int32_t monthsPerYear = CalendarMonthsPerYear(calendarId); 3541 3542 while (true) { 3543 // Check if subtracting |months| is still in the current year. 3544 int32_t month = OrdinalMonth(firstDayOfMonth.get()); 3545 if (month + months >= 1) { 3546 break; 3547 } 3548 3549 // We've crossed a year boundary. Decrease |year| and adjust |months|. 3550 year -= 1; 3551 months += month; 3552 3553 // Restart the loop with the last month of the previous year. 3554 eraYear = CalendarEraYear(calendarId, year); 3555 firstDayOfMonth = 3556 CreateDateFrom(cx, calendarId, calendar, eraYear, monthsPerYear, 1, 3557 TemporalOverflow::Constrain); 3558 if (!firstDayOfMonth) { 3559 return false; 3560 } 3561 } 3562 } 3563 3564 // Compute the actual month to find the correct month code. 3565 int32_t month = OrdinalMonth(firstDayOfMonth.get()) + months; 3566 firstDayOfMonth = CreateDateFrom(cx, calendarId, calendar, eraYear, month, 3567 1, TemporalOverflow::Constrain); 3568 if (!firstDayOfMonth) { 3569 return false; 3570 } 3571 3572 monthCode = CalendarDateMonthCode(calendarId, firstDayOfMonth.get()); 3573 } 3574 3575 *result = {year, monthCode, day}; 3576 return true; 3577 } 3578 3579 static bool AddNonISODate(JSContext* cx, CalendarId calendarId, 3580 const ISODate& isoDate, const DateDuration& duration, 3581 TemporalOverflow overflow, ISODate* result) { 3582 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 3583 MOZ_ASSERT(IsValidDuration(duration)); 3584 3585 auto cal = CreateICU4XCalendar(calendarId); 3586 3587 auto dt = CreateICU4XDate(cx, isoDate, calendarId, cal.get()); 3588 if (!dt) { 3589 return false; 3590 } 3591 3592 CalendarDate calendarDate; 3593 if (!CalendarHasLeapMonths(calendarId)) { 3594 auto date = ToCalendarDateWithOrdinalMonth(calendarId, dt.get()); 3595 if (!AddYearMonthDuration(cx, calendarId, date, duration, &calendarDate)) { 3596 return false; 3597 } 3598 } else { 3599 auto date = ToCalendarDate(calendarId, dt.get()); 3600 if (!AddYearMonthDuration(cx, calendarId, cal.get(), date, duration, 3601 &calendarDate)) { 3602 return false; 3603 } 3604 } 3605 3606 // Regulate according to |overflow|. 3607 auto eraYear = CalendarEraYear(calendarId, calendarDate.year); 3608 auto regulated = 3609 CreateDateFromCodes(cx, calendarId, cal.get(), eraYear, 3610 calendarDate.monthCode, calendarDate.day, overflow); 3611 if (!regulated) { 3612 return false; 3613 } 3614 3615 // Compute the corresponding ISO date. 3616 auto regulatedIso = ToISODate(regulated.get()); 3617 if (!ISODateWithinLimits(regulatedIso)) { 3618 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3619 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3620 return false; 3621 } 3622 3623 // Add duration days and weeks. 3624 int64_t days = duration.days + duration.weeks * 7; 3625 3626 // Adding days isn't calendar-specific, so we can use BalanceISODate. 3627 ISODate balancedIso; 3628 if (!BalanceISODate(cx, regulatedIso, days, &balancedIso)) { 3629 return false; 3630 } 3631 MOZ_ASSERT(IsValidISODate(balancedIso)); 3632 3633 *result = balancedIso; 3634 return true; 3635 } 3636 3637 /** 3638 * NonISODateAdd ( calendar, isoDate, duration, overflow ) 3639 */ 3640 static bool NonISODateAdd(JSContext* cx, CalendarId calendarId, 3641 const ISODate& isoDate, const DateDuration& duration, 3642 TemporalOverflow overflow, ISODate* result) { 3643 // ICU4X doesn't yet provide a public API for CalendarDateAdd. 3644 // 3645 // https://github.com/unicode-org/icu4x/issues/3964 3646 3647 // If neither |years| nor |months| are present, just delegate to the ISO 8601 3648 // calendar version. This works because all supported calendars use a 7-days 3649 // week. 3650 if (duration.years == 0 && duration.months == 0) { 3651 return AddISODate(cx, isoDate, duration, overflow, result); 3652 } 3653 3654 switch (calendarId) { 3655 case CalendarId::ISO8601: 3656 case CalendarId::Buddhist: 3657 case CalendarId::Gregorian: 3658 case CalendarId::Japanese: 3659 case CalendarId::ROC: 3660 // Use the ISO 8601 calendar if the calendar system starts its year at the 3661 // same time as the ISO 8601 calendar and all months exactly match the 3662 // ISO 8601 calendar months. 3663 return AddISODate(cx, isoDate, duration, overflow, result); 3664 3665 case CalendarId::Chinese: 3666 case CalendarId::Coptic: 3667 case CalendarId::Dangi: 3668 case CalendarId::Ethiopian: 3669 case CalendarId::EthiopianAmeteAlem: 3670 case CalendarId::Hebrew: 3671 case CalendarId::Indian: 3672 case CalendarId::IslamicCivil: 3673 case CalendarId::IslamicTabular: 3674 case CalendarId::IslamicUmmAlQura: 3675 case CalendarId::Persian: 3676 return AddNonISODate(cx, calendarId, isoDate, duration, overflow, result); 3677 } 3678 MOZ_CRASH("invalid calendar id"); 3679 } 3680 3681 /** 3682 * CalendarDateAdd ( calendar, isoDate, duration, overflow ) 3683 */ 3684 bool js::temporal::CalendarDateAdd(JSContext* cx, 3685 Handle<CalendarValue> calendar, 3686 const ISODate& isoDate, 3687 const DateDuration& duration, 3688 TemporalOverflow overflow, ISODate* result) { 3689 MOZ_ASSERT(ISODateWithinLimits(isoDate)); 3690 MOZ_ASSERT(IsValidDuration(duration)); 3691 3692 auto calendarId = calendar.identifier(); 3693 3694 // Steps 1-2. 3695 if (calendarId == CalendarId::ISO8601) { 3696 if (!AddISODate(cx, isoDate, duration, overflow, result)) { 3697 return false; 3698 } 3699 } else { 3700 if (!NonISODateAdd(cx, calendarId, isoDate, duration, overflow, result)) { 3701 return false; 3702 } 3703 } 3704 3705 // Step 3. 3706 if (!ISODateWithinLimits(*result)) { 3707 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 3708 JSMSG_TEMPORAL_PLAIN_DATE_INVALID); 3709 return false; 3710 } 3711 3712 // Step 4. 3713 return true; 3714 } 3715 3716 /** 3717 * CalendarDateUntil ( calendar, one, two, largestUnit ) 3718 */ 3719 static DateDuration DifferenceISODate(const ISODate& one, const ISODate& two, 3720 TemporalUnit largestUnit) { 3721 MOZ_ASSERT(IsValidISODate(one)); 3722 MOZ_ASSERT(IsValidISODate(two)); 3723 3724 // Both inputs are also within the date limits. 3725 MOZ_ASSERT(ISODateWithinLimits(one)); 3726 MOZ_ASSERT(ISODateWithinLimits(two)); 3727 3728 MOZ_ASSERT(TemporalUnit::Year <= largestUnit && 3729 largestUnit <= TemporalUnit::Day); 3730 3731 // Step 1.a. 3732 int32_t sign = -CompareISODate(one, two); 3733 3734 // Step 1.b. 3735 if (sign == 0) { 3736 return {}; 3737 } 3738 3739 // Step 1.c. 3740 int32_t years = 0; 3741 3742 // Step 1.e. (Reordered) 3743 int32_t months = 0; 3744 3745 // Steps 1.d and 1.f. 3746 if (largestUnit == TemporalUnit::Year || largestUnit == TemporalUnit::Month) { 3747 years = two.year - one.year; 3748 months = two.month - one.month; 3749 3750 auto intermediate = ISODate{one.year + years, one.month, one.day}; 3751 if (CompareISODate(intermediate, two) * sign > 0) { 3752 years -= sign; 3753 months += 12 * sign; 3754 } 3755 3756 intermediate = ISODate{one.year + years, one.month + months, one.day}; 3757 if (intermediate.month > 12) { 3758 intermediate.month -= 12; 3759 intermediate.year += 1; 3760 } else if (intermediate.month < 1) { 3761 intermediate.month += 12; 3762 intermediate.year -= 1; 3763 } 3764 if (CompareISODate(intermediate, two) * sign > 0) { 3765 months -= sign; 3766 } 3767 3768 if (largestUnit == TemporalUnit::Month) { 3769 months += years * 12; 3770 years = 0; 3771 } 3772 } 3773 3774 // Balance intermediate result per ISODateSurpasses. 3775 auto intermediate = BalanceISOYearMonth(one.year + years, one.month + months); 3776 auto constrained = ConstrainISODate( 3777 ISODate{int32_t(intermediate.year), intermediate.month, one.day}); 3778 3779 // Step 1.g. 3780 int64_t weeks = 0; 3781 3782 // Steps 1.i-k. 3783 int64_t days = MakeDay(two) - MakeDay(constrained); 3784 3785 // Step 1.h. (Weeks computed from days.) 3786 if (largestUnit == TemporalUnit::Week) { 3787 weeks = days / 7; 3788 days %= 7; 3789 } 3790 3791 // Step 1.l. 3792 auto result = DateDuration{ 3793 int64_t(years), 3794 int64_t(months), 3795 int64_t(weeks), 3796 int64_t(days), 3797 }; 3798 MOZ_ASSERT(IsValidDuration(result)); 3799 return result; 3800 } 3801 3802 static bool DifferenceNonISODate(JSContext* cx, CalendarId calendarId, 3803 const ISODate& one, const ISODate& two, 3804 TemporalUnit largestUnit, 3805 DateDuration* result) { 3806 // Both inputs are also within the date limits. 3807 MOZ_ASSERT(ISODateWithinLimits(one)); 3808 MOZ_ASSERT(ISODateWithinLimits(two)); 3809 3810 MOZ_ASSERT(TemporalUnit::Year <= largestUnit && 3811 largestUnit <= TemporalUnit::Month); 3812 3813 if (one == two) { 3814 *result = {}; 3815 return true; 3816 } 3817 3818 auto cal = CreateICU4XCalendar(calendarId); 3819 3820 auto dtOne = CreateICU4XDate(cx, one, calendarId, cal.get()); 3821 if (!dtOne) { 3822 return false; 3823 } 3824 3825 auto dtTwo = CreateICU4XDate(cx, two, calendarId, cal.get()); 3826 if (!dtTwo) { 3827 return false; 3828 } 3829 3830 int32_t years = 0; 3831 int32_t months = 0; 3832 3833 ISODate constrainedIso; 3834 if (!CalendarHasLeapMonths(calendarId)) { 3835 // If the months per year are fixed, we can use a modified DifferenceISODate 3836 // implementation to compute the date duration. 3837 int32_t monthsPerYear = CalendarMonthsPerYear(calendarId); 3838 3839 auto oneDate = ToCalendarDateWithOrdinalMonth(calendarId, dtOne.get()); 3840 auto twoDate = ToCalendarDateWithOrdinalMonth(calendarId, dtTwo.get()); 3841 3842 int32_t sign = -CompareCalendarDate(oneDate, twoDate); 3843 MOZ_ASSERT(sign != 0); 3844 3845 years = twoDate.year - oneDate.year; 3846 months = twoDate.month - oneDate.month; 3847 3848 // If |oneDate + years| surpasses |twoDate|, reduce |years| by one and add 3849 // |monthsPerYear| to |months|. The next step will balance the intermediate 3850 // result. 3851 auto intermediate = CalendarDateWithOrdinalMonth{ 3852 oneDate.year + years, oneDate.month, oneDate.day}; 3853 if (CompareCalendarDate(intermediate, twoDate) * sign > 0) { 3854 years -= sign; 3855 months += monthsPerYear * sign; 3856 } 3857 3858 // Add both |years| and |months| and then balance the intermediate result to 3859 // ensure its month is within the valid bounds. 3860 intermediate = CalendarDateWithOrdinalMonth{ 3861 oneDate.year + years, oneDate.month + months, oneDate.day}; 3862 if (intermediate.month > monthsPerYear) { 3863 intermediate.month -= monthsPerYear; 3864 intermediate.year += 1; 3865 } else if (intermediate.month < 1) { 3866 intermediate.month += monthsPerYear; 3867 intermediate.year -= 1; 3868 } 3869 3870 // If |intermediate| surpasses |twoDate|, reduce |month| by one. 3871 if (CompareCalendarDate(intermediate, twoDate) * sign > 0) { 3872 months -= sign; 3873 } 3874 3875 // Convert years to months if necessary. 3876 if (largestUnit == TemporalUnit::Month) { 3877 months += years * monthsPerYear; 3878 years = 0; 3879 } 3880 3881 // Constrain to a proper date. 3882 auto balanced = BalanceYearMonth(oneDate.year + years, 3883 oneDate.month + months, monthsPerYear); 3884 3885 auto eraYear = CalendarEraYear(calendarId, balanced.year); 3886 auto constrained = 3887 CreateDateFrom(cx, calendarId, cal.get(), eraYear, balanced.month, 3888 oneDate.day, TemporalOverflow::Constrain); 3889 if (!constrained) { 3890 return false; 3891 } 3892 constrainedIso = ToISODate(constrained.get()); 3893 3894 MOZ_ASSERT(CompareISODate(constrainedIso, two) * sign <= 0, 3895 "constrained doesn't surpass two"); 3896 } else { 3897 auto oneDate = ToCalendarDate(calendarId, dtOne.get()); 3898 auto twoDate = ToCalendarDate(calendarId, dtTwo.get()); 3899 3900 int32_t sign = -CompareCalendarDate(oneDate, twoDate); 3901 MOZ_ASSERT(sign != 0); 3902 3903 years = twoDate.year - oneDate.year; 3904 3905 // If |oneDate + years| surpasses |twoDate|, reduce |years| by one and add 3906 // |monthsPerYear| to |months|. The next step will balance the intermediate 3907 // result. 3908 auto eraYear = CalendarEraYear(calendarId, oneDate.year + years); 3909 auto constrained = CreateDateFromCodes(cx, calendarId, cal.get(), eraYear, 3910 oneDate.monthCode, oneDate.day, 3911 TemporalOverflow::Constrain); 3912 if (!constrained) { 3913 return false; 3914 } 3915 3916 auto constrainedDate = ToCalendarDate(calendarId, constrained.get()); 3917 if (CompareCalendarDate(constrainedDate, twoDate) * sign > 0) { 3918 years -= sign; 3919 } 3920 3921 // Add as many months as possible without surpassing |twoDate|. 3922 while (true) { 3923 CalendarDate intermediateDate; 3924 if (!AddYearMonthDuration(cx, calendarId, cal.get(), oneDate, 3925 {years, months + sign}, &intermediateDate)) { 3926 return false; 3927 } 3928 if (CompareCalendarDate(intermediateDate, twoDate) * sign > 0) { 3929 break; 3930 } 3931 months += sign; 3932 constrainedDate = intermediateDate; 3933 } 3934 MOZ_ASSERT(std::abs(months) < CalendarMonthsPerYear(calendarId)); 3935 3936 // Convert years to months if necessary. 3937 if (largestUnit == TemporalUnit::Month && years != 0) { 3938 auto monthsUntilEndOfYear = [](const icu4x::capi::Date* date) { 3939 int32_t month = OrdinalMonth(date); 3940 int32_t monthsInYear = MonthsInYear(date); 3941 MOZ_ASSERT(1 <= month && month <= monthsInYear); 3942 3943 return monthsInYear - month + 1; 3944 }; 3945 3946 auto monthsSinceStartOfYear = [](const icu4x::capi::Date* date) { 3947 return OrdinalMonth(date) - 1; 3948 }; 3949 3950 // Add months until end of year resp. since start of year. 3951 if (sign > 0) { 3952 months += monthsUntilEndOfYear(dtOne.get()); 3953 } else { 3954 months -= monthsSinceStartOfYear(dtOne.get()); 3955 } 3956 3957 // Months in full year. 3958 for (int32_t y = sign; y != years; y += sign) { 3959 auto eraYear = CalendarEraYear(calendarId, oneDate.year + y); 3960 auto dt = 3961 CreateDateFromCodes(cx, calendarId, cal.get(), eraYear, 3962 MonthCode{1}, 1, TemporalOverflow::Constrain); 3963 if (!dt) { 3964 return false; 3965 } 3966 months += MonthsInYear(dt.get()) * sign; 3967 } 3968 3969 // Add months since start of year resp. until end of year. 3970 auto eraYear = CalendarEraYear(calendarId, oneDate.year + years); 3971 auto dt = CreateDateFromCodes(cx, calendarId, cal.get(), eraYear, 3972 oneDate.monthCode, 1, 3973 TemporalOverflow::Constrain); 3974 if (!dt) { 3975 return false; 3976 } 3977 if (sign > 0) { 3978 months += monthsSinceStartOfYear(dt.get()); 3979 } else { 3980 months -= monthsUntilEndOfYear(dt.get()); 3981 } 3982 3983 years = 0; 3984 } 3985 3986 eraYear = CalendarEraYear(calendarId, constrainedDate.year); 3987 constrained = CreateDateFromCodes( 3988 cx, calendarId, cal.get(), eraYear, constrainedDate.monthCode, 3989 constrainedDate.day, TemporalOverflow::Constrain); 3990 if (!constrained) { 3991 return false; 3992 } 3993 constrainedIso = ToISODate(constrained.get()); 3994 3995 MOZ_ASSERT(CompareISODate(constrainedIso, two) * sign <= 0, 3996 "constrained doesn't surpass two"); 3997 } 3998 3999 int64_t days = MakeDay(two) - MakeDay(constrainedIso); 4000 4001 *result = DateDuration{ 4002 int64_t(years), 4003 int64_t(months), 4004 0, 4005 int64_t(days), 4006 }; 4007 MOZ_ASSERT(IsValidDuration(*result)); 4008 return true; 4009 } 4010 4011 /** 4012 * NonISODateUntil ( calendar, one, two, largestUnit ) 4013 */ 4014 static bool NonISODateUntil(JSContext* cx, CalendarId calendarId, 4015 const ISODate& one, const ISODate& two, 4016 TemporalUnit largestUnit, DateDuration* result) { 4017 // ICU4X doesn't yet provide a public API for CalendarDateUntil. 4018 // 4019 // https://github.com/unicode-org/icu4x/issues/3964 4020 4021 // Delegate to the ISO 8601 calendar for "weeks" and "days". This works 4022 // because all supported calendars use a 7-days week. 4023 if (largestUnit >= TemporalUnit::Week) { 4024 *result = DifferenceISODate(one, two, largestUnit); 4025 return true; 4026 } 4027 4028 switch (calendarId) { 4029 case CalendarId::ISO8601: 4030 case CalendarId::Buddhist: 4031 case CalendarId::Gregorian: 4032 case CalendarId::Japanese: 4033 case CalendarId::ROC: 4034 // Use the ISO 8601 calendar if the calendar system starts its year at the 4035 // same time as the ISO 8601 calendar and all months exactly match the 4036 // ISO 8601 calendar months. 4037 *result = DifferenceISODate(one, two, largestUnit); 4038 return true; 4039 4040 case CalendarId::Chinese: 4041 case CalendarId::Coptic: 4042 case CalendarId::Dangi: 4043 case CalendarId::Ethiopian: 4044 case CalendarId::EthiopianAmeteAlem: 4045 case CalendarId::Hebrew: 4046 case CalendarId::Indian: 4047 case CalendarId::IslamicCivil: 4048 case CalendarId::IslamicTabular: 4049 case CalendarId::IslamicUmmAlQura: 4050 case CalendarId::Persian: 4051 return DifferenceNonISODate(cx, calendarId, one, two, largestUnit, 4052 result); 4053 } 4054 MOZ_CRASH("invalid calendar id"); 4055 } 4056 4057 /** 4058 * CalendarDateUntil ( calendar, one, two, largestUnit ) 4059 */ 4060 bool js::temporal::CalendarDateUntil(JSContext* cx, 4061 Handle<CalendarValue> calendar, 4062 const ISODate& one, const ISODate& two, 4063 TemporalUnit largestUnit, 4064 DateDuration* result) { 4065 MOZ_ASSERT(largestUnit <= TemporalUnit::Day); 4066 4067 auto calendarId = calendar.identifier(); 4068 4069 // Step 1. 4070 if (calendarId == CalendarId::ISO8601) { 4071 *result = DifferenceISODate(one, two, largestUnit); 4072 return true; 4073 } 4074 4075 // Step 2. 4076 return NonISODateUntil(cx, calendarId, one, two, largestUnit, result); 4077 }