IntlDateTimeFormat.cpp
1 /* 2 * Copyright (C) 2015 Andy VanWagoner (andy@vanwagoner.family) 3 * Copyright (C) 2016-2020 Apple Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 16 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 18 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 24 * THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27 #include "config.h" 28 #include "IntlDateTimeFormat.h" 29 30 #include "IntlCache.h" 31 #include "IntlObjectInlines.h" 32 #include "JSBoundFunction.h" 33 #include "JSCInlines.h" 34 #include "ObjectConstructor.h" 35 #include <unicode/ucal.h> 36 #include <unicode/uenum.h> 37 #include <wtf/Range.h> 38 #include <wtf/text/StringBuilder.h> 39 #include <wtf/unicode/icu/ICUHelpers.h> 40 41 #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 42 #include <unicode/uformattedvalue.h> 43 #ifdef U_HIDE_DRAFT_API 44 #undef U_HIDE_DRAFT_API 45 #endif 46 #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 47 #include <unicode/udateintervalformat.h> 48 #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 49 #define U_HIDE_DRAFT_API 1 50 #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 51 52 namespace JSC { 53 54 // We do not use ICUDeleter<udtitvfmt_close> because we do not want to include udateintervalformat.h in IntlDateTimeFormat.h. 55 // udateintervalformat.h needs to be included with #undef U_HIDE_DRAFT_API, and we would like to minimize this effect in IntlDateTimeFormat.cpp. 56 void UDateIntervalFormatDeleter::operator()(UDateIntervalFormat* formatter) 57 { 58 if (formatter) 59 udtitvfmt_close(formatter); 60 } 61 62 static constexpr double minECMAScriptTime = -8.64E15; 63 64 const ClassInfo IntlDateTimeFormat::s_info = { "Object", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(IntlDateTimeFormat) }; 65 66 namespace IntlDateTimeFormatInternal { 67 static constexpr bool verbose = false; 68 } 69 70 IntlDateTimeFormat* IntlDateTimeFormat::create(VM& vm, Structure* structure) 71 { 72 IntlDateTimeFormat* format = new (NotNull, allocateCell<IntlDateTimeFormat>(vm.heap)) IntlDateTimeFormat(vm, structure); 73 format->finishCreation(vm); 74 return format; 75 } 76 77 Structure* IntlDateTimeFormat::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) 78 { 79 return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); 80 } 81 82 IntlDateTimeFormat::IntlDateTimeFormat(VM& vm, Structure* structure) 83 : Base(vm, structure) 84 { 85 } 86 87 void IntlDateTimeFormat::finishCreation(VM& vm) 88 { 89 Base::finishCreation(vm); 90 ASSERT(inherits(vm, info())); 91 } 92 93 void IntlDateTimeFormat::visitChildren(JSCell* cell, SlotVisitor& visitor) 94 { 95 IntlDateTimeFormat* thisObject = jsCast<IntlDateTimeFormat*>(cell); 96 ASSERT_GC_OBJECT_INHERITS(thisObject, info()); 97 98 Base::visitChildren(thisObject, visitor); 99 100 visitor.append(thisObject->m_boundFormat); 101 } 102 103 void IntlDateTimeFormat::setBoundFormat(VM& vm, JSBoundFunction* format) 104 { 105 m_boundFormat.set(vm, this, format); 106 } 107 108 static String canonicalizeTimeZoneName(const String& timeZoneName) 109 { 110 // 6.4.1 IsValidTimeZoneName (timeZone) 111 // The abstract operation returns true if timeZone, converted to upper case as described in 6.1, is equal to one of the Zone or Link names of the IANA Time Zone Database, converted to upper case as described in 6.1. It returns false otherwise. 112 UErrorCode status = U_ZERO_ERROR; 113 UEnumeration* timeZones = ucal_openTimeZones(&status); 114 ASSERT(U_SUCCESS(status)); 115 116 String canonical; 117 do { 118 status = U_ZERO_ERROR; 119 int32_t ianaTimeZoneLength; 120 // Time zone names are represented as UChar[] in all related ICU APIs. 121 const UChar* ianaTimeZone = uenum_unext(timeZones, &ianaTimeZoneLength, &status); 122 ASSERT(U_SUCCESS(status)); 123 124 // End of enumeration. 125 if (!ianaTimeZone) 126 break; 127 128 StringView ianaTimeZoneView(ianaTimeZone, ianaTimeZoneLength); 129 if (!equalIgnoringASCIICase(timeZoneName, ianaTimeZoneView)) 130 continue; 131 132 // Found a match, now canonicalize. 133 // 6.4.2 CanonicalizeTimeZoneName (timeZone) (ECMA-402 2.0) 134 // 1. Let ianaTimeZone be the Zone or Link name of the IANA Time Zone Database such that timeZone, converted to upper case as described in 6.1, is equal to ianaTimeZone, converted to upper case as described in 6.1. 135 // 2. If ianaTimeZone is a Link name, then let ianaTimeZone be the corresponding Zone name as specified in the “backward” file of the IANA Time Zone Database. 136 137 Vector<UChar, 32> buffer; 138 auto status = callBufferProducingFunction(ucal_getCanonicalTimeZoneID, ianaTimeZone, ianaTimeZoneLength, buffer, nullptr); 139 ASSERT_UNUSED(status, U_SUCCESS(status)); 140 canonical = String(buffer); 141 } while (canonical.isNull()); 142 uenum_close(timeZones); 143 144 // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", then return "UTC". 145 if (isUTCEquivalent(canonical)) 146 return "UTC"_s; 147 148 // 4. Return ianaTimeZone. 149 return canonical; 150 } 151 152 Vector<String> IntlDateTimeFormat::localeData(const String& locale, RelevantExtensionKey key) 153 { 154 Vector<String> keyLocaleData; 155 switch (key) { 156 case RelevantExtensionKey::Ca: { 157 UErrorCode status = U_ZERO_ERROR; 158 UEnumeration* calendars = ucal_getKeywordValuesForLocale("calendar", locale.utf8().data(), false, &status); 159 ASSERT(U_SUCCESS(status)); 160 161 int32_t nameLength; 162 while (const char* availableName = uenum_next(calendars, &nameLength, &status)) { 163 ASSERT(U_SUCCESS(status)); 164 String calendar = String(availableName, nameLength); 165 keyLocaleData.append(calendar); 166 // Ensure aliases used in language tag are allowed. 167 if (calendar == "gregorian") 168 keyLocaleData.append("gregory"_s); 169 else if (calendar == "islamic-civil") 170 keyLocaleData.append("islamicc"_s); 171 else if (calendar == "ethiopic-amete-alem") 172 keyLocaleData.append("ethioaa"_s); 173 } 174 uenum_close(calendars); 175 break; 176 } 177 case RelevantExtensionKey::Hc: 178 // Null default so we know to use 'j' in pattern. 179 keyLocaleData.append(String()); 180 keyLocaleData.append("h11"_s); 181 keyLocaleData.append("h12"_s); 182 keyLocaleData.append("h23"_s); 183 keyLocaleData.append("h24"_s); 184 break; 185 case RelevantExtensionKey::Nu: 186 keyLocaleData = numberingSystemsForLocale(locale); 187 break; 188 default: 189 ASSERT_NOT_REACHED(); 190 } 191 return keyLocaleData; 192 } 193 194 static JSObject* toDateTimeOptionsAnyDate(JSGlobalObject* globalObject, JSValue originalOptions) 195 { 196 // 12.1.1 ToDateTimeOptions abstract operation (ECMA-402 2.0) 197 VM& vm = globalObject->vm(); 198 auto scope = DECLARE_THROW_SCOPE(vm); 199 200 // 1. If options is undefined, then let options be null, else let options be ToObject(options). 201 // 2. ReturnIfAbrupt(options). 202 // 3. Let options be ObjectCreate(options). 203 JSObject* options; 204 if (originalOptions.isUndefined()) 205 options = constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); 206 else { 207 JSObject* originalToObject = originalOptions.toObject(globalObject); 208 RETURN_IF_EXCEPTION(scope, nullptr); 209 options = constructEmptyObject(globalObject, originalToObject); 210 } 211 212 // 4. Let needDefaults be true. 213 bool needDefaults = true; 214 215 // 5. If required is "date" or "any", 216 // Always "any". 217 218 // a. For each of the property names "weekday", "year", "month", "day": 219 // i. Let prop be the property name. 220 // ii. Let value be Get(options, prop). 221 // iii. ReturnIfAbrupt(value). 222 // iv. If value is not undefined, then let needDefaults be false. 223 JSValue weekday = options->get(globalObject, vm.propertyNames->weekday); 224 RETURN_IF_EXCEPTION(scope, nullptr); 225 if (!weekday.isUndefined()) 226 needDefaults = false; 227 228 JSValue year = options->get(globalObject, vm.propertyNames->year); 229 RETURN_IF_EXCEPTION(scope, nullptr); 230 if (!year.isUndefined()) 231 needDefaults = false; 232 233 JSValue month = options->get(globalObject, vm.propertyNames->month); 234 RETURN_IF_EXCEPTION(scope, nullptr); 235 if (!month.isUndefined()) 236 needDefaults = false; 237 238 JSValue day = options->get(globalObject, vm.propertyNames->day); 239 RETURN_IF_EXCEPTION(scope, nullptr); 240 if (!day.isUndefined()) 241 needDefaults = false; 242 243 // 6. If required is "time" or "any", 244 // Always "any". 245 246 // a. For each of the property names ""dayPeriod", hour", "minute", "second", "fractionalSecondDigits": 247 // i. Let prop be the property name. 248 // ii. Let value be Get(options, prop). 249 // iii. ReturnIfAbrupt(value). 250 // iv. If value is not undefined, then let needDefaults be false. 251 if (Options::useIntlDateTimeFormatDayPeriod()) { 252 JSValue dayPeriod = options->get(globalObject, vm.propertyNames->dayPeriod); 253 RETURN_IF_EXCEPTION(scope, nullptr); 254 if (!dayPeriod.isUndefined()) 255 needDefaults = false; 256 } 257 258 JSValue hour = options->get(globalObject, vm.propertyNames->hour); 259 RETURN_IF_EXCEPTION(scope, nullptr); 260 if (!hour.isUndefined()) 261 needDefaults = false; 262 263 JSValue minute = options->get(globalObject, vm.propertyNames->minute); 264 RETURN_IF_EXCEPTION(scope, nullptr); 265 if (!minute.isUndefined()) 266 needDefaults = false; 267 268 JSValue second = options->get(globalObject, vm.propertyNames->second); 269 RETURN_IF_EXCEPTION(scope, nullptr); 270 if (!second.isUndefined()) 271 needDefaults = false; 272 273 JSValue fractionalSecondDigits = options->get(globalObject, vm.propertyNames->fractionalSecondDigits); 274 RETURN_IF_EXCEPTION(scope, nullptr); 275 if (!fractionalSecondDigits.isUndefined()) 276 needDefaults = false; 277 278 JSValue dateStyle = options->get(globalObject, vm.propertyNames->dateStyle); 279 RETURN_IF_EXCEPTION(scope, nullptr); 280 JSValue timeStyle = options->get(globalObject, vm.propertyNames->timeStyle); 281 RETURN_IF_EXCEPTION(scope, nullptr); 282 283 if (!dateStyle.isUndefined() || !timeStyle.isUndefined()) 284 needDefaults = false; 285 286 // 7. If needDefaults is true and defaults is either "date" or "all", then 287 // Defaults is always "date". 288 if (needDefaults) { 289 // a. For each of the property names "year", "month", "day": 290 // i. Let status be CreateDatePropertyOrThrow(options, prop, "numeric"). 291 // ii. ReturnIfAbrupt(status). 292 JSString* numeric = jsNontrivialString(vm, "numeric"_s); 293 294 options->putDirect(vm, vm.propertyNames->year, numeric); 295 RETURN_IF_EXCEPTION(scope, nullptr); 296 297 options->putDirect(vm, vm.propertyNames->month, numeric); 298 RETURN_IF_EXCEPTION(scope, nullptr); 299 300 options->putDirect(vm, vm.propertyNames->day, numeric); 301 RETURN_IF_EXCEPTION(scope, nullptr); 302 } 303 304 // 8. If needDefaults is true and defaults is either "time" or "all", then 305 // Defaults is always "date". Ignore this branch. 306 307 // 9. Return options. 308 return options; 309 } 310 311 void IntlDateTimeFormat::setFormatsFromPattern(const StringView& pattern) 312 { 313 // Get all symbols from the pattern, and set format fields accordingly. 314 // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 315 unsigned length = pattern.length(); 316 for (unsigned i = 0; i < length; ++i) { 317 UChar currentCharacter = pattern[i]; 318 if (!isASCIIAlpha(currentCharacter)) 319 continue; 320 321 unsigned count = 1; 322 while (i + 1 < length && pattern[i + 1] == currentCharacter) { 323 ++count; 324 ++i; 325 } 326 327 switch (currentCharacter) { 328 case 'G': 329 if (count <= 3) 330 m_era = Era::Short; 331 else if (count == 4) 332 m_era = Era::Long; 333 else if (count == 5) 334 m_era = Era::Narrow; 335 break; 336 case 'y': 337 if (count == 1) 338 m_year = Year::Numeric; 339 else if (count == 2) 340 m_year = Year::TwoDigit; 341 break; 342 case 'M': 343 case 'L': 344 if (count == 1) 345 m_month = Month::Numeric; 346 else if (count == 2) 347 m_month = Month::TwoDigit; 348 else if (count == 3) 349 m_month = Month::Short; 350 else if (count == 4) 351 m_month = Month::Long; 352 else if (count == 5) 353 m_month = Month::Narrow; 354 break; 355 case 'E': 356 case 'e': 357 case 'c': 358 if (count <= 3) 359 m_weekday = Weekday::Short; 360 else if (count == 4) 361 m_weekday = Weekday::Long; 362 else if (count == 5) 363 m_weekday = Weekday::Narrow; 364 break; 365 case 'd': 366 if (count == 1) 367 m_day = Day::Numeric; 368 else if (count == 2) 369 m_day = Day::TwoDigit; 370 break; 371 case 'a': 372 case 'b': 373 case 'B': 374 if (count <= 3) 375 m_dayPeriod = DayPeriod::Short; 376 else if (count == 4) 377 m_dayPeriod = DayPeriod::Long; 378 else if (count == 5) 379 m_dayPeriod = DayPeriod::Narrow; 380 break; 381 case 'h': 382 case 'H': 383 case 'k': 384 case 'K': { 385 // Populate hourCycle from actually generated patterns. It is possible that locale or option is specifying hourCycle explicitly, 386 // but the generated pattern does not include related part since the pattern does not include hours. 387 // This is tested in test262/test/intl402/DateTimeFormat/prototype/resolvedOptions/hourCycle-dateStyle.js and our stress tests. 388 // Example: 389 // new Intl.DateTimeFormat(`de-u-hc-h11`, { 390 // dateStyle: "full" 391 // }).resolvedOptions().hourCycle === undefined 392 m_hourCycle = hourCycleFromSymbol(currentCharacter); 393 if (count == 1) 394 m_hour = Hour::Numeric; 395 else if (count == 2) 396 m_hour = Hour::TwoDigit; 397 break; 398 } 399 case 'm': 400 if (count == 1) 401 m_minute = Minute::Numeric; 402 else if (count == 2) 403 m_minute = Minute::TwoDigit; 404 break; 405 case 's': 406 if (count == 1) 407 m_second = Second::Numeric; 408 else if (count == 2) 409 m_second = Second::TwoDigit; 410 break; 411 case 'z': 412 case 'v': 413 case 'V': 414 if (count == 1) 415 m_timeZoneName = TimeZoneName::Short; 416 else if (count == 4) 417 m_timeZoneName = TimeZoneName::Long; 418 break; 419 case 'S': 420 m_fractionalSecondDigits = count; 421 break; 422 } 423 } 424 } 425 426 IntlDateTimeFormat::HourCycle IntlDateTimeFormat::parseHourCycle(const String& hourCycle) 427 { 428 if (hourCycle == "h11"_s) 429 return HourCycle::H11; 430 if (hourCycle == "h12"_s) 431 return HourCycle::H12; 432 if (hourCycle == "h23"_s) 433 return HourCycle::H23; 434 if (hourCycle == "h24"_s) 435 return HourCycle::H24; 436 return HourCycle::None; 437 } 438 439 inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromSymbol(UChar symbol) 440 { 441 switch (symbol) { 442 case 'K': 443 return HourCycle::H11; 444 case 'h': 445 return HourCycle::H12; 446 case 'H': 447 return HourCycle::H23; 448 case 'k': 449 return HourCycle::H24; 450 } 451 return HourCycle::None; 452 } 453 454 inline IntlDateTimeFormat::HourCycle IntlDateTimeFormat::hourCycleFromPattern(const Vector<UChar, 32>& pattern) 455 { 456 for (auto character : pattern) { 457 switch (character) { 458 case 'K': 459 case 'h': 460 case 'H': 461 case 'k': 462 return hourCycleFromSymbol(character); 463 } 464 } 465 return HourCycle::None; 466 } 467 468 inline void IntlDateTimeFormat::replaceHourCycleInSkeleton(Vector<UChar, 32>& skeleton, bool isHour12) 469 { 470 UChar skeletonCharacter = 'H'; 471 if (isHour12) 472 skeletonCharacter = 'h'; 473 for (auto& character : skeleton) { 474 switch (character) { 475 case 'h': 476 case 'H': 477 case 'j': 478 character = skeletonCharacter; 479 break; 480 } 481 } 482 } 483 484 inline void IntlDateTimeFormat::replaceHourCycleInPattern(Vector<UChar, 32>& pattern, HourCycle hourCycle) 485 { 486 UChar hourFromHourCycle = 'H'; 487 switch (hourCycle) { 488 case HourCycle::H11: 489 hourFromHourCycle = 'K'; 490 break; 491 case HourCycle::H12: 492 hourFromHourCycle = 'h'; 493 break; 494 case HourCycle::H23: 495 hourFromHourCycle = 'H'; 496 break; 497 case HourCycle::H24: 498 hourFromHourCycle = 'k'; 499 break; 500 case HourCycle::None: 501 return; 502 } 503 504 for (auto& character : pattern) { 505 switch (character) { 506 case 'K': 507 case 'h': 508 case 'H': 509 case 'k': 510 character = hourFromHourCycle; 511 break; 512 } 513 } 514 } 515 516 // https://tc39.github.io/ecma402/#sec-initializedatetimeformat 517 void IntlDateTimeFormat::initializeDateTimeFormat(JSGlobalObject* globalObject, JSValue locales, JSValue originalOptions) 518 { 519 VM& vm = globalObject->vm(); 520 auto scope = DECLARE_THROW_SCOPE(vm); 521 522 Vector<String> requestedLocales = canonicalizeLocaleList(globalObject, locales); 523 RETURN_IF_EXCEPTION(scope, void()); 524 525 JSObject* options = toDateTimeOptionsAnyDate(globalObject, originalOptions); 526 RETURN_IF_EXCEPTION(scope, void()); 527 528 ResolveLocaleOptions localeOptions; 529 530 LocaleMatcher localeMatcher = intlOption<LocaleMatcher>(globalObject, options, vm.propertyNames->localeMatcher, { { "lookup"_s, LocaleMatcher::Lookup }, { "best fit"_s, LocaleMatcher::BestFit } }, "localeMatcher must be either \"lookup\" or \"best fit\""_s, LocaleMatcher::BestFit); 531 RETURN_IF_EXCEPTION(scope, void()); 532 533 String calendar = intlStringOption(globalObject, options, vm.propertyNames->calendar, { }, nullptr, nullptr); 534 RETURN_IF_EXCEPTION(scope, void()); 535 if (!calendar.isNull()) { 536 if (!isUnicodeLocaleIdentifierType(calendar)) { 537 throwRangeError(globalObject, scope, "calendar is not a well-formed calendar value"_s); 538 return; 539 } 540 localeOptions[static_cast<unsigned>(RelevantExtensionKey::Ca)] = calendar; 541 } 542 543 String numberingSystem = intlStringOption(globalObject, options, vm.propertyNames->numberingSystem, { }, nullptr, nullptr); 544 RETURN_IF_EXCEPTION(scope, void()); 545 if (!numberingSystem.isNull()) { 546 if (!isUnicodeLocaleIdentifierType(numberingSystem)) { 547 throwRangeError(globalObject, scope, "numberingSystem is not a well-formed numbering system value"_s); 548 return; 549 } 550 localeOptions[static_cast<unsigned>(RelevantExtensionKey::Nu)] = numberingSystem; 551 } 552 553 TriState hour12 = intlBooleanOption(globalObject, options, vm.propertyNames->hour12); 554 RETURN_IF_EXCEPTION(scope, void()); 555 556 HourCycle hourCycle = intlOption<HourCycle>(globalObject, options, vm.propertyNames->hourCycle, { { "h11"_s, HourCycle::H11 }, { "h12"_s, HourCycle::H12 }, { "h23"_s, HourCycle::H23 }, { "h24"_s, HourCycle::H24 } }, "hourCycle must be \"h11\", \"h12\", \"h23\", or \"h24\""_s, HourCycle::None); 557 RETURN_IF_EXCEPTION(scope, void()); 558 if (hour12 == TriState::Indeterminate) { 559 if (hourCycle != HourCycle::None) 560 localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String(hourCycleString(hourCycle)); 561 } else { 562 // If there is hour12, hourCycle is ignored. 563 // We are setting null String explicitly here (localeOptions' entries are Optional<String>). This leads us to use HourCycle::None later. 564 localeOptions[static_cast<unsigned>(RelevantExtensionKey::Hc)] = String(); 565 } 566 567 const HashSet<String>& availableLocales = intlDateTimeFormatAvailableLocales(); 568 auto resolved = resolveLocale(globalObject, availableLocales, requestedLocales, localeMatcher, localeOptions, { RelevantExtensionKey::Ca, RelevantExtensionKey::Hc, RelevantExtensionKey::Nu }, localeData); 569 570 m_locale = resolved.locale; 571 if (m_locale.isEmpty()) { 572 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat due to invalid locale"_s); 573 return; 574 } 575 576 m_calendar = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Ca)]; 577 if (m_calendar == "gregorian") 578 m_calendar = "gregory"_s; 579 else if (m_calendar == "islamicc") 580 m_calendar = "islamic-civil"_s; 581 else if (m_calendar == "ethioaa") 582 m_calendar = "ethiopic-amete-alem"_s; 583 584 hourCycle = parseHourCycle(resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Hc)]); 585 m_numberingSystem = resolved.extensions[static_cast<unsigned>(RelevantExtensionKey::Nu)]; 586 m_dataLocale = resolved.dataLocale; 587 CString dataLocaleWithExtensions = makeString(m_dataLocale, "-u-ca-", m_calendar, "-nu-", m_numberingSystem).utf8(); 588 589 JSValue tzValue = options->get(globalObject, vm.propertyNames->timeZone); 590 RETURN_IF_EXCEPTION(scope, void()); 591 String tz; 592 if (!tzValue.isUndefined()) { 593 String originalTz = tzValue.toWTFString(globalObject); 594 RETURN_IF_EXCEPTION(scope, void()); 595 tz = canonicalizeTimeZoneName(originalTz); 596 if (tz.isNull()) { 597 throwRangeError(globalObject, scope, "invalid time zone: " + originalTz); 598 return; 599 } 600 } else 601 tz = vm.dateCache.defaultTimeZone(); 602 m_timeZone = tz; 603 604 StringBuilder skeletonBuilder; 605 606 Weekday weekday = intlOption<Weekday>(globalObject, options, vm.propertyNames->weekday, { { "narrow"_s, Weekday::Narrow }, { "short"_s, Weekday::Short }, { "long"_s, Weekday::Long } }, "weekday must be \"narrow\", \"short\", or \"long\""_s, Weekday::None); 607 RETURN_IF_EXCEPTION(scope, void()); 608 switch (weekday) { 609 case Weekday::Narrow: 610 skeletonBuilder.appendLiteral("EEEEE"); 611 break; 612 case Weekday::Short: 613 skeletonBuilder.appendLiteral("EEE"); 614 break; 615 case Weekday::Long: 616 skeletonBuilder.appendLiteral("EEEE"); 617 break; 618 case Weekday::None: 619 break; 620 } 621 622 Era era = intlOption<Era>(globalObject, options, vm.propertyNames->era, { { "narrow"_s, Era::Narrow }, { "short"_s, Era::Short }, { "long"_s, Era::Long } }, "era must be \"narrow\", \"short\", or \"long\""_s, Era::None); 623 RETURN_IF_EXCEPTION(scope, void()); 624 switch (era) { 625 case Era::Narrow: 626 skeletonBuilder.appendLiteral("GGGGG"); 627 break; 628 case Era::Short: 629 skeletonBuilder.appendLiteral("GGG"); 630 break; 631 case Era::Long: 632 skeletonBuilder.appendLiteral("GGGG"); 633 break; 634 case Era::None: 635 break; 636 } 637 638 Year year = intlOption<Year>(globalObject, options, vm.propertyNames->year, { { "2-digit"_s, Year::TwoDigit }, { "numeric"_s, Year::Numeric } }, "year must be \"2-digit\" or \"numeric\""_s, Year::None); 639 RETURN_IF_EXCEPTION(scope, void()); 640 switch (year) { 641 case Year::TwoDigit: 642 skeletonBuilder.appendLiteral("yy"); 643 break; 644 case Year::Numeric: 645 skeletonBuilder.append('y'); 646 break; 647 case Year::None: 648 break; 649 } 650 651 Month month = intlOption<Month>(globalObject, options, vm.propertyNames->month, { { "2-digit"_s, Month::TwoDigit }, { "numeric"_s, Month::Numeric }, { "narrow"_s, Month::Narrow }, { "short"_s, Month::Short }, { "long"_s, Month::Long } }, "month must be \"2-digit\", \"numeric\", \"narrow\", \"short\", or \"long\""_s, Month::None); 652 RETURN_IF_EXCEPTION(scope, void()); 653 switch (month) { 654 case Month::TwoDigit: 655 skeletonBuilder.appendLiteral("MM"); 656 break; 657 case Month::Numeric: 658 skeletonBuilder.append('M'); 659 break; 660 case Month::Narrow: 661 skeletonBuilder.appendLiteral("MMMMM"); 662 break; 663 case Month::Short: 664 skeletonBuilder.appendLiteral("MMM"); 665 break; 666 case Month::Long: 667 skeletonBuilder.appendLiteral("MMMM"); 668 break; 669 case Month::None: 670 break; 671 } 672 673 Day day = intlOption<Day>(globalObject, options, vm.propertyNames->day, { { "2-digit"_s, Day::TwoDigit }, { "numeric"_s, Day::Numeric } }, "day must be \"2-digit\" or \"numeric\""_s, Day::None); 674 RETURN_IF_EXCEPTION(scope, void()); 675 switch (day) { 676 case Day::TwoDigit: 677 skeletonBuilder.appendLiteral("dd"); 678 break; 679 case Day::Numeric: 680 skeletonBuilder.append('d'); 681 break; 682 case Day::None: 683 break; 684 } 685 686 DayPeriod dayPeriod = DayPeriod::None; 687 if (Options::useIntlDateTimeFormatDayPeriod()) { 688 dayPeriod = intlOption<DayPeriod>(globalObject, options, vm.propertyNames->dayPeriod, { { "narrow"_s, DayPeriod::Narrow }, { "short"_s, DayPeriod::Short }, { "long"_s, DayPeriod::Long } }, "dayPeriod must be \"narrow\", \"short\", or \"long\""_s, DayPeriod::None); 689 RETURN_IF_EXCEPTION(scope, void()); 690 } 691 692 Hour hour = intlOption<Hour>(globalObject, options, vm.propertyNames->hour, { { "2-digit"_s, Hour::TwoDigit }, { "numeric"_s, Hour::Numeric } }, "hour must be \"2-digit\" or \"numeric\""_s, Hour::None); 693 RETURN_IF_EXCEPTION(scope, void()); 694 { 695 // Specifically, this hour-cycle / hour12 behavior is slightly different from the spec. 696 // But the spec behavior is known to cause surprising behaviors, and the spec change is ongoing. 697 // We implement SpiderMonkey's behavior. 698 // 699 // > No option present: "j" 700 // > hour12 = true: "h" 701 // > hour12 = false: "H" 702 // > hourCycle = h11: "h", plus modifying the resolved pattern to use the hour symbol "K". 703 // > hourCycle = h12: "h", plus modifying the resolved pattern to use the hour symbol "h". 704 // > hourCycle = h23: "H", plus modifying the resolved pattern to use the hour symbol "H". 705 // > hourCycle = h24: "H", plus modifying the resolved pattern to use the hour symbol "k". 706 // 707 UChar skeletonCharacter = 'j'; 708 if (hour12 == TriState::Indeterminate) { 709 switch (hourCycle) { 710 case HourCycle::None: 711 break; 712 case HourCycle::H11: 713 case HourCycle::H12: 714 skeletonCharacter = 'h'; 715 break; 716 case HourCycle::H23: 717 case HourCycle::H24: 718 skeletonCharacter = 'H'; 719 break; 720 } 721 } else { 722 if (hour12 == TriState::True) 723 skeletonCharacter = 'h'; 724 else 725 skeletonCharacter = 'H'; 726 } 727 728 switch (hour) { 729 case Hour::TwoDigit: 730 skeletonBuilder.append(skeletonCharacter); 731 skeletonBuilder.append(skeletonCharacter); 732 break; 733 case Hour::Numeric: 734 skeletonBuilder.append(skeletonCharacter); 735 break; 736 case Hour::None: 737 break; 738 } 739 } 740 741 if (Options::useIntlDateTimeFormatDayPeriod()) { 742 // dayPeriod must be set after setting hour. 743 // https://unicode-org.atlassian.net/browse/ICU-20731 744 switch (dayPeriod) { 745 case DayPeriod::Narrow: 746 skeletonBuilder.appendLiteral("BBBBB"); 747 break; 748 case DayPeriod::Short: 749 skeletonBuilder.append('B'); 750 break; 751 case DayPeriod::Long: 752 skeletonBuilder.appendLiteral("BBBB"); 753 break; 754 case DayPeriod::None: 755 break; 756 } 757 } 758 759 Minute minute = intlOption<Minute>(globalObject, options, vm.propertyNames->minute, { { "2-digit"_s, Minute::TwoDigit }, { "numeric"_s, Minute::Numeric } }, "minute must be \"2-digit\" or \"numeric\""_s, Minute::None); 760 RETURN_IF_EXCEPTION(scope, void()); 761 switch (minute) { 762 case Minute::TwoDigit: 763 skeletonBuilder.appendLiteral("mm"); 764 break; 765 case Minute::Numeric: 766 skeletonBuilder.append('m'); 767 break; 768 case Minute::None: 769 break; 770 } 771 772 Second second = intlOption<Second>(globalObject, options, vm.propertyNames->second, { { "2-digit"_s, Second::TwoDigit }, { "numeric"_s, Second::Numeric } }, "second must be \"2-digit\" or \"numeric\""_s, Second::None); 773 RETURN_IF_EXCEPTION(scope, void()); 774 switch (second) { 775 case Second::TwoDigit: 776 skeletonBuilder.appendLiteral("ss"); 777 break; 778 case Second::Numeric: 779 skeletonBuilder.append('s'); 780 break; 781 case Second::None: 782 break; 783 } 784 785 unsigned fractionalSecondDigits = intlNumberOption(globalObject, options, vm.propertyNames->fractionalSecondDigits, 1, 3, 0); 786 RETURN_IF_EXCEPTION(scope, void()); 787 for (unsigned i = 0; i < fractionalSecondDigits; ++i) 788 skeletonBuilder.append('S'); 789 790 TimeZoneName timeZoneName = intlOption<TimeZoneName>(globalObject, options, vm.propertyNames->timeZoneName, { { "short"_s, TimeZoneName::Short }, { "long"_s, TimeZoneName::Long } }, "timeZoneName must be \"short\" or \"long\""_s, TimeZoneName::None); 791 RETURN_IF_EXCEPTION(scope, void()); 792 switch (timeZoneName) { 793 case TimeZoneName::Short: 794 skeletonBuilder.append('z'); 795 break; 796 case TimeZoneName::Long: 797 skeletonBuilder.appendLiteral("zzzz"); 798 break; 799 case TimeZoneName::None: 800 break; 801 } 802 803 intlStringOption(globalObject, options, vm.propertyNames->formatMatcher, { "basic", "best fit" }, "formatMatcher must be either \"basic\" or \"best fit\"", "best fit"); 804 RETURN_IF_EXCEPTION(scope, void()); 805 806 m_dateStyle = intlOption<DateTimeStyle>(globalObject, options, vm.propertyNames->dateStyle, { { "full"_s, DateTimeStyle::Full }, { "long"_s, DateTimeStyle::Long }, { "medium"_s, DateTimeStyle::Medium }, { "short"_s, DateTimeStyle::Short } }, "dateStyle must be \"full\", \"long\", \"medium\", or \"short\""_s, DateTimeStyle::None); 807 RETURN_IF_EXCEPTION(scope, void()); 808 809 m_timeStyle = intlOption<DateTimeStyle>(globalObject, options, vm.propertyNames->timeStyle, { { "full"_s, DateTimeStyle::Full }, { "long"_s, DateTimeStyle::Long }, { "medium"_s, DateTimeStyle::Medium }, { "short"_s, DateTimeStyle::Short } }, "timeStyle must be \"full\", \"long\", \"medium\", or \"short\""_s, DateTimeStyle::None); 810 RETURN_IF_EXCEPTION(scope, void()); 811 812 Vector<UChar, 32> patternBuffer; 813 if (m_dateStyle != DateTimeStyle::None || m_timeStyle != DateTimeStyle::None) { 814 // 30. For each row in Table 1, except the header row, do 815 // i. Let prop be the name given in the Property column of the row. 816 // ii. Let p be opt.[[<prop>]]. 817 // iii. If p is not undefined, then 818 // 1. Throw a TypeError exception. 819 if (weekday != Weekday::None || era != Era::None || year != Year::None || month != Month::None || day != Day::None || dayPeriod != DayPeriod::None || hour != Hour::None || minute != Minute::None || second != Second::None || fractionalSecondDigits != 0 || timeZoneName != TimeZoneName::None) { 820 throwTypeError(globalObject, scope, "dateStyle and timeStyle may not be used with other DateTimeFormat options"_s); 821 return; 822 } 823 824 auto parseUDateFormatStyle = [](DateTimeStyle style) { 825 switch (style) { 826 case DateTimeStyle::Full: 827 return UDAT_FULL; 828 case DateTimeStyle::Long: 829 return UDAT_LONG; 830 case DateTimeStyle::Medium: 831 return UDAT_MEDIUM; 832 case DateTimeStyle::Short: 833 return UDAT_SHORT; 834 case DateTimeStyle::None: 835 return UDAT_NONE; 836 } 837 return UDAT_NONE; 838 }; 839 840 // We cannot use this UDateFormat directly yet because we need to enforce specified hourCycle. 841 // First, we create UDateFormat via dateStyle and timeStyle. And then convert it to pattern string. 842 // After updating this pattern string with hourCycle, we create a final UDateFormat with the updated pattern string. 843 UErrorCode status = U_ZERO_ERROR; 844 StringView timeZoneView(m_timeZone); 845 auto dateFormatFromStyle = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(parseUDateFormatStyle(m_timeStyle), parseUDateFormatStyle(m_dateStyle), dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), nullptr, -1, &status)); 846 if (U_FAILURE(status)) { 847 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 848 return; 849 } 850 constexpr bool localized = false; // Aligned with how ICU SimpleDateTimeFormat::format works. We do not need to translate this to localized pattern. 851 status = callBufferProducingFunction(udat_toPattern, dateFormatFromStyle.get(), localized, patternBuffer); 852 if (U_FAILURE(status)) { 853 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 854 return; 855 } 856 857 // It is possible that timeStyle includes dayPeriod, which is sensitive to hour-cycle. 858 // If dayPeriod is included, just replacing hour based on hourCycle / hour12 produces strange results. 859 // Let's consider about the example. The formatted result looks like "02:12:47 PM Coordinated Universal Time" 860 // If we simply replace 02 to 14, this becomes "14:12:47 PM Coordinated Universal Time", this looks strange since "PM" is unnecessary! 861 // 862 // If the generated pattern's hour12 does not match against the option's one, we retrieve skeleton from the pattern, enforcing hour-cycle, 863 // and re-generating the best pattern from the modified skeleton. ICU will look into the generated skeleton, and pick the best format for the request. 864 // We do not care about h11 vs. h12 and h23 vs. h24 difference here since this will be later adjusted by replaceHourCycleInPattern. 865 // 866 // test262/test/intl402/DateTimeFormat/prototype/format/timedatestyle-en.js includes the test for this behavior. 867 if (m_timeStyle != DateTimeStyle::None && (hourCycle != HourCycle::None || hour12 != TriState::Indeterminate)) { 868 auto isHour12 = [](HourCycle hourCycle) { 869 return hourCycle == HourCycle::H11 || hourCycle == HourCycle::H12; 870 }; 871 bool specifiedHour12 = false; 872 // If hour12 is specified, we prefer it and ignore hourCycle. 873 if (hour12 != TriState::Indeterminate) 874 specifiedHour12 = hour12 == TriState::True; 875 else 876 specifiedHour12 = isHour12(hourCycle); 877 HourCycle extractedHourCycle = hourCycleFromPattern(patternBuffer); 878 if (extractedHourCycle != HourCycle::None && isHour12(extractedHourCycle) != specifiedHour12) { 879 Vector<UChar, 32> skeleton; 880 auto status = callBufferProducingFunction(udatpg_getSkeleton, nullptr, patternBuffer.data(), patternBuffer.size(), skeleton); 881 if (U_FAILURE(status)) { 882 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 883 return; 884 } 885 replaceHourCycleInSkeleton(skeleton, specifiedHour12); 886 dataLogLnIf(IntlDateTimeFormatInternal::verbose, "replaced:(", StringView(skeleton.data(), skeleton.size()), ")"); 887 888 patternBuffer = vm.intlCache().getBestDateTimePattern(dataLocaleWithExtensions, skeleton.data(), skeleton.size(), status); 889 if (U_FAILURE(status)) { 890 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 891 return; 892 } 893 } 894 } 895 } else { 896 UErrorCode status = U_ZERO_ERROR; 897 String skeleton = skeletonBuilder.toString(); 898 patternBuffer = vm.intlCache().getBestDateTimePattern(dataLocaleWithExtensions, StringView(skeleton).upconvertedCharacters().get(), skeleton.length(), status); 899 if (U_FAILURE(status)) { 900 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 901 return; 902 } 903 } 904 905 // After generating pattern from skeleton, we need to change h11 vs. h12 and h23 vs. h24 if hourCycle is specified. 906 if (hourCycle != HourCycle::None) 907 replaceHourCycleInPattern(patternBuffer, hourCycle); 908 909 StringView pattern(patternBuffer.data(), patternBuffer.size()); 910 setFormatsFromPattern(pattern); 911 912 dataLogLnIf(IntlDateTimeFormatInternal::verbose, "locale:(", m_locale, "),dataLocale:(", dataLocaleWithExtensions, "),pattern:(", pattern, ")"); 913 914 UErrorCode status = U_ZERO_ERROR; 915 StringView timeZoneView(m_timeZone); 916 m_dateFormat = std::unique_ptr<UDateFormat, UDateFormatDeleter>(udat_open(UDAT_PATTERN, UDAT_PATTERN, dataLocaleWithExtensions.data(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), pattern.upconvertedCharacters(), pattern.length(), &status)); 917 if (U_FAILURE(status)) { 918 throwTypeError(globalObject, scope, "failed to initialize DateTimeFormat"_s); 919 return; 920 } 921 922 // Gregorian calendar should be used from the beginning of ECMAScript time. 923 // Failure here means unsupported calendar, and can safely be ignored. 924 UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(m_dateFormat.get())); 925 ucal_setGregorianChange(cal, minECMAScriptTime, &status); 926 } 927 928 ASCIILiteral IntlDateTimeFormat::hourCycleString(HourCycle hourCycle) 929 { 930 switch (hourCycle) { 931 case HourCycle::H11: 932 return "h11"_s; 933 case HourCycle::H12: 934 return "h12"_s; 935 case HourCycle::H23: 936 return "h23"_s; 937 case HourCycle::H24: 938 return "h24"_s; 939 case HourCycle::None: 940 ASSERT_NOT_REACHED(); 941 return ASCIILiteral::null(); 942 } 943 ASSERT_NOT_REACHED(); 944 return ASCIILiteral::null(); 945 } 946 947 ASCIILiteral IntlDateTimeFormat::weekdayString(Weekday weekday) 948 { 949 switch (weekday) { 950 case Weekday::Narrow: 951 return "narrow"_s; 952 case Weekday::Short: 953 return "short"_s; 954 case Weekday::Long: 955 return "long"_s; 956 case Weekday::None: 957 ASSERT_NOT_REACHED(); 958 return ASCIILiteral::null(); 959 } 960 ASSERT_NOT_REACHED(); 961 return ASCIILiteral::null(); 962 } 963 964 ASCIILiteral IntlDateTimeFormat::eraString(Era era) 965 { 966 switch (era) { 967 case Era::Narrow: 968 return "narrow"_s; 969 case Era::Short: 970 return "short"_s; 971 case Era::Long: 972 return "long"_s; 973 case Era::None: 974 ASSERT_NOT_REACHED(); 975 return ASCIILiteral::null(); 976 } 977 ASSERT_NOT_REACHED(); 978 return ASCIILiteral::null(); 979 } 980 981 ASCIILiteral IntlDateTimeFormat::yearString(Year year) 982 { 983 switch (year) { 984 case Year::TwoDigit: 985 return "2-digit"_s; 986 case Year::Numeric: 987 return "numeric"_s; 988 case Year::None: 989 ASSERT_NOT_REACHED(); 990 return ASCIILiteral::null(); 991 } 992 ASSERT_NOT_REACHED(); 993 return ASCIILiteral::null(); 994 } 995 996 ASCIILiteral IntlDateTimeFormat::monthString(Month month) 997 { 998 switch (month) { 999 case Month::TwoDigit: 1000 return "2-digit"_s; 1001 case Month::Numeric: 1002 return "numeric"_s; 1003 case Month::Narrow: 1004 return "narrow"_s; 1005 case Month::Short: 1006 return "short"_s; 1007 case Month::Long: 1008 return "long"_s; 1009 case Month::None: 1010 ASSERT_NOT_REACHED(); 1011 return ASCIILiteral::null(); 1012 } 1013 ASSERT_NOT_REACHED(); 1014 return ASCIILiteral::null(); 1015 } 1016 1017 ASCIILiteral IntlDateTimeFormat::dayString(Day day) 1018 { 1019 switch (day) { 1020 case Day::TwoDigit: 1021 return "2-digit"_s; 1022 case Day::Numeric: 1023 return "numeric"_s; 1024 case Day::None: 1025 ASSERT_NOT_REACHED(); 1026 return ASCIILiteral::null(); 1027 } 1028 ASSERT_NOT_REACHED(); 1029 return ASCIILiteral::null(); 1030 } 1031 1032 ASCIILiteral IntlDateTimeFormat::dayPeriodString(DayPeriod dayPeriod) 1033 { 1034 switch (dayPeriod) { 1035 case DayPeriod::Narrow: 1036 return "narrow"_s; 1037 case DayPeriod::Short: 1038 return "short"_s; 1039 case DayPeriod::Long: 1040 return "long"_s; 1041 case DayPeriod::None: 1042 ASSERT_NOT_REACHED(); 1043 return ASCIILiteral::null(); 1044 } 1045 ASSERT_NOT_REACHED(); 1046 return ASCIILiteral::null(); 1047 } 1048 1049 ASCIILiteral IntlDateTimeFormat::hourString(Hour hour) 1050 { 1051 switch (hour) { 1052 case Hour::TwoDigit: 1053 return "2-digit"_s; 1054 case Hour::Numeric: 1055 return "numeric"_s; 1056 case Hour::None: 1057 ASSERT_NOT_REACHED(); 1058 return ASCIILiteral::null(); 1059 } 1060 ASSERT_NOT_REACHED(); 1061 return ASCIILiteral::null(); 1062 } 1063 1064 ASCIILiteral IntlDateTimeFormat::minuteString(Minute minute) 1065 { 1066 switch (minute) { 1067 case Minute::TwoDigit: 1068 return "2-digit"_s; 1069 case Minute::Numeric: 1070 return "numeric"_s; 1071 case Minute::None: 1072 ASSERT_NOT_REACHED(); 1073 return ASCIILiteral::null(); 1074 } 1075 ASSERT_NOT_REACHED(); 1076 return ASCIILiteral::null(); 1077 } 1078 1079 ASCIILiteral IntlDateTimeFormat::secondString(Second second) 1080 { 1081 switch (second) { 1082 case Second::TwoDigit: 1083 return "2-digit"_s; 1084 case Second::Numeric: 1085 return "numeric"_s; 1086 case Second::None: 1087 ASSERT_NOT_REACHED(); 1088 return ASCIILiteral::null(); 1089 } 1090 ASSERT_NOT_REACHED(); 1091 return ASCIILiteral::null(); 1092 } 1093 1094 ASCIILiteral IntlDateTimeFormat::timeZoneNameString(TimeZoneName timeZoneName) 1095 { 1096 switch (timeZoneName) { 1097 case TimeZoneName::Short: 1098 return "short"_s; 1099 case TimeZoneName::Long: 1100 return "long"_s; 1101 case TimeZoneName::None: 1102 ASSERT_NOT_REACHED(); 1103 return ASCIILiteral::null(); 1104 } 1105 ASSERT_NOT_REACHED(); 1106 return ASCIILiteral::null(); 1107 } 1108 1109 ASCIILiteral IntlDateTimeFormat::formatStyleString(DateTimeStyle style) 1110 { 1111 switch (style) { 1112 case DateTimeStyle::Full: 1113 return "full"_s; 1114 case DateTimeStyle::Long: 1115 return "long"_s; 1116 case DateTimeStyle::Medium: 1117 return "medium"_s; 1118 case DateTimeStyle::Short: 1119 return "short"_s; 1120 case DateTimeStyle::None: 1121 ASSERT_NOT_REACHED(); 1122 return ASCIILiteral::null(); 1123 } 1124 ASSERT_NOT_REACHED(); 1125 return ASCIILiteral::null(); 1126 } 1127 1128 // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions 1129 JSObject* IntlDateTimeFormat::resolvedOptions(JSGlobalObject* globalObject) const 1130 { 1131 VM& vm = globalObject->vm(); 1132 1133 JSObject* options = constructEmptyObject(globalObject); 1134 options->putDirect(vm, vm.propertyNames->locale, jsNontrivialString(vm, m_locale)); 1135 options->putDirect(vm, vm.propertyNames->calendar, jsNontrivialString(vm, m_calendar)); 1136 options->putDirect(vm, vm.propertyNames->numberingSystem, jsNontrivialString(vm, m_numberingSystem)); 1137 options->putDirect(vm, vm.propertyNames->timeZone, jsNontrivialString(vm, m_timeZone)); 1138 1139 if (m_hourCycle != HourCycle::None) { 1140 options->putDirect(vm, vm.propertyNames->hourCycle, jsNontrivialString(vm, hourCycleString(m_hourCycle))); 1141 options->putDirect(vm, vm.propertyNames->hour12, jsBoolean(m_hourCycle == HourCycle::H11 || m_hourCycle == HourCycle::H12)); 1142 } 1143 1144 if (m_weekday != Weekday::None) 1145 options->putDirect(vm, vm.propertyNames->weekday, jsNontrivialString(vm, weekdayString(m_weekday))); 1146 1147 if (m_era != Era::None) 1148 options->putDirect(vm, vm.propertyNames->era, jsNontrivialString(vm, eraString(m_era))); 1149 1150 if (m_year != Year::None) 1151 options->putDirect(vm, vm.propertyNames->year, jsNontrivialString(vm, yearString(m_year))); 1152 1153 if (m_month != Month::None) 1154 options->putDirect(vm, vm.propertyNames->month, jsNontrivialString(vm, monthString(m_month))); 1155 1156 if (m_day != Day::None) 1157 options->putDirect(vm, vm.propertyNames->day, jsNontrivialString(vm, dayString(m_day))); 1158 1159 if (Options::useIntlDateTimeFormatDayPeriod()) { 1160 if (m_dayPeriod != DayPeriod::None) 1161 options->putDirect(vm, vm.propertyNames->dayPeriod, jsNontrivialString(vm, dayPeriodString(m_dayPeriod))); 1162 } 1163 1164 if (m_hour != Hour::None) 1165 options->putDirect(vm, vm.propertyNames->hour, jsNontrivialString(vm, hourString(m_hour))); 1166 1167 if (m_minute != Minute::None) 1168 options->putDirect(vm, vm.propertyNames->minute, jsNontrivialString(vm, minuteString(m_minute))); 1169 1170 if (m_second != Second::None) 1171 options->putDirect(vm, vm.propertyNames->second, jsNontrivialString(vm, secondString(m_second))); 1172 1173 if (m_fractionalSecondDigits) 1174 options->putDirect(vm, vm.propertyNames->fractionalSecondDigits, jsNumber(m_fractionalSecondDigits)); 1175 1176 if (m_timeZoneName != TimeZoneName::None) 1177 options->putDirect(vm, vm.propertyNames->timeZoneName, jsNontrivialString(vm, timeZoneNameString(m_timeZoneName))); 1178 1179 if (m_dateStyle != DateTimeStyle::None) 1180 options->putDirect(vm, vm.propertyNames->dateStyle, jsNontrivialString(vm, formatStyleString(m_dateStyle))); 1181 1182 if (m_timeStyle != DateTimeStyle::None) 1183 options->putDirect(vm, vm.propertyNames->timeStyle, jsNontrivialString(vm, formatStyleString(m_timeStyle))); 1184 1185 return options; 1186 } 1187 1188 // https://tc39.es/ecma402/#sec-formatdatetime 1189 JSValue IntlDateTimeFormat::format(JSGlobalObject* globalObject, double value) const 1190 { 1191 ASSERT(m_dateFormat); 1192 1193 VM& vm = globalObject->vm(); 1194 auto scope = DECLARE_THROW_SCOPE(vm); 1195 1196 if (!std::isfinite(value)) 1197 return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat format()"_s); 1198 1199 Vector<UChar, 32> result; 1200 auto status = callBufferProducingFunction(udat_format, m_dateFormat.get(), value, result, nullptr); 1201 if (U_FAILURE(status)) 1202 return throwTypeError(globalObject, scope, "failed to format date value"_s); 1203 1204 return jsString(vm, String(result)); 1205 } 1206 1207 static ASCIILiteral partTypeString(UDateFormatField field) 1208 { 1209 switch (field) { 1210 case UDAT_ERA_FIELD: 1211 return "era"_s; 1212 case UDAT_YEAR_FIELD: 1213 case UDAT_EXTENDED_YEAR_FIELD: 1214 return "year"_s; 1215 case UDAT_YEAR_NAME_FIELD: 1216 return "yearName"_s; 1217 case UDAT_MONTH_FIELD: 1218 case UDAT_STANDALONE_MONTH_FIELD: 1219 return "month"_s; 1220 case UDAT_DATE_FIELD: 1221 return "day"_s; 1222 case UDAT_HOUR_OF_DAY1_FIELD: 1223 case UDAT_HOUR_OF_DAY0_FIELD: 1224 case UDAT_HOUR1_FIELD: 1225 case UDAT_HOUR0_FIELD: 1226 return "hour"_s; 1227 case UDAT_MINUTE_FIELD: 1228 return "minute"_s; 1229 case UDAT_SECOND_FIELD: 1230 return "second"_s; 1231 case UDAT_FRACTIONAL_SECOND_FIELD: 1232 return "fractionalSecond"_s; 1233 case UDAT_DAY_OF_WEEK_FIELD: 1234 case UDAT_DOW_LOCAL_FIELD: 1235 case UDAT_STANDALONE_DAY_FIELD: 1236 return "weekday"_s; 1237 case UDAT_AM_PM_FIELD: 1238 case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: 1239 case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: 1240 return "dayPeriod"_s; 1241 case UDAT_TIMEZONE_FIELD: 1242 case UDAT_TIMEZONE_RFC_FIELD: 1243 case UDAT_TIMEZONE_GENERIC_FIELD: 1244 case UDAT_TIMEZONE_SPECIAL_FIELD: 1245 case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: 1246 case UDAT_TIMEZONE_ISO_FIELD: 1247 case UDAT_TIMEZONE_ISO_LOCAL_FIELD: 1248 return "timeZoneName"_s; 1249 case UDAT_RELATED_YEAR_FIELD: 1250 return "relatedYear"_s; 1251 // These should not show up because there is no way to specify them in DateTimeFormat options. 1252 // If they do, they don't fit well into any of known part types, so consider it an "unknown". 1253 case UDAT_DAY_OF_YEAR_FIELD: 1254 case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: 1255 case UDAT_WEEK_OF_YEAR_FIELD: 1256 case UDAT_WEEK_OF_MONTH_FIELD: 1257 case UDAT_YEAR_WOY_FIELD: 1258 case UDAT_JULIAN_DAY_FIELD: 1259 case UDAT_MILLISECONDS_IN_DAY_FIELD: 1260 case UDAT_QUARTER_FIELD: 1261 case UDAT_STANDALONE_QUARTER_FIELD: 1262 case UDAT_TIME_SEPARATOR_FIELD: 1263 // Any newer additions to the UDateFormatField enum should just be considered an "unknown" part. 1264 default: 1265 return "unknown"_s; 1266 } 1267 return "unknown"_s; 1268 } 1269 1270 // https://tc39.es/ecma402/#sec-formatdatetimetoparts 1271 JSValue IntlDateTimeFormat::formatToParts(JSGlobalObject* globalObject, double value, JSString* sourceType) const 1272 { 1273 ASSERT(m_dateFormat); 1274 1275 VM& vm = globalObject->vm(); 1276 auto scope = DECLARE_THROW_SCOPE(vm); 1277 1278 if (!std::isfinite(value)) 1279 return throwRangeError(globalObject, scope, "date value is not finite in DateTimeFormat formatToParts()"_s); 1280 1281 UErrorCode status = U_ZERO_ERROR; 1282 auto fields = std::unique_ptr<UFieldPositionIterator, UFieldPositionIteratorDeleter>(ufieldpositer_open(&status)); 1283 if (U_FAILURE(status)) 1284 return throwTypeError(globalObject, scope, "failed to open field position iterator"_s); 1285 1286 Vector<UChar, 32> result; 1287 status = callBufferProducingFunction(udat_formatForFields, m_dateFormat.get(), value, result, fields.get()); 1288 if (U_FAILURE(status)) 1289 return throwTypeError(globalObject, scope, "failed to format date value"_s); 1290 1291 JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); 1292 if (!parts) 1293 return throwOutOfMemoryError(globalObject, scope); 1294 1295 auto resultString = String(result); 1296 auto literalString = jsNontrivialString(vm, "literal"_s); 1297 1298 int32_t resultLength = result.size(); 1299 int32_t previousEndIndex = 0; 1300 int32_t beginIndex = 0; 1301 int32_t endIndex = 0; 1302 while (previousEndIndex < resultLength) { 1303 auto fieldType = ufieldpositer_next(fields.get(), &beginIndex, &endIndex); 1304 if (fieldType < 0) 1305 beginIndex = endIndex = resultLength; 1306 1307 if (previousEndIndex < beginIndex) { 1308 auto value = jsString(vm, resultString.substring(previousEndIndex, beginIndex - previousEndIndex)); 1309 JSObject* part = constructEmptyObject(globalObject); 1310 part->putDirect(vm, vm.propertyNames->type, literalString); 1311 part->putDirect(vm, vm.propertyNames->value, value); 1312 if (sourceType) 1313 part->putDirect(vm, vm.propertyNames->source, sourceType); 1314 parts->push(globalObject, part); 1315 RETURN_IF_EXCEPTION(scope, { }); 1316 } 1317 previousEndIndex = endIndex; 1318 1319 if (fieldType >= 0) { 1320 auto type = jsString(vm, partTypeString(UDateFormatField(fieldType))); 1321 auto value = jsString(vm, resultString.substring(beginIndex, endIndex - beginIndex)); 1322 JSObject* part = constructEmptyObject(globalObject); 1323 part->putDirect(vm, vm.propertyNames->type, type); 1324 part->putDirect(vm, vm.propertyNames->value, value); 1325 if (sourceType) 1326 part->putDirect(vm, vm.propertyNames->source, sourceType); 1327 parts->push(globalObject, part); 1328 RETURN_IF_EXCEPTION(scope, { }); 1329 } 1330 } 1331 1332 return parts; 1333 } 1334 1335 UDateIntervalFormat* IntlDateTimeFormat::createDateIntervalFormatIfNecessary(JSGlobalObject* globalObject) 1336 { 1337 ASSERT(m_dateFormat); 1338 1339 VM& vm = globalObject->vm(); 1340 auto scope = DECLARE_THROW_SCOPE(vm); 1341 1342 if (m_dateIntervalFormat) 1343 return m_dateIntervalFormat.get(); 1344 1345 Vector<UChar, 32> pattern; 1346 { 1347 auto status = callBufferProducingFunction(udat_toPattern, m_dateFormat.get(), false, pattern); 1348 if (U_FAILURE(status)) { 1349 throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s); 1350 return nullptr; 1351 } 1352 } 1353 1354 Vector<UChar, 32> skeleton; 1355 { 1356 auto status = callBufferProducingFunction(udatpg_getSkeleton, nullptr, pattern.data(), pattern.size(), skeleton); 1357 if (U_FAILURE(status)) { 1358 throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s); 1359 return nullptr; 1360 } 1361 } 1362 1363 dataLogLnIf(IntlDateTimeFormatInternal::verbose, "interval format pattern:(", String(pattern), "),skeleton:(", String(skeleton), ")"); 1364 1365 // While the pattern is including right HourCycle patterns, UDateIntervalFormat does not follow. 1366 // We need to enforce HourCycle by setting "hc" extension if it is specified. 1367 StringBuilder localeBuilder; 1368 localeBuilder.append(m_dataLocale, "-u-ca-", m_calendar, "-nu-", m_numberingSystem); 1369 if (m_hourCycle != HourCycle::None) 1370 localeBuilder.append("-hc-", hourCycleString(m_hourCycle)); 1371 CString dataLocaleWithExtensions = localeBuilder.toString().utf8(); 1372 1373 UErrorCode status = U_ZERO_ERROR; 1374 StringView timeZoneView(m_timeZone); 1375 m_dateIntervalFormat = std::unique_ptr<UDateIntervalFormat, UDateIntervalFormatDeleter>(udtitvfmt_open(dataLocaleWithExtensions.data(), skeleton.data(), skeleton.size(), timeZoneView.upconvertedCharacters(), timeZoneView.length(), &status)); 1376 if (U_FAILURE(status)) { 1377 throwTypeError(globalObject, scope, "failed to initialize DateIntervalFormat"_s); 1378 return nullptr; 1379 } 1380 return m_dateIntervalFormat.get(); 1381 } 1382 1383 #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 1384 1385 static std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>> formattedValueFromDateRange(UDateIntervalFormat& dateIntervalFormat, UDateFormat& dateFormat, double startDate, double endDate, UErrorCode& status) 1386 { 1387 auto result = std::unique_ptr<UFormattedDateInterval, ICUDeleter<udtitvfmt_closeResult>>(udtitvfmt_openResult(&status)); 1388 if (U_FAILURE(status)) 1389 return nullptr; 1390 1391 // After ICU 67, udtitvfmt_formatToResult's signature is changed. 1392 #if U_ICU_VERSION_MAJOR_NUM >= 67 1393 // If a date is after Oct 15, 1582, the configuration of gregorian calendar change date in UCalendar does not affect 1394 // on the formatted string. To ensure that it is after Oct 15 in all timezones, we add one day to gregorian calendar 1395 // change date in UTC, so that this check can conservatively answer whether the date is definitely after gregorian 1396 // calendar change date. 1397 auto definitelyAfterGregorianCalendarChangeDate = [](double millisecondsFromEpoch) { 1398 constexpr double gregorianCalendarReformDateInUTC = -12219292800000.0; 1399 return millisecondsFromEpoch >= (gregorianCalendarReformDateInUTC + msPerDay); 1400 }; 1401 1402 // UFormattedDateInterval does not have a way to configure gregorian calendar change date while ECMAScript requires that 1403 // gregorian calendar change should not have effect (we are setting ucal_setGregorianChange(cal, minECMAScriptTime, &status) explicitly). 1404 // As a result, if the input date is older than gregorian calendar change date (Oct 15, 1582), the formatted string becomes 1405 // julian calendar date. 1406 // udtitvfmt_formatCalendarToResult API offers the way to set calendar to each date of the input, so that we can use UDateFormat's 1407 // calendar which is already configured to meet ECMAScript's requirement (effectively clearing gregorian calendar change date). 1408 // 1409 // If we can ensure that startDate is after gregorian calendar change date, we can just use udtitvfmt_formatToResult since gregorian 1410 // calendar change date does not affect on the formatted string. 1411 // 1412 // https://unicode-org.atlassian.net/browse/ICU-20705 1413 if (definitelyAfterGregorianCalendarChangeDate(startDate)) 1414 udtitvfmt_formatToResult(&dateIntervalFormat, startDate, endDate, result.get(), &status); 1415 else { 1416 auto createCalendarForDate = [](const UCalendar* calendar, double date, UErrorCode& status) -> std::unique_ptr<UCalendar, ICUDeleter<ucal_close>> { 1417 auto result = std::unique_ptr<UCalendar, ICUDeleter<ucal_close>>(ucal_clone(calendar, &status)); 1418 if (U_FAILURE(status)) 1419 return nullptr; 1420 ucal_setMillis(result.get(), date, &status); 1421 if (U_FAILURE(status)) 1422 return nullptr; 1423 return result; 1424 }; 1425 1426 auto calendar = udat_getCalendar(&dateFormat); 1427 1428 auto startCalendar = createCalendarForDate(calendar, startDate, status); 1429 if (U_FAILURE(status)) 1430 return nullptr; 1431 1432 auto endCalendar = createCalendarForDate(calendar, endDate, status); 1433 if (U_FAILURE(status)) 1434 return nullptr; 1435 1436 udtitvfmt_formatCalendarToResult(&dateIntervalFormat, startCalendar.get(), endCalendar.get(), result.get(), &status); 1437 } 1438 #else 1439 UNUSED_PARAM(dateFormat); 1440 udtitvfmt_formatToResult(&dateIntervalFormat, result.get(), startDate, endDate, &status); 1441 #endif 1442 return result; 1443 } 1444 1445 static bool dateFieldsPracticallyEqual(const UFormattedValue* formattedValue, UErrorCode& status) 1446 { 1447 auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status)); 1448 if (U_FAILURE(status)) 1449 return false; 1450 1451 // We only care about UFIELD_CATEGORY_DATE_INTERVAL_SPAN category. 1452 ucfpos_constrainCategory(iterator.get(), UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status); 1453 if (U_FAILURE(status)) 1454 return false; 1455 1456 bool hasSpan = ufmtval_nextPosition(formattedValue, iterator.get(), &status); 1457 if (U_FAILURE(status)) 1458 return false; 1459 1460 return !hasSpan; 1461 } 1462 1463 #endif // HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 1464 1465 JSValue IntlDateTimeFormat::formatRange(JSGlobalObject* globalObject, double startDate, double endDate) 1466 { 1467 ASSERT(m_dateFormat); 1468 1469 VM& vm = globalObject->vm(); 1470 auto scope = DECLARE_THROW_SCOPE(vm); 1471 1472 // http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-partitiondatetimerangepattern 1473 startDate = timeClip(startDate); 1474 endDate = timeClip(endDate); 1475 if (std::isnan(startDate) || std::isnan(endDate)) { 1476 throwRangeError(globalObject, scope, "Passed date is out of range"_s); 1477 return { }; 1478 } 1479 1480 auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject); 1481 RETURN_IF_EXCEPTION(scope, { }); 1482 1483 #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 1484 UErrorCode status = U_ZERO_ERROR; 1485 auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status); 1486 if (U_FAILURE(status)) { 1487 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1488 return { }; 1489 } 1490 1491 // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it. 1492 auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status); 1493 if (U_FAILURE(status)) { 1494 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1495 return { }; 1496 } 1497 1498 // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range. 1499 // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year. 1500 // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with 1501 // `source: "shared"` additional fields. 1502 bool equal = dateFieldsPracticallyEqual(formattedValue, status); 1503 if (U_FAILURE(status)) { 1504 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1505 return { }; 1506 } 1507 1508 if (equal) 1509 RELEASE_AND_RETURN(scope, format(globalObject, startDate)); 1510 1511 int32_t formattedStringLength = 0; 1512 const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status); 1513 if (U_FAILURE(status)) { 1514 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1515 return { }; 1516 } 1517 1518 return jsString(vm, String(formattedStringPointer, formattedStringLength)); 1519 #else 1520 Vector<UChar, 32> buffer; 1521 auto status = callBufferProducingFunction(udtitvfmt_format, dateIntervalFormat, startDate, endDate, buffer, nullptr); 1522 if (U_FAILURE(status)) { 1523 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1524 return { }; 1525 } 1526 1527 return jsString(vm, String(buffer)); 1528 #endif 1529 } 1530 1531 JSValue IntlDateTimeFormat::formatRangeToParts(JSGlobalObject* globalObject, double startDate, double endDate) 1532 { 1533 ASSERT(m_dateFormat); 1534 1535 VM& vm = globalObject->vm(); 1536 auto scope = DECLARE_THROW_SCOPE(vm); 1537 1538 #if HAVE(ICU_U_DATE_INTERVAL_FORMAT_FORMAT_RANGE_TO_PARTS) 1539 // http://tc39.es/proposal-intl-DateTimeFormat-formatRange/#sec-partitiondatetimerangepattern 1540 startDate = timeClip(startDate); 1541 endDate = timeClip(endDate); 1542 if (std::isnan(startDate) || std::isnan(endDate)) { 1543 throwRangeError(globalObject, scope, "Passed date is out of range"_s); 1544 return { }; 1545 } 1546 1547 auto* dateIntervalFormat = createDateIntervalFormatIfNecessary(globalObject); 1548 RETURN_IF_EXCEPTION(scope, { }); 1549 1550 UErrorCode status = U_ZERO_ERROR; 1551 auto result = formattedValueFromDateRange(*dateIntervalFormat, *m_dateFormat, startDate, endDate, status); 1552 if (U_FAILURE(status)) { 1553 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1554 return { }; 1555 } 1556 1557 // UFormattedValue is owned by UFormattedDateInterval. We do not need to close it. 1558 auto formattedValue = udtitvfmt_resultAsValue(result.get(), &status); 1559 if (U_FAILURE(status)) { 1560 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1561 return { }; 1562 } 1563 1564 auto sharedString = jsNontrivialString(vm, "shared"_s); 1565 1566 // If the formatted parts of startDate and endDate are the same, it is possible that the resulted string does not look like range. 1567 // For example, if the requested format only includes "year" and startDate and endDate are the same year, the result just contains one year. 1568 // In that case, startDate and endDate are *practically-equal* (spec term), and we generate parts as we call `formatToParts(startDate)` with 1569 // `source: "shared"` additional fields. 1570 bool equal = dateFieldsPracticallyEqual(formattedValue, status); 1571 if (U_FAILURE(status)) { 1572 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1573 return { }; 1574 } 1575 1576 if (equal) 1577 RELEASE_AND_RETURN(scope, formatToParts(globalObject, startDate, sharedString)); 1578 1579 // ICU produces ranges for the formatted string, and we construct parts array from that. 1580 // For example, startDate = Jan 3, 2019, endDate = Jan 5, 2019 with en-US locale is, 1581 // 1582 // Formatted string: "1/3/2019 – 1/5/2019" 1583 // | | | | | | | | 1584 // B C | | F G | | 1585 // | +-D+ | +-H+ 1586 // | | | | 1587 // +--A---+ +--E---+ 1588 // 1589 // Ranges ICU generates: 1590 // A: (0, 8) UFIELD_CATEGORY_DATE_INTERVAL_SPAN startRange 1591 // B: (0, 1) UFIELD_CATEGORY_DATE month 1592 // C: (2, 3) UFIELD_CATEGORY_DATE day 1593 // D: (4, 8) UFIELD_CATEGORY_DATE year 1594 // E: (11, 19) UFIELD_CATEGORY_DATE_INTERVAL_SPAN endRange 1595 // F: (11, 12) UFIELD_CATEGORY_DATE month 1596 // G: (13, 14) UFIELD_CATEGORY_DATE day 1597 // H: (15, 19) UFIELD_CATEGORY_DATE year 1598 // 1599 // We use UFIELD_CATEGORY_DATE_INTERVAL_SPAN range to determine each part is either "startRange", "endRange", or "shared". 1600 // It is gurarnteed that UFIELD_CATEGORY_DATE_INTERVAL_SPAN comes first before any other parts including that range. 1601 // For example, in the above formatted string, " – " is "shared" part. For UFIELD_CATEGORY_DATE ranges, we generate corresponding 1602 // part object with types such as "month". And non populated parts (e.g. "/") become "literal" parts. 1603 // In the above case, expected parts are, 1604 // 1605 // { type: "month", value: "1", source: "startRange" }, 1606 // { type: "literal", value: "/", source: "startRange" }, 1607 // { type: "day", value: "3", source: "startRange" }, 1608 // { type: "literal", value: "/", source: "startRange" }, 1609 // { type: "year", value: "2019", source: "startRange" }, 1610 // { type: "literal", value: " - ", source: "shared" }, 1611 // { type: "month", value: "1", source: "endRange" }, 1612 // { type: "literal", value: "/", source: "endRange" }, 1613 // { type: "day", value: "5", source: "endRange" }, 1614 // { type: "literal", value: "/", source: "endRange" }, 1615 // { type: "year", value: "2019", source: "endRange" }, 1616 // 1617 1618 JSArray* parts = JSArray::tryCreate(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 0); 1619 if (!parts) { 1620 throwOutOfMemoryError(globalObject, scope); 1621 return { }; 1622 } 1623 1624 int32_t formattedStringLength = 0; 1625 const UChar* formattedStringPointer = ufmtval_getString(formattedValue, &formattedStringLength, &status); 1626 if (U_FAILURE(status)) { 1627 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1628 return { }; 1629 } 1630 String resultString(formattedStringPointer, formattedStringLength); 1631 1632 // We care multiple categories (UFIELD_CATEGORY_DATE and UFIELD_CATEGORY_DATE_INTERVAL_SPAN). 1633 // So we do not constraint iterator. 1634 auto iterator = std::unique_ptr<UConstrainedFieldPosition, ICUDeleter<ucfpos_close>>(ucfpos_open(&status)); 1635 if (U_FAILURE(status)) { 1636 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1637 return { }; 1638 } 1639 1640 auto startRangeString = jsNontrivialString(vm, "startRange"_s); 1641 auto endRangeString = jsNontrivialString(vm, "endRange"_s); 1642 auto literalString = jsNontrivialString(vm, "literal"_s); 1643 1644 WTF::Range<int32_t> startRange { -1, -1 }; 1645 WTF::Range<int32_t> endRange { -1, -1 }; 1646 1647 auto createPart = [&] (JSString* type, int32_t beginIndex, int32_t length) { 1648 auto sourceType = [&](int32_t index) -> JSString* { 1649 if (startRange.contains(index)) 1650 return startRangeString; 1651 if (endRange.contains(index)) 1652 return endRangeString; 1653 return sharedString; 1654 }; 1655 1656 auto value = jsString(vm, resultString.substring(beginIndex, length)); 1657 JSObject* part = constructEmptyObject(globalObject); 1658 part->putDirect(vm, vm.propertyNames->type, type); 1659 part->putDirect(vm, vm.propertyNames->value, value); 1660 part->putDirect(vm, vm.propertyNames->source, sourceType(beginIndex)); 1661 return part; 1662 }; 1663 1664 int32_t resultLength = resultString.length(); 1665 int32_t previousEndIndex = 0; 1666 while (true) { 1667 bool next = ufmtval_nextPosition(formattedValue, iterator.get(), &status); 1668 if (U_FAILURE(status)) { 1669 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1670 return { }; 1671 } 1672 if (!next) 1673 break; 1674 1675 int32_t category = ucfpos_getCategory(iterator.get(), &status); 1676 if (U_FAILURE(status)) { 1677 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1678 return { }; 1679 } 1680 1681 int32_t fieldType = ucfpos_getField(iterator.get(), &status); 1682 if (U_FAILURE(status)) { 1683 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1684 return { }; 1685 } 1686 1687 int32_t beginIndex = 0; 1688 int32_t endIndex = 0; 1689 ucfpos_getIndexes(iterator.get(), &beginIndex, &endIndex, &status); 1690 if (U_FAILURE(status)) { 1691 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1692 return { }; 1693 } 1694 1695 dataLogLnIf(IntlDateTimeFormatInternal::verbose, category, " ", fieldType, " (", beginIndex, ", ", endIndex, ")"); 1696 1697 if (category != UFIELD_CATEGORY_DATE && category != UFIELD_CATEGORY_DATE_INTERVAL_SPAN) 1698 continue; 1699 if (category == UFIELD_CATEGORY_DATE && fieldType < 0) 1700 continue; 1701 1702 if (previousEndIndex < beginIndex) { 1703 JSObject* part = createPart(literalString, previousEndIndex, beginIndex - previousEndIndex); 1704 parts->push(globalObject, part); 1705 RETURN_IF_EXCEPTION(scope, { }); 1706 previousEndIndex = beginIndex; 1707 } 1708 1709 if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) { 1710 // > The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN is used to indicate which datetime 1711 // > primitives came from which arguments: 0 means fromCalendar, and 1 means toCalendar. The span category 1712 // > will always occur before the corresponding fields in UFIELD_CATEGORY_DATE in the nextPosition() iterator. 1713 // from ICU comment. So, field 0 is startRange, field 1 is endRange. 1714 if (!fieldType) 1715 startRange = WTF::Range<int32_t>(beginIndex, endIndex); 1716 else { 1717 ASSERT(fieldType == 1); 1718 endRange = WTF::Range<int32_t>(beginIndex, endIndex); 1719 } 1720 continue; 1721 } 1722 1723 ASSERT(category == UFIELD_CATEGORY_DATE); 1724 1725 auto type = jsString(vm, partTypeString(UDateFormatField(fieldType))); 1726 JSObject* part = createPart(type, beginIndex, endIndex - beginIndex); 1727 parts->push(globalObject, part); 1728 RETURN_IF_EXCEPTION(scope, { }); 1729 previousEndIndex = endIndex; 1730 } 1731 1732 if (previousEndIndex < resultLength) { 1733 JSObject* part = createPart(literalString, previousEndIndex, resultLength - previousEndIndex); 1734 parts->push(globalObject, part); 1735 RETURN_IF_EXCEPTION(scope, { }); 1736 } 1737 1738 return parts; 1739 #else 1740 UNUSED_PARAM(startDate); 1741 UNUSED_PARAM(endDate); 1742 throwTypeError(globalObject, scope, "Failed to format date interval"_s); 1743 return { }; 1744 #endif 1745 } 1746 1747 1748 } // namespace JSC