1 // Copyright 2013 the V8 project authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // ECMAScript 402 API implementation. 6 7 /** 8 * Intl object is a single object that has some named properties, 9 * all of which are constructors. 10 */ 11 (function(global, utils) { 12 13 "use strict"; 14 15 %CheckIsBootstrapping(); 16 17 // ------------------------------------------------------------------- 18 // Imports 19 20 var ArrayIndexOf; 21 var ArrayJoin; 22 var ArrayPush; 23 var FLAG_intl_extra; 24 var GlobalDate = global.Date; 25 var GlobalNumber = global.Number; 26 var GlobalRegExp = global.RegExp; 27 var GlobalString = global.String; 28 var InstallFunctions = utils.InstallFunctions; 29 var InstallGetter = utils.InstallGetter; 30 var InternalArray = utils.InternalArray; 31 var InternalRegExpMatch; 32 var InternalRegExpReplace 33 var IsNaN; 34 var MakeError; 35 var MakeRangeError; 36 var MakeTypeError; 37 var ObjectHasOwnProperty = utils.ImportNow("ObjectHasOwnProperty"); 38 var OverrideFunction = utils.OverrideFunction; 39 var patternSymbol = utils.ImportNow("intl_pattern_symbol"); 40 var resolvedSymbol = utils.ImportNow("intl_resolved_symbol"); 41 var SetFunctionName = utils.SetFunctionName; 42 var StringIndexOf; 43 var StringLastIndexOf; 44 var StringSplit; 45 var StringSubstr; 46 var StringSubstring; 47 48 utils.Import(function(from) { 49 ArrayIndexOf = from.ArrayIndexOf; 50 ArrayJoin = from.ArrayJoin; 51 ArrayPush = from.ArrayPush; 52 IsNaN = from.IsNaN; 53 MakeError = from.MakeError; 54 MakeRangeError = from.MakeRangeError; 55 MakeTypeError = from.MakeTypeError; 56 InternalRegExpMatch = from.InternalRegExpMatch; 57 InternalRegExpReplace = from.InternalRegExpReplace; 58 StringIndexOf = from.StringIndexOf; 59 StringLastIndexOf = from.StringLastIndexOf; 60 StringSplit = from.StringSplit; 61 StringSubstr = from.StringSubstr; 62 StringSubstring = from.StringSubstring; 63 }); 64 65 utils.ImportFromExperimental(function(from) { 66 FLAG_intl_extra = from.FLAG_intl_extra; 67 }); 68 69 // Utilities for definitions 70 71 function InstallFunction(object, name, func) { 72 InstallFunctions(object, DONT_ENUM, [name, func]); 73 } 74 75 76 function InstallConstructor(object, name, func) { 77 %CheckIsBootstrapping(); 78 SetFunctionName(func, name); 79 %AddNamedProperty(object, name, func, DONT_ENUM); 80 %SetNativeFlag(func); 81 %ToFastProperties(object); 82 } 83 84 /** 85 * Adds bound method to the prototype of the given object. 86 */ 87 function AddBoundMethod(obj, methodName, implementation, length, type) { 88 %CheckIsBootstrapping(); 89 var internalName = %CreatePrivateSymbol(methodName); 90 var getter = function() { 91 if (!%IsInitializedIntlObjectOfType(this, type)) { 92 throw MakeTypeError(kMethodCalledOnWrongObject, methodName); 93 } 94 if (IS_UNDEFINED(this[internalName])) { 95 var boundMethod; 96 if (IS_UNDEFINED(length) || length === 2) { 97 boundMethod = (x, y) => implementation(this, x, y); 98 } else if (length === 1) { 99 boundMethod = x => implementation(this, x); 100 } else { 101 boundMethod = (...args) => { 102 // DateTimeFormat.format needs to be 0 arg method, but can stil 103 // receive optional dateValue param. If one was provided, pass it 104 // along. 105 if (args.length > 0) { 106 return implementation(this, args[0]); 107 } else { 108 return implementation(this); 109 } 110 } 111 } 112 // TODO(littledan): Once function name reform is shipped, remove the 113 // following line and wrap the boundMethod definition in an anonymous 114 // function macro. 115 %FunctionSetName(boundMethod, '__bound' + methodName + '__'); 116 %FunctionRemovePrototype(boundMethod); 117 %SetNativeFlag(boundMethod); 118 this[internalName] = boundMethod; 119 } 120 return this[internalName]; 121 }; 122 123 InstallGetter(obj.prototype, methodName, getter, DONT_ENUM); 124 } 125 126 // ------------------------------------------------------------------- 127 128 var Intl = {}; 129 130 %AddNamedProperty(global, "Intl", Intl, DONT_ENUM); 131 132 /** 133 * Caches available locales for each service. 134 */ 135 var AVAILABLE_LOCALES = { 136 'collator': UNDEFINED, 137 'numberformat': UNDEFINED, 138 'dateformat': UNDEFINED, 139 'breakiterator': UNDEFINED 140 }; 141 142 /** 143 * Caches default ICU locale. 144 */ 145 var DEFAULT_ICU_LOCALE = UNDEFINED; 146 147 function GetDefaultICULocaleJS() { 148 if (IS_UNDEFINED(DEFAULT_ICU_LOCALE)) { 149 DEFAULT_ICU_LOCALE = %GetDefaultICULocale(); 150 } 151 return DEFAULT_ICU_LOCALE; 152 } 153 154 /** 155 * Unicode extension regular expression. 156 */ 157 var UNICODE_EXTENSION_RE = UNDEFINED; 158 159 function GetUnicodeExtensionRE() { 160 if (IS_UNDEFINED(UNDEFINED)) { 161 UNICODE_EXTENSION_RE = new GlobalRegExp('-u(-[a-z0-9]{2,8})+', 'g'); 162 } 163 return UNICODE_EXTENSION_RE; 164 } 165 166 /** 167 * Matches any Unicode extension. 168 */ 169 var ANY_EXTENSION_RE = UNDEFINED; 170 171 function GetAnyExtensionRE() { 172 if (IS_UNDEFINED(ANY_EXTENSION_RE)) { 173 ANY_EXTENSION_RE = new GlobalRegExp('-[a-z0-9]{1}-.*', 'g'); 174 } 175 return ANY_EXTENSION_RE; 176 } 177 178 /** 179 * Replace quoted text (single quote, anything but the quote and quote again). 180 */ 181 var QUOTED_STRING_RE = UNDEFINED; 182 183 function GetQuotedStringRE() { 184 if (IS_UNDEFINED(QUOTED_STRING_RE)) { 185 QUOTED_STRING_RE = new GlobalRegExp("'[^']+'", 'g'); 186 } 187 return QUOTED_STRING_RE; 188 } 189 190 /** 191 * Matches valid service name. 192 */ 193 var SERVICE_RE = UNDEFINED; 194 195 function GetServiceRE() { 196 if (IS_UNDEFINED(SERVICE_RE)) { 197 SERVICE_RE = 198 new GlobalRegExp('^(collator|numberformat|dateformat|breakiterator)$'); 199 } 200 return SERVICE_RE; 201 } 202 203 /** 204 * Validates a language tag against bcp47 spec. 205 * Actual value is assigned on first run. 206 */ 207 var LANGUAGE_TAG_RE = UNDEFINED; 208 209 function GetLanguageTagRE() { 210 if (IS_UNDEFINED(LANGUAGE_TAG_RE)) { 211 BuildLanguageTagREs(); 212 } 213 return LANGUAGE_TAG_RE; 214 } 215 216 /** 217 * Helps find duplicate variants in the language tag. 218 */ 219 var LANGUAGE_VARIANT_RE = UNDEFINED; 220 221 function GetLanguageVariantRE() { 222 if (IS_UNDEFINED(LANGUAGE_VARIANT_RE)) { 223 BuildLanguageTagREs(); 224 } 225 return LANGUAGE_VARIANT_RE; 226 } 227 228 /** 229 * Helps find duplicate singletons in the language tag. 230 */ 231 var LANGUAGE_SINGLETON_RE = UNDEFINED; 232 233 function GetLanguageSingletonRE() { 234 if (IS_UNDEFINED(LANGUAGE_SINGLETON_RE)) { 235 BuildLanguageTagREs(); 236 } 237 return LANGUAGE_SINGLETON_RE; 238 } 239 240 /** 241 * Matches valid IANA time zone names. 242 */ 243 var TIMEZONE_NAME_CHECK_RE = UNDEFINED; 244 245 function GetTimezoneNameCheckRE() { 246 if (IS_UNDEFINED(TIMEZONE_NAME_CHECK_RE)) { 247 TIMEZONE_NAME_CHECK_RE = new GlobalRegExp( 248 '^([A-Za-z]+)/([A-Za-z_-]+)((?:\/[A-Za-z_-]+)+)*$'); 249 } 250 return TIMEZONE_NAME_CHECK_RE; 251 } 252 253 /** 254 * Matches valid location parts of IANA time zone names. 255 */ 256 var TIMEZONE_NAME_LOCATION_PART_RE = UNDEFINED; 257 258 function GetTimezoneNameLocationPartRE() { 259 if (IS_UNDEFINED(TIMEZONE_NAME_LOCATION_PART_RE)) { 260 TIMEZONE_NAME_LOCATION_PART_RE = 261 new GlobalRegExp('^([A-Za-z]+)((?:[_-][A-Za-z]+)+)*$'); 262 } 263 return TIMEZONE_NAME_LOCATION_PART_RE; 264 } 265 266 267 /** 268 * Returns an intersection of locales and service supported locales. 269 * Parameter locales is treated as a priority list. 270 */ 271 function supportedLocalesOf(service, locales, options) { 272 if (IS_NULL(InternalRegExpMatch(GetServiceRE(), service))) { 273 throw MakeError(kWrongServiceType, service); 274 } 275 276 // Provide defaults if matcher was not specified. 277 if (IS_UNDEFINED(options)) { 278 options = {}; 279 } else { 280 options = TO_OBJECT(options); 281 } 282 283 var matcher = options.localeMatcher; 284 if (!IS_UNDEFINED(matcher)) { 285 matcher = TO_STRING(matcher); 286 if (matcher !== 'lookup' && matcher !== 'best fit') { 287 throw MakeRangeError(kLocaleMatcher, matcher); 288 } 289 } else { 290 matcher = 'best fit'; 291 } 292 293 var requestedLocales = initializeLocaleList(locales); 294 295 // Cache these, they don't ever change per service. 296 if (IS_UNDEFINED(AVAILABLE_LOCALES[service])) { 297 AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service); 298 } 299 300 // Use either best fit or lookup algorithm to match locales. 301 if (matcher === 'best fit') { 302 return initializeLocaleList(bestFitSupportedLocalesOf( 303 requestedLocales, AVAILABLE_LOCALES[service])); 304 } 305 306 return initializeLocaleList(lookupSupportedLocalesOf( 307 requestedLocales, AVAILABLE_LOCALES[service])); 308 } 309 310 311 /** 312 * Returns the subset of the provided BCP 47 language priority list for which 313 * this service has a matching locale when using the BCP 47 Lookup algorithm. 314 * Locales appear in the same order in the returned list as in the input list. 315 */ 316 function lookupSupportedLocalesOf(requestedLocales, availableLocales) { 317 var matchedLocales = new InternalArray(); 318 for (var i = 0; i < requestedLocales.length; ++i) { 319 // Remove -u- extension. 320 var locale = InternalRegExpReplace( 321 GetUnicodeExtensionRE(), requestedLocales[i], ''); 322 do { 323 if (!IS_UNDEFINED(availableLocales[locale])) { 324 // Push requested locale not the resolved one. 325 %_Call(ArrayPush, matchedLocales, requestedLocales[i]); 326 break; 327 } 328 // Truncate locale if possible, if not break. 329 var pos = %_Call(StringLastIndexOf, locale, '-'); 330 if (pos === -1) { 331 break; 332 } 333 locale = %_Call(StringSubstring, locale, 0, pos); 334 } while (true); 335 } 336 337 return matchedLocales; 338 } 339 340 341 /** 342 * Returns the subset of the provided BCP 47 language priority list for which 343 * this service has a matching locale when using the implementation 344 * dependent algorithm. 345 * Locales appear in the same order in the returned list as in the input list. 346 */ 347 function bestFitSupportedLocalesOf(requestedLocales, availableLocales) { 348 // TODO(cira): implement better best fit algorithm. 349 return lookupSupportedLocalesOf(requestedLocales, availableLocales); 350 } 351 352 353 /** 354 * Returns a getOption function that extracts property value for given 355 * options object. If property is missing it returns defaultValue. If value 356 * is out of range for that property it throws RangeError. 357 */ 358 function getGetOption(options, caller) { 359 if (IS_UNDEFINED(options)) throw MakeError(kDefaultOptionsMissing, caller); 360 361 var getOption = function getOption(property, type, values, defaultValue) { 362 if (!IS_UNDEFINED(options[property])) { 363 var value = options[property]; 364 switch (type) { 365 case 'boolean': 366 value = TO_BOOLEAN(value); 367 break; 368 case 'string': 369 value = TO_STRING(value); 370 break; 371 case 'number': 372 value = TO_NUMBER(value); 373 break; 374 default: 375 throw MakeError(kWrongValueType); 376 } 377 378 if (!IS_UNDEFINED(values) && %_Call(ArrayIndexOf, values, value) === -1) { 379 throw MakeRangeError(kValueOutOfRange, value, caller, property); 380 } 381 382 return value; 383 } 384 385 return defaultValue; 386 } 387 388 return getOption; 389 } 390 391 392 /** 393 * Compares a BCP 47 language priority list requestedLocales against the locales 394 * in availableLocales and determines the best available language to meet the 395 * request. Two algorithms are available to match the locales: the Lookup 396 * algorithm described in RFC 4647 section 3.4, and an implementation dependent 397 * best-fit algorithm. Independent of the locale matching algorithm, options 398 * specified through Unicode locale extension sequences are negotiated 399 * separately, taking the caller's relevant extension keys and locale data as 400 * well as client-provided options into consideration. Returns an object with 401 * a locale property whose value is the language tag of the selected locale, 402 * and properties for each key in relevantExtensionKeys providing the selected 403 * value for that key. 404 */ 405 function resolveLocale(service, requestedLocales, options) { 406 requestedLocales = initializeLocaleList(requestedLocales); 407 408 var getOption = getGetOption(options, service); 409 var matcher = getOption('localeMatcher', 'string', 410 ['lookup', 'best fit'], 'best fit'); 411 var resolved; 412 if (matcher === 'lookup') { 413 resolved = lookupMatcher(service, requestedLocales); 414 } else { 415 resolved = bestFitMatcher(service, requestedLocales); 416 } 417 418 return resolved; 419 } 420 421 422 /** 423 * Returns best matched supported locale and extension info using basic 424 * lookup algorithm. 425 */ 426 function lookupMatcher(service, requestedLocales) { 427 if (IS_NULL(InternalRegExpMatch(GetServiceRE(), service))) { 428 throw MakeError(kWrongServiceType, service); 429 } 430 431 // Cache these, they don't ever change per service. 432 if (IS_UNDEFINED(AVAILABLE_LOCALES[service])) { 433 AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service); 434 } 435 436 for (var i = 0; i < requestedLocales.length; ++i) { 437 // Remove all extensions. 438 var locale = InternalRegExpReplace( 439 GetAnyExtensionRE(), requestedLocales[i], ''); 440 do { 441 if (!IS_UNDEFINED(AVAILABLE_LOCALES[service][locale])) { 442 // Return the resolved locale and extension. 443 var extensionMatch = InternalRegExpMatch( 444 GetUnicodeExtensionRE(), requestedLocales[i]); 445 var extension = IS_NULL(extensionMatch) ? '' : extensionMatch[0]; 446 return {'locale': locale, 'extension': extension, 'position': i}; 447 } 448 // Truncate locale if possible. 449 var pos = %_Call(StringLastIndexOf, locale, '-'); 450 if (pos === -1) { 451 break; 452 } 453 locale = %_Call(StringSubstring, locale, 0, pos); 454 } while (true); 455 } 456 457 // Didn't find a match, return default. 458 return {'locale': GetDefaultICULocaleJS(), 'extension': '', 'position': -1}; 459 } 460 461 462 /** 463 * Returns best matched supported locale and extension info using 464 * implementation dependend algorithm. 465 */ 466 function bestFitMatcher(service, requestedLocales) { 467 // TODO(cira): implement better best fit algorithm. 468 return lookupMatcher(service, requestedLocales); 469 } 470 471 472 /** 473 * Parses Unicode extension into key - value map. 474 * Returns empty object if the extension string is invalid. 475 * We are not concerned with the validity of the values at this point. 476 */ 477 function parseExtension(extension) { 478 var extensionSplit = %_Call(StringSplit, extension, '-'); 479 480 // Assume ['', 'u', ...] input, but don't throw. 481 if (extensionSplit.length <= 2 || 482 (extensionSplit[0] !== '' && extensionSplit[1] !== 'u')) { 483 return {}; 484 } 485 486 // Key is {2}alphanum, value is {3,8}alphanum. 487 // Some keys may not have explicit values (booleans). 488 var extensionMap = {}; 489 var previousKey = UNDEFINED; 490 for (var i = 2; i < extensionSplit.length; ++i) { 491 var length = extensionSplit[i].length; 492 var element = extensionSplit[i]; 493 if (length === 2) { 494 extensionMap[element] = UNDEFINED; 495 previousKey = element; 496 } else if (length >= 3 && length <=8 && !IS_UNDEFINED(previousKey)) { 497 extensionMap[previousKey] = element; 498 previousKey = UNDEFINED; 499 } else { 500 // There is a value that's too long, or that doesn't have a key. 501 return {}; 502 } 503 } 504 505 return extensionMap; 506 } 507 508 509 /** 510 * Populates internalOptions object with boolean key-value pairs 511 * from extensionMap and options. 512 * Returns filtered extension (number and date format constructors use 513 * Unicode extensions for passing parameters to ICU). 514 * It's used for extension-option pairs only, e.g. kn-normalization, but not 515 * for 'sensitivity' since it doesn't have extension equivalent. 516 * Extensions like nu and ca don't have options equivalent, so we place 517 * undefined in the map.property to denote that. 518 */ 519 function setOptions(inOptions, extensionMap, keyValues, getOption, outOptions) { 520 var extension = ''; 521 522 var updateExtension = function updateExtension(key, value) { 523 return '-' + key + '-' + TO_STRING(value); 524 } 525 526 var updateProperty = function updateProperty(property, type, value) { 527 if (type === 'boolean' && (typeof value === 'string')) { 528 value = (value === 'true') ? true : false; 529 } 530 531 if (!IS_UNDEFINED(property)) { 532 defineWEProperty(outOptions, property, value); 533 } 534 } 535 536 for (var key in keyValues) { 537 if (HAS_OWN_PROPERTY(keyValues, key)) { 538 var value = UNDEFINED; 539 var map = keyValues[key]; 540 if (!IS_UNDEFINED(map.property)) { 541 // This may return true if user specifies numeric: 'false', since 542 // Boolean('nonempty') === true. 543 value = getOption(map.property, map.type, map.values); 544 } 545 if (!IS_UNDEFINED(value)) { 546 updateProperty(map.property, map.type, value); 547 extension += updateExtension(key, value); 548 continue; 549 } 550 // User options didn't have it, check Unicode extension. 551 // Here we want to convert strings 'true', 'false' into proper Boolean 552 // values (not a user error). 553 if (HAS_OWN_PROPERTY(extensionMap, key)) { 554 value = extensionMap[key]; 555 if (!IS_UNDEFINED(value)) { 556 updateProperty(map.property, map.type, value); 557 extension += updateExtension(key, value); 558 } else if (map.type === 'boolean') { 559 // Boolean keys are allowed not to have values in Unicode extension. 560 // Those default to true. 561 updateProperty(map.property, map.type, true); 562 extension += updateExtension(key, true); 563 } 564 } 565 } 566 } 567 568 return extension === ''? '' : '-u' + extension; 569 } 570 571 572 /** 573 * Given an array-like, outputs an Array with the numbered 574 * properties copied over and defined 575 * configurable: false, writable: false, enumerable: true. 576 */ 577 function freezeArray(input) { 578 var array = []; 579 var l = input.length; 580 for (var i = 0; i < l; i++) { 581 if (i in input) { 582 %object_define_property(array, i, {value: input[i], 583 configurable: false, 584 writable: false, 585 enumerable: true}); 586 } 587 } 588 589 %object_define_property(array, 'length', {value: l, writable: false}); 590 return array; 591 } 592 593 594 /** 595 * It's sometimes desireable to leave user requested locale instead of ICU 596 * supported one (zh-TW is equivalent to zh-Hant-TW, so we should keep shorter 597 * one, if that was what user requested). 598 * This function returns user specified tag if its maximized form matches ICU 599 * resolved locale. If not we return ICU result. 600 */ 601 function getOptimalLanguageTag(original, resolved) { 602 // Returns Array<Object>, where each object has maximized and base properties. 603 // Maximized: zh -> zh-Hans-CN 604 // Base: zh-CN-u-ca-gregory -> zh-CN 605 // Take care of grandfathered or simple cases. 606 if (original === resolved) { 607 return original; 608 } 609 610 var locales = %GetLanguageTagVariants([original, resolved]); 611 if (locales[0].maximized !== locales[1].maximized) { 612 return resolved; 613 } 614 615 // Preserve extensions of resolved locale, but swap base tags with original. 616 var resolvedBase = new GlobalRegExp('^' + locales[1].base, 'g'); 617 return InternalRegExpReplace(resolvedBase, resolved, locales[0].base); 618 } 619 620 621 /** 622 * Returns an Object that contains all of supported locales for a given 623 * service. 624 * In addition to the supported locales we add xx-ZZ locale for each xx-Yyyy-ZZ 625 * that is supported. This is required by the spec. 626 */ 627 function getAvailableLocalesOf(service) { 628 var available = %AvailableLocalesOf(service); 629 630 for (var i in available) { 631 if (HAS_OWN_PROPERTY(available, i)) { 632 var parts = InternalRegExpMatch( 633 /^([a-z]{2,3})-([A-Z][a-z]{3})-([A-Z]{2})$/, i); 634 if (!IS_NULL(parts)) { 635 // Build xx-ZZ. We don't care about the actual value, 636 // as long it's not undefined. 637 available[parts[1] + '-' + parts[3]] = null; 638 } 639 } 640 } 641 642 return available; 643 } 644 645 646 /** 647 * Defines a property and sets writable and enumerable to true. 648 * Configurable is false by default. 649 */ 650 function defineWEProperty(object, property, value) { 651 %object_define_property(object, property, 652 {value: value, writable: true, enumerable: true}); 653 } 654 655 656 /** 657 * Adds property to an object if the value is not undefined. 658 * Sets configurable descriptor to false. 659 */ 660 function addWEPropertyIfDefined(object, property, value) { 661 if (!IS_UNDEFINED(value)) { 662 defineWEProperty(object, property, value); 663 } 664 } 665 666 667 /** 668 * Defines a property and sets writable, enumerable and configurable to true. 669 */ 670 function defineWECProperty(object, property, value) { 671 %object_define_property(object, property, {value: value, 672 writable: true, 673 enumerable: true, 674 configurable: true}); 675 } 676 677 678 /** 679 * Adds property to an object if the value is not undefined. 680 * Sets all descriptors to true. 681 */ 682 function addWECPropertyIfDefined(object, property, value) { 683 if (!IS_UNDEFINED(value)) { 684 defineWECProperty(object, property, value); 685 } 686 } 687 688 689 /** 690 * Returns titlecased word, aMeRricA -> America. 691 */ 692 function toTitleCaseWord(word) { 693 return %StringToUpperCase(%_Call(StringSubstr, word, 0, 1)) + 694 %StringToLowerCase(%_Call(StringSubstr, word, 1)); 695 } 696 697 /** 698 * Returns titlecased location, bueNos_airES -> Buenos_Aires 699 * or ho_cHi_minH -> Ho_Chi_Minh. It is locale-agnostic and only 700 * deals with ASCII only characters. 701 * 'of', 'au' and 'es' are special-cased and lowercased. 702 */ 703 function toTitleCaseTimezoneLocation(location) { 704 var match = InternalRegExpMatch(GetTimezoneNameLocationPartRE(), location) 705 if (IS_NULL(match)) throw MakeRangeError(kExpectedLocation, location); 706 707 var result = toTitleCaseWord(match[1]); 708 if (!IS_UNDEFINED(match[2]) && 2 < match.length) { 709 // The first character is a separator, '_' or '-'. 710 // None of IANA zone names has both '_' and '-'. 711 var separator = %_Call(StringSubstring, match[2], 0, 1); 712 var parts = %_Call(StringSplit, match[2], separator); 713 for (var i = 1; i < parts.length; i++) { 714 var part = parts[i] 715 var lowercasedPart = %StringToLowerCase(part); 716 result = result + separator + 717 ((lowercasedPart !== 'es' && 718 lowercasedPart !== 'of' && lowercasedPart !== 'au') ? 719 toTitleCaseWord(part) : lowercasedPart); 720 } 721 } 722 return result; 723 } 724 725 /** 726 * Canonicalizes the language tag, or throws in case the tag is invalid. 727 */ 728 function canonicalizeLanguageTag(localeID) { 729 // null is typeof 'object' so we have to do extra check. 730 if ((!IS_STRING(localeID) && !IS_RECEIVER(localeID)) || 731 IS_NULL(localeID)) { 732 throw MakeTypeError(kLanguageID); 733 } 734 735 // Optimize for the most common case; a language code alone in 736 // the canonical form/lowercase (e.g. "en", "fil"). 737 if (IS_STRING(localeID) && 738 !IS_NULL(InternalRegExpMatch(/^[a-z]{2,3}$/, localeID))) { 739 return localeID; 740 } 741 742 var localeString = TO_STRING(localeID); 743 744 if (isValidLanguageTag(localeString) === false) { 745 throw MakeRangeError(kInvalidLanguageTag, localeString); 746 } 747 748 var tag = %CanonicalizeLanguageTag(localeString); 749 if (tag === 'invalid-tag') { 750 throw MakeRangeError(kInvalidLanguageTag, localeString); 751 } 752 753 return tag; 754 } 755 756 757 /** 758 * Returns an array where all locales are canonicalized and duplicates removed. 759 * Throws on locales that are not well formed BCP47 tags. 760 */ 761 function initializeLocaleList(locales) { 762 var seen = new InternalArray(); 763 if (!IS_UNDEFINED(locales)) { 764 // We allow single string localeID. 765 if (typeof locales === 'string') { 766 %_Call(ArrayPush, seen, canonicalizeLanguageTag(locales)); 767 return freezeArray(seen); 768 } 769 770 var o = TO_OBJECT(locales); 771 var len = TO_UINT32(o.length); 772 773 for (var k = 0; k < len; k++) { 774 if (k in o) { 775 var value = o[k]; 776 777 var tag = canonicalizeLanguageTag(value); 778 779 if (%_Call(ArrayIndexOf, seen, tag) === -1) { 780 %_Call(ArrayPush, seen, tag); 781 } 782 } 783 } 784 } 785 786 return freezeArray(seen); 787 } 788 789 790 /** 791 * Validates the language tag. Section 2.2.9 of the bcp47 spec 792 * defines a valid tag. 793 * 794 * ICU is too permissible and lets invalid tags, like 795 * hant-cmn-cn, through. 796 * 797 * Returns false if the language tag is invalid. 798 */ 799 function isValidLanguageTag(locale) { 800 // Check if it's well-formed, including grandfadered tags. 801 if (IS_NULL(InternalRegExpMatch(GetLanguageTagRE(), locale))) { 802 return false; 803 } 804 805 // Just return if it's a x- form. It's all private. 806 if (%_Call(StringIndexOf, locale, 'x-') === 0) { 807 return true; 808 } 809 810 // Check if there are any duplicate variants or singletons (extensions). 811 812 // Remove private use section. 813 locale = %_Call(StringSplit, locale, '-x-')[0]; 814 815 // Skip language since it can match variant regex, so we start from 1. 816 // We are matching i-klingon here, but that's ok, since i-klingon-klingon 817 // is not valid and would fail LANGUAGE_TAG_RE test. 818 var variants = new InternalArray(); 819 var extensions = new InternalArray(); 820 var parts = %_Call(StringSplit, locale, '-'); 821 for (var i = 1; i < parts.length; i++) { 822 var value = parts[i]; 823 if (!IS_NULL(InternalRegExpMatch(GetLanguageVariantRE(), value)) && 824 extensions.length === 0) { 825 if (%_Call(ArrayIndexOf, variants, value) === -1) { 826 %_Call(ArrayPush, variants, value); 827 } else { 828 return false; 829 } 830 } 831 832 if (!IS_NULL(InternalRegExpMatch(GetLanguageSingletonRE(), value))) { 833 if (%_Call(ArrayIndexOf, extensions, value) === -1) { 834 %_Call(ArrayPush, extensions, value); 835 } else { 836 return false; 837 } 838 } 839 } 840 841 return true; 842 } 843 844 845 /** 846 * Builds a regular expresion that validates the language tag 847 * against bcp47 spec. 848 * Uses http://tools.ietf.org/html/bcp47, section 2.1, ABNF. 849 * Runs on load and initializes the global REs. 850 */ 851 function BuildLanguageTagREs() { 852 var alpha = '[a-zA-Z]'; 853 var digit = '[0-9]'; 854 var alphanum = '(' + alpha + '|' + digit + ')'; 855 var regular = '(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|' + 856 'zh-min|zh-min-nan|zh-xiang)'; 857 var irregular = '(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|' + 858 'i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|' + 859 'i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)'; 860 var grandfathered = '(' + irregular + '|' + regular + ')'; 861 var privateUse = '(x(-' + alphanum + '{1,8})+)'; 862 863 var singleton = '(' + digit + '|[A-WY-Za-wy-z])'; 864 LANGUAGE_SINGLETON_RE = new GlobalRegExp('^' + singleton + '$', 'i'); 865 866 var extension = '(' + singleton + '(-' + alphanum + '{2,8})+)'; 867 868 var variant = '(' + alphanum + '{5,8}|(' + digit + alphanum + '{3}))'; 869 LANGUAGE_VARIANT_RE = new GlobalRegExp('^' + variant + '$', 'i'); 870 871 var region = '(' + alpha + '{2}|' + digit + '{3})'; 872 var script = '(' + alpha + '{4})'; 873 var extLang = '(' + alpha + '{3}(-' + alpha + '{3}){0,2})'; 874 var language = '(' + alpha + '{2,3}(-' + extLang + ')?|' + alpha + '{4}|' + 875 alpha + '{5,8})'; 876 var langTag = language + '(-' + script + ')?(-' + region + ')?(-' + 877 variant + ')*(-' + extension + ')*(-' + privateUse + ')?'; 878 879 var languageTag = 880 '^(' + langTag + '|' + privateUse + '|' + grandfathered + ')$'; 881 LANGUAGE_TAG_RE = new GlobalRegExp(languageTag, 'i'); 882 } 883 884 var resolvedAccessor = { 885 get() { 886 %IncrementUseCounter(kIntlResolved); 887 return this[resolvedSymbol]; 888 }, 889 set(value) { 890 this[resolvedSymbol] = value; 891 } 892 }; 893 894 /** 895 * Initializes the given object so it's a valid Collator instance. 896 * Useful for subclassing. 897 */ 898 function initializeCollator(collator, locales, options) { 899 if (%IsInitializedIntlObject(collator)) { 900 throw MakeTypeError(kReinitializeIntl, "Collator"); 901 } 902 903 if (IS_UNDEFINED(options)) { 904 options = {}; 905 } 906 907 var getOption = getGetOption(options, 'collator'); 908 909 var internalOptions = {}; 910 911 defineWEProperty(internalOptions, 'usage', getOption( 912 'usage', 'string', ['sort', 'search'], 'sort')); 913 914 var sensitivity = getOption('sensitivity', 'string', 915 ['base', 'accent', 'case', 'variant']); 916 if (IS_UNDEFINED(sensitivity) && internalOptions.usage === 'sort') { 917 sensitivity = 'variant'; 918 } 919 defineWEProperty(internalOptions, 'sensitivity', sensitivity); 920 921 defineWEProperty(internalOptions, 'ignorePunctuation', getOption( 922 'ignorePunctuation', 'boolean', UNDEFINED, false)); 923 924 var locale = resolveLocale('collator', locales, options); 925 926 // ICU can't take kb, kc... parameters through localeID, so we need to pass 927 // them as options. 928 // One exception is -co- which has to be part of the extension, but only for 929 // usage: sort, and its value can't be 'standard' or 'search'. 930 var extensionMap = parseExtension(locale.extension); 931 932 /** 933 * Map of Unicode extensions to option properties, and their values and types, 934 * for a collator. 935 */ 936 var COLLATOR_KEY_MAP = { 937 'kn': {'property': 'numeric', 'type': 'boolean'}, 938 'kf': {'property': 'caseFirst', 'type': 'string', 939 'values': ['false', 'lower', 'upper']} 940 }; 941 942 setOptions( 943 options, extensionMap, COLLATOR_KEY_MAP, getOption, internalOptions); 944 945 var collation = 'default'; 946 var extension = ''; 947 if (HAS_OWN_PROPERTY(extensionMap, 'co') && internalOptions.usage === 'sort') { 948 949 /** 950 * Allowed -u-co- values. List taken from: 951 * http://unicode.org/repos/cldr/trunk/common/bcp47/collation.xml 952 */ 953 var ALLOWED_CO_VALUES = [ 954 'big5han', 'dict', 'direct', 'ducet', 'gb2312', 'phonebk', 'phonetic', 955 'pinyin', 'reformed', 'searchjl', 'stroke', 'trad', 'unihan', 'zhuyin' 956 ]; 957 958 if (%_Call(ArrayIndexOf, ALLOWED_CO_VALUES, extensionMap.co) !== -1) { 959 extension = '-u-co-' + extensionMap.co; 960 // ICU can't tell us what the collation is, so save user's input. 961 collation = extensionMap.co; 962 } 963 } else if (internalOptions.usage === 'search') { 964 extension = '-u-co-search'; 965 } 966 defineWEProperty(internalOptions, 'collation', collation); 967 968 var requestedLocale = locale.locale + extension; 969 970 // We define all properties C++ code may produce, to prevent security 971 // problems. If malicious user decides to redefine Object.prototype.locale 972 // we can't just use plain x.locale = 'us' or in C++ Set("locale", "us"). 973 // %object_define_properties will either succeed defining or throw an error. 974 var resolved = %object_define_properties({}, { 975 caseFirst: {writable: true}, 976 collation: {value: internalOptions.collation, writable: true}, 977 ignorePunctuation: {writable: true}, 978 locale: {writable: true}, 979 numeric: {writable: true}, 980 requestedLocale: {value: requestedLocale, writable: true}, 981 sensitivity: {writable: true}, 982 strength: {writable: true}, 983 usage: {value: internalOptions.usage, writable: true} 984 }); 985 986 var internalCollator = %CreateCollator(requestedLocale, 987 internalOptions, 988 resolved); 989 990 // Writable, configurable and enumerable are set to false by default. 991 %MarkAsInitializedIntlObjectOfType(collator, 'collator', internalCollator); 992 collator[resolvedSymbol] = resolved; 993 if (FLAG_intl_extra) { 994 %object_define_property(collator, 'resolved', resolvedAccessor); 995 } 996 997 return collator; 998 } 999 1000 1001 /** 1002 * Constructs Intl.Collator object given optional locales and options 1003 * parameters. 1004 * 1005 * @constructor 1006 */ 1007 InstallConstructor(Intl, 'Collator', function() { 1008 var locales = arguments[0]; 1009 var options = arguments[1]; 1010 1011 if (!this || this === Intl) { 1012 // Constructor is called as a function. 1013 return new Intl.Collator(locales, options); 1014 } 1015 1016 return initializeCollator(TO_OBJECT(this), locales, options); 1017 } 1018 ); 1019 1020 1021 /** 1022 * Collator resolvedOptions method. 1023 */ 1024 InstallFunction(Intl.Collator.prototype, 'resolvedOptions', function() { 1025 if (!IS_UNDEFINED(new.target)) { 1026 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1027 } 1028 1029 if (!%IsInitializedIntlObjectOfType(this, 'collator')) { 1030 throw MakeTypeError(kResolvedOptionsCalledOnNonObject, "Collator"); 1031 } 1032 1033 var coll = this; 1034 var locale = getOptimalLanguageTag(coll[resolvedSymbol].requestedLocale, 1035 coll[resolvedSymbol].locale); 1036 1037 return { 1038 locale: locale, 1039 usage: coll[resolvedSymbol].usage, 1040 sensitivity: coll[resolvedSymbol].sensitivity, 1041 ignorePunctuation: coll[resolvedSymbol].ignorePunctuation, 1042 numeric: coll[resolvedSymbol].numeric, 1043 caseFirst: coll[resolvedSymbol].caseFirst, 1044 collation: coll[resolvedSymbol].collation 1045 }; 1046 } 1047 ); 1048 1049 1050 /** 1051 * Returns the subset of the given locale list for which this locale list 1052 * has a matching (possibly fallback) locale. Locales appear in the same 1053 * order in the returned list as in the input list. 1054 * Options are optional parameter. 1055 */ 1056 InstallFunction(Intl.Collator, 'supportedLocalesOf', function(locales) { 1057 if (!IS_UNDEFINED(new.target)) { 1058 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1059 } 1060 1061 return supportedLocalesOf('collator', locales, arguments[1]); 1062 } 1063 ); 1064 1065 1066 /** 1067 * When the compare method is called with two arguments x and y, it returns a 1068 * Number other than NaN that represents the result of a locale-sensitive 1069 * String comparison of x with y. 1070 * The result is intended to order String values in the sort order specified 1071 * by the effective locale and collation options computed during construction 1072 * of this Collator object, and will be negative, zero, or positive, depending 1073 * on whether x comes before y in the sort order, the Strings are equal under 1074 * the sort order, or x comes after y in the sort order, respectively. 1075 */ 1076 function compare(collator, x, y) { 1077 return %InternalCompare(%GetImplFromInitializedIntlObject(collator), 1078 TO_STRING(x), TO_STRING(y)); 1079 }; 1080 1081 1082 AddBoundMethod(Intl.Collator, 'compare', compare, 2, 'collator'); 1083 1084 /** 1085 * Verifies that the input is a well-formed ISO 4217 currency code. 1086 * Don't uppercase to test. It could convert invalid code into a valid one. 1087 * For example \u00DFP (Eszett+P) becomes SSP. 1088 */ 1089 function isWellFormedCurrencyCode(currency) { 1090 return typeof currency == "string" && currency.length == 3 && 1091 IS_NULL(InternalRegExpMatch(/[^A-Za-z]/, currency)); 1092 } 1093 1094 1095 /** 1096 * Returns the valid digit count for a property, or throws RangeError on 1097 * a value out of the range. 1098 */ 1099 function getNumberOption(options, property, min, max, fallback) { 1100 var value = options[property]; 1101 if (!IS_UNDEFINED(value)) { 1102 value = TO_NUMBER(value); 1103 if (NUMBER_IS_NAN(value) || value < min || value > max) { 1104 throw MakeRangeError(kPropertyValueOutOfRange, property); 1105 } 1106 return %math_floor(value); 1107 } 1108 1109 return fallback; 1110 } 1111 1112 var patternAccessor = { 1113 get() { 1114 %IncrementUseCounter(kIntlPattern); 1115 return this[patternSymbol]; 1116 }, 1117 set(value) { 1118 this[patternSymbol] = value; 1119 } 1120 }; 1121 1122 /** 1123 * Initializes the given object so it's a valid NumberFormat instance. 1124 * Useful for subclassing. 1125 */ 1126 function initializeNumberFormat(numberFormat, locales, options) { 1127 if (%IsInitializedIntlObject(numberFormat)) { 1128 throw MakeTypeError(kReinitializeIntl, "NumberFormat"); 1129 } 1130 1131 if (IS_UNDEFINED(options)) { 1132 options = {}; 1133 } 1134 1135 var getOption = getGetOption(options, 'numberformat'); 1136 1137 var locale = resolveLocale('numberformat', locales, options); 1138 1139 var internalOptions = {}; 1140 defineWEProperty(internalOptions, 'style', getOption( 1141 'style', 'string', ['decimal', 'percent', 'currency'], 'decimal')); 1142 1143 var currency = getOption('currency', 'string'); 1144 if (!IS_UNDEFINED(currency) && !isWellFormedCurrencyCode(currency)) { 1145 throw MakeRangeError(kInvalidCurrencyCode, currency); 1146 } 1147 1148 if (internalOptions.style === 'currency' && IS_UNDEFINED(currency)) { 1149 throw MakeTypeError(kCurrencyCode); 1150 } 1151 1152 var currencyDisplay = getOption( 1153 'currencyDisplay', 'string', ['code', 'symbol', 'name'], 'symbol'); 1154 if (internalOptions.style === 'currency') { 1155 defineWEProperty(internalOptions, 'currency', %StringToUpperCase(currency)); 1156 defineWEProperty(internalOptions, 'currencyDisplay', currencyDisplay); 1157 } 1158 1159 // Digit ranges. 1160 var mnid = getNumberOption(options, 'minimumIntegerDigits', 1, 21, 1); 1161 defineWEProperty(internalOptions, 'minimumIntegerDigits', mnid); 1162 1163 var mnfd = options['minimumFractionDigits']; 1164 var mxfd = options['maximumFractionDigits']; 1165 if (!IS_UNDEFINED(mnfd) || internalOptions.style !== 'currency') { 1166 mnfd = getNumberOption(options, 'minimumFractionDigits', 0, 20, 0); 1167 defineWEProperty(internalOptions, 'minimumFractionDigits', mnfd); 1168 } 1169 1170 if (!IS_UNDEFINED(mxfd) || internalOptions.style !== 'currency') { 1171 var min_mxfd = internalOptions.style === 'percent' ? 0 : 3; 1172 mnfd = IS_UNDEFINED(mnfd) ? 0 : mnfd; 1173 var fallback_limit = (mnfd > min_mxfd) ? mnfd : min_mxfd; 1174 mxfd = getNumberOption(options, 'maximumFractionDigits', mnfd, 20, fallback_limit); 1175 defineWEProperty(internalOptions, 'maximumFractionDigits', mxfd); 1176 } 1177 1178 var mnsd = options['minimumSignificantDigits']; 1179 var mxsd = options['maximumSignificantDigits']; 1180 if (!IS_UNDEFINED(mnsd) || !IS_UNDEFINED(mxsd)) { 1181 mnsd = getNumberOption(options, 'minimumSignificantDigits', 1, 21, 0); 1182 defineWEProperty(internalOptions, 'minimumSignificantDigits', mnsd); 1183 1184 mxsd = getNumberOption(options, 'maximumSignificantDigits', mnsd, 21, 21); 1185 defineWEProperty(internalOptions, 'maximumSignificantDigits', mxsd); 1186 } 1187 1188 // Grouping. 1189 defineWEProperty(internalOptions, 'useGrouping', getOption( 1190 'useGrouping', 'boolean', UNDEFINED, true)); 1191 1192 // ICU prefers options to be passed using -u- extension key/values for 1193 // number format, so we need to build that. 1194 var extensionMap = parseExtension(locale.extension); 1195 1196 /** 1197 * Map of Unicode extensions to option properties, and their values and types, 1198 * for a number format. 1199 */ 1200 var NUMBER_FORMAT_KEY_MAP = { 1201 'nu': {'property': UNDEFINED, 'type': 'string'} 1202 }; 1203 1204 var extension = setOptions(options, extensionMap, NUMBER_FORMAT_KEY_MAP, 1205 getOption, internalOptions); 1206 1207 var requestedLocale = locale.locale + extension; 1208 var resolved = %object_define_properties({}, { 1209 currency: {writable: true}, 1210 currencyDisplay: {writable: true}, 1211 locale: {writable: true}, 1212 maximumFractionDigits: {writable: true}, 1213 minimumFractionDigits: {writable: true}, 1214 minimumIntegerDigits: {writable: true}, 1215 numberingSystem: {writable: true}, 1216 requestedLocale: {value: requestedLocale, writable: true}, 1217 style: {value: internalOptions.style, writable: true}, 1218 useGrouping: {writable: true} 1219 }); 1220 if (HAS_OWN_PROPERTY(internalOptions, 'minimumSignificantDigits')) { 1221 defineWEProperty(resolved, 'minimumSignificantDigits', UNDEFINED); 1222 } 1223 if (HAS_OWN_PROPERTY(internalOptions, 'maximumSignificantDigits')) { 1224 defineWEProperty(resolved, 'maximumSignificantDigits', UNDEFINED); 1225 } 1226 var formatter = %CreateNumberFormat(requestedLocale, 1227 internalOptions, 1228 resolved); 1229 1230 if (internalOptions.style === 'currency') { 1231 %object_define_property(resolved, 'currencyDisplay', 1232 {value: currencyDisplay, writable: true}); 1233 } 1234 1235 %MarkAsInitializedIntlObjectOfType(numberFormat, 'numberformat', formatter); 1236 numberFormat[resolvedSymbol] = resolved; 1237 if (FLAG_intl_extra) { 1238 %object_define_property(resolved, 'pattern', patternAccessor); 1239 %object_define_property(numberFormat, 'resolved', resolvedAccessor); 1240 } 1241 1242 return numberFormat; 1243 } 1244 1245 1246 /** 1247 * Constructs Intl.NumberFormat object given optional locales and options 1248 * parameters. 1249 * 1250 * @constructor 1251 */ 1252 InstallConstructor(Intl, 'NumberFormat', function() { 1253 var locales = arguments[0]; 1254 var options = arguments[1]; 1255 1256 if (!this || this === Intl) { 1257 // Constructor is called as a function. 1258 return new Intl.NumberFormat(locales, options); 1259 } 1260 1261 return initializeNumberFormat(TO_OBJECT(this), locales, options); 1262 } 1263 ); 1264 1265 1266 /** 1267 * NumberFormat resolvedOptions method. 1268 */ 1269 InstallFunction(Intl.NumberFormat.prototype, 'resolvedOptions', function() { 1270 if (!IS_UNDEFINED(new.target)) { 1271 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1272 } 1273 1274 if (!%IsInitializedIntlObjectOfType(this, 'numberformat')) { 1275 throw MakeTypeError(kResolvedOptionsCalledOnNonObject, "NumberFormat"); 1276 } 1277 1278 var format = this; 1279 var locale = getOptimalLanguageTag(format[resolvedSymbol].requestedLocale, 1280 format[resolvedSymbol].locale); 1281 1282 var result = { 1283 locale: locale, 1284 numberingSystem: format[resolvedSymbol].numberingSystem, 1285 style: format[resolvedSymbol].style, 1286 useGrouping: format[resolvedSymbol].useGrouping, 1287 minimumIntegerDigits: format[resolvedSymbol].minimumIntegerDigits, 1288 minimumFractionDigits: format[resolvedSymbol].minimumFractionDigits, 1289 maximumFractionDigits: format[resolvedSymbol].maximumFractionDigits, 1290 }; 1291 1292 if (result.style === 'currency') { 1293 defineWECProperty(result, 'currency', format[resolvedSymbol].currency); 1294 defineWECProperty(result, 'currencyDisplay', 1295 format[resolvedSymbol].currencyDisplay); 1296 } 1297 1298 if (HAS_OWN_PROPERTY(format[resolvedSymbol], 'minimumSignificantDigits')) { 1299 defineWECProperty(result, 'minimumSignificantDigits', 1300 format[resolvedSymbol].minimumSignificantDigits); 1301 } 1302 1303 if (HAS_OWN_PROPERTY(format[resolvedSymbol], 'maximumSignificantDigits')) { 1304 defineWECProperty(result, 'maximumSignificantDigits', 1305 format[resolvedSymbol].maximumSignificantDigits); 1306 } 1307 1308 return result; 1309 } 1310 ); 1311 1312 1313 /** 1314 * Returns the subset of the given locale list for which this locale list 1315 * has a matching (possibly fallback) locale. Locales appear in the same 1316 * order in the returned list as in the input list. 1317 * Options are optional parameter. 1318 */ 1319 InstallFunction(Intl.NumberFormat, 'supportedLocalesOf', function(locales) { 1320 if (!IS_UNDEFINED(new.target)) { 1321 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1322 } 1323 1324 return supportedLocalesOf('numberformat', locales, arguments[1]); 1325 } 1326 ); 1327 1328 1329 /** 1330 * Returns a String value representing the result of calling ToNumber(value) 1331 * according to the effective locale and the formatting options of this 1332 * NumberFormat. 1333 */ 1334 function formatNumber(formatter, value) { 1335 // Spec treats -0 and +0 as 0. 1336 var number = TO_NUMBER(value) + 0; 1337 1338 return %InternalNumberFormat(%GetImplFromInitializedIntlObject(formatter), 1339 number); 1340 } 1341 1342 1343 /** 1344 * Returns a Number that represents string value that was passed in. 1345 */ 1346 function IntlParseNumber(formatter, value) { 1347 return %InternalNumberParse(%GetImplFromInitializedIntlObject(formatter), 1348 TO_STRING(value)); 1349 } 1350 1351 AddBoundMethod(Intl.NumberFormat, 'format', formatNumber, 1, 'numberformat'); 1352 1353 /** 1354 * Returns a string that matches LDML representation of the options object. 1355 */ 1356 function toLDMLString(options) { 1357 var getOption = getGetOption(options, 'dateformat'); 1358 1359 var ldmlString = ''; 1360 1361 var option = getOption('weekday', 'string', ['narrow', 'short', 'long']); 1362 ldmlString += appendToLDMLString( 1363 option, {narrow: 'EEEEE', short: 'EEE', long: 'EEEE'}); 1364 1365 option = getOption('era', 'string', ['narrow', 'short', 'long']); 1366 ldmlString += appendToLDMLString( 1367 option, {narrow: 'GGGGG', short: 'GGG', long: 'GGGG'}); 1368 1369 option = getOption('year', 'string', ['2-digit', 'numeric']); 1370 ldmlString += appendToLDMLString(option, {'2-digit': 'yy', 'numeric': 'y'}); 1371 1372 option = getOption('month', 'string', 1373 ['2-digit', 'numeric', 'narrow', 'short', 'long']); 1374 ldmlString += appendToLDMLString(option, {'2-digit': 'MM', 'numeric': 'M', 1375 'narrow': 'MMMMM', 'short': 'MMM', 'long': 'MMMM'}); 1376 1377 option = getOption('day', 'string', ['2-digit', 'numeric']); 1378 ldmlString += appendToLDMLString( 1379 option, {'2-digit': 'dd', 'numeric': 'd'}); 1380 1381 var hr12 = getOption('hour12', 'boolean'); 1382 option = getOption('hour', 'string', ['2-digit', 'numeric']); 1383 if (IS_UNDEFINED(hr12)) { 1384 ldmlString += appendToLDMLString(option, {'2-digit': 'jj', 'numeric': 'j'}); 1385 } else if (hr12 === true) { 1386 ldmlString += appendToLDMLString(option, {'2-digit': 'hh', 'numeric': 'h'}); 1387 } else { 1388 ldmlString += appendToLDMLString(option, {'2-digit': 'HH', 'numeric': 'H'}); 1389 } 1390 1391 option = getOption('minute', 'string', ['2-digit', 'numeric']); 1392 ldmlString += appendToLDMLString(option, {'2-digit': 'mm', 'numeric': 'm'}); 1393 1394 option = getOption('second', 'string', ['2-digit', 'numeric']); 1395 ldmlString += appendToLDMLString(option, {'2-digit': 'ss', 'numeric': 's'}); 1396 1397 option = getOption('timeZoneName', 'string', ['short', 'long']); 1398 ldmlString += appendToLDMLString(option, {short: 'z', long: 'zzzz'}); 1399 1400 return ldmlString; 1401 } 1402 1403 1404 /** 1405 * Returns either LDML equivalent of the current option or empty string. 1406 */ 1407 function appendToLDMLString(option, pairs) { 1408 if (!IS_UNDEFINED(option)) { 1409 return pairs[option]; 1410 } else { 1411 return ''; 1412 } 1413 } 1414 1415 1416 /** 1417 * Returns object that matches LDML representation of the date. 1418 */ 1419 function fromLDMLString(ldmlString) { 1420 // First remove '' quoted text, so we lose 'Uhr' strings. 1421 ldmlString = InternalRegExpReplace(GetQuotedStringRE(), ldmlString, ''); 1422 1423 var options = {}; 1424 var match = InternalRegExpMatch(/E{3,5}/, ldmlString); 1425 options = appendToDateTimeObject( 1426 options, 'weekday', match, {EEEEE: 'narrow', EEE: 'short', EEEE: 'long'}); 1427 1428 match = InternalRegExpMatch(/G{3,5}/, ldmlString); 1429 options = appendToDateTimeObject( 1430 options, 'era', match, {GGGGG: 'narrow', GGG: 'short', GGGG: 'long'}); 1431 1432 match = InternalRegExpMatch(/y{1,2}/, ldmlString); 1433 options = appendToDateTimeObject( 1434 options, 'year', match, {y: 'numeric', yy: '2-digit'}); 1435 1436 match = InternalRegExpMatch(/M{1,5}/, ldmlString); 1437 options = appendToDateTimeObject(options, 'month', match, {MM: '2-digit', 1438 M: 'numeric', MMMMM: 'narrow', MMM: 'short', MMMM: 'long'}); 1439 1440 // Sometimes we get L instead of M for month - standalone name. 1441 match = InternalRegExpMatch(/L{1,5}/, ldmlString); 1442 options = appendToDateTimeObject(options, 'month', match, {LL: '2-digit', 1443 L: 'numeric', LLLLL: 'narrow', LLL: 'short', LLLL: 'long'}); 1444 1445 match = InternalRegExpMatch(/d{1,2}/, ldmlString); 1446 options = appendToDateTimeObject( 1447 options, 'day', match, {d: 'numeric', dd: '2-digit'}); 1448 1449 match = InternalRegExpMatch(/h{1,2}/, ldmlString); 1450 if (match !== null) { 1451 options['hour12'] = true; 1452 } 1453 options = appendToDateTimeObject( 1454 options, 'hour', match, {h: 'numeric', hh: '2-digit'}); 1455 1456 match = InternalRegExpMatch(/H{1,2}/, ldmlString); 1457 if (match !== null) { 1458 options['hour12'] = false; 1459 } 1460 options = appendToDateTimeObject( 1461 options, 'hour', match, {H: 'numeric', HH: '2-digit'}); 1462 1463 match = InternalRegExpMatch(/m{1,2}/, ldmlString); 1464 options = appendToDateTimeObject( 1465 options, 'minute', match, {m: 'numeric', mm: '2-digit'}); 1466 1467 match = InternalRegExpMatch(/s{1,2}/, ldmlString); 1468 options = appendToDateTimeObject( 1469 options, 'second', match, {s: 'numeric', ss: '2-digit'}); 1470 1471 match = InternalRegExpMatch(/z|zzzz/, ldmlString); 1472 options = appendToDateTimeObject( 1473 options, 'timeZoneName', match, {z: 'short', zzzz: 'long'}); 1474 1475 return options; 1476 } 1477 1478 1479 function appendToDateTimeObject(options, option, match, pairs) { 1480 if (IS_NULL(match)) { 1481 if (!HAS_OWN_PROPERTY(options, option)) { 1482 defineWEProperty(options, option, UNDEFINED); 1483 } 1484 return options; 1485 } 1486 1487 var property = match[0]; 1488 defineWEProperty(options, option, pairs[property]); 1489 1490 return options; 1491 } 1492 1493 1494 /** 1495 * Returns options with at least default values in it. 1496 */ 1497 function toDateTimeOptions(options, required, defaults) { 1498 if (IS_UNDEFINED(options)) { 1499 options = {}; 1500 } else { 1501 options = TO_OBJECT(options); 1502 } 1503 1504 var needsDefault = true; 1505 if ((required === 'date' || required === 'any') && 1506 (!IS_UNDEFINED(options.weekday) || !IS_UNDEFINED(options.year) || 1507 !IS_UNDEFINED(options.month) || !IS_UNDEFINED(options.day))) { 1508 needsDefault = false; 1509 } 1510 1511 if ((required === 'time' || required === 'any') && 1512 (!IS_UNDEFINED(options.hour) || !IS_UNDEFINED(options.minute) || 1513 !IS_UNDEFINED(options.second))) { 1514 needsDefault = false; 1515 } 1516 1517 if (needsDefault && (defaults === 'date' || defaults === 'all')) { 1518 %object_define_property(options, 'year', {value: 'numeric', 1519 writable: true, 1520 enumerable: true, 1521 configurable: true}); 1522 %object_define_property(options, 'month', {value: 'numeric', 1523 writable: true, 1524 enumerable: true, 1525 configurable: true}); 1526 %object_define_property(options, 'day', {value: 'numeric', 1527 writable: true, 1528 enumerable: true, 1529 configurable: true}); 1530 } 1531 1532 if (needsDefault && (defaults === 'time' || defaults === 'all')) { 1533 %object_define_property(options, 'hour', {value: 'numeric', 1534 writable: true, 1535 enumerable: true, 1536 configurable: true}); 1537 %object_define_property(options, 'minute', {value: 'numeric', 1538 writable: true, 1539 enumerable: true, 1540 configurable: true}); 1541 %object_define_property(options, 'second', {value: 'numeric', 1542 writable: true, 1543 enumerable: true, 1544 configurable: true}); 1545 } 1546 1547 return options; 1548 } 1549 1550 1551 /** 1552 * Initializes the given object so it's a valid DateTimeFormat instance. 1553 * Useful for subclassing. 1554 */ 1555 function initializeDateTimeFormat(dateFormat, locales, options) { 1556 1557 if (%IsInitializedIntlObject(dateFormat)) { 1558 throw MakeTypeError(kReinitializeIntl, "DateTimeFormat"); 1559 } 1560 1561 if (IS_UNDEFINED(options)) { 1562 options = {}; 1563 } 1564 1565 var locale = resolveLocale('dateformat', locales, options); 1566 1567 options = toDateTimeOptions(options, 'any', 'date'); 1568 1569 var getOption = getGetOption(options, 'dateformat'); 1570 1571 // We implement only best fit algorithm, but still need to check 1572 // if the formatMatcher values are in range. 1573 var matcher = getOption('formatMatcher', 'string', 1574 ['basic', 'best fit'], 'best fit'); 1575 1576 // Build LDML string for the skeleton that we pass to the formatter. 1577 var ldmlString = toLDMLString(options); 1578 1579 // Filter out supported extension keys so we know what to put in resolved 1580 // section later on. 1581 // We need to pass calendar and number system to the method. 1582 var tz = canonicalizeTimeZoneID(options.timeZone); 1583 1584 // ICU prefers options to be passed using -u- extension key/values, so 1585 // we need to build that. 1586 var internalOptions = {}; 1587 var extensionMap = parseExtension(locale.extension); 1588 1589 /** 1590 * Map of Unicode extensions to option properties, and their values and types, 1591 * for a date/time format. 1592 */ 1593 var DATETIME_FORMAT_KEY_MAP = { 1594 'ca': {'property': UNDEFINED, 'type': 'string'}, 1595 'nu': {'property': UNDEFINED, 'type': 'string'} 1596 }; 1597 1598 var extension = setOptions(options, extensionMap, DATETIME_FORMAT_KEY_MAP, 1599 getOption, internalOptions); 1600 1601 var requestedLocale = locale.locale + extension; 1602 var resolved = %object_define_properties({}, { 1603 calendar: {writable: true}, 1604 day: {writable: true}, 1605 era: {writable: true}, 1606 hour12: {writable: true}, 1607 hour: {writable: true}, 1608 locale: {writable: true}, 1609 minute: {writable: true}, 1610 month: {writable: true}, 1611 numberingSystem: {writable: true}, 1612 [patternSymbol]: {writable: true}, 1613 requestedLocale: {value: requestedLocale, writable: true}, 1614 second: {writable: true}, 1615 timeZone: {writable: true}, 1616 timeZoneName: {writable: true}, 1617 tz: {value: tz, writable: true}, 1618 weekday: {writable: true}, 1619 year: {writable: true} 1620 }); 1621 1622 var formatter = %CreateDateTimeFormat( 1623 requestedLocale, {skeleton: ldmlString, timeZone: tz}, resolved); 1624 1625 if (resolved.timeZone === "Etc/Unknown") { 1626 throw MakeRangeError(kUnsupportedTimeZone, tz); 1627 } 1628 1629 %MarkAsInitializedIntlObjectOfType(dateFormat, 'dateformat', formatter); 1630 dateFormat[resolvedSymbol] = resolved; 1631 if (FLAG_intl_extra) { 1632 %object_define_property(resolved, 'pattern', patternAccessor); 1633 %object_define_property(dateFormat, 'resolved', resolvedAccessor); 1634 } 1635 1636 return dateFormat; 1637 } 1638 1639 1640 /** 1641 * Constructs Intl.DateTimeFormat object given optional locales and options 1642 * parameters. 1643 * 1644 * @constructor 1645 */ 1646 InstallConstructor(Intl, 'DateTimeFormat', function() { 1647 var locales = arguments[0]; 1648 var options = arguments[1]; 1649 1650 if (!this || this === Intl) { 1651 // Constructor is called as a function. 1652 return new Intl.DateTimeFormat(locales, options); 1653 } 1654 1655 return initializeDateTimeFormat(TO_OBJECT(this), locales, options); 1656 } 1657 ); 1658 1659 1660 /** 1661 * DateTimeFormat resolvedOptions method. 1662 */ 1663 InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() { 1664 if (!IS_UNDEFINED(new.target)) { 1665 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1666 } 1667 1668 if (!%IsInitializedIntlObjectOfType(this, 'dateformat')) { 1669 throw MakeTypeError(kResolvedOptionsCalledOnNonObject, "DateTimeFormat"); 1670 } 1671 1672 /** 1673 * Maps ICU calendar names into LDML type. 1674 */ 1675 var ICU_CALENDAR_MAP = { 1676 'gregorian': 'gregory', 1677 'japanese': 'japanese', 1678 'buddhist': 'buddhist', 1679 'roc': 'roc', 1680 'persian': 'persian', 1681 'islamic-civil': 'islamicc', 1682 'islamic': 'islamic', 1683 'hebrew': 'hebrew', 1684 'chinese': 'chinese', 1685 'indian': 'indian', 1686 'coptic': 'coptic', 1687 'ethiopic': 'ethiopic', 1688 'ethiopic-amete-alem': 'ethioaa' 1689 }; 1690 1691 var format = this; 1692 var fromPattern = fromLDMLString(format[resolvedSymbol][patternSymbol]); 1693 var userCalendar = ICU_CALENDAR_MAP[format[resolvedSymbol].calendar]; 1694 if (IS_UNDEFINED(userCalendar)) { 1695 // Use ICU name if we don't have a match. It shouldn't happen, but 1696 // it would be too strict to throw for this. 1697 userCalendar = format[resolvedSymbol].calendar; 1698 } 1699 1700 var locale = getOptimalLanguageTag(format[resolvedSymbol].requestedLocale, 1701 format[resolvedSymbol].locale); 1702 1703 var result = { 1704 locale: locale, 1705 numberingSystem: format[resolvedSymbol].numberingSystem, 1706 calendar: userCalendar, 1707 timeZone: format[resolvedSymbol].timeZone 1708 }; 1709 1710 addWECPropertyIfDefined(result, 'timeZoneName', fromPattern.timeZoneName); 1711 addWECPropertyIfDefined(result, 'era', fromPattern.era); 1712 addWECPropertyIfDefined(result, 'year', fromPattern.year); 1713 addWECPropertyIfDefined(result, 'month', fromPattern.month); 1714 addWECPropertyIfDefined(result, 'day', fromPattern.day); 1715 addWECPropertyIfDefined(result, 'weekday', fromPattern.weekday); 1716 addWECPropertyIfDefined(result, 'hour12', fromPattern.hour12); 1717 addWECPropertyIfDefined(result, 'hour', fromPattern.hour); 1718 addWECPropertyIfDefined(result, 'minute', fromPattern.minute); 1719 addWECPropertyIfDefined(result, 'second', fromPattern.second); 1720 1721 return result; 1722 } 1723 ); 1724 1725 1726 /** 1727 * Returns the subset of the given locale list for which this locale list 1728 * has a matching (possibly fallback) locale. Locales appear in the same 1729 * order in the returned list as in the input list. 1730 * Options are optional parameter. 1731 */ 1732 InstallFunction(Intl.DateTimeFormat, 'supportedLocalesOf', function(locales) { 1733 if (!IS_UNDEFINED(new.target)) { 1734 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1735 } 1736 1737 return supportedLocalesOf('dateformat', locales, arguments[1]); 1738 } 1739 ); 1740 1741 1742 /** 1743 * Returns a String value representing the result of calling ToNumber(date) 1744 * according to the effective locale and the formatting options of this 1745 * DateTimeFormat. 1746 */ 1747 function formatDate(formatter, dateValue) { 1748 var dateMs; 1749 if (IS_UNDEFINED(dateValue)) { 1750 dateMs = %DateCurrentTime(); 1751 } else { 1752 dateMs = TO_NUMBER(dateValue); 1753 } 1754 1755 if (!NUMBER_IS_FINITE(dateMs)) throw MakeRangeError(kDateRange); 1756 1757 return %InternalDateFormat(%GetImplFromInitializedIntlObject(formatter), 1758 new GlobalDate(dateMs)); 1759 } 1760 1761 1762 /** 1763 * Returns a Date object representing the result of calling ToString(value) 1764 * according to the effective locale and the formatting options of this 1765 * DateTimeFormat. 1766 * Returns undefined if date string cannot be parsed. 1767 */ 1768 function IntlParseDate(formatter, value) { 1769 return %InternalDateParse(%GetImplFromInitializedIntlObject(formatter), 1770 TO_STRING(value)); 1771 } 1772 1773 1774 // 0 because date is optional argument. 1775 AddBoundMethod(Intl.DateTimeFormat, 'format', formatDate, 0, 'dateformat'); 1776 1777 1778 /** 1779 * Returns canonical Area/Location(/Location) name, or throws an exception 1780 * if the zone name is invalid IANA name. 1781 */ 1782 function canonicalizeTimeZoneID(tzID) { 1783 // Skip undefined zones. 1784 if (IS_UNDEFINED(tzID)) { 1785 return tzID; 1786 } 1787 1788 // Special case handling (UTC, GMT). 1789 var upperID = %StringToUpperCase(tzID); 1790 if (upperID === 'UTC' || upperID === 'GMT' || 1791 upperID === 'ETC/UTC' || upperID === 'ETC/GMT') { 1792 return 'UTC'; 1793 } 1794 1795 // TODO(jshin): Add support for Etc/GMT[+-]([1-9]|1[0-2]) 1796 1797 // We expect only _, '-' and / beside ASCII letters. 1798 // All inputs should conform to Area/Location(/Location)* from now on. 1799 var match = InternalRegExpMatch(GetTimezoneNameCheckRE(), tzID); 1800 if (IS_NULL(match)) throw MakeRangeError(kExpectedTimezoneID, tzID); 1801 1802 var result = toTitleCaseTimezoneLocation(match[1]) + '/' + 1803 toTitleCaseTimezoneLocation(match[2]); 1804 1805 if (!IS_UNDEFINED(match[3]) && 3 < match.length) { 1806 var locations = %_Call(StringSplit, match[3], '/'); 1807 // The 1st element is empty. Starts with i=1. 1808 for (var i = 1; i < locations.length; i++) { 1809 result = result + '/' + toTitleCaseTimezoneLocation(locations[i]); 1810 } 1811 } 1812 1813 return result; 1814 } 1815 1816 /** 1817 * Initializes the given object so it's a valid BreakIterator instance. 1818 * Useful for subclassing. 1819 */ 1820 function initializeBreakIterator(iterator, locales, options) { 1821 if (%IsInitializedIntlObject(iterator)) { 1822 throw MakeTypeError(kReinitializeIntl, "v8BreakIterator"); 1823 } 1824 1825 if (IS_UNDEFINED(options)) { 1826 options = {}; 1827 } 1828 1829 var getOption = getGetOption(options, 'breakiterator'); 1830 1831 var internalOptions = {}; 1832 1833 defineWEProperty(internalOptions, 'type', getOption( 1834 'type', 'string', ['character', 'word', 'sentence', 'line'], 'word')); 1835 1836 var locale = resolveLocale('breakiterator', locales, options); 1837 var resolved = %object_define_properties({}, { 1838 requestedLocale: {value: locale.locale, writable: true}, 1839 type: {value: internalOptions.type, writable: true}, 1840 locale: {writable: true} 1841 }); 1842 1843 var internalIterator = %CreateBreakIterator(locale.locale, 1844 internalOptions, 1845 resolved); 1846 1847 %MarkAsInitializedIntlObjectOfType(iterator, 'breakiterator', 1848 internalIterator); 1849 iterator[resolvedSymbol] = resolved; 1850 if (FLAG_intl_extra) { 1851 %object_define_property(iterator, 'resolved', resolvedAccessor); 1852 } 1853 1854 return iterator; 1855 } 1856 1857 1858 /** 1859 * Constructs Intl.v8BreakIterator object given optional locales and options 1860 * parameters. 1861 * 1862 * @constructor 1863 */ 1864 InstallConstructor(Intl, 'v8BreakIterator', function() { 1865 var locales = arguments[0]; 1866 var options = arguments[1]; 1867 1868 if (!this || this === Intl) { 1869 // Constructor is called as a function. 1870 return new Intl.v8BreakIterator(locales, options); 1871 } 1872 1873 return initializeBreakIterator(TO_OBJECT(this), locales, options); 1874 } 1875 ); 1876 1877 1878 /** 1879 * BreakIterator resolvedOptions method. 1880 */ 1881 InstallFunction(Intl.v8BreakIterator.prototype, 'resolvedOptions', 1882 function() { 1883 if (!IS_UNDEFINED(new.target)) { 1884 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1885 } 1886 1887 if (!%IsInitializedIntlObjectOfType(this, 'breakiterator')) { 1888 throw MakeTypeError(kResolvedOptionsCalledOnNonObject, "v8BreakIterator"); 1889 } 1890 1891 var segmenter = this; 1892 var locale = 1893 getOptimalLanguageTag(segmenter[resolvedSymbol].requestedLocale, 1894 segmenter[resolvedSymbol].locale); 1895 1896 return { 1897 locale: locale, 1898 type: segmenter[resolvedSymbol].type 1899 }; 1900 } 1901 ); 1902 1903 1904 /** 1905 * Returns the subset of the given locale list for which this locale list 1906 * has a matching (possibly fallback) locale. Locales appear in the same 1907 * order in the returned list as in the input list. 1908 * Options are optional parameter. 1909 */ 1910 InstallFunction(Intl.v8BreakIterator, 'supportedLocalesOf', 1911 function(locales) { 1912 if (!IS_UNDEFINED(new.target)) { 1913 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 1914 } 1915 1916 return supportedLocalesOf('breakiterator', locales, arguments[1]); 1917 } 1918 ); 1919 1920 1921 /** 1922 * Adopts text to segment using the iterator. Old text, if present, 1923 * gets discarded. 1924 */ 1925 function adoptText(iterator, text) { 1926 %BreakIteratorAdoptText(%GetImplFromInitializedIntlObject(iterator), 1927 TO_STRING(text)); 1928 } 1929 1930 1931 /** 1932 * Returns index of the first break in the string and moves current pointer. 1933 */ 1934 function first(iterator) { 1935 return %BreakIteratorFirst(%GetImplFromInitializedIntlObject(iterator)); 1936 } 1937 1938 1939 /** 1940 * Returns the index of the next break and moves the pointer. 1941 */ 1942 function next(iterator) { 1943 return %BreakIteratorNext(%GetImplFromInitializedIntlObject(iterator)); 1944 } 1945 1946 1947 /** 1948 * Returns index of the current break. 1949 */ 1950 function current(iterator) { 1951 return %BreakIteratorCurrent(%GetImplFromInitializedIntlObject(iterator)); 1952 } 1953 1954 1955 /** 1956 * Returns type of the current break. 1957 */ 1958 function breakType(iterator) { 1959 return %BreakIteratorBreakType(%GetImplFromInitializedIntlObject(iterator)); 1960 } 1961 1962 1963 AddBoundMethod(Intl.v8BreakIterator, 'adoptText', adoptText, 1, 1964 'breakiterator'); 1965 AddBoundMethod(Intl.v8BreakIterator, 'first', first, 0, 'breakiterator'); 1966 AddBoundMethod(Intl.v8BreakIterator, 'next', next, 0, 'breakiterator'); 1967 AddBoundMethod(Intl.v8BreakIterator, 'current', current, 0, 'breakiterator'); 1968 AddBoundMethod(Intl.v8BreakIterator, 'breakType', breakType, 0, 1969 'breakiterator'); 1970 1971 // Save references to Intl objects and methods we use, for added security. 1972 var savedObjects = { 1973 'collator': Intl.Collator, 1974 'numberformat': Intl.NumberFormat, 1975 'dateformatall': Intl.DateTimeFormat, 1976 'dateformatdate': Intl.DateTimeFormat, 1977 'dateformattime': Intl.DateTimeFormat 1978 }; 1979 1980 1981 // Default (created with undefined locales and options parameters) collator, 1982 // number and date format instances. They'll be created as needed. 1983 var defaultObjects = { 1984 'collator': UNDEFINED, 1985 'numberformat': UNDEFINED, 1986 'dateformatall': UNDEFINED, 1987 'dateformatdate': UNDEFINED, 1988 'dateformattime': UNDEFINED, 1989 }; 1990 1991 function clearDefaultObjects() { 1992 defaultObjects['dateformatall'] = UNDEFINED; 1993 defaultObjects['dateformatdate'] = UNDEFINED; 1994 defaultObjects['dateformattime'] = UNDEFINED; 1995 } 1996 1997 var date_cache_version = 0; 1998 1999 function checkDateCacheCurrent() { 2000 var new_date_cache_version = %DateCacheVersion(); 2001 if (new_date_cache_version == date_cache_version) { 2002 return; 2003 } 2004 date_cache_version = new_date_cache_version; 2005 2006 clearDefaultObjects(); 2007 } 2008 2009 /** 2010 * Returns cached or newly created instance of a given service. 2011 * We cache only default instances (where no locales or options are provided). 2012 */ 2013 function cachedOrNewService(service, locales, options, defaults) { 2014 var useOptions = (IS_UNDEFINED(defaults)) ? options : defaults; 2015 if (IS_UNDEFINED(locales) && IS_UNDEFINED(options)) { 2016 checkDateCacheCurrent(); 2017 if (IS_UNDEFINED(defaultObjects[service])) { 2018 defaultObjects[service] = new savedObjects[service](locales, useOptions); 2019 } 2020 return defaultObjects[service]; 2021 } 2022 return new savedObjects[service](locales, useOptions); 2023 } 2024 2025 function LocaleConvertCase(s, locales, isToUpper) { 2026 // ECMA 402 section 13.1.2 steps 1 through 12. 2027 var language; 2028 // Optimize for the most common two cases. initializeLocaleList() can handle 2029 // them as well, but it's rather slow accounting for over 60% of 2030 // toLocale{U,L}Case() and about 40% of toLocale{U,L}Case("<locale>"). 2031 if (IS_UNDEFINED(locales)) { 2032 language = GetDefaultICULocaleJS(); 2033 } else if (IS_STRING(locales)) { 2034 language = canonicalizeLanguageTag(locales); 2035 } else { 2036 var locales = initializeLocaleList(locales); 2037 language = locales.length > 0 ? locales[0] : GetDefaultICULocaleJS(); 2038 } 2039 2040 // StringSplit is slower than this. 2041 var pos = %_Call(StringIndexOf, language, '-'); 2042 if (pos != -1) { 2043 language = %_Call(StringSubstring, language, 0, pos); 2044 } 2045 2046 var CUSTOM_CASE_LANGUAGES = ['az', 'el', 'lt', 'tr']; 2047 var langIndex = %_Call(ArrayIndexOf, CUSTOM_CASE_LANGUAGES, language); 2048 if (langIndex == -1) { 2049 // language-independent case conversion. 2050 return isToUpper ? %StringToUpperCaseI18N(s) : %StringToLowerCaseI18N(s); 2051 } 2052 return %StringLocaleConvertCase(s, isToUpper, 2053 CUSTOM_CASE_LANGUAGES[langIndex]); 2054 } 2055 2056 /** 2057 * Compares this and that, and returns less than 0, 0 or greater than 0 value. 2058 * Overrides the built-in method. 2059 */ 2060 OverrideFunction(GlobalString.prototype, 'localeCompare', function(that) { 2061 if (!IS_UNDEFINED(new.target)) { 2062 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2063 } 2064 2065 if (IS_NULL_OR_UNDEFINED(this)) { 2066 throw MakeTypeError(kMethodInvokedOnNullOrUndefined); 2067 } 2068 2069 var locales = arguments[1]; 2070 var options = arguments[2]; 2071 var collator = cachedOrNewService('collator', locales, options); 2072 return compare(collator, this, that); 2073 } 2074 ); 2075 2076 2077 /** 2078 * Unicode normalization. This method is called with one argument that 2079 * specifies the normalization form. 2080 * If none is specified, "NFC" is assumed. 2081 * If the form is not one of "NFC", "NFD", "NFKC", or "NFKD", then throw 2082 * a RangeError Exception. 2083 */ 2084 2085 OverrideFunction(GlobalString.prototype, 'normalize', function() { 2086 if (!IS_UNDEFINED(new.target)) { 2087 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2088 } 2089 2090 CHECK_OBJECT_COERCIBLE(this, "String.prototype.normalize"); 2091 var s = TO_STRING(this); 2092 2093 var formArg = arguments[0]; 2094 var form = IS_UNDEFINED(formArg) ? 'NFC' : TO_STRING(formArg); 2095 2096 var NORMALIZATION_FORMS = ['NFC', 'NFD', 'NFKC', 'NFKD']; 2097 2098 var normalizationForm = %_Call(ArrayIndexOf, NORMALIZATION_FORMS, form); 2099 if (normalizationForm === -1) { 2100 throw MakeRangeError(kNormalizationForm, 2101 %_Call(ArrayJoin, NORMALIZATION_FORMS, ', ')); 2102 } 2103 2104 return %StringNormalize(s, normalizationForm); 2105 } 2106 ); 2107 2108 function ToLowerCaseI18N() { 2109 if (!IS_UNDEFINED(new.target)) { 2110 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2111 } 2112 CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLowerCase"); 2113 var s = TO_STRING(this); 2114 return %StringToLowerCaseI18N(s); 2115 } 2116 2117 function ToUpperCaseI18N() { 2118 if (!IS_UNDEFINED(new.target)) { 2119 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2120 } 2121 CHECK_OBJECT_COERCIBLE(this, "String.prototype.toUpperCase"); 2122 var s = TO_STRING(this); 2123 return %StringToUpperCaseI18N(s); 2124 } 2125 2126 function ToLocaleLowerCaseI18N(locales) { 2127 if (!IS_UNDEFINED(new.target)) { 2128 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2129 } 2130 CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLocaleLowerCase"); 2131 return LocaleConvertCase(TO_STRING(this), locales, false); 2132 } 2133 2134 %FunctionSetLength(ToLocaleLowerCaseI18N, 0); 2135 2136 function ToLocaleUpperCaseI18N(locales) { 2137 if (!IS_UNDEFINED(new.target)) { 2138 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2139 } 2140 CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLocaleUpperCase"); 2141 return LocaleConvertCase(TO_STRING(this), locales, true); 2142 } 2143 2144 %FunctionSetLength(ToLocaleUpperCaseI18N, 0); 2145 2146 %FunctionRemovePrototype(ToLowerCaseI18N); 2147 %FunctionRemovePrototype(ToUpperCaseI18N); 2148 %FunctionRemovePrototype(ToLocaleLowerCaseI18N); 2149 %FunctionRemovePrototype(ToLocaleUpperCaseI18N); 2150 2151 utils.Export(function(to) { 2152 to.ToLowerCaseI18N = ToLowerCaseI18N; 2153 to.ToUpperCaseI18N = ToUpperCaseI18N; 2154 to.ToLocaleLowerCaseI18N = ToLocaleLowerCaseI18N; 2155 to.ToLocaleUpperCaseI18N = ToLocaleUpperCaseI18N; 2156 }); 2157 2158 2159 /** 2160 * Formats a Number object (this) using locale and options values. 2161 * If locale or options are omitted, defaults are used. 2162 */ 2163 OverrideFunction(GlobalNumber.prototype, 'toLocaleString', function() { 2164 if (!IS_UNDEFINED(new.target)) { 2165 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2166 } 2167 2168 if (!(this instanceof GlobalNumber) && typeof(this) !== 'number') { 2169 throw MakeTypeError(kMethodInvokedOnWrongType, "Number"); 2170 } 2171 2172 var locales = arguments[0]; 2173 var options = arguments[1]; 2174 var numberFormat = cachedOrNewService('numberformat', locales, options); 2175 return formatNumber(numberFormat, this); 2176 } 2177 ); 2178 2179 2180 /** 2181 * Returns actual formatted date or fails if date parameter is invalid. 2182 */ 2183 function toLocaleDateTime(date, locales, options, required, defaults, service) { 2184 if (!(date instanceof GlobalDate)) { 2185 throw MakeTypeError(kMethodInvokedOnWrongType, "Date"); 2186 } 2187 2188 if (IsNaN(date)) return 'Invalid Date'; 2189 2190 var internalOptions = toDateTimeOptions(options, required, defaults); 2191 2192 var dateFormat = 2193 cachedOrNewService(service, locales, options, internalOptions); 2194 2195 return formatDate(dateFormat, date); 2196 } 2197 2198 2199 /** 2200 * Formats a Date object (this) using locale and options values. 2201 * If locale or options are omitted, defaults are used - both date and time are 2202 * present in the output. 2203 */ 2204 OverrideFunction(GlobalDate.prototype, 'toLocaleString', function() { 2205 if (!IS_UNDEFINED(new.target)) { 2206 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2207 } 2208 2209 var locales = arguments[0]; 2210 var options = arguments[1]; 2211 return toLocaleDateTime( 2212 this, locales, options, 'any', 'all', 'dateformatall'); 2213 } 2214 ); 2215 2216 2217 /** 2218 * Formats a Date object (this) using locale and options values. 2219 * If locale or options are omitted, defaults are used - only date is present 2220 * in the output. 2221 */ 2222 OverrideFunction(GlobalDate.prototype, 'toLocaleDateString', function() { 2223 if (!IS_UNDEFINED(new.target)) { 2224 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2225 } 2226 2227 var locales = arguments[0]; 2228 var options = arguments[1]; 2229 return toLocaleDateTime( 2230 this, locales, options, 'date', 'date', 'dateformatdate'); 2231 } 2232 ); 2233 2234 2235 /** 2236 * Formats a Date object (this) using locale and options values. 2237 * If locale or options are omitted, defaults are used - only time is present 2238 * in the output. 2239 */ 2240 OverrideFunction(GlobalDate.prototype, 'toLocaleTimeString', function() { 2241 if (!IS_UNDEFINED(new.target)) { 2242 throw MakeTypeError(kOrdinaryFunctionCalledAsConstructor); 2243 } 2244 2245 var locales = arguments[0]; 2246 var options = arguments[1]; 2247 return toLocaleDateTime( 2248 this, locales, options, 'time', 'time', 'dateformattime'); 2249 } 2250 ); 2251 2252 utils.Export(function(to) { 2253 to.AddBoundMethod = AddBoundMethod; 2254 to.IntlParseDate = IntlParseDate; 2255 to.IntlParseNumber = IntlParseNumber; 2256 }); 2257 2258 }) 2259