ICU4XCalendar.cpp (18362B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "mozilla/intl/calendar/ICU4XCalendar.h" 6 7 #include "mozilla/Assertions.h" 8 #include "mozilla/TextUtils.h" 9 10 #include <cstring> 11 #include <mutex> 12 #include <stdint.h> 13 #include <type_traits> 14 15 #include "unicode/timezone.h" 16 17 #include "diplomat_runtime.hpp" 18 #include "icu4x/CalendarError.hpp" 19 20 namespace mozilla::intl::calendar { 21 22 // Copied from js/src/util/Text.h 23 template <typename CharT> 24 static constexpr uint8_t AsciiDigitToNumber(CharT c) { 25 using UnsignedCharT = std::make_unsigned_t<CharT>; 26 auto uc = static_cast<UnsignedCharT>(c); 27 return uc - '0'; 28 } 29 30 static UniqueICU4XCalendar CreateICU4XCalendar(icu4x::capi::CalendarKind kind) { 31 auto* result = icu4x::capi::icu4x_Calendar_create_mv1(kind); 32 return UniqueICU4XCalendar{result}; 33 } 34 35 static UniqueICU4XDate CreateICU4XDate(const ISODate& date, 36 const icu4x::capi::Calendar* calendar) { 37 auto result = icu4x::capi::icu4x_Date_from_iso_in_calendar_mv1( 38 date.year, date.month, date.day, calendar); 39 if (!result.is_ok) { 40 return nullptr; 41 } 42 return UniqueICU4XDate{result.ok}; 43 } 44 45 static UniqueICU4XDate CreateDateFromCodes( 46 const icu4x::capi::Calendar* calendar, std::string_view era, 47 int32_t eraYear, MonthCode monthCode, int32_t day) { 48 auto monthCodeView = std::string_view{monthCode}; 49 auto date = icu4x::capi::icu4x_Date_from_codes_in_calendar_mv1( 50 diplomat::capi::DiplomatStringView{era.data(), era.length()}, eraYear, 51 diplomat::capi::DiplomatStringView{monthCodeView.data(), 52 monthCodeView.length()}, 53 day, calendar); 54 if (date.is_ok) { 55 return UniqueICU4XDate{date.ok}; 56 } 57 return nullptr; 58 } 59 60 // Copied from js/src/builtin/temporal/Calendar.cpp 61 static UniqueICU4XDate CreateDateFrom(const icu4x::capi::Calendar* calendar, 62 std::string_view era, int32_t eraYear, 63 int32_t month, int32_t day) { 64 MOZ_ASSERT(1 <= month && month <= 13); 65 66 // Create date with month number replaced by month-code. 67 auto monthCode = MonthCode{std::min(month, 12)}; 68 auto date = CreateDateFromCodes(calendar, era, eraYear, monthCode, day); 69 if (!date) { 70 return nullptr; 71 } 72 73 // If the ordinal month of |date| matches the input month, no additional 74 // changes are necessary and we can directly return |date|. 75 int32_t ordinal = icu4x::capi::icu4x_Date_ordinal_month_mv1(date.get()); 76 if (ordinal == month) { 77 return date; 78 } 79 80 // Otherwise we need to handle three cases: 81 // 1. The input year contains a leap month and we need to adjust the 82 // month-code. 83 // 2. The thirteenth month of a year without leap months was requested. 84 // 3. The thirteenth month of a year with leap months was requested. 85 if (ordinal > month) { 86 MOZ_ASSERT(1 < month && month <= 12); 87 88 // This case can only happen in leap years. 89 MOZ_ASSERT(icu4x::capi::icu4x_Date_months_in_year_mv1(date.get()) == 13); 90 91 // Leap months can occur after any month in the Chinese calendar. 92 // 93 // Example when the fourth month is a leap month between M03 and M04. 94 // 95 // Month code: M01 M02 M03 M03L M04 M05 M06 ... 96 // Ordinal month: 1 2 3 4 5 6 7 97 98 // The month can be off by exactly one. 99 MOZ_ASSERT((ordinal - month) == 1); 100 101 // First try the case when the previous month isn't a leap month. This 102 // case can only occur when |month > 2|, because otherwise we know that 103 // "M01L" is the correct answer. 104 if (month > 2) { 105 auto previousMonthCode = MonthCode{month - 1}; 106 date = 107 CreateDateFromCodes(calendar, era, eraYear, previousMonthCode, day); 108 if (!date) { 109 return nullptr; 110 } 111 int32_t ordinal = icu4x::capi::icu4x_Date_ordinal_month_mv1(date.get()); 112 if (ordinal == month) { 113 return date; 114 } 115 } 116 117 // Fall-through when the previous month is a leap month. 118 } else { 119 MOZ_ASSERT(month == 13); 120 MOZ_ASSERT(ordinal == 12); 121 122 // Years with leap months contain thirteen months. 123 if (icu4x::capi::icu4x_Date_months_in_year_mv1(date.get()) != 13) { 124 return nullptr; 125 } 126 127 // Fall-through to return leap month "M12L" at the end of the year. 128 } 129 130 // Finally handle the case when the previous month is a leap month. 131 auto leapMonthCode = MonthCode{month - 1, /* isLeapMonth= */ true}; 132 return CreateDateFromCodes(calendar, era, eraYear, leapMonthCode, day); 133 } 134 135 static ISODate ToISODate(const icu4x::capi::Date* date) { 136 UniqueICU4XIsoDate isoDate{icu4x::capi::icu4x_Date_to_iso_mv1(date)}; 137 138 int32_t isoYear = icu4x::capi::icu4x_IsoDate_year_mv1(isoDate.get()); 139 int32_t isoMonth = icu4x::capi::icu4x_IsoDate_month_mv1(isoDate.get()); 140 int32_t isoDay = icu4x::capi::icu4x_IsoDate_day_of_month_mv1(isoDate.get()); 141 142 return {isoYear, isoMonth, isoDay}; 143 } 144 145 //////////////////////////////////////////////////////////////////////////////// 146 147 ICU4XCalendar::ICU4XCalendar(icu4x::capi::CalendarKind kind, 148 const icu::Locale& locale, UErrorCode& success) 149 : icu::Calendar(icu::TimeZone::forLocaleOrDefault(locale), locale, success), 150 kind_(kind) {} 151 152 ICU4XCalendar::ICU4XCalendar(icu4x::capi::CalendarKind kind, 153 const icu::TimeZone& timeZone, 154 const icu::Locale& locale, UErrorCode& success) 155 : icu::Calendar(timeZone, locale, success), kind_(kind) {} 156 157 ICU4XCalendar::ICU4XCalendar(const ICU4XCalendar& other) 158 : icu::Calendar(other), kind_(other.kind_) {} 159 160 ICU4XCalendar::~ICU4XCalendar() = default; 161 162 /** 163 * Get or create the underlying ICU4X calendar. 164 */ 165 icu4x::capi::Calendar* ICU4XCalendar::getICU4XCalendar( 166 UErrorCode& status) const { 167 if (U_FAILURE(status)) { 168 return nullptr; 169 } 170 if (!calendar_) { 171 auto result = CreateICU4XCalendar(kind_); 172 if (!result) { 173 status = U_INTERNAL_PROGRAM_ERROR; 174 return nullptr; 175 } 176 calendar_ = std::move(result); 177 } 178 return calendar_.get(); 179 } 180 181 /** 182 * Get or create the fallback ICU4C calendar. Used for dates outside the range 183 * supported by ICU4X. 184 */ 185 icu::Calendar* ICU4XCalendar::getFallbackCalendar(UErrorCode& status) const { 186 if (U_FAILURE(status)) { 187 return nullptr; 188 } 189 if (!fallback_) { 190 icu::Locale locale = getLocale(ULOC_ACTUAL_LOCALE, status); 191 locale.setKeywordValue("calendar", getType(), status); 192 fallback_.reset( 193 icu::Calendar::createInstance(getTimeZone(), locale, status)); 194 } 195 return fallback_.get(); 196 } 197 198 UniqueICU4XDate ICU4XCalendar::createICU4XDate(const ISODate& date, 199 UErrorCode& status) const { 200 MOZ_ASSERT(U_SUCCESS(status)); 201 202 auto* calendar = getICU4XCalendar(status); 203 if (U_FAILURE(status)) { 204 return nullptr; 205 } 206 207 auto dt = CreateICU4XDate(date, calendar); 208 if (!dt) { 209 status = U_INTERNAL_PROGRAM_ERROR; 210 } 211 return dt; 212 } 213 214 MonthCode ICU4XCalendar::monthCodeFrom(const icu4x::capi::Date* date) { 215 // Storage for the largest valid month code and the terminating NUL-character. 216 // DiplomatWrite doesn't have std::span version. 217 // https://github.com/rust-diplomat/diplomat/issues/866 218 std::string buf; 219 auto writable = diplomat::WriteFromString(buf); 220 221 icu4x::capi::icu4x_Date_month_code_mv1(date, &writable); 222 223 MOZ_ASSERT(buf.length() >= 3); 224 MOZ_ASSERT(buf[0] == 'M'); 225 MOZ_ASSERT(mozilla::IsAsciiDigit(buf[1])); 226 MOZ_ASSERT(mozilla::IsAsciiDigit(buf[2])); 227 MOZ_ASSERT_IF(buf.length() > 3, buf[3] == 'L'); 228 229 int32_t ordinal = 230 AsciiDigitToNumber(buf[1]) * 10 + AsciiDigitToNumber(buf[2]); 231 bool isLeapMonth = buf.length() > 3; 232 233 return MonthCode{ordinal, isLeapMonth}; 234 } 235 236 //////////////////////////////////////////// 237 // icu::Calendar implementation overrides // 238 //////////////////////////////////////////// 239 240 const char* ICU4XCalendar::getTemporalMonthCode(UErrorCode& status) const { 241 int32_t month = get(UCAL_MONTH, status); 242 int32_t isLeapMonth = get(UCAL_IS_LEAP_MONTH, status); 243 if (U_FAILURE(status)) { 244 return nullptr; 245 } 246 247 static const char* MonthCodes[] = { 248 // Non-leap months. 249 "M01", 250 "M02", 251 "M03", 252 "M04", 253 "M05", 254 "M06", 255 "M07", 256 "M08", 257 "M09", 258 "M10", 259 "M11", 260 "M12", 261 "M13", 262 263 // Leap months. (Note: There's no thirteenth leap month.) 264 "M01L", 265 "M02L", 266 "M03L", 267 "M04L", 268 "M05L", 269 "M06L", 270 "M07L", 271 "M08L", 272 "M09L", 273 "M10L", 274 "M11L", 275 "M12L", 276 }; 277 278 size_t index = month + (isLeapMonth ? 12 : 0); 279 if (index >= std::size(MonthCodes)) { 280 status = U_ILLEGAL_ARGUMENT_ERROR; 281 return nullptr; 282 } 283 return MonthCodes[index]; 284 } 285 286 void ICU4XCalendar::setTemporalMonthCode(const char* code, UErrorCode& status) { 287 if (U_FAILURE(status)) { 288 return; 289 } 290 291 size_t len = std::strlen(code); 292 if (len < 3 || len > 4 || code[0] != 'M' || !IsAsciiDigit(code[1]) || 293 !IsAsciiDigit(code[2]) || (len == 4 && code[3] != 'L')) { 294 status = U_ILLEGAL_ARGUMENT_ERROR; 295 return; 296 } 297 298 int32_t month = 299 AsciiDigitToNumber(code[1]) * 10 + AsciiDigitToNumber(code[2]); 300 bool isLeapMonth = len == 4; 301 302 if (month < 1 || month > 13 || (month == 13 && isLeapMonth)) { 303 status = U_ILLEGAL_ARGUMENT_ERROR; 304 return; 305 } 306 307 // Check if this calendar supports the requested month code. 308 auto monthCode = MonthCode{month, isLeapMonth}; 309 if (!hasMonthCode(monthCode)) { 310 status = U_ILLEGAL_ARGUMENT_ERROR; 311 return; 312 } 313 314 set(UCAL_MONTH, monthCode.ordinal() - 1); 315 set(UCAL_IS_LEAP_MONTH, int32_t(monthCode.isLeapMonth())); 316 } 317 318 int32_t ICU4XCalendar::internalGetMonth(int32_t defaultValue, 319 UErrorCode& status) const { 320 if (U_FAILURE(status)) { 321 return 0; 322 } 323 if (resolveFields(kMonthPrecedence) == UCAL_MONTH) { 324 return internalGet(UCAL_MONTH, defaultValue); 325 } 326 if (!hasLeapMonths()) { 327 return internalGet(UCAL_ORDINAL_MONTH); 328 } 329 return internalGetMonth(status); 330 } 331 332 /** 333 * Return the current month, possibly by computing it from |UCAL_ORDINAL_MONTH|. 334 */ 335 int32_t ICU4XCalendar::internalGetMonth(UErrorCode& status) const { 336 if (U_FAILURE(status)) { 337 return 0; 338 } 339 if (resolveFields(kMonthPrecedence) == UCAL_MONTH) { 340 return internalGet(UCAL_MONTH); 341 } 342 if (!hasLeapMonths()) { 343 return internalGet(UCAL_ORDINAL_MONTH); 344 } 345 346 int32_t extendedYear = internalGet(UCAL_EXTENDED_YEAR); 347 int32_t ordinalMonth = internalGet(UCAL_ORDINAL_MONTH); 348 349 int32_t month; 350 int32_t isLeapMonth; 351 if (requiresFallbackForExtendedYear(extendedYear)) { 352 // Use the fallback calendar for years outside the range supported by ICU4X. 353 auto* fallback = getFallbackCalendar(status); 354 if (U_FAILURE(status)) { 355 return 0; 356 } 357 fallback->clear(); 358 fallback->set(UCAL_EXTENDED_YEAR, extendedYear); 359 fallback->set(UCAL_ORDINAL_MONTH, ordinalMonth); 360 fallback->set(UCAL_DAY_OF_MONTH, 1); 361 362 month = fallback->get(UCAL_MONTH, status); 363 isLeapMonth = fallback->get(UCAL_IS_LEAP_MONTH, status); 364 if (U_FAILURE(status)) { 365 return 0; 366 } 367 } else { 368 auto* cal = getICU4XCalendar(status); 369 if (U_FAILURE(status)) { 370 return 0; 371 } 372 373 UniqueICU4XDate date = CreateDateFrom(cal, eraName(extendedYear), 374 extendedYear, ordinalMonth + 1, 1); 375 if (!date) { 376 status = U_INTERNAL_PROGRAM_ERROR; 377 return 0; 378 } 379 380 MonthCode monthCode = monthCodeFrom(date.get()); 381 month = monthCode.ordinal() - 1; 382 isLeapMonth = monthCode.isLeapMonth(); 383 } 384 385 auto* nonConstThis = const_cast<ICU4XCalendar*>(this); 386 nonConstThis->internalSet(UCAL_IS_LEAP_MONTH, isLeapMonth); 387 nonConstThis->internalSet(UCAL_MONTH, month); 388 389 return month; 390 } 391 392 void ICU4XCalendar::add(UCalendarDateFields field, int32_t amount, 393 UErrorCode& status) { 394 switch (field) { 395 case UCAL_MONTH: 396 case UCAL_ORDINAL_MONTH: 397 if (amount != 0) { 398 // Our implementation doesn't yet support this action. 399 status = U_ILLEGAL_ARGUMENT_ERROR; 400 break; 401 } 402 break; 403 default: 404 Calendar::add(field, amount, status); 405 break; 406 } 407 } 408 409 void ICU4XCalendar::add(EDateFields field, int32_t amount, UErrorCode& status) { 410 add(static_cast<UCalendarDateFields>(field), amount, status); 411 } 412 413 void ICU4XCalendar::roll(UCalendarDateFields field, int32_t amount, 414 UErrorCode& status) { 415 switch (field) { 416 case UCAL_MONTH: 417 case UCAL_ORDINAL_MONTH: 418 if (amount != 0) { 419 // Our implementation doesn't yet support this action. 420 status = U_ILLEGAL_ARGUMENT_ERROR; 421 break; 422 } 423 break; 424 default: 425 Calendar::roll(field, amount, status); 426 break; 427 } 428 } 429 430 void ICU4XCalendar::roll(EDateFields field, int32_t amount, 431 UErrorCode& status) { 432 roll(static_cast<UCalendarDateFields>(field), amount, status); 433 } 434 435 int32_t ICU4XCalendar::handleGetExtendedYear(UErrorCode& status) { 436 if (U_FAILURE(status)) { 437 return 0; 438 } 439 if (newerField(UCAL_EXTENDED_YEAR, UCAL_YEAR) == UCAL_EXTENDED_YEAR) { 440 return internalGet(UCAL_EXTENDED_YEAR, 1); 441 } 442 443 // We don't yet support the case when UCAL_YEAR is newer. 444 status = U_UNSUPPORTED_ERROR; 445 return 0; 446 } 447 448 int32_t ICU4XCalendar::handleGetYearLength(int32_t extendedYear, 449 UErrorCode& status) const { 450 // Use the (slower) default implementation for years outside the range 451 // supported by ICU4X. 452 if (requiresFallbackForExtendedYear(extendedYear)) { 453 return icu::Calendar::handleGetYearLength(extendedYear, status); 454 } 455 456 auto* cal = getICU4XCalendar(status); 457 if (U_FAILURE(status)) { 458 return 0; 459 } 460 461 UniqueICU4XDate date = 462 CreateDateFrom(cal, eraName(extendedYear), extendedYear, 1, 1); 463 if (!date) { 464 status = U_INTERNAL_PROGRAM_ERROR; 465 return 0; 466 } 467 return icu4x::capi::icu4x_Date_days_in_year_mv1(date.get()); 468 } 469 470 /** 471 * Return the number of days in a month. 472 */ 473 int32_t ICU4XCalendar::handleGetMonthLength(int32_t extendedYear, int32_t month, 474 UErrorCode& status) const { 475 if (U_FAILURE(status)) { 476 return 0; 477 } 478 479 // ICU4C supports wrap around. We don't support this case. 480 if (month < 0 || month > 11) { 481 status = U_ILLEGAL_ARGUMENT_ERROR; 482 return 0; 483 } 484 485 // Use the fallback calendar for years outside the range supported by ICU4X. 486 if (requiresFallbackForExtendedYear(extendedYear)) { 487 auto* fallback = getFallbackCalendar(status); 488 if (U_FAILURE(status)) { 489 return 0; 490 } 491 fallback->clear(); 492 fallback->set(UCAL_EXTENDED_YEAR, extendedYear); 493 fallback->set(UCAL_MONTH, month); 494 fallback->set(UCAL_DAY_OF_MONTH, 1); 495 496 return fallback->getActualMaximum(UCAL_DAY_OF_MONTH, status); 497 } 498 499 auto* cal = getICU4XCalendar(status); 500 if (U_FAILURE(status)) { 501 return 0; 502 } 503 504 bool isLeapMonth = internalGet(UCAL_IS_LEAP_MONTH) != 0; 505 auto monthCode = MonthCode{month + 1, isLeapMonth}; 506 UniqueICU4XDate date = CreateDateFromCodes(cal, eraName(extendedYear), 507 extendedYear, monthCode, 1); 508 if (!date) { 509 status = U_INTERNAL_PROGRAM_ERROR; 510 return 0; 511 } 512 513 return icu4x::capi::icu4x_Date_days_in_month_mv1(date.get()); 514 } 515 516 /** 517 * Return the start of the month as a Julian date. 518 */ 519 int64_t ICU4XCalendar::handleComputeMonthStart(int32_t extendedYear, 520 int32_t month, UBool useMonth, 521 UErrorCode& status) const { 522 if (U_FAILURE(status)) { 523 return 0; 524 } 525 526 // ICU4C supports wrap around. We don't support this case. 527 if (month < 0 || month > 11) { 528 status = U_ILLEGAL_ARGUMENT_ERROR; 529 return 0; 530 } 531 532 // Use the fallback calendar for years outside the range supported by ICU4X. 533 if (requiresFallbackForExtendedYear(extendedYear)) { 534 auto* fallback = getFallbackCalendar(status); 535 if (U_FAILURE(status)) { 536 return 0; 537 } 538 fallback->clear(); 539 fallback->set(UCAL_EXTENDED_YEAR, extendedYear); 540 if (useMonth) { 541 fallback->set(UCAL_MONTH, month); 542 fallback->set(UCAL_IS_LEAP_MONTH, internalGet(UCAL_IS_LEAP_MONTH)); 543 } else { 544 fallback->set(UCAL_ORDINAL_MONTH, month); 545 } 546 fallback->set(UCAL_DAY_OF_MONTH, 1); 547 548 int32_t newMoon = fallback->get(UCAL_JULIAN_DAY, status); 549 if (U_FAILURE(status)) { 550 return 0; 551 } 552 return newMoon - 1; 553 } 554 555 auto* cal = getICU4XCalendar(status); 556 if (U_FAILURE(status)) { 557 return 0; 558 } 559 560 UniqueICU4XDate date{}; 561 if (useMonth) { 562 bool isLeapMonth = internalGet(UCAL_IS_LEAP_MONTH) != 0; 563 auto monthCode = MonthCode{month + 1, isLeapMonth}; 564 date = CreateDateFromCodes(cal, eraName(extendedYear), extendedYear, 565 monthCode, 1); 566 } else { 567 date = 568 CreateDateFrom(cal, eraName(extendedYear), extendedYear, month + 1, 1); 569 } 570 if (!date) { 571 status = U_INTERNAL_PROGRAM_ERROR; 572 return 0; 573 } 574 575 auto isoDate = ToISODate(date.get()); 576 int32_t newMoon = MakeDay(isoDate); 577 578 return (newMoon - 1) + kEpochStartAsJulianDay; 579 } 580 581 /** 582 * Default implementation of handleComputeFields when using the fallback 583 * calendar. 584 */ 585 void ICU4XCalendar::handleComputeFieldsFromFallback(int32_t julianDay, 586 UErrorCode& status) { 587 auto* fallback = getFallbackCalendar(status); 588 if (U_FAILURE(status)) { 589 return; 590 } 591 fallback->clear(); 592 fallback->set(UCAL_JULIAN_DAY, julianDay); 593 594 internalSet(UCAL_ERA, fallback->get(UCAL_ERA, status)); 595 internalSet(UCAL_YEAR, fallback->get(UCAL_YEAR, status)); 596 internalSet(UCAL_EXTENDED_YEAR, fallback->get(UCAL_EXTENDED_YEAR, status)); 597 internalSet(UCAL_MONTH, fallback->get(UCAL_MONTH, status)); 598 internalSet(UCAL_ORDINAL_MONTH, fallback->get(UCAL_ORDINAL_MONTH, status)); 599 internalSet(UCAL_IS_LEAP_MONTH, fallback->get(UCAL_IS_LEAP_MONTH, status)); 600 internalSet(UCAL_DAY_OF_MONTH, fallback->get(UCAL_DAY_OF_MONTH, status)); 601 internalSet(UCAL_DAY_OF_YEAR, fallback->get(UCAL_DAY_OF_YEAR, status)); 602 } 603 604 } // namespace mozilla::intl::calendar