/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/intl/calendar/ICU4XCalendar.h" #include "mozilla/Assertions.h" #include "mozilla/TextUtils.h" #include #include #include #include #include "unicode/timezone.h" #include "icu4x/diplomat_runtime.hpp" #include "icu4x/CalendarError.hpp" namespace mozilla::intl::calendar { // Copied from js/src/util/Text.h template static constexpr uint8_t AsciiDigitToNumber(CharT c) { using UnsignedCharT = std::make_unsigned_t; auto uc = static_cast(c); return uc - '0'; } static UniqueICU4XCalendar CreateICU4XCalendar(icu4x::capi::CalendarKind kind) { auto* result = icu4x::capi::icu4x_Calendar_create_mv1(kind); return UniqueICU4XCalendar{result}; } static UniqueICU4XDate CreateICU4XDate(const ISODate& date, const icu4x::capi::Calendar* calendar) { auto result = icu4x::capi::icu4x_Date_from_iso_in_calendar_mv1( date.year, date.month, date.day, calendar); if (!result.is_ok) { return nullptr; } return UniqueICU4XDate{result.ok}; } static UniqueICU4XDate CreateDateFromCodes( const icu4x::capi::Calendar* calendar, std::string_view era, int32_t eraYear, MonthCode monthCode, int32_t day) { auto monthCodeView = std::string_view{monthCode}; auto date = icu4x::capi::icu4x_Date_from_codes_in_calendar_mv1( icu4x::diplomat::capi::DiplomatStringView{era.data(), era.length()}, eraYear, icu4x::diplomat::capi::DiplomatStringView{monthCodeView.data(), monthCodeView.length()}, day, calendar); if (date.is_ok) { return UniqueICU4XDate{date.ok}; } return nullptr; } // Copied from js/src/builtin/temporal/Calendar.cpp static UniqueICU4XDate CreateDateFrom(const icu4x::capi::Calendar* calendar, std::string_view era, int32_t eraYear, int32_t month, int32_t day) { MOZ_ASSERT(1 <= month && month <= 13); // Create date with month number replaced by month-code. auto monthCode = MonthCode{std::min(month, 12)}; auto date = CreateDateFromCodes(calendar, era, eraYear, monthCode, day); if (!date) { return nullptr; } // If the ordinal month of |date| matches the input month, no additional // changes are necessary and we can directly return |date|. int32_t ordinal = icu4x::capi::icu4x_Date_ordinal_month_mv1(date.get()); if (ordinal == month) { return date; } // Otherwise we need to handle three cases: // 1. The input year contains a leap month and we need to adjust the // month-code. // 2. The thirteenth month of a year without leap months was requested. // 3. The thirteenth month of a year with leap months was requested. if (ordinal > month) { MOZ_ASSERT(1 < month && month <= 12); // This case can only happen in leap years. MOZ_ASSERT(icu4x::capi::icu4x_Date_months_in_year_mv1(date.get()) == 13); // Leap months can occur after any month in the Chinese calendar. // // Example when the fourth month is a leap month between M03 and M04. // // Month code: M01 M02 M03 M03L M04 M05 M06 ... // Ordinal month: 1 2 3 4 5 6 7 // The month can be off by exactly one. MOZ_ASSERT((ordinal - month) == 1); // First try the case when the previous month isn't a leap month. This // case can only occur when |month > 2|, because otherwise we know that // "M01L" is the correct answer. if (month > 2) { auto previousMonthCode = MonthCode{month - 1}; date = CreateDateFromCodes(calendar, era, eraYear, previousMonthCode, day); if (!date) { return nullptr; } int32_t ordinal = icu4x::capi::icu4x_Date_ordinal_month_mv1(date.get()); if (ordinal == month) { return date; } } // Fall-through when the previous month is a leap month. } else { MOZ_ASSERT(month == 13); MOZ_ASSERT(ordinal == 12); // Years with leap months contain thirteen months. if (icu4x::capi::icu4x_Date_months_in_year_mv1(date.get()) != 13) { return nullptr; } // Fall-through to return leap month "M12L" at the end of the year. } // Finally handle the case when the previous month is a leap month. auto leapMonthCode = MonthCode{month - 1, /* isLeapMonth= */ true}; return CreateDateFromCodes(calendar, era, eraYear, leapMonthCode, day); } static ISODate ToISODate(const icu4x::capi::Date* date) { UniqueICU4XIsoDate isoDate{icu4x::capi::icu4x_Date_to_iso_mv1(date)}; int32_t isoYear = icu4x::capi::icu4x_IsoDate_year_mv1(isoDate.get()); int32_t isoMonth = icu4x::capi::icu4x_IsoDate_month_mv1(isoDate.get()); int32_t isoDay = icu4x::capi::icu4x_IsoDate_day_of_month_mv1(isoDate.get()); return {isoYear, isoMonth, isoDay}; } //////////////////////////////////////////////////////////////////////////////// ICU4XCalendar::ICU4XCalendar(icu4x::capi::CalendarKind kind, const icu::Locale& locale, UErrorCode& success) : icu::Calendar(icu::TimeZone::forLocaleOrDefault(locale), locale, success), kind_(kind) {} ICU4XCalendar::ICU4XCalendar(icu4x::capi::CalendarKind kind, const icu::TimeZone& timeZone, const icu::Locale& locale, UErrorCode& success) : icu::Calendar(timeZone, locale, success), kind_(kind) {} ICU4XCalendar::ICU4XCalendar(const ICU4XCalendar& other) : icu::Calendar(other), kind_(other.kind_) {} ICU4XCalendar::~ICU4XCalendar() = default; /** * Get or create the underlying ICU4X calendar. */ icu4x::capi::Calendar* ICU4XCalendar::getICU4XCalendar( UErrorCode& status) const { if (U_FAILURE(status)) { return nullptr; } if (!calendar_) { auto result = CreateICU4XCalendar(kind_); if (!result) { status = U_INTERNAL_PROGRAM_ERROR; return nullptr; } calendar_ = std::move(result); } return calendar_.get(); } UniqueICU4XDate ICU4XCalendar::createICU4XDate(const ISODate& date, UErrorCode& status) const { MOZ_ASSERT(U_SUCCESS(status)); auto* calendar = getICU4XCalendar(status); if (U_FAILURE(status)) { return nullptr; } auto dt = CreateICU4XDate(date, calendar); if (!dt) { status = U_INTERNAL_PROGRAM_ERROR; } return dt; } MonthCode ICU4XCalendar::monthCodeFrom(const icu4x::capi::Date* date) { // Storage for the largest valid month code and the terminating NUL-character. // DiplomatWrite doesn't have std::span version. // https://github.com/rust-diplomat/diplomat/issues/866 std::string buf; auto writable = icu4x::diplomat::WriteFromString(buf); icu4x::capi::icu4x_Date_month_code_mv1(date, &writable); MOZ_ASSERT(buf.length() >= 3); MOZ_ASSERT(buf[0] == 'M'); MOZ_ASSERT(mozilla::IsAsciiDigit(buf[1])); MOZ_ASSERT(mozilla::IsAsciiDigit(buf[2])); MOZ_ASSERT_IF(buf.length() > 3, buf[3] == 'L'); int32_t ordinal = AsciiDigitToNumber(buf[1]) * 10 + AsciiDigitToNumber(buf[2]); bool isLeapMonth = buf.length() > 3; return MonthCode{ordinal, isLeapMonth}; } //////////////////////////////////////////// // icu::Calendar implementation overrides // //////////////////////////////////////////// const char* ICU4XCalendar::getTemporalMonthCode(UErrorCode& status) const { int32_t month = get(UCAL_MONTH, status); int32_t isLeapMonth = get(UCAL_IS_LEAP_MONTH, status); if (U_FAILURE(status)) { return nullptr; } static const char* MonthCodes[] = { // Non-leap months. "M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12", "M13", // Leap months. (Note: There's no thirteenth leap month.) "M01L", "M02L", "M03L", "M04L", "M05L", "M06L", "M07L", "M08L", "M09L", "M10L", "M11L", "M12L", }; size_t index = month + (isLeapMonth ? 12 : 0); if (index >= std::size(MonthCodes)) { status = U_ILLEGAL_ARGUMENT_ERROR; return nullptr; } return MonthCodes[index]; } void ICU4XCalendar::setTemporalMonthCode(const char* code, UErrorCode& status) { if (U_FAILURE(status)) { return; } size_t len = std::strlen(code); if (len < 3 || len > 4 || code[0] != 'M' || !IsAsciiDigit(code[1]) || !IsAsciiDigit(code[2]) || (len == 4 && code[3] != 'L')) { status = U_ILLEGAL_ARGUMENT_ERROR; return; } int32_t month = AsciiDigitToNumber(code[1]) * 10 + AsciiDigitToNumber(code[2]); bool isLeapMonth = len == 4; if (month < 1 || month > 13 || (month == 13 && isLeapMonth)) { status = U_ILLEGAL_ARGUMENT_ERROR; return; } // Check if this calendar supports the requested month code. auto monthCode = MonthCode{month, isLeapMonth}; if (!hasMonthCode(monthCode)) { status = U_ILLEGAL_ARGUMENT_ERROR; return; } set(UCAL_MONTH, monthCode.ordinal() - 1); set(UCAL_IS_LEAP_MONTH, int32_t(monthCode.isLeapMonth())); } int32_t ICU4XCalendar::internalGetMonth(int32_t defaultValue, UErrorCode& status) const { if (U_FAILURE(status)) { return 0; } if (resolveFields(kMonthPrecedence) == UCAL_MONTH) { return internalGet(UCAL_MONTH, defaultValue); } if (!hasLeapMonths()) { return internalGet(UCAL_ORDINAL_MONTH); } return internalGetMonth(status); } /** * Return the current month, possibly by computing it from |UCAL_ORDINAL_MONTH|. */ int32_t ICU4XCalendar::internalGetMonth(UErrorCode& status) const { if (U_FAILURE(status)) { return 0; } if (resolveFields(kMonthPrecedence) == UCAL_MONTH) { return internalGet(UCAL_MONTH); } if (!hasLeapMonths()) { return internalGet(UCAL_ORDINAL_MONTH); } int32_t extendedYear = internalGet(UCAL_EXTENDED_YEAR); int32_t ordinalMonth = internalGet(UCAL_ORDINAL_MONTH); auto* cal = getICU4XCalendar(status); if (U_FAILURE(status)) { return 0; } UniqueICU4XDate date = CreateDateFrom(cal, eraName(extendedYear), extendedYear, ordinalMonth + 1, 1); if (!date) { status = U_INTERNAL_PROGRAM_ERROR; return 0; } MonthCode monthCode = monthCodeFrom(date.get()); int32_t month = monthCode.ordinal() - 1; int32_t isLeapMonth = monthCode.isLeapMonth(); auto* nonConstThis = const_cast(this); nonConstThis->internalSet(UCAL_IS_LEAP_MONTH, isLeapMonth); nonConstThis->internalSet(UCAL_MONTH, month); return month; } void ICU4XCalendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status) { switch (field) { case UCAL_MONTH: case UCAL_ORDINAL_MONTH: if (amount != 0) { // Our implementation doesn't yet support this action. status = U_ILLEGAL_ARGUMENT_ERROR; break; } break; default: Calendar::add(field, amount, status); break; } } void ICU4XCalendar::add(EDateFields field, int32_t amount, UErrorCode& status) { add(static_cast(field), amount, status); } void ICU4XCalendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode& status) { switch (field) { case UCAL_MONTH: case UCAL_ORDINAL_MONTH: if (amount != 0) { // Our implementation doesn't yet support this action. status = U_ILLEGAL_ARGUMENT_ERROR; break; } break; default: Calendar::roll(field, amount, status); break; } } void ICU4XCalendar::roll(EDateFields field, int32_t amount, UErrorCode& status) { roll(static_cast(field), amount, status); } int32_t ICU4XCalendar::handleGetExtendedYear(UErrorCode& status) { if (U_FAILURE(status)) { return 0; } if (newerField(UCAL_EXTENDED_YEAR, UCAL_YEAR) == UCAL_EXTENDED_YEAR) { return internalGet(UCAL_EXTENDED_YEAR, 1); } // We don't yet support the case when UCAL_YEAR is newer. status = U_UNSUPPORTED_ERROR; return 0; } int32_t ICU4XCalendar::handleGetYearLength(int32_t extendedYear, UErrorCode& status) const { auto* cal = getICU4XCalendar(status); if (U_FAILURE(status)) { return 0; } UniqueICU4XDate date = CreateDateFrom(cal, eraName(extendedYear), extendedYear, 1, 1); if (!date) { status = U_INTERNAL_PROGRAM_ERROR; return 0; } return icu4x::capi::icu4x_Date_days_in_year_mv1(date.get()); } /** * Return the number of days in a month. */ int32_t ICU4XCalendar::handleGetMonthLength(int32_t extendedYear, int32_t month, UErrorCode& status) const { if (U_FAILURE(status)) { return 0; } // ICU4C supports wrap around. We don't support this case. if (month < 0 || month > 11) { status = U_ILLEGAL_ARGUMENT_ERROR; return 0; } auto* cal = getICU4XCalendar(status); if (U_FAILURE(status)) { return 0; } bool isLeapMonth = internalGet(UCAL_IS_LEAP_MONTH) != 0; auto monthCode = MonthCode{month + 1, isLeapMonth}; UniqueICU4XDate date = CreateDateFromCodes(cal, eraName(extendedYear), extendedYear, monthCode, 1); if (!date) { status = U_INTERNAL_PROGRAM_ERROR; return 0; } return icu4x::capi::icu4x_Date_days_in_month_mv1(date.get()); } /** * Return the start of the month as a Julian date. */ int64_t ICU4XCalendar::handleComputeMonthStart(int32_t extendedYear, int32_t month, UBool useMonth, UErrorCode& status) const { if (U_FAILURE(status)) { return 0; } // ICU4C supports wrap around. We don't support this case. if (month < 0 || month > 11) { status = U_ILLEGAL_ARGUMENT_ERROR; return 0; } auto* cal = getICU4XCalendar(status); if (U_FAILURE(status)) { return 0; } UniqueICU4XDate date{}; if (useMonth) { bool isLeapMonth = internalGet(UCAL_IS_LEAP_MONTH) != 0; auto monthCode = MonthCode{month + 1, isLeapMonth}; date = CreateDateFromCodes(cal, eraName(extendedYear), extendedYear, monthCode, 1); } else { date = CreateDateFrom(cal, eraName(extendedYear), extendedYear, month + 1, 1); } if (!date) { status = U_INTERNAL_PROGRAM_ERROR; return 0; } auto isoDate = ToISODate(date.get()); int32_t newMoon = MakeDay(isoDate); return (newMoon - 1) + kEpochStartAsJulianDay; } } // namespace mozilla::intl::calendar