1/** 2 * @license 3 * Copyright (C) 2010 The Libphonenumber Authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18/** 19 * @fileoverview A formatter which formats phone numbers as they are entered. 20 * (based on the java implementation). 21 * 22 * <p>An AsYouTypeFormatter can be created by new AsYouTypeFormatter(). After 23 * that, digits can be added by invoking {@link #inputDigit} on the formatter 24 * instance, and the partially formatted phone number will be returned each time 25 * a digit is added. {@link #clear} can be invoked before formatting a new 26 * number. 27 * 28 * <p>See the unittests for more details on how the formatter is to be used. 29 * 30 * @author Nikolaos Trogkanis 31 */ 32 33goog.provide('i18n.phonenumbers.AsYouTypeFormatter'); 34 35goog.require('goog.string.StringBuffer'); 36goog.require('i18n.phonenumbers.NumberFormat'); 37goog.require('i18n.phonenumbers.PhoneMetadata'); 38goog.require('i18n.phonenumbers.PhoneNumberUtil'); 39 40 41 42/** 43 * Constructs an AsYouTypeFormatter for the specific region. 44 * 45 * @param {string} regionCode the CLDR two-letter region code that denotes the 46 * region where the phone number is being entered. 47 * @constructor 48 */ 49i18n.phonenumbers.AsYouTypeFormatter = function(regionCode) { 50 /** 51 * The digits that have not been entered yet will be represented by a \u2008, 52 * the punctuation space. 53 * @const 54 * @type {string} 55 * @private 56 */ 57 this.DIGIT_PLACEHOLDER_ = '\u2008'; 58 /** 59 * @type {RegExp} 60 * @private 61 */ 62 this.DIGIT_PATTERN_ = new RegExp(this.DIGIT_PLACEHOLDER_); 63 /** 64 * @type {string} 65 * @private 66 */ 67 this.currentOutput_ = ''; 68 /** 69 * @type {!goog.string.StringBuffer} 70 * @private 71 */ 72 this.formattingTemplate_ = new goog.string.StringBuffer(); 73 /** 74 * The pattern from numberFormat that is currently used to create 75 * formattingTemplate. 76 * @type {string} 77 * @private 78 */ 79 this.currentFormattingPattern_ = ''; 80 /** 81 * @type {!goog.string.StringBuffer} 82 * @private 83 */ 84 this.accruedInput_ = new goog.string.StringBuffer(); 85 /** 86 * @type {!goog.string.StringBuffer} 87 * @private 88 */ 89 this.accruedInputWithoutFormatting_ = new goog.string.StringBuffer(); 90 /** 91 * This indicates whether AsYouTypeFormatter is currently doing the 92 * formatting. 93 * @type {boolean} 94 * @private 95 */ 96 this.ableToFormat_ = true; 97 /** 98 * Set to true when users enter their own formatting. AsYouTypeFormatter will 99 * do no formatting at all when this is set to true. 100 * @type {boolean} 101 * @private 102 */ 103 this.inputHasFormatting_ = false; 104 /** 105 * This is set to true when we know the user is entering a full national 106 * significant number, since we have either detected a national prefix or an 107 * international dialing prefix. When this is true, we will no longer use 108 * local number formatting patterns. 109 * @type {boolean} 110 * @private 111 */ 112 this.isCompleteNumber_ = false; 113 /** 114 * @type {boolean} 115 * @private 116 */ 117 this.isExpectingCountryCallingCode_ = false; 118 /** 119 * @type {i18n.phonenumbers.PhoneNumberUtil} 120 * @private 121 */ 122 this.phoneUtil_ = i18n.phonenumbers.PhoneNumberUtil.getInstance(); 123 /** 124 * @type {number} 125 * @private 126 */ 127 this.lastMatchPosition_ = 0; 128 /** 129 * The position of a digit upon which inputDigitAndRememberPosition is most 130 * recently invoked, as found in the original sequence of characters the user 131 * entered. 132 * @type {number} 133 * @private 134 */ 135 this.originalPosition_ = 0; 136 /** 137 * The position of a digit upon which inputDigitAndRememberPosition is most 138 * recently invoked, as found in accruedInputWithoutFormatting. 139 * entered. 140 * @type {number} 141 * @private 142 */ 143 this.positionToRemember_ = 0; 144 /** 145 * This contains anything that has been entered so far preceding the national 146 * significant number, and it is formatted (e.g. with space inserted). For 147 * example, this can contain IDD, country code, and/or NDD, etc. 148 * @type {!goog.string.StringBuffer} 149 * @private 150 */ 151 this.prefixBeforeNationalNumber_ = new goog.string.StringBuffer(); 152 /** 153 * @type {boolean} 154 * @private 155 */ 156 this.shouldAddSpaceAfterNationalPrefix_ = false; 157 /** 158 * This contains the national prefix that has been extracted. It contains only 159 * digits without formatting. 160 * @type {string} 161 * @private 162 */ 163 this.extractedNationalPrefix_ = ''; 164 /** 165 * @type {!goog.string.StringBuffer} 166 * @private 167 */ 168 this.nationalNumber_ = new goog.string.StringBuffer(); 169 /** 170 * @type {Array.<i18n.phonenumbers.NumberFormat>} 171 * @private 172 */ 173 this.possibleFormats_ = []; 174 /** 175 * @type {string} 176 * @private 177 */ 178 this.defaultCountry_ = regionCode; 179 this.currentMetadata_ = this.getMetadataForRegion_(this.defaultCountry_); 180 /** 181 * @type {i18n.phonenumbers.PhoneMetadata} 182 * @private 183 */ 184 this.defaultMetadata_ = this.currentMetadata_; 185}; 186 187 188/** 189 * Character used when appropriate to separate a prefix, such as a long NDD or a 190 * country calling code, from the national number. 191 * @const 192 * @type {string} 193 * @private 194 */ 195i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_ = ' '; 196 197 198/** 199 * @const 200 * @type {i18n.phonenumbers.PhoneMetadata} 201 * @private 202 */ 203i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_ = 204 new i18n.phonenumbers.PhoneMetadata(); 205i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_ 206 .setInternationalPrefix('NA'); 207 208 209/** 210 * A pattern that is used to determine if a numberFormat under availableFormats 211 * is eligible to be used by the AYTF. It is eligible when the format element 212 * under numberFormat contains groups of the dollar sign followed by a single 213 * digit, separated by valid phone number punctuation. This prevents invalid 214 * punctuation (such as the star sign in Israeli star numbers) getting into the 215 * output of the AYTF. We require that the first group is present in the output 216 * pattern to ensure no data is lost while formatting; when we format as you 217 * type, this should always be the case. 218 * @const 219 * @type {RegExp} 220 * @private 221 */ 222i18n.phonenumbers.AsYouTypeFormatter.ELIGIBLE_FORMAT_PATTERN_ = new RegExp( 223 '^[' + i18n.phonenumbers.PhoneNumberUtil.VALID_PUNCTUATION + ']*' + '\\$1' 224 + '[' + i18n.phonenumbers.PhoneNumberUtil.VALID_PUNCTUATION + ']*(\\$\\d' 225 + '[' + i18n.phonenumbers.PhoneNumberUtil.VALID_PUNCTUATION + ']*)*$'); 226 227 228/** 229 * A set of characters that, if found in a national prefix formatting rules, are 230 * an indicator to us that we should separate the national prefix from the 231 * number when formatting. 232 * @const 233 * @type {RegExp} 234 * @private 235 */ 236i18n.phonenumbers.AsYouTypeFormatter.NATIONAL_PREFIX_SEPARATORS_PATTERN_ = 237 /[- ]/; 238 239 240/** 241 * This is the minimum length of national number accrued that is required to 242 * trigger the formatter. The first element of the leadingDigitsPattern of 243 * each numberFormat contains a regular expression that matches up to this 244 * number of digits. 245 * @const 246 * @type {number} 247 * @private 248 */ 249i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_ = 3; 250 251 252/** 253 * The metadata needed by this class is the same for all regions sharing the 254 * same country calling code. Therefore, we return the metadata for "main" 255 * region for this country calling code. 256 * @param {string} regionCode an ISO 3166-1 two-letter region code. 257 * @return {i18n.phonenumbers.PhoneMetadata} main metadata for this region. 258 * @private 259 */ 260i18n.phonenumbers.AsYouTypeFormatter.prototype.getMetadataForRegion_ = 261 function(regionCode) { 262 263 /** @type {number} */ 264 var countryCallingCode = this.phoneUtil_.getCountryCodeForRegion(regionCode); 265 /** @type {string} */ 266 var mainCountry = 267 this.phoneUtil_.getRegionCodeForCountryCode(countryCallingCode); 268 /** @type {i18n.phonenumbers.PhoneMetadata} */ 269 var metadata = this.phoneUtil_.getMetadataForRegion(mainCountry); 270 if (metadata != null) { 271 return metadata; 272 } 273 // Set to a default instance of the metadata. This allows us to function with 274 // an incorrect region code, even if formatting only works for numbers 275 // specified with '+'. 276 return i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_; 277}; 278 279 280/** 281 * @return {boolean} true if a new template is created as opposed to reusing the 282 * existing template. 283 * @private 284 */ 285i18n.phonenumbers.AsYouTypeFormatter.prototype.maybeCreateNewTemplate_ = 286 function() { 287 288 // When there are multiple available formats, the formatter uses the first 289 // format where a formatting template could be created. 290 /** @type {number} */ 291 var possibleFormatsLength = this.possibleFormats_.length; 292 for (var i = 0; i < possibleFormatsLength; ++i) { 293 /** @type {i18n.phonenumbers.NumberFormat} */ 294 var numberFormat = this.possibleFormats_[i]; 295 /** @type {string} */ 296 var pattern = numberFormat.getPatternOrDefault(); 297 if (this.currentFormattingPattern_ == pattern) { 298 return false; 299 } 300 if (this.createFormattingTemplate_(numberFormat)) { 301 this.currentFormattingPattern_ = pattern; 302 this.shouldAddSpaceAfterNationalPrefix_ = 303 i18n.phonenumbers.AsYouTypeFormatter. 304 NATIONAL_PREFIX_SEPARATORS_PATTERN_.test( 305 numberFormat.getNationalPrefixFormattingRule()); 306 // With a new formatting template, the matched position using the old 307 // template needs to be reset. 308 this.lastMatchPosition_ = 0; 309 return true; 310 } 311 } 312 this.ableToFormat_ = false; 313 return false; 314}; 315 316 317/** 318 * @param {string} leadingDigits leading digits of entered number. 319 * @private 320 */ 321i18n.phonenumbers.AsYouTypeFormatter.prototype.getAvailableFormats_ = 322 function(leadingDigits) { 323 324 // First decide whether we should use international or national number rules. 325 /** @type {boolean} */ 326 var isInternationalNumber = this.isCompleteNumber_ && 327 this.extractedNationalPrefix_.length == 0; 328 /** @type {Array.<i18n.phonenumbers.NumberFormat>} */ 329 var formatList = 330 (isInternationalNumber && 331 this.currentMetadata_.intlNumberFormatCount() > 0) ? 332 this.currentMetadata_.intlNumberFormatArray() : 333 this.currentMetadata_.numberFormatArray(); 334 /** @type {number} */ 335 var formatListLength = formatList.length; 336 for (var i = 0; i < formatListLength; ++i) { 337 /** @type {i18n.phonenumbers.NumberFormat} */ 338 var format = formatList[i]; 339 // Discard a few formats that we know are not relevant based on the 340 // presence of the national prefix. 341 if (this.extractedNationalPrefix_.length > 0 && 342 this.phoneUtil_.formattingRuleHasFirstGroupOnly( 343 format.getNationalPrefixFormattingRuleOrDefault()) && 344 !format.getNationalPrefixOptionalWhenFormatting() && 345 !format.hasDomesticCarrierCodeFormattingRule()) { 346 // If it is a national number that had a national prefix, any rules that 347 // aren't valid with a national prefix should be excluded. A rule that 348 // has a carrier-code formatting rule is kept since the national prefix 349 // might actually be an extracted carrier code - we don't distinguish 350 // between these when extracting it in the AYTF. 351 continue; 352 } else if (this.extractedNationalPrefix_.length == 0 && 353 !this.isCompleteNumber_ && 354 !this.phoneUtil_.formattingRuleHasFirstGroupOnly( 355 format.getNationalPrefixFormattingRuleOrDefault()) && 356 !format.getNationalPrefixOptionalWhenFormatting()) { 357 // This number was entered without a national prefix, and this formatting 358 // rule requires one, so we discard it. 359 continue; 360 } 361 if (i18n.phonenumbers.AsYouTypeFormatter.ELIGIBLE_FORMAT_PATTERN_.test( 362 format.getFormatOrDefault())) { 363 this.possibleFormats_.push(format); 364 } 365 } 366 this.narrowDownPossibleFormats_(leadingDigits); 367}; 368 369 370/** 371 * @param {string} leadingDigits 372 * @private 373 */ 374i18n.phonenumbers.AsYouTypeFormatter.prototype.narrowDownPossibleFormats_ = 375 function(leadingDigits) { 376 377 /** @type {Array.<i18n.phonenumbers.NumberFormat>} */ 378 var possibleFormats = []; 379 /** @type {number} */ 380 var indexOfLeadingDigitsPattern = 381 leadingDigits.length - 382 i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_; 383 /** @type {number} */ 384 var possibleFormatsLength = this.possibleFormats_.length; 385 for (var i = 0; i < possibleFormatsLength; ++i) { 386 /** @type {i18n.phonenumbers.NumberFormat} */ 387 var format = this.possibleFormats_[i]; 388 if (format.leadingDigitsPatternCount() == 0) { 389 // Keep everything that isn't restricted by leading digits. 390 possibleFormats.push(this.possibleFormats_[i]); 391 continue; 392 } 393 /** @type {number} */ 394 var lastLeadingDigitsPattern = Math.min( 395 indexOfLeadingDigitsPattern, format.leadingDigitsPatternCount() - 1); 396 /** @type {string} */ 397 var leadingDigitsPattern = /** @type {string} */ 398 (format.getLeadingDigitsPattern(lastLeadingDigitsPattern)); 399 if (leadingDigits.search(leadingDigitsPattern) == 0) { 400 possibleFormats.push(this.possibleFormats_[i]); 401 } 402 } 403 this.possibleFormats_ = possibleFormats; 404}; 405 406 407/** 408 * @param {i18n.phonenumbers.NumberFormat} format 409 * @return {boolean} 410 * @private 411 */ 412i18n.phonenumbers.AsYouTypeFormatter.prototype.createFormattingTemplate_ = 413 function(format) { 414 415 /** @type {string} */ 416 var numberPattern = format.getPatternOrDefault(); 417 418 this.formattingTemplate_.clear(); 419 /** @type {string} */ 420 var tempTemplate = this.getFormattingTemplate_(numberPattern, 421 format.getFormatOrDefault()); 422 if (tempTemplate.length > 0) { 423 this.formattingTemplate_.append(tempTemplate); 424 return true; 425 } 426 return false; 427}; 428 429 430/** 431 * Gets a formatting template which can be used to efficiently format a 432 * partial number where digits are added one by one. 433 * 434 * @param {string} numberPattern 435 * @param {string} numberFormat 436 * @return {string} 437 * @private 438 */ 439i18n.phonenumbers.AsYouTypeFormatter.prototype.getFormattingTemplate_ = 440 function(numberPattern, numberFormat) { 441 442 // Creates a phone number consisting only of the digit 9 that matches the 443 // numberPattern by applying the pattern to the longestPhoneNumber string. 444 /** @type {string} */ 445 var longestPhoneNumber = '999999999999999'; 446 /** @type {Array.<string>} */ 447 var m = longestPhoneNumber.match(numberPattern); 448 // this match will always succeed 449 /** @type {string} */ 450 var aPhoneNumber = m[0]; 451 // No formatting template can be created if the number of digits entered so 452 // far is longer than the maximum the current formatting rule can accommodate. 453 if (aPhoneNumber.length < this.nationalNumber_.getLength()) { 454 return ''; 455 } 456 // Formats the number according to numberFormat 457 /** @type {string} */ 458 var template = aPhoneNumber.replace(new RegExp(numberPattern, 'g'), 459 numberFormat); 460 // Replaces each digit with character DIGIT_PLACEHOLDER 461 template = template.replace(new RegExp('9', 'g'), this.DIGIT_PLACEHOLDER_); 462 return template; 463}; 464 465 466/** 467 * Clears the internal state of the formatter, so it can be reused. 468 */ 469i18n.phonenumbers.AsYouTypeFormatter.prototype.clear = function() { 470 this.currentOutput_ = ''; 471 this.accruedInput_.clear(); 472 this.accruedInputWithoutFormatting_.clear(); 473 this.formattingTemplate_.clear(); 474 this.lastMatchPosition_ = 0; 475 this.currentFormattingPattern_ = ''; 476 this.prefixBeforeNationalNumber_.clear(); 477 this.extractedNationalPrefix_ = ''; 478 this.nationalNumber_.clear(); 479 this.ableToFormat_ = true; 480 this.inputHasFormatting_ = false; 481 this.positionToRemember_ = 0; 482 this.originalPosition_ = 0; 483 this.isCompleteNumber_ = false; 484 this.isExpectingCountryCallingCode_ = false; 485 this.possibleFormats_ = []; 486 this.shouldAddSpaceAfterNationalPrefix_ = false; 487 if (this.currentMetadata_ != this.defaultMetadata_) { 488 this.currentMetadata_ = this.getMetadataForRegion_(this.defaultCountry_); 489 } 490}; 491 492 493/** 494 * Formats a phone number on-the-fly as each digit is entered. 495 * 496 * @param {string} nextChar the most recently entered digit of a phone number. 497 * Formatting characters are allowed, but as soon as they are encountered 498 * this method formats the number as entered and not 'as you type' anymore. 499 * Full width digits and Arabic-indic digits are allowed, and will be shown 500 * as they are. 501 * @return {string} the partially formatted phone number. 502 */ 503i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigit = function(nextChar) { 504 this.currentOutput_ = 505 this.inputDigitWithOptionToRememberPosition_(nextChar, false); 506 return this.currentOutput_; 507}; 508 509 510/** 511 * Same as {@link #inputDigit}, but remembers the position where 512 * {@code nextChar} is inserted, so that it can be retrieved later by using 513 * {@link #getRememberedPosition}. The remembered position will be automatically 514 * adjusted if additional formatting characters are later inserted/removed in 515 * front of {@code nextChar}. 516 * 517 * @param {string} nextChar 518 * @return {string} 519 */ 520i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigitAndRememberPosition = 521 function(nextChar) { 522 523 this.currentOutput_ = 524 this.inputDigitWithOptionToRememberPosition_(nextChar, true); 525 return this.currentOutput_; 526}; 527 528 529/** 530 * @param {string} nextChar 531 * @param {boolean} rememberPosition 532 * @return {string} 533 * @private 534 */ 535i18n.phonenumbers.AsYouTypeFormatter.prototype. 536 inputDigitWithOptionToRememberPosition_ = function(nextChar, 537 rememberPosition) { 538 539 this.accruedInput_.append(nextChar); 540 if (rememberPosition) { 541 this.originalPosition_ = this.accruedInput_.getLength(); 542 } 543 // We do formatting on-the-fly only when each character entered is either a 544 // digit, or a plus sign (accepted at the start of the number only). 545 if (!this.isDigitOrLeadingPlusSign_(nextChar)) { 546 this.ableToFormat_ = false; 547 this.inputHasFormatting_ = true; 548 } else { 549 nextChar = this.normalizeAndAccrueDigitsAndPlusSign_(nextChar, 550 rememberPosition); 551 } 552 if (!this.ableToFormat_) { 553 // When we are unable to format because of reasons other than that 554 // formatting chars have been entered, it can be due to really long IDDs or 555 // NDDs. If that is the case, we might be able to do formatting again after 556 // extracting them. 557 if (this.inputHasFormatting_) { 558 return this.accruedInput_.toString(); 559 } else if (this.attemptToExtractIdd_()) { 560 if (this.attemptToExtractCountryCallingCode_()) { 561 return this.attemptToChoosePatternWithPrefixExtracted_(); 562 } 563 } else if (this.ableToExtractLongerNdd_()) { 564 // Add an additional space to separate long NDD and national significant 565 // number for readability. We don't set shouldAddSpaceAfterNationalPrefix_ 566 // to true, since we don't want this to change later when we choose 567 // formatting templates. 568 this.prefixBeforeNationalNumber_.append( 569 i18n.phonenumbers.AsYouTypeFormatter. 570 SEPARATOR_BEFORE_NATIONAL_NUMBER_); 571 return this.attemptToChoosePatternWithPrefixExtracted_(); 572 } 573 return this.accruedInput_.toString(); 574 } 575 576 // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH 577 // digits (the plus sign is counted as a digit as well for this purpose) have 578 // been entered. 579 switch (this.accruedInputWithoutFormatting_.getLength()) { 580 case 0: 581 case 1: 582 case 2: 583 return this.accruedInput_.toString(); 584 case 3: 585 if (this.attemptToExtractIdd_()) { 586 this.isExpectingCountryCallingCode_ = true; 587 } else { 588 // No IDD or plus sign is found, might be entering in national format. 589 this.extractedNationalPrefix_ = 590 this.removeNationalPrefixFromNationalNumber_(); 591 return this.attemptToChooseFormattingPattern_(); 592 } 593 default: 594 if (this.isExpectingCountryCallingCode_) { 595 if (this.attemptToExtractCountryCallingCode_()) { 596 this.isExpectingCountryCallingCode_ = false; 597 } 598 return this.prefixBeforeNationalNumber_.toString() + 599 this.nationalNumber_.toString(); 600 } 601 if (this.possibleFormats_.length > 0) { 602 // The formatting patterns are already chosen. 603 /** @type {string} */ 604 var tempNationalNumber = this.inputDigitHelper_(nextChar); 605 // See if the accrued digits can be formatted properly already. If not, 606 // use the results from inputDigitHelper, which does formatting based on 607 // the formatting pattern chosen. 608 /** @type {string} */ 609 var formattedNumber = this.attemptToFormatAccruedDigits_(); 610 if (formattedNumber.length > 0) { 611 return formattedNumber; 612 } 613 this.narrowDownPossibleFormats_(this.nationalNumber_.toString()); 614 if (this.maybeCreateNewTemplate_()) { 615 return this.inputAccruedNationalNumber_(); 616 } 617 return this.ableToFormat_ ? 618 this.appendNationalNumber_(tempNationalNumber) : 619 this.accruedInput_.toString(); 620 } else { 621 return this.attemptToChooseFormattingPattern_(); 622 } 623 } 624}; 625 626 627/** 628 * @return {string} 629 * @private 630 */ 631i18n.phonenumbers.AsYouTypeFormatter.prototype. 632 attemptToChoosePatternWithPrefixExtracted_ = function() { 633 634 this.ableToFormat_ = true; 635 this.isExpectingCountryCallingCode_ = false; 636 this.possibleFormats_ = []; 637 this.lastMatchPosition_ = 0; 638 this.formattingTemplate_.clear(); 639 this.currentFormattingPattern_ = ''; 640 return this.attemptToChooseFormattingPattern_(); 641}; 642 643 644/** 645 * @return {string} 646 * @private 647 */ 648i18n.phonenumbers.AsYouTypeFormatter.prototype.getExtractedNationalPrefix_ = 649 function() { 650 return this.extractedNationalPrefix_; 651}; 652 653 654/** 655 * Some national prefixes are a substring of others. If extracting the shorter 656 * NDD doesn't result in a number we can format, we try to see if we can extract 657 * a longer version here. 658 * @return {boolean} 659 * @private 660 */ 661i18n.phonenumbers.AsYouTypeFormatter.prototype.ableToExtractLongerNdd_ = 662 function() { 663 if (this.extractedNationalPrefix_.length > 0) { 664 // Put the extracted NDD back to the national number before attempting to 665 // extract a new NDD. 666 /** @type {string} */ 667 var nationalNumberStr = this.nationalNumber_.toString(); 668 this.nationalNumber_.clear(); 669 this.nationalNumber_.append(this.extractedNationalPrefix_); 670 this.nationalNumber_.append(nationalNumberStr); 671 // Remove the previously extracted NDD from prefixBeforeNationalNumber. We 672 // cannot simply set it to empty string because people sometimes incorrectly 673 // enter national prefix after the country code, e.g. +44 (0)20-1234-5678. 674 /** @type {string} */ 675 var prefixBeforeNationalNumberStr = 676 this.prefixBeforeNationalNumber_.toString(); 677 /** @type {number} */ 678 var indexOfPreviousNdd = prefixBeforeNationalNumberStr.lastIndexOf( 679 this.extractedNationalPrefix_); 680 this.prefixBeforeNationalNumber_.clear(); 681 this.prefixBeforeNationalNumber_.append( 682 prefixBeforeNationalNumberStr.substring(0, indexOfPreviousNdd)); 683 } 684 return this.extractedNationalPrefix_ != 685 this.removeNationalPrefixFromNationalNumber_(); 686}; 687 688 689/** 690 * @param {string} nextChar 691 * @return {boolean} 692 * @private 693 */ 694i18n.phonenumbers.AsYouTypeFormatter.prototype.isDigitOrLeadingPlusSign_ = 695 function(nextChar) { 696 return i18n.phonenumbers.PhoneNumberUtil.CAPTURING_DIGIT_PATTERN 697 .test(nextChar) || 698 (this.accruedInput_.getLength() == 1 && 699 i18n.phonenumbers.PhoneNumberUtil.PLUS_CHARS_PATTERN.test(nextChar)); 700}; 701 702 703/** 704 * Check to see if there is an exact pattern match for these digits. If so, we 705 * should use this instead of any other formatting template whose 706 * leadingDigitsPattern also matches the input. 707 * @return {string} 708 * @private 709 */ 710i18n.phonenumbers.AsYouTypeFormatter.prototype.attemptToFormatAccruedDigits_ = 711 function() { 712 713 /** @type {string} */ 714 var nationalNumber = this.nationalNumber_.toString(); 715 /** @type {number} */ 716 var possibleFormatsLength = this.possibleFormats_.length; 717 for (var i = 0; i < possibleFormatsLength; ++i) { 718 /** @type {i18n.phonenumbers.NumberFormat} */ 719 var numberFormat = this.possibleFormats_[i]; 720 /** @type {string} */ 721 var pattern = numberFormat.getPatternOrDefault(); 722 /** @type {RegExp} */ 723 var patternRegExp = new RegExp('^(?:' + pattern + ')$'); 724 if (patternRegExp.test(nationalNumber)) { 725 this.shouldAddSpaceAfterNationalPrefix_ = 726 i18n.phonenumbers.AsYouTypeFormatter. 727 NATIONAL_PREFIX_SEPARATORS_PATTERN_.test( 728 numberFormat.getNationalPrefixFormattingRule()); 729 /** @type {string} */ 730 var formattedNumber = nationalNumber.replace(new RegExp(pattern, 'g'), 731 numberFormat.getFormat()); 732 // Check that we didn't remove nor add any extra digits when we matched 733 // this formatting pattern. This usually happens after we entered the last 734 // digit during AYTF. Eg: In case of MX, we swallow mobile token (1) when 735 // formatted but AYTF should retain all the number entered and not change 736 // in order to match a format (of same leading digits and length) display 737 // in that way. 738 var fullOutput = this.appendNationalNumber_(formattedNumber); 739 var formattedNumberDigitsOnly = 740 i18n.phonenumbers.PhoneNumberUtil.normalizeDiallableCharsOnly( 741 fullOutput); 742 if (formattedNumberDigitsOnly == this.accruedInputWithoutFormatting_) { 743 // If it's the same (i.e entered number and format is same), then it's 744 // safe to return this in formatted number as nothing is lost / added. 745 return fullOutput; 746 } 747 } 748 } 749 return ''; 750}; 751 752 753/** 754 * Combines the national number with any prefix (IDD/+ and country code or 755 * national prefix) that was collected. A space will be inserted between them if 756 * the current formatting template indicates this to be suitable. 757 * @param {string} nationalNumber The number to be appended. 758 * @return {string} The combined number. 759 * @private 760 */ 761i18n.phonenumbers.AsYouTypeFormatter.prototype.appendNationalNumber_ = 762 function(nationalNumber) { 763 /** @type {number} */ 764 var prefixBeforeNationalNumberLength = 765 this.prefixBeforeNationalNumber_.getLength(); 766 if (this.shouldAddSpaceAfterNationalPrefix_ && 767 prefixBeforeNationalNumberLength > 0 && 768 this.prefixBeforeNationalNumber_.toString().charAt( 769 prefixBeforeNationalNumberLength - 1) != 770 i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_) { 771 // We want to add a space after the national prefix if the national prefix 772 // formatting rule indicates that this would normally be done, with the 773 // exception of the case where we already appended a space because the NDD 774 // was surprisingly long. 775 return this.prefixBeforeNationalNumber_ + 776 i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_ + 777 nationalNumber; 778 } else { 779 return this.prefixBeforeNationalNumber_ + nationalNumber; 780 } 781}; 782 783 784/** 785 * Returns the current position in the partially formatted phone number of the 786 * character which was previously passed in as the parameter of 787 * {@link #inputDigitAndRememberPosition}. 788 * 789 * @return {number} 790 */ 791i18n.phonenumbers.AsYouTypeFormatter.prototype.getRememberedPosition = 792 function() { 793 794 if (!this.ableToFormat_) { 795 return this.originalPosition_; 796 } 797 /** @type {number} */ 798 var accruedInputIndex = 0; 799 /** @type {number} */ 800 var currentOutputIndex = 0; 801 /** @type {string} */ 802 var accruedInputWithoutFormatting = 803 this.accruedInputWithoutFormatting_.toString(); 804 /** @type {string} */ 805 var currentOutput = this.currentOutput_.toString(); 806 while (accruedInputIndex < this.positionToRemember_ && 807 currentOutputIndex < currentOutput.length) { 808 if (accruedInputWithoutFormatting.charAt(accruedInputIndex) == 809 currentOutput.charAt(currentOutputIndex)) { 810 accruedInputIndex++; 811 } 812 currentOutputIndex++; 813 } 814 return currentOutputIndex; 815}; 816 817 818/** 819 * Attempts to set the formatting template and returns a string which contains 820 * the formatted version of the digits entered so far. 821 * 822 * @return {string} 823 * @private 824 */ 825i18n.phonenumbers.AsYouTypeFormatter.prototype. 826 attemptToChooseFormattingPattern_ = function() { 827 828 /** @type {string} */ 829 var nationalNumber = this.nationalNumber_.toString(); 830 // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH 831 // digits of national number (excluding national prefix) have been entered. 832 if (nationalNumber.length >= 833 i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_) { 834 this.getAvailableFormats_(nationalNumber); 835 // See if the accrued digits can be formatted properly already. 836 var formattedNumber = this.attemptToFormatAccruedDigits_(); 837 if (formattedNumber.length > 0) { 838 return formattedNumber; 839 } 840 return this.maybeCreateNewTemplate_() ? 841 this.inputAccruedNationalNumber_() : this.accruedInput_.toString(); 842 } else { 843 return this.appendNationalNumber_(nationalNumber); 844 } 845}; 846 847 848/** 849 * Invokes inputDigitHelper on each digit of the national number accrued, and 850 * returns a formatted string in the end. 851 * 852 * @return {string} 853 * @private 854 */ 855i18n.phonenumbers.AsYouTypeFormatter.prototype.inputAccruedNationalNumber_ = 856 function() { 857 858 /** @type {string} */ 859 var nationalNumber = this.nationalNumber_.toString(); 860 /** @type {number} */ 861 var lengthOfNationalNumber = nationalNumber.length; 862 if (lengthOfNationalNumber > 0) { 863 /** @type {string} */ 864 var tempNationalNumber = ''; 865 for (var i = 0; i < lengthOfNationalNumber; i++) { 866 tempNationalNumber = 867 this.inputDigitHelper_(nationalNumber.charAt(i)); 868 } 869 return this.ableToFormat_ ? 870 this.appendNationalNumber_(tempNationalNumber) : 871 this.accruedInput_.toString(); 872 } else { 873 return this.prefixBeforeNationalNumber_.toString(); 874 } 875}; 876 877 878/** 879 * @return {boolean} true if the current country is a NANPA country and the 880 * national number begins with the national prefix. 881 * @private 882 */ 883i18n.phonenumbers.AsYouTypeFormatter.prototype. 884 isNanpaNumberWithNationalPrefix_ = function() { 885 // For NANPA numbers beginning with 1[2-9], treat the 1 as the national 886 // prefix. The reason is that national significant numbers in NANPA always 887 // start with [2-9] after the national prefix. Numbers beginning with 1[01] 888 // can only be short/emergency numbers, which don't need the national prefix. 889 if (this.currentMetadata_.getCountryCode() != 1) { 890 return false; 891 } 892 /** @type {string} */ 893 var nationalNumber = this.nationalNumber_.toString(); 894 return (nationalNumber.charAt(0) == '1') && 895 (nationalNumber.charAt(1) != '0') && 896 (nationalNumber.charAt(1) != '1'); 897}; 898 899 900/** 901 * Returns the national prefix extracted, or an empty string if it is not 902 * present. 903 * @return {string} 904 * @private 905 */ 906i18n.phonenumbers.AsYouTypeFormatter.prototype. 907 removeNationalPrefixFromNationalNumber_ = function() { 908 909 /** @type {string} */ 910 var nationalNumber = this.nationalNumber_.toString(); 911 /** @type {number} */ 912 var startOfNationalNumber = 0; 913 if (this.isNanpaNumberWithNationalPrefix_()) { 914 startOfNationalNumber = 1; 915 this.prefixBeforeNationalNumber_.append('1').append( 916 i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_); 917 this.isCompleteNumber_ = true; 918 } else if (this.currentMetadata_.hasNationalPrefixForParsing()) { 919 /** @type {RegExp} */ 920 var nationalPrefixForParsing = new RegExp( 921 '^(?:' + this.currentMetadata_.getNationalPrefixForParsing() + ')'); 922 /** @type {Array.<string>} */ 923 var m = nationalNumber.match(nationalPrefixForParsing); 924 // Since some national prefix patterns are entirely optional, check that a 925 // national prefix could actually be extracted. 926 if (m != null && m[0] != null && m[0].length > 0) { 927 // When the national prefix is detected, we use international formatting 928 // rules instead of national ones, because national formatting rules could 929 // contain local formatting rules for numbers entered without area code. 930 this.isCompleteNumber_ = true; 931 startOfNationalNumber = m[0].length; 932 this.prefixBeforeNationalNumber_.append(nationalNumber.substring(0, 933 startOfNationalNumber)); 934 } 935 } 936 this.nationalNumber_.clear(); 937 this.nationalNumber_.append(nationalNumber.substring(startOfNationalNumber)); 938 return nationalNumber.substring(0, startOfNationalNumber); 939}; 940 941 942/** 943 * Extracts IDD and plus sign to prefixBeforeNationalNumber when they are 944 * available, and places the remaining input into nationalNumber. 945 * 946 * @return {boolean} true when accruedInputWithoutFormatting begins with the 947 * plus sign or valid IDD for defaultCountry. 948 * @private 949 */ 950i18n.phonenumbers.AsYouTypeFormatter.prototype.attemptToExtractIdd_ = 951 function() { 952 953 /** @type {string} */ 954 var accruedInputWithoutFormatting = 955 this.accruedInputWithoutFormatting_.toString(); 956 /** @type {RegExp} */ 957 var internationalPrefix = new RegExp( 958 '^(?:' + '\\' + i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN + '|' + 959 this.currentMetadata_.getInternationalPrefix() + ')'); 960 /** @type {Array.<string>} */ 961 var m = accruedInputWithoutFormatting.match(internationalPrefix); 962 if (m != null && m[0] != null && m[0].length > 0) { 963 this.isCompleteNumber_ = true; 964 /** @type {number} */ 965 var startOfCountryCallingCode = m[0].length; 966 this.nationalNumber_.clear(); 967 this.nationalNumber_.append( 968 accruedInputWithoutFormatting.substring(startOfCountryCallingCode)); 969 this.prefixBeforeNationalNumber_.clear(); 970 this.prefixBeforeNationalNumber_.append( 971 accruedInputWithoutFormatting.substring(0, startOfCountryCallingCode)); 972 if (accruedInputWithoutFormatting.charAt(0) != 973 i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN) { 974 this.prefixBeforeNationalNumber_.append( 975 i18n.phonenumbers.AsYouTypeFormatter. 976 SEPARATOR_BEFORE_NATIONAL_NUMBER_); 977 } 978 return true; 979 } 980 return false; 981}; 982 983 984/** 985 * Extracts the country calling code from the beginning of nationalNumber to 986 * prefixBeforeNationalNumber when they are available, and places the remaining 987 * input into nationalNumber. 988 * 989 * @return {boolean} true when a valid country calling code can be found. 990 * @private 991 */ 992i18n.phonenumbers.AsYouTypeFormatter.prototype. 993 attemptToExtractCountryCallingCode_ = function() { 994 995 if (this.nationalNumber_.getLength() == 0) { 996 return false; 997 } 998 /** @type {!goog.string.StringBuffer} */ 999 var numberWithoutCountryCallingCode = new goog.string.StringBuffer(); 1000 /** @type {number} */ 1001 var countryCode = this.phoneUtil_.extractCountryCode( 1002 this.nationalNumber_, numberWithoutCountryCallingCode); 1003 if (countryCode == 0) { 1004 return false; 1005 } 1006 this.nationalNumber_.clear(); 1007 this.nationalNumber_.append(numberWithoutCountryCallingCode.toString()); 1008 /** @type {string} */ 1009 var newRegionCode = this.phoneUtil_.getRegionCodeForCountryCode(countryCode); 1010 if (i18n.phonenumbers.PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY == 1011 newRegionCode) { 1012 this.currentMetadata_ = 1013 this.phoneUtil_.getMetadataForNonGeographicalRegion(countryCode); 1014 } else if (newRegionCode != this.defaultCountry_) { 1015 this.currentMetadata_ = this.getMetadataForRegion_(newRegionCode); 1016 } 1017 /** @type {string} */ 1018 var countryCodeString = '' + countryCode; 1019 this.prefixBeforeNationalNumber_.append(countryCodeString).append( 1020 i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_); 1021 // When we have successfully extracted the IDD, the previously extracted NDD 1022 // should be cleared because it is no longer valid. 1023 this.extractedNationalPrefix_ = ''; 1024 return true; 1025}; 1026 1027 1028/** 1029 * Accrues digits and the plus sign to accruedInputWithoutFormatting for later 1030 * use. If nextChar contains a digit in non-ASCII format (e.g. the full-width 1031 * version of digits), it is first normalized to the ASCII version. The return 1032 * value is nextChar itself, or its normalized version, if nextChar is a digit 1033 * in non-ASCII format. This method assumes its input is either a digit or the 1034 * plus sign. 1035 * 1036 * @param {string} nextChar 1037 * @param {boolean} rememberPosition 1038 * @return {string} 1039 * @private 1040 */ 1041i18n.phonenumbers.AsYouTypeFormatter.prototype. 1042 normalizeAndAccrueDigitsAndPlusSign_ = function(nextChar, 1043 rememberPosition) { 1044 1045 /** @type {string} */ 1046 var normalizedChar; 1047 if (nextChar == i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN) { 1048 normalizedChar = nextChar; 1049 this.accruedInputWithoutFormatting_.append(nextChar); 1050 } else { 1051 normalizedChar = i18n.phonenumbers.PhoneNumberUtil.DIGIT_MAPPINGS[nextChar]; 1052 this.accruedInputWithoutFormatting_.append(normalizedChar); 1053 this.nationalNumber_.append(normalizedChar); 1054 } 1055 if (rememberPosition) { 1056 this.positionToRemember_ = this.accruedInputWithoutFormatting_.getLength(); 1057 } 1058 return normalizedChar; 1059}; 1060 1061 1062/** 1063 * @param {string} nextChar 1064 * @return {string} 1065 * @private 1066 */ 1067i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigitHelper_ = 1068 function(nextChar) { 1069 1070 // Note that formattingTemplate is not guaranteed to have a value, it could be 1071 // empty, e.g. when the next digit is entered after extracting an IDD or NDD. 1072 /** @type {string} */ 1073 var formattingTemplate = this.formattingTemplate_.toString(); 1074 if (formattingTemplate.substring(this.lastMatchPosition_) 1075 .search(this.DIGIT_PATTERN_) >= 0) { 1076 /** @type {number} */ 1077 var digitPatternStart = formattingTemplate.search(this.DIGIT_PATTERN_); 1078 /** @type {string} */ 1079 var tempTemplate = 1080 formattingTemplate.replace(this.DIGIT_PATTERN_, nextChar); 1081 this.formattingTemplate_.clear(); 1082 this.formattingTemplate_.append(tempTemplate); 1083 this.lastMatchPosition_ = digitPatternStart; 1084 return tempTemplate.substring(0, this.lastMatchPosition_ + 1); 1085 } else { 1086 if (this.possibleFormats_.length == 1) { 1087 // More digits are entered than we could handle, and there are no other 1088 // valid patterns to try. 1089 this.ableToFormat_ = false; 1090 } // else, we just reset the formatting pattern. 1091 this.currentFormattingPattern_ = ''; 1092 return this.accruedInput_.toString(); 1093 } 1094}; 1095