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