diff options
Diffstat (limited to 'intl/components/src/DateTimeFormat.cpp')
-rw-r--r-- | intl/components/src/DateTimeFormat.cpp | 1140 |
1 files changed, 1140 insertions, 0 deletions
diff --git a/intl/components/src/DateTimeFormat.cpp b/intl/components/src/DateTimeFormat.cpp new file mode 100644 index 0000000000..5a6429e976 --- /dev/null +++ b/intl/components/src/DateTimeFormat.cpp @@ -0,0 +1,1140 @@ +/* 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 <cstring> + +#include "unicode/ucal.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/ures.h" + +#include "DateTimeFormatUtils.h" +#include "ScopedICUObject.h" + +#include "mozilla/EnumSet.h" +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/DateTimeFormat.h" +#include "mozilla/intl/DateTimePatternGenerator.h" + +namespace mozilla::intl { + +DateTimeFormat::~DateTimeFormat() { + MOZ_ASSERT(mDateFormat); + udat_close(mDateFormat); +} + +static UDateFormatStyle ToUDateFormatStyle( + Maybe<DateTimeFormat::Style> aLength) { + if (!aLength) { + return UDAT_NONE; + } + switch (*aLength) { + case DateTimeFormat::Style::Full: + return UDAT_FULL; + case DateTimeFormat::Style::Long: + return UDAT_LONG; + case DateTimeFormat::Style::Medium: + return UDAT_MEDIUM; + case DateTimeFormat::Style::Short: + return UDAT_SHORT; + } + MOZ_ASSERT_UNREACHABLE(); + // Do not use the default: branch so that the enum is exhaustively checked. + return UDAT_NONE; +} + +/** + * Parse a pattern according to the format specified in + * <https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>. + */ +template <typename CharT> +class PatternIterator { + CharT* iter; + const CharT* const end; + + public: + explicit PatternIterator(mozilla::Span<CharT> aPattern) + : iter(aPattern.data()), end(aPattern.data() + aPattern.size()) {} + + CharT* next() { + MOZ_ASSERT(iter != nullptr); + + bool inQuote = false; + while (iter < end) { + CharT* cur = iter++; + if (*cur == '\'') { + inQuote = !inQuote; + } else if (!inQuote) { + return cur; + } + } + + iter = nullptr; + return nullptr; + } +}; + +Maybe<DateTimeFormat::HourCycle> DateTimeFormat::HourCycleFromPattern( + Span<const char16_t> aPattern) { + PatternIterator<const char16_t> iter(aPattern); + while (const auto* ptr = iter.next()) { + switch (*ptr) { + case 'K': + return Some(DateTimeFormat::HourCycle::H11); + case 'h': + return Some(DateTimeFormat::HourCycle::H12); + case 'H': + return Some(DateTimeFormat::HourCycle::H23); + case 'k': + return Some(DateTimeFormat::HourCycle::H24); + } + } + return Nothing(); +} + +static bool IsHour12(DateTimeFormat::HourCycle aHourCycle) { + return aHourCycle == DateTimeFormat::HourCycle::H11 || + aHourCycle == DateTimeFormat::HourCycle::H12; +} + +static char16_t HourSymbol(DateTimeFormat::HourCycle aHourCycle) { + switch (aHourCycle) { + case DateTimeFormat::HourCycle::H11: + return 'K'; + case DateTimeFormat::HourCycle::H12: + return 'h'; + case DateTimeFormat::HourCycle::H23: + return 'H'; + case DateTimeFormat::HourCycle::H24: + return 'k'; + } + MOZ_CRASH("unexpected hour cycle"); +} + +enum class PatternField { Hour, Minute, Second, Other }; + +template <typename CharT> +static PatternField ToPatternField(CharT aCh) { + if (aCh == 'K' || aCh == 'h' || aCh == 'H' || aCh == 'k' || aCh == 'j') { + return PatternField::Hour; + } + if (aCh == 'm') { + return PatternField::Minute; + } + if (aCh == 's') { + return PatternField::Second; + } + return PatternField::Other; +} + +/** + * Replaces all hour pattern characters in |patternOrSkeleton| to use the + * matching hour representation for |hourCycle|. + */ +/* static */ +void DateTimeFormat::ReplaceHourSymbol( + mozilla::Span<char16_t> aPatternOrSkeleton, + DateTimeFormat::HourCycle aHourCycle) { + char16_t replacement = HourSymbol(aHourCycle); + PatternIterator<char16_t> iter(aPatternOrSkeleton); + while (auto* ptr = iter.next()) { + auto field = ToPatternField(*ptr); + if (field == PatternField::Hour) { + *ptr = replacement; + } + } +} + +/** + * Find a matching pattern using the requested hour-12 options. + * + * This function is needed to work around the following two issues. + * - https://unicode-org.atlassian.net/browse/ICU-21023 + * - https://unicode-org.atlassian.net/browse/CLDR-13425 + * + * We're currently using a relatively simple workaround, which doesn't give the + * most accurate results. For example: + * + * ``` + * var dtf = new Intl.DateTimeFormat("en", { + * timeZone: "UTC", + * dateStyle: "long", + * timeStyle: "long", + * hourCycle: "h12", + * }); + * print(dtf.format(new Date("2020-01-01T00:00Z"))); + * ``` + * + * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through + * |DateTimePatternGenerator::GetSkeleton| and then + * |DateTimePatternGenerator::GetBestPattern| to find an equivalent pattern for + * "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so the + * combinator element " 'at' " was lost in the process. + */ +/* static */ +ICUResult DateTimeFormat::FindPatternWithHourCycle( + DateTimePatternGenerator& aDateTimePatternGenerator, + DateTimeFormat::PatternVector& aPattern, bool aHour12, + DateTimeFormat::SkeletonVector& aSkeleton) { + MOZ_TRY(mozilla::intl::DateTimePatternGenerator::GetSkeleton(aPattern, + aSkeleton)); + + // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". + DateTimeFormat::ReplaceHourSymbol(aSkeleton, + aHour12 ? DateTimeFormat::HourCycle::H12 + : DateTimeFormat::HourCycle::H23); + + MOZ_TRY(aDateTimePatternGenerator.GetBestPattern(aSkeleton, aPattern)); + + return Ok(); +} + +static auto PatternMatchOptions(mozilla::Span<const char16_t> aSkeleton) { + // Values for hour, minute, and second are: + // - absent: 0 + // - numeric: 1 + // - 2-digit: 2 + int32_t hour = 0; + int32_t minute = 0; + int32_t second = 0; + + PatternIterator<const char16_t> iter(aSkeleton); + while (const auto* ptr = iter.next()) { + switch (ToPatternField(*ptr)) { + case PatternField::Hour: + MOZ_ASSERT(hour < 2); + hour += 1; + break; + case PatternField::Minute: + MOZ_ASSERT(minute < 2); + minute += 1; + break; + case PatternField::Second: + MOZ_ASSERT(second < 2); + second += 1; + break; + case PatternField::Other: + break; + } + } + + // Adjust the field length when the user requested '2-digit' representation. + // + // We can't just always adjust the field length, because + // 1. The default value for hour, minute, and second fields is 'numeric'. If + // the length is always adjusted, |date.toLocaleTime()| will start to + // return strings like "1:5:9 AM" instead of "1:05:09 AM". + // 2. ICU doesn't support to adjust the field length to 'numeric' in certain + // cases. For example when the locale is "de" (German): + // a. hour='numeric' and minute='2-digit' will return "1:05". + // b. whereas hour='numeric' and minute='numeric' will return "01:05". + // + // Therefore we only support adjusting the field length when the user + // explicitly requested the '2-digit' representation. + + using PatternMatchOption = + mozilla::intl::DateTimePatternGenerator::PatternMatchOption; + mozilla::EnumSet<PatternMatchOption> options; + if (hour == 2) { + options += PatternMatchOption::HourField; + } + if (minute == 2) { + options += PatternMatchOption::MinuteField; + } + if (second == 2) { + options += PatternMatchOption::SecondField; + } + return options; +} + +/* static */ +Result<UniquePtr<DateTimeFormat>, ICUError> DateTimeFormat::TryCreateFromStyle( + Span<const char> aLocale, const StyleBag& aStyleBag, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe<Span<const char16_t>> aTimeZoneOverride) { + auto dateStyle = ToUDateFormatStyle(aStyleBag.date); + auto timeStyle = ToUDateFormatStyle(aStyleBag.time); + + if (dateStyle == UDAT_NONE && timeStyle == UDAT_NONE) { + dateStyle = UDAT_DEFAULT; + timeStyle = UDAT_DEFAULT; + } + + // The time zone is optional. + int32_t tzIDLength = -1; + const UChar* tzID = nullptr; + if (aTimeZoneOverride) { + tzIDLength = static_cast<int32_t>(aTimeZoneOverride->size()); + tzID = aTimeZoneOverride->Elements(); + } + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* dateFormat = + udat_open(timeStyle, dateStyle, IcuLocale(aLocale), tzID, tzIDLength, + /* pattern */ nullptr, /* pattern length */ -1, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + auto df = UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); + + if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) { + // Only adjust the style pattern for time if there is an override. + // Extract the pattern and adjust it for the preferred hour cycle. + DateTimeFormat::PatternVector pattern{}; + + VectorToBufferAdaptor buffer(pattern); + MOZ_TRY(df->GetPattern(buffer)); + + Maybe<DateTimeFormat::HourCycle> hcPattern = HourCycleFromPattern(pattern); + DateTimeFormat::SkeletonVector skeleton{}; + + if (hcPattern) { + bool wantHour12 = + aStyleBag.hour12 ? *aStyleBag.hour12 : IsHour12(*aStyleBag.hourCycle); + if (wantHour12 == IsHour12(*hcPattern)) { + // Return the date-time format when its hour-cycle settings match the + // requested options. + if (aStyleBag.hour12 || *hcPattern == *aStyleBag.hourCycle) { + return df; + } + } else { + MOZ_ASSERT(aDateTimePatternGenerator); + MOZ_TRY(DateTimeFormat::FindPatternWithHourCycle( + *aDateTimePatternGenerator, pattern, wantHour12, skeleton)); + } + // Replace the hourCycle, if present, in the pattern string. But only do + // this if no hour12 option is present, because the latter takes + // precedence over hourCycle. + if (!aStyleBag.hour12) { + DateTimeFormat::ReplaceHourSymbol(pattern, *aStyleBag.hourCycle); + } + + auto result = DateTimeFormat::TryCreateFromPattern(aLocale, pattern, + aTimeZoneOverride); + if (result.isErr()) { + return Err(result.unwrapErr()); + } + auto dateTimeFormat = result.unwrap(); + MOZ_TRY(dateTimeFormat->CacheSkeleton(skeleton)); + return dateTimeFormat; + } + } + + return df; +} + +DateTimeFormat::DateTimeFormat(UDateFormat* aDateFormat) { + MOZ_RELEASE_ASSERT(aDateFormat, "Expected aDateFormat to not be a nullptr."); + mDateFormat = aDateFormat; +} + +// A helper to ergonomically push a string onto a string vector. +template <typename V, size_t N> +static ICUResult PushString(V& aVec, const char16_t (&aString)[N]) { + if (!aVec.append(aString, N - 1)) { + return Err(ICUError::OutOfMemory); + } + return Ok(); +} + +// A helper to ergonomically push a char onto a string vector. +template <typename V> +static ICUResult PushChar(V& aVec, char16_t aCh) { + if (!aVec.append(aCh)) { + return Err(ICUError::OutOfMemory); + } + return Ok(); +} + +/** + * Returns an ICU skeleton string representing the specified options. + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + */ +ICUResult ToICUSkeleton(const DateTimeFormat::ComponentsBag& aBag, + DateTimeFormat::SkeletonVector& aSkeleton) { + // Create an ICU skeleton representing the specified aBag. See + if (aBag.weekday) { + switch (*aBag.weekday) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"EEEEE")); + break; + case DateTimeFormat::Text::Short: + MOZ_TRY(PushString(aSkeleton, u"E")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"EEEE")); + } + } + if (aBag.era) { + switch (*aBag.era) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"GGGGG")); + break; + case DateTimeFormat::Text::Short: + // Use "GGG" instead of "G" to return the same results as other + // browsers. This is exploiting the following ICU bug + // <https://unicode-org.atlassian.net/browse/ICU-22138>. As soon as that + // bug has been fixed, we can change this back to "G". + // + // In practice the bug only affects "G", so we only apply it for "G" + // and not for other symbols like "B" or "z". + MOZ_TRY(PushString(aSkeleton, u"GGG")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"GGGG")); + break; + } + } + if (aBag.year) { + switch (*aBag.year) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"yy")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"y")); + break; + } + } + if (aBag.month) { + switch (*aBag.month) { + case DateTimeFormat::Month::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"MM")); + break; + case DateTimeFormat::Month::Numeric: + MOZ_TRY(PushString(aSkeleton, u"M")); + break; + case DateTimeFormat::Month::Narrow: + MOZ_TRY(PushString(aSkeleton, u"MMMMM")); + break; + case DateTimeFormat::Month::Short: + MOZ_TRY(PushString(aSkeleton, u"MMM")); + break; + case DateTimeFormat::Month::Long: + MOZ_TRY(PushString(aSkeleton, u"MMMM")); + break; + } + } + if (aBag.day) { + switch (*aBag.day) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"dd")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"d")); + break; + } + } + + // If hour12 and hourCycle are both present, hour12 takes precedence. + char16_t hourSkeletonChar = 'j'; + if (aBag.hour12) { + if (*aBag.hour12) { + hourSkeletonChar = 'h'; + } else { + hourSkeletonChar = 'H'; + } + } else if (aBag.hourCycle) { + switch (*aBag.hourCycle) { + case DateTimeFormat::HourCycle::H11: + case DateTimeFormat::HourCycle::H12: + hourSkeletonChar = 'h'; + break; + case DateTimeFormat::HourCycle::H23: + case DateTimeFormat::HourCycle::H24: + hourSkeletonChar = 'H'; + break; + } + } + if (aBag.hour) { + switch (*aBag.hour) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + break; + } + } + // ICU requires that "B" is set after the "j" hour skeleton symbol. + // https://unicode-org.atlassian.net/browse/ICU-20731 + if (aBag.dayPeriod) { + switch (*aBag.dayPeriod) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"BBBBB")); + break; + case DateTimeFormat::Text::Short: + MOZ_TRY(PushString(aSkeleton, u"B")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"BBBB")); + break; + } + } + if (aBag.minute) { + switch (*aBag.minute) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"mm")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"m")); + break; + } + } + if (aBag.second) { + switch (*aBag.second) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"ss")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"s")); + break; + } + } + if (aBag.fractionalSecondDigits) { + switch (*aBag.fractionalSecondDigits) { + case 1: + MOZ_TRY(PushString(aSkeleton, u"S")); + break; + case 2: + MOZ_TRY(PushString(aSkeleton, u"SS")); + break; + default: + MOZ_TRY(PushString(aSkeleton, u"SSS")); + break; + } + } + if (aBag.timeZoneName) { + switch (*aBag.timeZoneName) { + case DateTimeFormat::TimeZoneName::Short: + MOZ_TRY(PushString(aSkeleton, u"z")); + break; + case DateTimeFormat::TimeZoneName::Long: + MOZ_TRY(PushString(aSkeleton, u"zzzz")); + break; + case DateTimeFormat::TimeZoneName::ShortOffset: + MOZ_TRY(PushString(aSkeleton, u"O")); + break; + case DateTimeFormat::TimeZoneName::LongOffset: + MOZ_TRY(PushString(aSkeleton, u"OOOO")); + break; + case DateTimeFormat::TimeZoneName::ShortGeneric: + MOZ_TRY(PushString(aSkeleton, u"v")); + break; + case DateTimeFormat::TimeZoneName::LongGeneric: + MOZ_TRY(PushString(aSkeleton, u"vvvv")); + break; + } + } + return Ok(); +} + +/* static */ +Result<UniquePtr<DateTimeFormat>, ICUError> +DateTimeFormat::TryCreateFromComponents( + Span<const char> aLocale, const DateTimeFormat::ComponentsBag& aBag, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe<Span<const char16_t>> aTimeZoneOverride) { + DateTimeFormat::SkeletonVector skeleton; + MOZ_TRY(ToICUSkeleton(aBag, skeleton)); + return TryCreateFromSkeleton(aLocale, skeleton, aDateTimePatternGenerator, + aBag.hourCycle, aTimeZoneOverride); +} + +/* static */ +Result<UniquePtr<DateTimeFormat>, ICUError> +DateTimeFormat::TryCreateFromPattern( + Span<const char> aLocale, Span<const char16_t> aPattern, + Maybe<Span<const char16_t>> aTimeZoneOverride) { + UErrorCode status = U_ZERO_ERROR; + + // The time zone is optional. + int32_t tzIDLength = -1; + const UChar* tzID = nullptr; + if (aTimeZoneOverride) { + tzIDLength = static_cast<int32_t>(aTimeZoneOverride->size()); + tzID = aTimeZoneOverride->data(); + } + + // Create the date formatter. + UDateFormat* dateFormat = udat_open( + UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength, + aPattern.data(), static_cast<int32_t>(aPattern.size()), &status); + + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + // The DateTimeFormat wrapper will control the life cycle of the ICU + // dateFormat object. + return UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); +} + +/* static */ +Result<UniquePtr<DateTimeFormat>, ICUError> +DateTimeFormat::TryCreateFromSkeleton( + Span<const char> aLocale, Span<const char16_t> aSkeleton, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe<DateTimeFormat::HourCycle> aHourCycle, + Maybe<Span<const char16_t>> aTimeZoneOverride) { + if (!aDateTimePatternGenerator) { + return Err(ICUError::InternalError); + } + + // Compute the best pattern for the skeleton. + DateTimeFormat::PatternVector pattern; + auto options = PatternMatchOptions(aSkeleton); + MOZ_TRY( + aDateTimePatternGenerator->GetBestPattern(aSkeleton, pattern, options)); + + if (aHourCycle) { + DateTimeFormat::ReplaceHourSymbol(pattern, *aHourCycle); + } + + auto result = + DateTimeFormat::TryCreateFromPattern(aLocale, pattern, aTimeZoneOverride); + if (result.isErr()) { + return Err(result.unwrapErr()); + } + auto dateTimeFormat = result.unwrap(); + MOZ_TRY(dateTimeFormat->CacheSkeleton(aSkeleton)); + return dateTimeFormat; +} + +ICUResult DateTimeFormat::CacheSkeleton(Span<const char16_t> aSkeleton) { + if (mOriginalSkeleton.append(aSkeleton.Elements(), aSkeleton.Length())) { + return Ok(); + } + return Err(ICUError::OutOfMemory); +} + +void DateTimeFormat::SetStartTimeIfGregorian(double aTime) { + UErrorCode status = U_ZERO_ERROR; + UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(mDateFormat)); + ucal_setGregorianChange(cal, aTime, &status); + // An error here means the calendar is not Gregorian, and can be ignored. +} + +/* static */ +Result<UniquePtr<Calendar>, ICUError> DateTimeFormat::CloneCalendar( + double aUnixEpoch) const { + UErrorCode status = U_ZERO_ERROR; + UCalendar* calendarRaw = ucal_clone(udat_getCalendar(mDateFormat), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + auto calendar = MakeUnique<Calendar>(calendarRaw); + + MOZ_TRY(calendar->SetTimeInMs(aUnixEpoch)); + + return calendar; +} + +/** + * ICU locale identifier consisting of a language and a region subtag. + */ +class LanguageRegionLocaleId { + // unicode_language_subtag = alpha{2,3} | alpha{5,8} ; + static constexpr size_t LanguageLength = 8; + + // unicode_region_subtag = (alpha{2} | digit{3}) ; + static constexpr size_t RegionLength = 3; + + // Add +1 to account for the separator. + static constexpr size_t LRLength = LanguageLength + RegionLength + 1; + + // Add +1 to zero terminate the string. + char mLocale[LRLength + 1] = {}; + + // Pointer to the start of the region subtag within |locale_|. + char* mRegion = nullptr; + + public: + LanguageRegionLocaleId(Span<const char> aLanguage, + Maybe<Span<const char>> aRegion); + + const char* languageRegion() const { return mLocale; } + const char* region() const { return mRegion; } +}; + +LanguageRegionLocaleId::LanguageRegionLocaleId( + Span<const char> aLanguage, Maybe<Span<const char>> aRegion) { + MOZ_RELEASE_ASSERT(aLanguage.Length() <= LanguageLength); + MOZ_RELEASE_ASSERT(!aRegion || aRegion->Length() <= RegionLength); + + size_t languageLength = aLanguage.Length(); + + std::memcpy(mLocale, aLanguage.Elements(), languageLength); + + // ICU locale identifiers are separated by underscores. + mLocale[languageLength] = '_'; + + mRegion = mLocale + languageLength + 1; + if (aRegion) { + std::memcpy(mRegion, aRegion->Elements(), aRegion->Length()); + } else { + // Use "001" (UN M.49 code for the World) as the fallback to match ICU. + std::strcpy(mRegion, "001"); + } +} + +/* static */ +Result<DateTimeFormat::HourCyclesVector, ICUError> +DateTimeFormat::GetAllowedHourCycles(Span<const char> aLanguage, + Maybe<Span<const char>> aRegion) { + // ICU doesn't expose a public API to retrieve the hour cyles for a locale, so + // we have to reconstruct |DateTimePatternGenerator::getAllowedHourFormats()| + // using the public UResourceBundle API. + // + // The time data format is specified in UTS 35 at [1] and the data itself is + // located at [2]. + // + // [1] https://unicode.org/reports/tr35/tr35-dates.html#Time_Data + // [2] + // https://github.com/unicode-org/cldr/blob/master/common/supplemental/supplementalData.xml + + HourCyclesVector result; + + // Reserve space for the maximum number of hour cycles. This call always + // succeeds because it matches the inline capacity. We can now infallibly + // append all hour cycles to the vector. + MOZ_ALWAYS_TRUE(result.reserve(HourCyclesVector::InlineLength)); + + LanguageRegionLocaleId localeId(aLanguage, aRegion); + + // First open the "supplementalData" resource bundle. + UErrorCode status = U_ZERO_ERROR; + UResourceBundle* res = ures_openDirect(nullptr, "supplementalData", &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject<UResourceBundle, ures_close> closeRes(res); + MOZ_ASSERT(ures_getType(res) == URES_TABLE); + + // Locate "timeDate" within the "supplementalData" resource bundle. + UResourceBundle* timeData = ures_getByKey(res, "timeData", nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject<UResourceBundle, ures_close> closeTimeData(timeData); + MOZ_ASSERT(ures_getType(timeData) == URES_TABLE); + + // Try to find a matching resource within "timeData". The two possible keys + // into the "timeData" resource bundle are `language_region` and `region`. + // Prefer `language_region` and otherwise fallback to `region`. + UResourceBundle* hclocale = + ures_getByKey(timeData, localeId.languageRegion(), nullptr, &status); + if (status == U_MISSING_RESOURCE_ERROR) { + status = U_ZERO_ERROR; + hclocale = ures_getByKey(timeData, localeId.region(), nullptr, &status); + } + if (status == U_MISSING_RESOURCE_ERROR) { + // Default to "h23" if no resource was found at all. This matches ICU. + result.infallibleAppend(HourCycle::H23); + return result; + } + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject<UResourceBundle, ures_close> closeHcLocale(hclocale); + MOZ_ASSERT(ures_getType(hclocale) == URES_TABLE); + + EnumSet<HourCycle> added{}; + + auto addToResult = [&](const UChar* str, int32_t len) { + // An hour cycle strings is one of "K", "h", "H", or "k"; optionally + // followed by the suffix "b" or "B". We ignore the suffix because day + // periods can't be expressed in the "hc" Unicode extension. + MOZ_ASSERT(len == 1 || len == 2); + + // Default to "h23" for unsupported hour cycle strings. + HourCycle hc = HourCycle::H23; + switch (str[0]) { + case 'K': + hc = HourCycle::H11; + break; + case 'h': + hc = HourCycle::H12; + break; + case 'H': + hc = HourCycle::H23; + break; + case 'k': + hc = HourCycle::H24; + break; + } + + // Add each unique hour cycle to the result array. + if (!added.contains(hc)) { + added += hc; + + result.infallibleAppend(hc); + } + }; + + // Determine the preferred hour cycle for the locale. + int32_t len = 0; + const UChar* hc = ures_getStringByKey(hclocale, "preferred", &len, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + addToResult(hc, len); + + // Find any additionally allowed hour cycles of the locale. + UResourceBundle* allowed = + ures_getByKey(hclocale, "allowed", nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject<UResourceBundle, ures_close> closeAllowed(allowed); + MOZ_ASSERT(ures_getType(allowed) == URES_ARRAY || + ures_getType(allowed) == URES_STRING); + + while (ures_hasNext(allowed)) { + int32_t len = 0; + const UChar* hc = ures_getNextString(allowed, &len, nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + addToResult(hc, len); + } + + return result; +} + +Result<DateTimeFormat::ComponentsBag, ICUError> +DateTimeFormat::ResolveComponents() { + // Maps an ICU pattern string to a corresponding set of date-time components + // and their values, and adds properties for these components to the result + // object, which will be returned by the resolvedOptions method. For the + // interpretation of ICU pattern characters, see + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + + DateTimeFormat::PatternVector pattern{}; + VectorToBufferAdaptor buffer(pattern); + MOZ_TRY(GetPattern(buffer)); + + DateTimeFormat::ComponentsBag bag{}; + + using Text = DateTimeFormat::Text; + using HourCycle = DateTimeFormat::HourCycle; + using Numeric = DateTimeFormat::Numeric; + using Month = DateTimeFormat::Month; + + auto text = Text::Long; + auto numeric = Numeric::Numeric; + auto month = Month::Long; + uint8_t fractionalSecondDigits = 0; + + for (size_t i = 0, len = pattern.length(); i < len;) { + char16_t c = pattern[i++]; + if (c == u'\'') { + // Skip past string literals. + while (i < len && pattern[i] != u'\'') { + i++; + } + i++; + continue; + } + + // Count how many times the character is repeated. + size_t count = 1; + while (i < len && pattern[i] == c) { + i++; + count++; + } + + // Determine the enum case of the field. + switch (c) { + // "text" cases + case u'G': + case u'E': + case u'c': + case u'B': + case u'z': + case u'O': + case u'v': + case u'V': + if (count <= 3) { + text = Text::Short; + } else if (count == 4) { + text = Text::Long; + } else { + text = Text::Narrow; + } + break; + // "number" cases + case u'y': + case u'd': + case u'h': + case u'H': + case u'm': + case u's': + case u'k': + case u'K': + if (count == 2) { + numeric = Numeric::TwoDigit; + } else { + numeric = Numeric::Numeric; + } + break; + // "text & number" cases + case u'M': + case u'L': + if (count == 1) { + month = Month::Numeric; + } else if (count == 2) { + month = Month::TwoDigit; + } else if (count == 3) { + month = Month::Short; + } else if (count == 4) { + month = Month::Long; + } else { + month = Month::Narrow; + } + break; + case u'S': + fractionalSecondDigits = count; + break; + default: { + // skip other pattern characters and literal text + } + } + + // Map ICU pattern characters back to the corresponding date-time + // components of DateTimeFormat. See + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + switch (c) { + case u'E': + case u'c': + bag.weekday = Some(text); + break; + case u'G': + bag.era = Some(text); + break; + case u'y': + bag.year = Some(numeric); + break; + case u'M': + case u'L': + bag.month = Some(month); + break; + case u'd': + bag.day = Some(numeric); + break; + case u'B': + bag.dayPeriod = Some(text); + break; + case u'K': + bag.hourCycle = Some(HourCycle::H11); + bag.hour = Some(numeric); + bag.hour12 = Some(true); + break; + case u'h': + bag.hourCycle = Some(HourCycle::H12); + bag.hour = Some(numeric); + bag.hour12 = Some(true); + break; + case u'H': + bag.hourCycle = Some(HourCycle::H23); + bag.hour = Some(numeric); + bag.hour12 = Some(false); + break; + case u'k': + bag.hourCycle = Some(HourCycle::H24); + bag.hour = Some(numeric); + bag.hour12 = Some(false); + break; + case u'm': + bag.minute = Some(numeric); + break; + case u's': + bag.second = Some(numeric); + break; + case u'S': + bag.fractionalSecondDigits = Some(fractionalSecondDigits); + break; + case u'z': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::Long); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::Short); + break; + } + break; + case u'O': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::LongOffset); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::ShortOffset); + break; + } + break; + case u'v': + case u'V': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::LongGeneric); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::ShortGeneric); + break; + } + break; + } + } + return bag; +} + +const char* DateTimeFormat::ToString( + DateTimeFormat::TimeZoneName aTimeZoneName) { + switch (aTimeZoneName) { + case TimeZoneName::Long: + return "long"; + case TimeZoneName::Short: + return "short"; + case TimeZoneName::ShortOffset: + return "shortOffset"; + case TimeZoneName::LongOffset: + return "longOffset"; + case TimeZoneName::ShortGeneric: + return "shortGeneric"; + case TimeZoneName::LongGeneric: + return "longGeneric"; + } + MOZ_CRASH("Unexpected DateTimeFormat::TimeZoneName"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Month aMonth) { + switch (aMonth) { + case Month::Numeric: + return "numeric"; + case Month::TwoDigit: + return "2-digit"; + case Month::Long: + return "long"; + case Month::Short: + return "short"; + case Month::Narrow: + return "narrow"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Month"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Text aText) { + switch (aText) { + case Text::Long: + return "long"; + case Text::Short: + return "short"; + case Text::Narrow: + return "narrow"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Text"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Numeric aNumeric) { + switch (aNumeric) { + case Numeric::Numeric: + return "numeric"; + case Numeric::TwoDigit: + return "2-digit"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Numeric"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Style aStyle) { + switch (aStyle) { + case Style::Full: + return "full"; + case Style::Long: + return "long"; + case Style::Medium: + return "medium"; + case Style::Short: + return "short"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Style"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::HourCycle aHourCycle) { + switch (aHourCycle) { + case HourCycle::H11: + return "h11"; + case HourCycle::H12: + return "h12"; + case HourCycle::H23: + return "h23"; + case HourCycle::H24: + return "h24"; + } + MOZ_CRASH("Unexpected DateTimeFormat::HourCycle"); +} + +ICUResult DateTimeFormat::TryFormatToParts( + UFieldPositionIterator* aFieldPositionIterator, size_t aSpanSize, + DateTimePartVector& aParts) const { + ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose( + aFieldPositionIterator); + + size_t lastEndIndex = 0; + auto AppendPart = [&](DateTimePartType type, size_t endIndex) { + // For the part defined in FormatDateTimeToParts, it doesn't have ||Source|| + // property, we store Shared for simplicity, + if (!aParts.emplaceBack(type, endIndex, DateTimePartSource::Shared)) { + return false; + } + + lastEndIndex = endIndex; + return true; + }; + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(aFieldPositionIterator, &beginIndexInt, + &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex = AssertedCast<size_t>(beginIndexInt); + size_t endIndex = AssertedCast<size_t>(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + DateTimePartType type = + ConvertUFormatFieldToPartType(static_cast<UDateFormatField>(fieldInt)); + if (lastEndIndex < beginIndex) { + if (!AppendPart(DateTimePartType::Literal, beginIndex)) { + return Err(ICUError::InternalError); + } + } + + if (!AppendPart(type, endIndex)) { + return Err(ICUError::InternalError); + } + } + + // Append any final literal. + if (lastEndIndex < aSpanSize) { + if (!AppendPart(DateTimePartType::Literal, aSpanSize)) { + return Err(ICUError::InternalError); + } + } + + return Ok(); +} + +} // namespace mozilla::intl |