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