DateTimeFormat.cpp (41424B)
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 <algorithm> 6 #include <cstring> 7 8 #include "unicode/ucal.h" 9 #include "unicode/udat.h" 10 #include "unicode/udatpg.h" 11 #include "unicode/ures.h" 12 13 #include "DateTimeFormatUtils.h" 14 #include "ScopedICUObject.h" 15 16 #include "mozilla/Buffer.h" 17 #include "mozilla/EnumSet.h" 18 #include "mozilla/intl/Calendar.h" 19 #include "mozilla/intl/DateTimeFormat.h" 20 #include "mozilla/intl/DateTimePatternGenerator.h" 21 22 namespace mozilla::intl { 23 24 DateTimeFormat::~DateTimeFormat() { 25 MOZ_ASSERT(mDateFormat); 26 udat_close(mDateFormat); 27 } 28 29 static UDateFormatStyle ToUDateFormatStyle( 30 Maybe<DateTimeFormat::Style> aLength) { 31 if (!aLength) { 32 return UDAT_NONE; 33 } 34 switch (*aLength) { 35 case DateTimeFormat::Style::Full: 36 return UDAT_FULL; 37 case DateTimeFormat::Style::Long: 38 return UDAT_LONG; 39 case DateTimeFormat::Style::Medium: 40 return UDAT_MEDIUM; 41 case DateTimeFormat::Style::Short: 42 return UDAT_SHORT; 43 } 44 MOZ_ASSERT_UNREACHABLE(); 45 // Do not use the default: branch so that the enum is exhaustively checked. 46 return UDAT_NONE; 47 } 48 49 /** 50 * Parse a pattern according to the format specified in 51 * <https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>. 52 */ 53 template <typename CharT> 54 class PatternIterator { 55 CharT* iter; 56 const CharT* const end; 57 58 public: 59 explicit PatternIterator(mozilla::Span<CharT> aPattern) 60 : iter(aPattern.data()), end(aPattern.data() + aPattern.size()) {} 61 62 CharT* next() { 63 MOZ_ASSERT(iter != nullptr); 64 65 bool inQuote = false; 66 while (iter < end) { 67 CharT* cur = iter++; 68 if (*cur == '\'') { 69 inQuote = !inQuote; 70 } else if (!inQuote) { 71 return cur; 72 } 73 } 74 75 iter = nullptr; 76 return nullptr; 77 } 78 }; 79 80 Maybe<DateTimeFormat::HourCycle> DateTimeFormat::HourCycleFromPattern( 81 Span<const char16_t> aPattern) { 82 PatternIterator<const char16_t> iter(aPattern); 83 while (const auto* ptr = iter.next()) { 84 switch (*ptr) { 85 case 'K': 86 return Some(DateTimeFormat::HourCycle::H11); 87 case 'h': 88 return Some(DateTimeFormat::HourCycle::H12); 89 case 'H': 90 return Some(DateTimeFormat::HourCycle::H23); 91 case 'k': 92 return Some(DateTimeFormat::HourCycle::H24); 93 } 94 } 95 return Nothing(); 96 } 97 98 static bool IsHour12(DateTimeFormat::HourCycle aHourCycle) { 99 return aHourCycle == DateTimeFormat::HourCycle::H11 || 100 aHourCycle == DateTimeFormat::HourCycle::H12; 101 } 102 103 static char16_t HourSymbol(DateTimeFormat::HourCycle aHourCycle) { 104 switch (aHourCycle) { 105 case DateTimeFormat::HourCycle::H11: 106 return 'K'; 107 case DateTimeFormat::HourCycle::H12: 108 return 'h'; 109 case DateTimeFormat::HourCycle::H23: 110 return 'H'; 111 case DateTimeFormat::HourCycle::H24: 112 return 'k'; 113 } 114 MOZ_CRASH("unexpected hour cycle"); 115 } 116 117 enum class PatternField { Hour, Minute, Second, Other }; 118 119 template <typename CharT> 120 static PatternField ToPatternField(CharT aCh) { 121 if (aCh == 'K' || aCh == 'h' || aCh == 'H' || aCh == 'k' || aCh == 'j') { 122 return PatternField::Hour; 123 } 124 if (aCh == 'm') { 125 return PatternField::Minute; 126 } 127 if (aCh == 's') { 128 return PatternField::Second; 129 } 130 return PatternField::Other; 131 } 132 133 /** 134 * Replaces all hour pattern characters in |patternOrSkeleton| to use the 135 * matching hour representation for |hourCycle|. 136 */ 137 /* static */ 138 void DateTimeFormat::ReplaceHourSymbol( 139 mozilla::Span<char16_t> aPatternOrSkeleton, 140 DateTimeFormat::HourCycle aHourCycle) { 141 char16_t replacement = HourSymbol(aHourCycle); 142 PatternIterator<char16_t> iter(aPatternOrSkeleton); 143 while (auto* ptr = iter.next()) { 144 auto field = ToPatternField(*ptr); 145 if (field == PatternField::Hour) { 146 *ptr = replacement; 147 } 148 } 149 } 150 151 /** 152 * Find a matching pattern using the requested hour-12 options. 153 * 154 * This function is needed to work around the following two issues. 155 * - https://unicode-org.atlassian.net/browse/ICU-21023 156 * - https://unicode-org.atlassian.net/browse/CLDR-13425 157 * 158 * We're currently using a relatively simple workaround, which doesn't give the 159 * most accurate results. For example: 160 * 161 * ``` 162 * var dtf = new Intl.DateTimeFormat("en", { 163 * timeZone: "UTC", 164 * dateStyle: "long", 165 * timeStyle: "long", 166 * hourCycle: "h12", 167 * }); 168 * print(dtf.format(new Date("2020-01-01T00:00Z"))); 169 * ``` 170 * 171 * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through 172 * |DateTimePatternGenerator::GetSkeleton| and then 173 * |DateTimePatternGenerator::GetBestPattern| to find an equivalent pattern for 174 * "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so the 175 * combinator element " 'at' " was lost in the process. 176 */ 177 /* static */ 178 ICUResult DateTimeFormat::FindPatternWithHourCycle( 179 DateTimePatternGenerator& aDateTimePatternGenerator, 180 DateTimeFormat::PatternVector& aPattern, bool aHour12, 181 DateTimeFormat::SkeletonVector& aSkeleton) { 182 MOZ_TRY(mozilla::intl::DateTimePatternGenerator::GetSkeleton(aPattern, 183 aSkeleton)); 184 185 // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". 186 DateTimeFormat::ReplaceHourSymbol(aSkeleton, 187 aHour12 ? DateTimeFormat::HourCycle::H12 188 : DateTimeFormat::HourCycle::H23); 189 190 MOZ_TRY(aDateTimePatternGenerator.GetBestPattern(aSkeleton, aPattern)); 191 192 return Ok(); 193 } 194 195 static auto PatternMatchOptions(mozilla::Span<const char16_t> aSkeleton) { 196 // Values for hour, minute, and second are: 197 // - absent: 0 198 // - numeric: 1 199 // - 2-digit: 2 200 int32_t hour = 0; 201 int32_t minute = 0; 202 int32_t second = 0; 203 204 PatternIterator<const char16_t> iter(aSkeleton); 205 while (const auto* ptr = iter.next()) { 206 switch (ToPatternField(*ptr)) { 207 case PatternField::Hour: 208 MOZ_ASSERT(hour < 2); 209 hour += 1; 210 break; 211 case PatternField::Minute: 212 MOZ_ASSERT(minute < 2); 213 minute += 1; 214 break; 215 case PatternField::Second: 216 MOZ_ASSERT(second < 2); 217 second += 1; 218 break; 219 case PatternField::Other: 220 break; 221 } 222 } 223 224 // Adjust the field length when the user requested '2-digit' representation. 225 // 226 // We can't just always adjust the field length, because 227 // 1. The default value for hour, minute, and second fields is 'numeric'. If 228 // the length is always adjusted, |date.toLocaleTime()| will start to 229 // return strings like "1:5:9 AM" instead of "1:05:09 AM". 230 // 2. ICU doesn't support to adjust the field length to 'numeric' in certain 231 // cases. For example when the locale is "de" (German): 232 // a. hour='numeric' and minute='2-digit' will return "1:05". 233 // b. whereas hour='numeric' and minute='numeric' will return "01:05". 234 // 235 // Therefore we only support adjusting the field length when the user 236 // explicitly requested the '2-digit' representation. 237 238 using PatternMatchOption = 239 mozilla::intl::DateTimePatternGenerator::PatternMatchOption; 240 mozilla::EnumSet<PatternMatchOption> options; 241 if (hour == 2) { 242 options += PatternMatchOption::HourField; 243 } 244 if (minute == 2) { 245 options += PatternMatchOption::MinuteField; 246 } 247 if (second == 2) { 248 options += PatternMatchOption::SecondField; 249 } 250 return options; 251 } 252 253 /* static */ 254 Result<UniquePtr<DateTimeFormat>, ICUError> DateTimeFormat::TryCreateFromStyle( 255 Span<const char> aLocale, const StyleBag& aStyleBag, 256 DateTimePatternGenerator* aDateTimePatternGenerator, 257 Maybe<Span<const char16_t>> aTimeZoneOverride) { 258 auto dateStyle = ToUDateFormatStyle(aStyleBag.date); 259 auto timeStyle = ToUDateFormatStyle(aStyleBag.time); 260 261 if (dateStyle == UDAT_NONE && timeStyle == UDAT_NONE) { 262 dateStyle = UDAT_DEFAULT; 263 timeStyle = UDAT_DEFAULT; 264 } 265 266 // The time zone is optional. 267 int32_t tzIDLength = -1; 268 const UChar* tzID = nullptr; 269 if (aTimeZoneOverride) { 270 tzIDLength = static_cast<int32_t>(aTimeZoneOverride->size()); 271 tzID = aTimeZoneOverride->Elements(); 272 } 273 274 UErrorCode status = U_ZERO_ERROR; 275 UDateFormat* dateFormat = 276 udat_open(timeStyle, dateStyle, IcuLocale(aLocale), tzID, tzIDLength, 277 /* pattern */ nullptr, /* pattern length */ -1, &status); 278 if (U_FAILURE(status)) { 279 return Err(ToICUError(status)); 280 } 281 282 MOZ_TRY(ApplyCalendarOverride(dateFormat)); 283 284 auto df = UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); 285 286 if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) { 287 // Only adjust the style pattern for time if there is an override. 288 // Extract the pattern and adjust it for the preferred hour cycle. 289 DateTimeFormat::PatternVector pattern{}; 290 291 VectorToBufferAdaptor buffer(pattern); 292 MOZ_TRY(df->GetPattern(buffer)); 293 294 Maybe<DateTimeFormat::HourCycle> hcPattern = HourCycleFromPattern(pattern); 295 DateTimeFormat::SkeletonVector skeleton{}; 296 297 if (hcPattern) { 298 bool wantHour12 = 299 aStyleBag.hour12 ? *aStyleBag.hour12 : IsHour12(*aStyleBag.hourCycle); 300 if (wantHour12 == IsHour12(*hcPattern)) { 301 // Return the date-time format when its hour-cycle settings match the 302 // requested options. 303 if (aStyleBag.hour12 || *hcPattern == *aStyleBag.hourCycle) { 304 return df; 305 } 306 } else { 307 MOZ_ASSERT(aDateTimePatternGenerator); 308 MOZ_TRY(DateTimeFormat::FindPatternWithHourCycle( 309 *aDateTimePatternGenerator, pattern, wantHour12, skeleton)); 310 } 311 // Replace the hourCycle, if present, in the pattern string. But only do 312 // this if no hour12 option is present, because the latter takes 313 // precedence over hourCycle. 314 if (!aStyleBag.hour12) { 315 DateTimeFormat::ReplaceHourSymbol(pattern, *aStyleBag.hourCycle); 316 } 317 318 auto result = DateTimeFormat::TryCreateFromPattern(aLocale, pattern, 319 aTimeZoneOverride); 320 if (result.isErr()) { 321 return Err(result.unwrapErr()); 322 } 323 auto dateTimeFormat = result.unwrap(); 324 MOZ_TRY(dateTimeFormat->CacheSkeleton(skeleton)); 325 return dateTimeFormat; 326 } 327 } 328 329 return df; 330 } 331 332 DateTimeFormat::DateTimeFormat(UDateFormat* aDateFormat) { 333 MOZ_RELEASE_ASSERT(aDateFormat, "Expected aDateFormat to not be a nullptr."); 334 mDateFormat = aDateFormat; 335 } 336 337 // A helper to ergonomically push a string onto a string vector. 338 template <typename V, size_t N> 339 static ICUResult PushString(V& aVec, const char16_t (&aString)[N]) { 340 if (!aVec.append(aString, N - 1)) { 341 return Err(ICUError::OutOfMemory); 342 } 343 return Ok(); 344 } 345 346 // A helper to ergonomically push a char onto a string vector. 347 template <typename V> 348 static ICUResult PushChar(V& aVec, char16_t aCh) { 349 if (!aVec.append(aCh)) { 350 return Err(ICUError::OutOfMemory); 351 } 352 return Ok(); 353 } 354 355 /** 356 * Returns an ICU skeleton string representing the specified options. 357 * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 358 */ 359 ICUResult ToICUSkeleton(const DateTimeFormat::ComponentsBag& aBag, 360 DateTimeFormat::SkeletonVector& aSkeleton) { 361 // Create an ICU skeleton representing the specified aBag. See 362 if (aBag.weekday) { 363 switch (*aBag.weekday) { 364 case DateTimeFormat::Text::Narrow: 365 MOZ_TRY(PushString(aSkeleton, u"EEEEE")); 366 break; 367 case DateTimeFormat::Text::Short: 368 MOZ_TRY(PushString(aSkeleton, u"E")); 369 break; 370 case DateTimeFormat::Text::Long: 371 MOZ_TRY(PushString(aSkeleton, u"EEEE")); 372 } 373 } 374 if (aBag.era) { 375 switch (*aBag.era) { 376 case DateTimeFormat::Text::Narrow: 377 MOZ_TRY(PushString(aSkeleton, u"GGGGG")); 378 break; 379 case DateTimeFormat::Text::Short: 380 // Use "GGG" instead of "G" to return the same results as other 381 // browsers. This is exploiting the following ICU bug 382 // <https://unicode-org.atlassian.net/browse/ICU-22138>. As soon as that 383 // bug has been fixed, we can change this back to "G". 384 // 385 // In practice the bug only affects "G", so we only apply it for "G" 386 // and not for other symbols like "B" or "z". 387 MOZ_TRY(PushString(aSkeleton, u"GGG")); 388 break; 389 case DateTimeFormat::Text::Long: 390 MOZ_TRY(PushString(aSkeleton, u"GGGG")); 391 break; 392 } 393 } 394 if (aBag.year) { 395 switch (*aBag.year) { 396 case DateTimeFormat::Numeric::TwoDigit: 397 MOZ_TRY(PushString(aSkeleton, u"yy")); 398 break; 399 case DateTimeFormat::Numeric::Numeric: 400 MOZ_TRY(PushString(aSkeleton, u"y")); 401 break; 402 } 403 } 404 if (aBag.month) { 405 switch (*aBag.month) { 406 case DateTimeFormat::Month::TwoDigit: 407 MOZ_TRY(PushString(aSkeleton, u"MM")); 408 break; 409 case DateTimeFormat::Month::Numeric: 410 MOZ_TRY(PushString(aSkeleton, u"M")); 411 break; 412 case DateTimeFormat::Month::Narrow: 413 MOZ_TRY(PushString(aSkeleton, u"MMMMM")); 414 break; 415 case DateTimeFormat::Month::Short: 416 MOZ_TRY(PushString(aSkeleton, u"MMM")); 417 break; 418 case DateTimeFormat::Month::Long: 419 MOZ_TRY(PushString(aSkeleton, u"MMMM")); 420 break; 421 } 422 } 423 if (aBag.day) { 424 switch (*aBag.day) { 425 case DateTimeFormat::Numeric::TwoDigit: 426 MOZ_TRY(PushString(aSkeleton, u"dd")); 427 break; 428 case DateTimeFormat::Numeric::Numeric: 429 MOZ_TRY(PushString(aSkeleton, u"d")); 430 break; 431 } 432 } 433 434 // If hour12 and hourCycle are both present, hour12 takes precedence. 435 char16_t hourSkeletonChar = 'j'; 436 if (aBag.hour12) { 437 if (*aBag.hour12) { 438 hourSkeletonChar = 'h'; 439 } else { 440 hourSkeletonChar = 'H'; 441 } 442 } else if (aBag.hourCycle) { 443 switch (*aBag.hourCycle) { 444 case DateTimeFormat::HourCycle::H11: 445 case DateTimeFormat::HourCycle::H12: 446 hourSkeletonChar = 'h'; 447 break; 448 case DateTimeFormat::HourCycle::H23: 449 case DateTimeFormat::HourCycle::H24: 450 hourSkeletonChar = 'H'; 451 break; 452 } 453 } 454 if (aBag.hour) { 455 switch (*aBag.hour) { 456 case DateTimeFormat::Numeric::TwoDigit: 457 MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); 458 MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); 459 break; 460 case DateTimeFormat::Numeric::Numeric: 461 MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); 462 break; 463 } 464 } 465 // ICU requires that "B" is set after the "j" hour skeleton symbol. 466 // https://unicode-org.atlassian.net/browse/ICU-20731 467 if (aBag.dayPeriod) { 468 switch (*aBag.dayPeriod) { 469 case DateTimeFormat::Text::Narrow: 470 MOZ_TRY(PushString(aSkeleton, u"BBBBB")); 471 break; 472 case DateTimeFormat::Text::Short: 473 MOZ_TRY(PushString(aSkeleton, u"B")); 474 break; 475 case DateTimeFormat::Text::Long: 476 MOZ_TRY(PushString(aSkeleton, u"BBBB")); 477 break; 478 } 479 } 480 if (aBag.minute) { 481 switch (*aBag.minute) { 482 case DateTimeFormat::Numeric::TwoDigit: 483 MOZ_TRY(PushString(aSkeleton, u"mm")); 484 break; 485 case DateTimeFormat::Numeric::Numeric: 486 MOZ_TRY(PushString(aSkeleton, u"m")); 487 break; 488 } 489 } 490 if (aBag.second) { 491 switch (*aBag.second) { 492 case DateTimeFormat::Numeric::TwoDigit: 493 MOZ_TRY(PushString(aSkeleton, u"ss")); 494 break; 495 case DateTimeFormat::Numeric::Numeric: 496 MOZ_TRY(PushString(aSkeleton, u"s")); 497 break; 498 } 499 } 500 if (aBag.fractionalSecondDigits) { 501 switch (*aBag.fractionalSecondDigits) { 502 case 1: 503 MOZ_TRY(PushString(aSkeleton, u"S")); 504 break; 505 case 2: 506 MOZ_TRY(PushString(aSkeleton, u"SS")); 507 break; 508 default: 509 MOZ_TRY(PushString(aSkeleton, u"SSS")); 510 break; 511 } 512 } 513 if (aBag.timeZoneName) { 514 switch (*aBag.timeZoneName) { 515 case DateTimeFormat::TimeZoneName::Short: 516 MOZ_TRY(PushString(aSkeleton, u"z")); 517 break; 518 case DateTimeFormat::TimeZoneName::Long: 519 MOZ_TRY(PushString(aSkeleton, u"zzzz")); 520 break; 521 case DateTimeFormat::TimeZoneName::ShortOffset: 522 MOZ_TRY(PushString(aSkeleton, u"O")); 523 break; 524 case DateTimeFormat::TimeZoneName::LongOffset: 525 MOZ_TRY(PushString(aSkeleton, u"OOOO")); 526 break; 527 case DateTimeFormat::TimeZoneName::ShortGeneric: 528 MOZ_TRY(PushString(aSkeleton, u"v")); 529 break; 530 case DateTimeFormat::TimeZoneName::LongGeneric: 531 MOZ_TRY(PushString(aSkeleton, u"vvvv")); 532 break; 533 } 534 } 535 return Ok(); 536 } 537 538 /* static */ 539 Result<UniquePtr<DateTimeFormat>, ICUError> 540 DateTimeFormat::TryCreateFromComponents( 541 Span<const char> aLocale, const DateTimeFormat::ComponentsBag& aBag, 542 DateTimePatternGenerator* aDateTimePatternGenerator, 543 Maybe<Span<const char16_t>> aTimeZoneOverride) { 544 DateTimeFormat::SkeletonVector skeleton; 545 MOZ_TRY(ToICUSkeleton(aBag, skeleton)); 546 return TryCreateFromSkeleton(aLocale, skeleton, aDateTimePatternGenerator, 547 aBag.hourCycle, aTimeZoneOverride); 548 } 549 550 /* static */ 551 Result<UniquePtr<DateTimeFormat>, ICUError> 552 DateTimeFormat::TryCreateFromPattern( 553 Span<const char> aLocale, Span<const char16_t> aPattern, 554 Maybe<Span<const char16_t>> aTimeZoneOverride) { 555 UErrorCode status = U_ZERO_ERROR; 556 557 // The time zone is optional. 558 int32_t tzIDLength = -1; 559 const UChar* tzID = nullptr; 560 if (aTimeZoneOverride) { 561 tzIDLength = static_cast<int32_t>(aTimeZoneOverride->size()); 562 tzID = aTimeZoneOverride->data(); 563 } 564 565 // Create the date formatter. 566 UDateFormat* dateFormat = udat_open( 567 UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength, 568 aPattern.data(), static_cast<int32_t>(aPattern.size()), &status); 569 if (U_FAILURE(status)) { 570 return Err(ToICUError(status)); 571 } 572 573 MOZ_TRY(ApplyCalendarOverride(dateFormat)); 574 575 // The DateTimeFormat wrapper will control the life cycle of the ICU 576 // dateFormat object. 577 return UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); 578 } 579 580 /* static */ 581 Result<UniquePtr<DateTimeFormat>, ICUError> 582 DateTimeFormat::TryCreateFromSkeleton( 583 Span<const char> aLocale, Span<const char16_t> aSkeleton, 584 DateTimePatternGenerator* aDateTimePatternGenerator, 585 Maybe<DateTimeFormat::HourCycle> aHourCycle, 586 Maybe<Span<const char16_t>> aTimeZoneOverride) { 587 if (!aDateTimePatternGenerator) { 588 return Err(ICUError::InternalError); 589 } 590 591 // Compute the best pattern for the skeleton. 592 DateTimeFormat::PatternVector pattern; 593 auto options = PatternMatchOptions(aSkeleton); 594 MOZ_TRY( 595 aDateTimePatternGenerator->GetBestPattern(aSkeleton, pattern, options)); 596 597 if (aHourCycle) { 598 DateTimeFormat::ReplaceHourSymbol(pattern, *aHourCycle); 599 } 600 601 auto result = 602 DateTimeFormat::TryCreateFromPattern(aLocale, pattern, aTimeZoneOverride); 603 if (result.isErr()) { 604 return Err(result.unwrapErr()); 605 } 606 auto dateTimeFormat = result.unwrap(); 607 MOZ_TRY(dateTimeFormat->CacheSkeleton(aSkeleton)); 608 return dateTimeFormat; 609 } 610 611 ICUResult DateTimeFormat::CacheSkeleton(Span<const char16_t> aSkeleton) { 612 if (mOriginalSkeleton.append(aSkeleton.Elements(), aSkeleton.Length())) { 613 return Ok(); 614 } 615 return Err(ICUError::OutOfMemory); 616 } 617 618 /* static */ 619 Result<UniquePtr<Calendar>, ICUError> DateTimeFormat::CloneCalendar( 620 double aUnixEpoch) const { 621 UErrorCode status = U_ZERO_ERROR; 622 UCalendar* calendarRaw = ucal_clone(udat_getCalendar(mDateFormat), &status); 623 if (U_FAILURE(status)) { 624 return Err(ToICUError(status)); 625 } 626 auto calendar = MakeUnique<Calendar>(calendarRaw); 627 628 MOZ_TRY(calendar->SetTimeInMs(aUnixEpoch)); 629 630 return calendar; 631 } 632 633 /** 634 * ICU locale identifier consisting of a language and a region subtag. 635 */ 636 class LanguageRegionLocaleId { 637 // unicode_language_subtag = alpha{2,3} | alpha{5,8} ; 638 static constexpr size_t LanguageLength = 8; 639 640 // unicode_region_subtag = (alpha{2} | digit{3}) ; 641 static constexpr size_t RegionLength = 3; 642 643 // Add +1 to account for the separator. 644 static constexpr size_t LRLength = LanguageLength + RegionLength + 1; 645 646 // Add +1 to zero terminate the string. 647 char mLocale[LRLength + 1] = {}; 648 649 // Pointer to the start of the region subtag within |locale_|. 650 char* mRegion = nullptr; 651 652 public: 653 LanguageRegionLocaleId(Span<const char> aLanguage, 654 Maybe<Span<const char>> aRegion); 655 656 const char* languageRegion() const { return mLocale; } 657 const char* region() const { return mRegion; } 658 }; 659 660 LanguageRegionLocaleId::LanguageRegionLocaleId( 661 Span<const char> aLanguage, Maybe<Span<const char>> aRegion) { 662 MOZ_RELEASE_ASSERT(aLanguage.Length() <= LanguageLength); 663 MOZ_RELEASE_ASSERT(!aRegion || aRegion->Length() <= RegionLength); 664 665 size_t languageLength = aLanguage.Length(); 666 667 std::memcpy(mLocale, aLanguage.Elements(), languageLength); 668 669 // ICU locale identifiers are separated by underscores. 670 mLocale[languageLength] = '_'; 671 672 mRegion = mLocale + languageLength + 1; 673 if (aRegion) { 674 std::memcpy(mRegion, aRegion->Elements(), aRegion->Length()); 675 } else { 676 // Use "001" (UN M.49 code for the World) as the fallback to match ICU. 677 std::strcpy(mRegion, "001"); 678 } 679 } 680 681 /* static */ 682 Result<DateTimeFormat::HourCyclesVector, ICUError> 683 DateTimeFormat::GetAllowedHourCycles(Span<const char> aLanguage, 684 Maybe<Span<const char>> aRegion) { 685 // ICU doesn't expose a public API to retrieve the hour cyles for a locale, so 686 // we have to reconstruct |DateTimePatternGenerator::getAllowedHourFormats()| 687 // using the public UResourceBundle API. 688 // 689 // The time data format is specified in UTS 35 at [1] and the data itself is 690 // located at [2]. 691 // 692 // [1] https://unicode.org/reports/tr35/tr35-dates.html#Time_Data 693 // [2] 694 // https://github.com/unicode-org/cldr/blob/master/common/supplemental/supplementalData.xml 695 696 HourCyclesVector result; 697 698 // Reserve space for the maximum number of hour cycles. This call always 699 // succeeds because it matches the inline capacity. We can now infallibly 700 // append all hour cycles to the vector. 701 MOZ_ALWAYS_TRUE(result.reserve(HourCyclesVector::InlineLength)); 702 703 LanguageRegionLocaleId localeId(aLanguage, aRegion); 704 705 // First open the "supplementalData" resource bundle. 706 UErrorCode status = U_ZERO_ERROR; 707 UResourceBundle* res = ures_openDirect(nullptr, "supplementalData", &status); 708 if (U_FAILURE(status)) { 709 return Err(ToICUError(status)); 710 } 711 ScopedICUObject<UResourceBundle, ures_close> closeRes(res); 712 MOZ_ASSERT(ures_getType(res) == URES_TABLE); 713 714 // Locate "timeDate" within the "supplementalData" resource bundle. 715 UResourceBundle* timeData = ures_getByKey(res, "timeData", nullptr, &status); 716 if (U_FAILURE(status)) { 717 return Err(ToICUError(status)); 718 } 719 ScopedICUObject<UResourceBundle, ures_close> closeTimeData(timeData); 720 MOZ_ASSERT(ures_getType(timeData) == URES_TABLE); 721 722 // Try to find a matching resource within "timeData". The two possible keys 723 // into the "timeData" resource bundle are `language_region` and `region`. 724 // Prefer `language_region` and otherwise fallback to `region`. 725 UResourceBundle* hclocale = 726 ures_getByKey(timeData, localeId.languageRegion(), nullptr, &status); 727 if (status == U_MISSING_RESOURCE_ERROR) { 728 status = U_ZERO_ERROR; 729 hclocale = ures_getByKey(timeData, localeId.region(), nullptr, &status); 730 } 731 if (status == U_MISSING_RESOURCE_ERROR) { 732 // Default to "h23" if no resource was found at all. This matches ICU. 733 result.infallibleAppend(HourCycle::H23); 734 return result; 735 } 736 if (U_FAILURE(status)) { 737 return Err(ToICUError(status)); 738 } 739 ScopedICUObject<UResourceBundle, ures_close> closeHcLocale(hclocale); 740 MOZ_ASSERT(ures_getType(hclocale) == URES_TABLE); 741 742 EnumSet<HourCycle> added{}; 743 744 auto addToResult = [&](const UChar* str, int32_t len) { 745 // An hour cycle strings is one of "K", "h", "H", or "k"; optionally 746 // followed by the suffix "b" or "B". We ignore the suffix because day 747 // periods can't be expressed in the "hc" Unicode extension. 748 MOZ_ASSERT(len == 1 || len == 2); 749 750 // Default to "h23" for unsupported hour cycle strings. 751 HourCycle hc = HourCycle::H23; 752 switch (str[0]) { 753 case 'K': 754 hc = HourCycle::H11; 755 break; 756 case 'h': 757 hc = HourCycle::H12; 758 break; 759 case 'H': 760 hc = HourCycle::H23; 761 break; 762 case 'k': 763 hc = HourCycle::H24; 764 break; 765 } 766 767 // Add each unique hour cycle to the result array. 768 if (!added.contains(hc)) { 769 added += hc; 770 771 result.infallibleAppend(hc); 772 } 773 }; 774 775 // Determine the preferred hour cycle for the locale. 776 int32_t len = 0; 777 const UChar* hc = ures_getStringByKey(hclocale, "preferred", &len, &status); 778 if (U_FAILURE(status)) { 779 return Err(ToICUError(status)); 780 } 781 addToResult(hc, len); 782 783 // Find any additionally allowed hour cycles of the locale. 784 UResourceBundle* allowed = 785 ures_getByKey(hclocale, "allowed", nullptr, &status); 786 if (U_FAILURE(status)) { 787 return Err(ToICUError(status)); 788 } 789 ScopedICUObject<UResourceBundle, ures_close> closeAllowed(allowed); 790 MOZ_ASSERT(ures_getType(allowed) == URES_ARRAY || 791 ures_getType(allowed) == URES_STRING); 792 793 while (ures_hasNext(allowed)) { 794 int32_t len = 0; 795 const UChar* hc = ures_getNextString(allowed, &len, nullptr, &status); 796 if (U_FAILURE(status)) { 797 return Err(ToICUError(status)); 798 } 799 addToResult(hc, len); 800 } 801 802 return result; 803 } 804 805 template <typename CharT> 806 static Result<Buffer<char>, ICUError> DuplicateChars(Span<CharT> aView) { 807 auto chars = MakeUnique<char[]>(aView.Length() + 1); 808 std::copy_n(aView.Elements(), aView.Length(), chars.get()); 809 chars[aView.Length()] = '\0'; 810 return Buffer{std::move(chars), aView.Length()}; 811 } 812 813 static Result<Buffer<char>, ICUError> GetParentLocale( 814 const UResourceBundle* aLocaleBundle) { 815 UErrorCode status = U_ZERO_ERROR; 816 817 // First check for an explicit parent locale using the "%%Parent" key. 818 int32_t length = 0; 819 const char16_t* parent = 820 ures_getStringByKey(aLocaleBundle, "%%Parent", &length, &status); 821 if (status == U_MISSING_RESOURCE_ERROR) { 822 status = U_ZERO_ERROR; 823 parent = nullptr; 824 } 825 if (U_FAILURE(status)) { 826 return Err(ToICUError(status)); 827 } 828 if (parent) { 829 return DuplicateChars(Span{parent, size_t(length)}); 830 } 831 832 // Retrieve the actual locale of the resource bundle. 833 const char* locale = 834 ures_getLocaleByType(aLocaleBundle, ULOC_ACTUAL_LOCALE, &status); 835 if (U_FAILURE(status)) { 836 return Err(ToICUError(status)); 837 } 838 839 // Strip off the last subtag, if possible. 840 if (const char* sep = std::strrchr(locale, '_')) { 841 return DuplicateChars(Span{locale, size_t(sep - locale)}); 842 } 843 844 // The parent locale of all locales is "root". 845 if (std::strcmp(locale, "root") != 0) { 846 static constexpr auto root = MakeStringSpan("root"); 847 return DuplicateChars(root); 848 } 849 850 // "root" itself doesn't have a parent locale. 851 static constexpr auto empty = MakeStringSpan(""); 852 return DuplicateChars(empty); 853 } 854 855 static Result<Span<const char16_t>, ICUError> FindTimeSeparator( 856 Span<const char> aRequestedLocale, Span<const char> aLocale, 857 Span<const char> aNumberingSystem) { 858 // We didn't find the numbering system. Retry using the default numbering 859 // system "latn". (We don't use the default numbering system of the requested 860 // locale to match ICU.) 861 if (aLocale == MakeStringSpan("")) { 862 return FindTimeSeparator(aRequestedLocale, aRequestedLocale, "latn"); 863 } 864 865 // First open the resource bundle of the input locale. 866 // 867 // Note: ICU's resource API accepts both Unicode CLDR locale identifiers and 868 // Unicode BCP 47 locale identifiers, so we don't have to convert the input 869 // into a Unicode CLDR locale identifier. 870 UErrorCode status = U_ZERO_ERROR; 871 UResourceBundle* localeBundle = 872 ures_open(nullptr, AssertNullTerminatedString(aLocale), &status); 873 if (U_FAILURE(status)) { 874 return Err(ToICUError(status)); 875 } 876 ScopedICUObject<UResourceBundle, ures_close> closeLocaleBundle(localeBundle); 877 878 do { 879 // Search for the "NumberElements" table. Fall back to the parent locale if 880 // no "NumberElements" table is present. 881 UResourceBundle* numberElements = 882 ures_getByKey(localeBundle, "NumberElements", nullptr, &status); 883 if (status == U_MISSING_RESOURCE_ERROR) { 884 break; 885 } 886 if (U_FAILURE(status)) { 887 return Err(ToICUError(status)); 888 } 889 ScopedICUObject<UResourceBundle, ures_close> closeNumberElements( 890 numberElements); 891 892 // Search for the table of the requested numbering system. Fall back to the 893 // parent locale if no table was found. 894 UResourceBundle* numberingSystem = ures_getByKey( 895 numberElements, AssertNullTerminatedString(aNumberingSystem), nullptr, 896 &status); 897 if (status == U_MISSING_RESOURCE_ERROR) { 898 break; 899 } 900 if (U_FAILURE(status)) { 901 return Err(ToICUError(status)); 902 } 903 ScopedICUObject<UResourceBundle, ures_close> closeNumberingSystem( 904 numberingSystem); 905 906 // Search for the "symbols" table. Fall back to the parent locale if no 907 // "symbols" table is present. 908 UResourceBundle* symbols = 909 ures_getByKey(numberingSystem, "symbols", nullptr, &status); 910 if (status == U_MISSING_RESOURCE_ERROR) { 911 break; 912 } 913 if (U_FAILURE(status)) { 914 return Err(ToICUError(status)); 915 } 916 ScopedICUObject<UResourceBundle, ures_close> closeSymbols(symbols); 917 918 // And finally look up the "timeSeparator" string in the "symbols" table. If 919 // the string isn't present, fall back to the parent locale. 920 int32_t length = 0; 921 const UChar* str = 922 ures_getStringByKey(symbols, "timeSeparator", &length, &status); 923 if (status == U_MISSING_RESOURCE_ERROR) { 924 break; 925 } 926 if (U_FAILURE(status)) { 927 return Err(ToICUError(status)); 928 } 929 930 Span<const char16_t> timeSeparator{str, size_t(length)}; 931 932 static constexpr auto defaultTimeSeparator = MakeStringSpan(u":"); 933 934 // Many numbering systems don't define their own symbols, but instead link 935 // to the symbols for "latn" of the requested locale. The link is performed 936 // through an alias entry like: 937 // `symbols:alias{"/LOCALE/NumberElements/latn/symbols"}` 938 // 939 // ICU doesn't provide a public API to detect these alias entries, but 940 // instead always automatically resolves the link. But that leads to 941 // incorrectly using the symbols from the "root" locale instead of the 942 // requested locale. 943 // 944 // Thankfully these alias entries are only present on the "root" locale. So 945 // we are using this heuristic to detect alias entries: 946 // 947 // - If the resolved time separator is the default time separator ":". 948 // - The current locale is "root". 949 // - And the numbering system is neither "latn" nor "arab". 950 // - Then search the time separator for "latn" of the requested locale. 951 // 952 // We have to exclude "arab", because it's also using ":" for the time 953 // separator, but doesn't use an alias link to "latn". 954 if (timeSeparator == defaultTimeSeparator && 955 aLocale == MakeStringSpan("root") && 956 aNumberingSystem != MakeStringSpan("latn") && 957 aNumberingSystem != MakeStringSpan("arab")) { 958 return FindTimeSeparator(aRequestedLocale, aRequestedLocale, 959 MakeStringSpan("latn")); 960 } 961 962 return timeSeparator; 963 } while (false); 964 965 // Fall back to the parent locale. 966 auto parent = GetParentLocale(localeBundle); 967 if (parent.isErr()) { 968 return parent.propagateErr(); 969 } 970 return FindTimeSeparator(aRequestedLocale, parent.inspect().AsSpan(), 971 aNumberingSystem); 972 } 973 974 /* static */ 975 Result<Span<const char16_t>, ICUError> DateTimeFormat::GetTimeSeparator( 976 Span<const char> aLocale, Span<const char> aNumberingSystem) { 977 return FindTimeSeparator(aLocale, aLocale, aNumberingSystem); 978 } 979 980 Result<DateTimeFormat::ComponentsBag, ICUError> 981 DateTimeFormat::ResolveComponents() { 982 // Maps an ICU pattern string to a corresponding set of date-time components 983 // and their values, and adds properties for these components to the result 984 // object, which will be returned by the resolvedOptions method. For the 985 // interpretation of ICU pattern characters, see 986 // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 987 988 DateTimeFormat::PatternVector pattern{}; 989 VectorToBufferAdaptor buffer(pattern); 990 MOZ_TRY(GetPattern(buffer)); 991 992 DateTimeFormat::ComponentsBag bag{}; 993 994 using Text = DateTimeFormat::Text; 995 using HourCycle = DateTimeFormat::HourCycle; 996 using Numeric = DateTimeFormat::Numeric; 997 using Month = DateTimeFormat::Month; 998 999 auto text = Text::Long; 1000 auto numeric = Numeric::Numeric; 1001 auto month = Month::Long; 1002 uint8_t fractionalSecondDigits = 0; 1003 1004 for (size_t i = 0, len = pattern.length(); i < len;) { 1005 char16_t c = pattern[i++]; 1006 if (c == u'\'') { 1007 // Skip past string literals. 1008 while (i < len && pattern[i] != u'\'') { 1009 i++; 1010 } 1011 i++; 1012 continue; 1013 } 1014 1015 // Count how many times the character is repeated. 1016 size_t count = 1; 1017 while (i < len && pattern[i] == c) { 1018 i++; 1019 count++; 1020 } 1021 1022 // Determine the enum case of the field. 1023 switch (c) { 1024 // "text" cases 1025 case u'G': 1026 case u'E': 1027 case u'c': 1028 case u'B': 1029 case u'z': 1030 case u'O': 1031 case u'v': 1032 case u'V': 1033 if (count <= 3) { 1034 text = Text::Short; 1035 } else if (count == 4) { 1036 text = Text::Long; 1037 } else { 1038 text = Text::Narrow; 1039 } 1040 break; 1041 // "number" cases 1042 case u'y': 1043 case u'd': 1044 case u'h': 1045 case u'H': 1046 case u'm': 1047 case u's': 1048 case u'k': 1049 case u'K': 1050 if (count == 2) { 1051 numeric = Numeric::TwoDigit; 1052 } else { 1053 numeric = Numeric::Numeric; 1054 } 1055 break; 1056 // "numeric" cases 1057 case u'r': 1058 case u'U': 1059 // Both are mapped to numeric years. 1060 numeric = Numeric::Numeric; 1061 break; 1062 // "text & number" cases 1063 case u'M': 1064 case u'L': 1065 if (count == 1) { 1066 month = Month::Numeric; 1067 } else if (count == 2) { 1068 month = Month::TwoDigit; 1069 } else if (count == 3) { 1070 month = Month::Short; 1071 } else if (count == 4) { 1072 month = Month::Long; 1073 } else { 1074 month = Month::Narrow; 1075 } 1076 break; 1077 case u'S': 1078 fractionalSecondDigits = count; 1079 break; 1080 default: { 1081 // skip other pattern characters and literal text 1082 } 1083 } 1084 1085 // Map ICU pattern characters back to the corresponding date-time 1086 // components of DateTimeFormat. See 1087 // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 1088 switch (c) { 1089 case u'E': 1090 case u'c': 1091 bag.weekday = Some(text); 1092 break; 1093 case u'G': 1094 bag.era = Some(text); 1095 break; 1096 case u'y': 1097 case u'r': 1098 case u'U': 1099 bag.year = Some(numeric); 1100 break; 1101 case u'M': 1102 case u'L': 1103 bag.month = Some(month); 1104 break; 1105 case u'd': 1106 bag.day = Some(numeric); 1107 break; 1108 case u'B': 1109 bag.dayPeriod = Some(text); 1110 break; 1111 case u'K': 1112 bag.hourCycle = Some(HourCycle::H11); 1113 bag.hour = Some(numeric); 1114 bag.hour12 = Some(true); 1115 break; 1116 case u'h': 1117 bag.hourCycle = Some(HourCycle::H12); 1118 bag.hour = Some(numeric); 1119 bag.hour12 = Some(true); 1120 break; 1121 case u'H': 1122 bag.hourCycle = Some(HourCycle::H23); 1123 bag.hour = Some(numeric); 1124 bag.hour12 = Some(false); 1125 break; 1126 case u'k': 1127 bag.hourCycle = Some(HourCycle::H24); 1128 bag.hour = Some(numeric); 1129 bag.hour12 = Some(false); 1130 break; 1131 case u'm': 1132 bag.minute = Some(numeric); 1133 break; 1134 case u's': 1135 bag.second = Some(numeric); 1136 break; 1137 case u'S': 1138 bag.fractionalSecondDigits = Some(fractionalSecondDigits); 1139 break; 1140 case u'z': 1141 switch (text) { 1142 case Text::Long: 1143 bag.timeZoneName = Some(TimeZoneName::Long); 1144 break; 1145 case Text::Short: 1146 case Text::Narrow: 1147 bag.timeZoneName = Some(TimeZoneName::Short); 1148 break; 1149 } 1150 break; 1151 case u'O': 1152 switch (text) { 1153 case Text::Long: 1154 bag.timeZoneName = Some(TimeZoneName::LongOffset); 1155 break; 1156 case Text::Short: 1157 case Text::Narrow: 1158 bag.timeZoneName = Some(TimeZoneName::ShortOffset); 1159 break; 1160 } 1161 break; 1162 case u'v': 1163 case u'V': 1164 switch (text) { 1165 case Text::Long: 1166 bag.timeZoneName = Some(TimeZoneName::LongGeneric); 1167 break; 1168 case Text::Short: 1169 case Text::Narrow: 1170 bag.timeZoneName = Some(TimeZoneName::ShortGeneric); 1171 break; 1172 } 1173 break; 1174 } 1175 } 1176 return bag; 1177 } 1178 1179 const char* DateTimeFormat::ToString( 1180 DateTimeFormat::TimeZoneName aTimeZoneName) { 1181 switch (aTimeZoneName) { 1182 case TimeZoneName::Long: 1183 return "long"; 1184 case TimeZoneName::Short: 1185 return "short"; 1186 case TimeZoneName::ShortOffset: 1187 return "shortOffset"; 1188 case TimeZoneName::LongOffset: 1189 return "longOffset"; 1190 case TimeZoneName::ShortGeneric: 1191 return "shortGeneric"; 1192 case TimeZoneName::LongGeneric: 1193 return "longGeneric"; 1194 } 1195 MOZ_CRASH("Unexpected DateTimeFormat::TimeZoneName"); 1196 } 1197 1198 const char* DateTimeFormat::ToString(DateTimeFormat::Month aMonth) { 1199 switch (aMonth) { 1200 case Month::Numeric: 1201 return "numeric"; 1202 case Month::TwoDigit: 1203 return "2-digit"; 1204 case Month::Long: 1205 return "long"; 1206 case Month::Short: 1207 return "short"; 1208 case Month::Narrow: 1209 return "narrow"; 1210 } 1211 MOZ_CRASH("Unexpected DateTimeFormat::Month"); 1212 } 1213 1214 const char* DateTimeFormat::ToString(DateTimeFormat::Text aText) { 1215 switch (aText) { 1216 case Text::Long: 1217 return "long"; 1218 case Text::Short: 1219 return "short"; 1220 case Text::Narrow: 1221 return "narrow"; 1222 } 1223 MOZ_CRASH("Unexpected DateTimeFormat::Text"); 1224 } 1225 1226 const char* DateTimeFormat::ToString(DateTimeFormat::Numeric aNumeric) { 1227 switch (aNumeric) { 1228 case Numeric::Numeric: 1229 return "numeric"; 1230 case Numeric::TwoDigit: 1231 return "2-digit"; 1232 } 1233 MOZ_CRASH("Unexpected DateTimeFormat::Numeric"); 1234 } 1235 1236 const char* DateTimeFormat::ToString(DateTimeFormat::Style aStyle) { 1237 switch (aStyle) { 1238 case Style::Full: 1239 return "full"; 1240 case Style::Long: 1241 return "long"; 1242 case Style::Medium: 1243 return "medium"; 1244 case Style::Short: 1245 return "short"; 1246 } 1247 MOZ_CRASH("Unexpected DateTimeFormat::Style"); 1248 } 1249 1250 const char* DateTimeFormat::ToString(DateTimeFormat::HourCycle aHourCycle) { 1251 switch (aHourCycle) { 1252 case HourCycle::H11: 1253 return "h11"; 1254 case HourCycle::H12: 1255 return "h12"; 1256 case HourCycle::H23: 1257 return "h23"; 1258 case HourCycle::H24: 1259 return "h24"; 1260 } 1261 MOZ_CRASH("Unexpected DateTimeFormat::HourCycle"); 1262 } 1263 1264 ICUResult DateTimeFormat::TryFormatToParts( 1265 UFieldPositionIterator* aFieldPositionIterator, size_t aSpanSize, 1266 DateTimePartVector& aParts) const { 1267 ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose( 1268 aFieldPositionIterator); 1269 1270 size_t lastEndIndex = 0; 1271 auto AppendPart = [&](DateTimePartType type, size_t endIndex) { 1272 // For the part defined in FormatDateTimeToParts, it doesn't have ||Source|| 1273 // property, we store Shared for simplicity, 1274 if (!aParts.emplaceBack(type, endIndex, DateTimePartSource::Shared)) { 1275 return false; 1276 } 1277 1278 lastEndIndex = endIndex; 1279 return true; 1280 }; 1281 1282 int32_t fieldInt, beginIndexInt, endIndexInt; 1283 while ((fieldInt = ufieldpositer_next(aFieldPositionIterator, &beginIndexInt, 1284 &endIndexInt)) >= 0) { 1285 MOZ_ASSERT(beginIndexInt <= endIndexInt, 1286 "field iterator returning invalid range"); 1287 1288 size_t beginIndex = AssertedCast<size_t>(beginIndexInt); 1289 size_t endIndex = AssertedCast<size_t>(endIndexInt); 1290 1291 // Technically this isn't guaranteed. But it appears true in pratice, 1292 // and http://bugs.icu-project.org/trac/ticket/12024 is expected to 1293 // correct the documentation lapse. 1294 MOZ_ASSERT(lastEndIndex <= beginIndex, 1295 "field iteration didn't return fields in order start to " 1296 "finish as expected"); 1297 1298 DateTimePartType type = 1299 ConvertUFormatFieldToPartType(static_cast<UDateFormatField>(fieldInt)); 1300 if (lastEndIndex < beginIndex) { 1301 if (!AppendPart(DateTimePartType::Literal, beginIndex)) { 1302 return Err(ICUError::InternalError); 1303 } 1304 } 1305 1306 if (!AppendPart(type, endIndex)) { 1307 return Err(ICUError::InternalError); 1308 } 1309 } 1310 1311 // Append any final literal. 1312 if (lastEndIndex < aSpanSize) { 1313 if (!AppendPart(DateTimePartType::Literal, aSpanSize)) { 1314 return Err(ICUError::InternalError); 1315 } 1316 } 1317 1318 return Ok(); 1319 } 1320 1321 } // namespace mozilla::intl