-/*
+/**
* @license
- * Copyright (C) 2010 Google Inc.
+ * Copyright (C) 2010 The Libphonenumber Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* @fileoverview A formatter which formats phone numbers as they are entered.
* (based on the java implementation).
*
- * An AsYouTypeFormatter could be created by new AsYouTypeFormatter(). After
- * that digits could be added by invoking the inputDigit method on the formatter
+ * <p>An AsYouTypeFormatter can be created by new AsYouTypeFormatter(). After
+ * that, digits can be added by invoking {@link #inputDigit} on the formatter
* instance, and the partially formatted phone number will be returned each time
- * a digit is added. The clear method should be invoked before a new number
- * needs to be formatted.
+ * a digit is added. {@link #clear} can be invoked before formatting a new
+ * number.
*
- * See testAsYouTypeFormatterUS(), testAsYouTestFormatterGB() and
- * testAsYouTypeFormatterDE() in asyoutypeformatter_test.js for more details
- * on how the formatter is to be used.
+ * <p>See the unittests for more details on how the formatter is to be used.
*
* @author Nikolaos Trogkanis
*/
goog.require('i18n.phonenumbers.PhoneNumberUtil');
goog.require('i18n.phonenumbers.metadata');
+
+
/**
- * Constructs a light-weight formatter which does no formatting, but outputs
- * exactly what is fed into the inputDigit method.
+ * Constructs an AsYouTypeFormatter for the specific region.
*
- * @param {string} regionCode the country/region where the phone number is being
- * entered.
+ * @param {string} regionCode the ISO 3166-1 two-letter region code that denotes
+ * the region where the phone number is being entered.
* @constructor
*/
i18n.phonenumbers.AsYouTypeFormatter = function(regionCode) {
/**
- * A pattern that is used to match character classes in regular expressions.
- * An example of a character class is [1-4].
- * @const
- * @type {RegExp}
- * @private
- */
- this.CHARACTER_CLASS_PATTERN_ = /\[([^\[\]])*\]/g;
- /**
- * Any digit in a regular expression that actually denotes a digit. For
- * example, in the regular expression 80[0-2]\d{6,10}, the first 2 digits
- * (8 and 0) are standalone digits, but the rest are not.
- * Two look-aheads are needed because the number following \\d could be a
- * two-digit number, since the phone number can be as long as 15 digits.
- * @const
- * @type {RegExp}
- * @private
- */
- this.STANDALONE_DIGIT_PATTERN_ = /\d(?=[^,}][^,}])/g;
- /**
- * This is the minimum length of national number accrued that is required to
- * trigger the formatter. The first element of the leadingDigitsPattern of
- * each numberFormat contains a regular expression that matches up to this
- * number of digits.
- * @const
- * @type {number}
- * @private
- */
- this.MIN_LEADING_DIGITS_LENGTH_ = 3;
- /**
* The digits that have not been entered yet will be represented by a \u2008,
* the punctuation space.
* @const
* @type {string}
* @private
*/
- this.digitPlaceholder_ = '\u2008';
+ this.DIGIT_PLACEHOLDER_ = '\u2008';
/**
* @type {RegExp}
* @private
*/
- this.digitPattern_ = new RegExp(this.digitPlaceholder_);
-
+ this.DIGIT_PATTERN_ = new RegExp(this.DIGIT_PLACEHOLDER_);
/**
* @type {string}
* @private
*/
this.accruedInputWithoutFormatting_ = new goog.string.StringBuffer();
/**
+ * This indicates whether AsYouTypeFormatter is currently doing the
+ * formatting.
* @type {boolean}
* @private
*/
this.ableToFormat_ = true;
/**
+ * Set to true when users enter their own formatting. AsYouTypeFormatter will
+ * do no formatting at all when this is set to true.
+ * @type {boolean}
+ * @private
+ */
+ this.inputHasFormatting_ = false;
+ /**
+ * This is set to true when we know the user is entering a full national
+ * significant number, since we have either detected a national prefix or an
+ * international dialing prefix. When this is true, we will no longer use
+ * local number formatting patterns.
* @type {boolean}
* @private
*/
- this.isInternationalFormatting_ = false;
+ this.isCompleteNumber_ = false;
/**
* @type {boolean}
* @private
*/
- this.isExpectingCountryCode_ = false;
+ this.isExpectingCountryCallingCode_ = false;
/**
* @type {i18n.phonenumbers.PhoneNumberUtil}
* @private
*/
this.positionToRemember_ = 0;
/**
+ * This contains anything that has been entered so far preceding the national
+ * significant number, and it is formatted (e.g. with space inserted). For
+ * example, this can contain IDD, country code, and/or NDD, etc.
* @type {!goog.string.StringBuffer}
* @private
*/
this.prefixBeforeNationalNumber_ = new goog.string.StringBuffer();
/**
+ * @type {boolean}
+ * @private
+ */
+ this.shouldAddSpaceAfterNationalPrefix_ = false;
+ /**
+ * This contains the national prefix that has been extracted. It contains only
+ * digits without formatting.
+ * @type {string}
+ * @private
+ */
+ this.extractedNationalPrefix_ = '';
+ /**
* @type {!goog.string.StringBuffer}
* @private
*/
* @private
*/
this.possibleFormats_ = [];
-
/**
- * @type {string}
+ * @type {string}
* @private
*/
this.defaultCountry_ = regionCode;
- this.initializeCountrySpecificInfo_(this.defaultCountry_);
+ this.currentMetadata_ = this.getMetadataForRegion_(this.defaultCountry_);
/**
* @type {i18n.phonenumbers.PhoneMetadata}
* @private
*/
- this.defaultMetaData_ = this.currentMetaData_;
+ this.defaultMetadata_ = this.currentMetadata_;
};
+
+/**
+ * Character used when appropriate to separate a prefix, such as a long NDD or a
+ * country calling code, from the national number.
+ * @const
+ * @type {string}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_ = ' ';
+
+
+/**
+ * @const
+ * @type {i18n.phonenumbers.PhoneMetadata}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_ =
+ new i18n.phonenumbers.PhoneMetadata();
+i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_
+ .setInternationalPrefix('NA');
+
+
+/**
+ * A pattern that is used to match character classes in regular expressions.
+ * An example of a character class is [1-4].
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.CHARACTER_CLASS_PATTERN_ =
+ /\[([^\[\]])*\]/g;
+
+
+/**
+ * Any digit in a regular expression that actually denotes a digit. For
+ * example, in the regular expression 80[0-2]\d{6,10}, the first 2 digits
+ * (8 and 0) are standalone digits, but the rest are not.
+ * Two look-aheads are needed because the number following \\d could be a
+ * two-digit number, since the phone number can be as long as 15 digits.
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.STANDALONE_DIGIT_PATTERN_ =
+ /\d(?=[^,}][^,}])/g;
+
+
+/**
+ * A pattern that is used to determine if a numberFormat under availableFormats
+ * is eligible to be used by the AYTF. It is eligible when the format element
+ * under numberFormat contains groups of the dollar sign followed by a single
+ * digit, separated by valid phone number punctuation. This prevents invalid
+ * punctuation (such as the star sign in Israeli star numbers) getting into the
+ * output of the AYTF.
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.ELIGIBLE_FORMAT_PATTERN_ = new RegExp(
+ '^[' + i18n.phonenumbers.PhoneNumberUtil.VALID_PUNCTUATION + ']*' +
+ '(\\$\\d[' + i18n.phonenumbers.PhoneNumberUtil.VALID_PUNCTUATION + ']*)+$');
+
+
+/**
+ * A set of characters that, if found in a national prefix formatting rules, are
+ * an indicator to us that we should separate the national prefix from the
+ * number when formatting.
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.NATIONAL_PREFIX_SEPARATORS_PATTERN_ =
+ /[- ]/;
+
+
/**
- * @param {string} regionCode
+ * This is the minimum length of national number accrued that is required to
+ * trigger the formatter. The first element of the leadingDigitsPattern of
+ * each numberFormat contains a regular expression that matches up to this
+ * number of digits.
+ * @const
+ * @type {number}
* @private
*/
-i18n.phonenumbers.AsYouTypeFormatter.prototype.initializeCountrySpecificInfo_ =
- function(regionCode) {
+i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_ = 3;
+
+/**
+ * The metadata needed by this class is the same for all regions sharing the
+ * same country calling code. Therefore, we return the metadata for "main"
+ * region for this country calling code.
+ * @param {string} regionCode an ISO 3166-1 two-letter region code.
+ * @return {i18n.phonenumbers.PhoneMetadata} main metadata for this region.
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.getMetadataForRegion_ =
+ function(regionCode) {
+
+ /** @type {number} */
+ var countryCallingCode = this.phoneUtil_.getCountryCodeForRegion(regionCode);
+ /** @type {string} */
+ var mainCountry =
+ this.phoneUtil_.getRegionCodeForCountryCode(countryCallingCode);
/** @type {i18n.phonenumbers.PhoneMetadata} */
- this.currentMetaData_ = this.phoneUtil_.getMetadataForRegion(regionCode);
- /** @type {RegExp} */
- this.nationalPrefixForParsing_ = new RegExp('^(' + this.currentMetaData_
- .getNationalPrefixForParsing() + ')');
- /** @type {RegExp} */
- this.internationalPrefix_ = new RegExp('^(' + '\\+|' +
- this.currentMetaData_.getInternationalPrefix() + ')');
+ var metadata = this.phoneUtil_.getMetadataForRegion(mainCountry);
+ if (metadata != null) {
+ return metadata;
+ }
+ // Set to a default instance of the metadata. This allows us to function with
+ // an incorrect region code, even if formatting only works for numbers
+ // specified with '+'.
+ return i18n.phonenumbers.AsYouTypeFormatter.EMPTY_METADATA_;
};
+
/**
* @return {boolean} true if a new template is created as opposed to reusing the
* existing template.
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.maybeCreateNewTemplate_ =
- function() {
+ function() {
// When there are multiple available formats, the formatter uses the first
// format where a formatting template could be created.
}
if (this.createFormattingTemplate_(numberFormat)) {
this.currentFormattingPattern_ = pattern;
+ this.shouldAddSpaceAfterNationalPrefix_ =
+ i18n.phonenumbers.AsYouTypeFormatter.
+ NATIONAL_PREFIX_SEPARATORS_PATTERN_.test(
+ numberFormat.getNationalPrefixFormattingRule());
+ // With a new formatting template, the matched position using the old
+ // template needs to be reset.
+ this.lastMatchPosition_ = 0;
return true;
}
}
return false;
};
+
/**
- * @param {string} leadingThreeDigits
+ * @param {string} leadingDigits leading digits of entered number.
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.getAvailableFormats_ =
- function(leadingThreeDigits) {
+ function(leadingDigits) {
/** @type {Array.<i18n.phonenumbers.NumberFormat>} */
- var formatList = (this.isInternationalFormatting_ && this.currentMetaData_
- .intlNumberFormatCount() > 0) ? this.currentMetaData_
- .intlNumberFormatArray() : this.currentMetaData_.numberFormatArray();
- this.possibleFormats_ = formatList;
- this.narrowDownPossibleFormats_(leadingThreeDigits);
+ var formatList =
+ (this.isCompleteNumber_ &&
+ this.currentMetadata_.intlNumberFormatCount() > 0) ?
+ this.currentMetadata_.intlNumberFormatArray() :
+ this.currentMetadata_.numberFormatArray();
+ /** @type {number} */
+ var formatListLength = formatList.length;
+ for (var i = 0; i < formatListLength; ++i) {
+ /** @type {i18n.phonenumbers.NumberFormat} */
+ var format = formatList[i];
+ /** @type {boolean} */
+ var nationalPrefixIsUsedByCountry =
+ this.currentMetadata_.hasNationalPrefix();
+ if (!nationalPrefixIsUsedByCountry || this.isCompleteNumber_ ||
+ format.getNationalPrefixOptionalWhenFormatting() ||
+ this.phoneUtil_.formattingRuleHasFirstGroupOnly(
+ format.getNationalPrefixFormattingRuleOrDefault())) {
+ if (this.isFormatEligible_(format.getFormatOrDefault())) {
+ this.possibleFormats_.push(format);
+ }
+ }
+ }
+ this.narrowDownPossibleFormats_(leadingDigits);
};
+
+/**
+ * @param {string} format
+ * @return {boolean}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.isFormatEligible_ =
+ function(format) {
+ return i18n.phonenumbers.AsYouTypeFormatter.ELIGIBLE_FORMAT_PATTERN_
+ .test(format);
+};
+
+
/**
* @param {string} leadingDigits
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.narrowDownPossibleFormats_ =
- function(leadingDigits) {
+ function(leadingDigits) {
/** @type {Array.<i18n.phonenumbers.NumberFormat>} */
var possibleFormats = [];
/** @type {number} */
- var lengthOfLeadingDigits = leadingDigits.length;
- /** @type {number} */
var indexOfLeadingDigitsPattern =
- lengthOfLeadingDigits - this.MIN_LEADING_DIGITS_LENGTH_;
+ leadingDigits.length -
+ i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_;
/** @type {number} */
var possibleFormatsLength = this.possibleFormats_.length;
for (var i = 0; i < possibleFormatsLength; ++i) {
/** @type {i18n.phonenumbers.NumberFormat} */
var format = this.possibleFormats_[i];
- if (format.leadingDigitsPatternCount() > indexOfLeadingDigitsPattern) {
- /** @type {RegExp} */
- var leadingDigitsPattern = new RegExp('^(' +
- format.getLeadingDigitsPattern(indexOfLeadingDigitsPattern) + ')');
- if (leadingDigitsPattern.test(leadingDigits)) {
- possibleFormats.push(this.possibleFormats_[i]);
- }
- } else {
- // else the particular format has no more specific leadingDigitsPattern,
- // and it should be retained.
+ if (format.leadingDigitsPatternCount() == 0) {
+ // Keep everything that isn't restricted by leading digits.
+ possibleFormats.push(this.possibleFormats_[i]);
+ continue;
+ }
+ /** @type {number} */
+ var lastLeadingDigitsPattern = Math.min(
+ indexOfLeadingDigitsPattern, format.leadingDigitsPatternCount() - 1);
+ /** @type {string} */
+ var leadingDigitsPattern = /** @type {string} */
+ (format.getLeadingDigitsPattern(lastLeadingDigitsPattern));
+ if (leadingDigits.search(leadingDigitsPattern) == 0) {
possibleFormats.push(this.possibleFormats_[i]);
}
}
this.possibleFormats_ = possibleFormats;
};
+
/**
* @param {i18n.phonenumbers.NumberFormat} format
* @return {boolean}
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.createFormattingTemplate_ =
- function(format) {
+ function(format) {
/** @type {string} */
- var numberFormat = format.getFormatOrDefault();
- /** @type {string} */
var numberPattern = format.getPatternOrDefault();
// The formatter doesn't format numbers when numberPattern contains '|', e.g.
}
// Replace anything in the form of [..] with \d
- numberPattern = numberPattern.replace(this.CHARACTER_CLASS_PATTERN_, '\\d');
+ numberPattern = numberPattern.replace(
+ i18n.phonenumbers.AsYouTypeFormatter.CHARACTER_CLASS_PATTERN_, '\\d');
// Replace any standalone digit (not the one in d{}) with \d
- numberPattern = numberPattern.replace(this.STANDALONE_DIGIT_PATTERN_, '\\d');
+ numberPattern = numberPattern.replace(
+ i18n.phonenumbers.AsYouTypeFormatter.STANDALONE_DIGIT_PATTERN_, '\\d');
this.formattingTemplate_.clear();
- this.formattingTemplate_.append(this.getFormattingTemplate_(numberPattern,
- numberFormat));
- return true;
+ /** @type {string} */
+ var tempTemplate = this.getFormattingTemplate_(numberPattern,
+ format.getFormatOrDefault());
+ if (tempTemplate.length > 0) {
+ this.formattingTemplate_.append(tempTemplate);
+ return true;
+ }
+ return false;
};
+
/**
- * Gets a formatting template which could be used to efficiently format a
+ * Gets a formatting template which can be used to efficiently format a
* partial number where digits are added one by one.
*
* @param {string} numberPattern
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.getFormattingTemplate_ =
- function(numberPattern, numberFormat) {
+ function(numberPattern, numberFormat) {
// Creates a phone number consisting only of the digit 9 that matches the
// numberPattern by applying the pattern to the longestPhoneNumber string.
// this match will always succeed
/** @type {string} */
var aPhoneNumber = m[0];
+ // No formatting template can be created if the number of digits entered so
+ // far is longer than the maximum the current formatting rule can accommodate.
+ if (aPhoneNumber.length < this.nationalNumber_.getLength()) {
+ return '';
+ }
// Formats the number according to numberFormat
/** @type {string} */
var template = aPhoneNumber.replace(new RegExp(numberPattern, 'g'),
numberFormat);
- // Replaces each digit with character digitPlaceholder
- template = template.replace(new RegExp('9', 'g'), this.digitPlaceholder_);
+ // Replaces each digit with character DIGIT_PLACEHOLDER
+ template = template.replace(new RegExp('9', 'g'), this.DIGIT_PLACEHOLDER_);
return template;
};
+
/**
- * Clears the internal state of the formatter, so it could be reused.
+ * Clears the internal state of the formatter, so it can be reused.
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.clear = function() {
this.currentOutput_ = '';
this.lastMatchPosition_ = 0;
this.currentFormattingPattern_ = '';
this.prefixBeforeNationalNumber_.clear();
+ this.extractedNationalPrefix_ = '';
this.nationalNumber_.clear();
this.ableToFormat_ = true;
+ this.inputHasFormatting_ = false;
this.positionToRemember_ = 0;
this.originalPosition_ = 0;
- this.isInternationalFormatting_ = false;
- this.isExpectingCountryCode_ = false;
+ this.isCompleteNumber_ = false;
+ this.isExpectingCountryCallingCode_ = false;
this.possibleFormats_ = [];
- if (this.currentMetaData_ != this.defaultMetaData_) {
- this.initializeCountrySpecificInfo_(this.defaultCountry_);
+ this.shouldAddSpaceAfterNationalPrefix_ = false;
+ if (this.currentMetadata_ != this.defaultMetadata_) {
+ this.currentMetadata_ = this.getMetadataForRegion_(this.defaultCountry_);
}
};
+
/**
* Formats a phone number on-the-fly as each digit is entered.
*
* @param {string} nextChar the most recently entered digit of a phone number.
- * Formatting characters are allowed, but they are removed from the result.
+ * Formatting characters are allowed, but as soon as they are encountered
+ * this method formats the number as entered and not 'as you type' anymore.
+ * Full width digits and Arabic-indic digits are allowed, and will be shown
+ * as they are.
* @return {string} the partially formatted phone number.
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigit = function(nextChar) {
return this.currentOutput_;
};
+
/**
- * Same as inputDigit, but remembers the position where nextChar is inserted, so
- * that it could be retrieved later by using getRememberedPosition(). The
- * remembered position will be automatically adjusted if additional formatting
- * characters are later inserted/removed in front of nextChar.
+ * Same as {@link #inputDigit}, but remembers the position where
+ * {@code nextChar} is inserted, so that it can be retrieved later by using
+ * {@link #getRememberedPosition}. The remembered position will be automatically
+ * adjusted if additional formatting characters are later inserted/removed in
+ * front of {@code nextChar}.
*
* @param {string} nextChar
* @return {string}
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigitAndRememberPosition =
- function(nextChar) {
+ function(nextChar) {
this.currentOutput_ =
this.inputDigitWithOptionToRememberPosition_(nextChar, true);
return this.currentOutput_;
};
+
/**
* @param {string} nextChar
* @param {boolean} rememberPosition
this.originalPosition_ = this.accruedInput_.getLength();
}
// We do formatting on-the-fly only when each character entered is either a
- // plus sign or a digit.
- if (!i18n.phonenumbers.PhoneNumberUtil.VALID_START_CHAR_PATTERN
- .test(nextChar)) {
+ // digit, or a plus sign (accepted at the start of the number only).
+ if (!this.isDigitOrLeadingPlusSign_(nextChar)) {
this.ableToFormat_ = false;
+ this.inputHasFormatting_ = true;
+ } else {
+ nextChar = this.normalizeAndAccrueDigitsAndPlusSign_(nextChar,
+ rememberPosition);
}
if (!this.ableToFormat_) {
+ // When we are unable to format because of reasons other than that
+ // formatting chars have been entered, it can be due to really long IDDs or
+ // NDDs. If that is the case, we might be able to do formatting again after
+ // extracting them.
+ if (this.inputHasFormatting_) {
+ return this.accruedInput_.toString();
+ } else if (this.attemptToExtractIdd_()) {
+ if (this.attemptToExtractCountryCallingCode_()) {
+ return this.attemptToChoosePatternWithPrefixExtracted_();
+ }
+ } else if (this.ableToExtractLongerNdd_()) {
+ // Add an additional space to separate long NDD and national significant
+ // number for readability. We don't set shouldAddSpaceAfterNationalPrefix_
+ // to true, since we don't want this to change later when we choose
+ // formatting templates.
+ this.prefixBeforeNationalNumber_.append(
+ i18n.phonenumbers.AsYouTypeFormatter.
+ SEPARATOR_BEFORE_NATIONAL_NUMBER_);
+ return this.attemptToChoosePatternWithPrefixExtracted_();
+ }
return this.accruedInput_.toString();
}
- nextChar = this.normalizeAndAccrueDigitsAndPlusSign_(nextChar,
- rememberPosition);
-
// We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH
// digits (the plus sign is counted as a digit as well for this purpose) have
// been entered.
switch (this.accruedInputWithoutFormatting_.getLength()) {
- case 0: // this is the case where the first few inputs are neither digits nor
- // the plus sign.
- case 1:
- case 2:
- return this.accruedInput_.toString();
- case 3:
- if (this.attemptToExtractIdd_()) {
- this.isExpectingCountryCode_ = true;
- } else {
- // No IDD or plus sign is found, must be entering in national format.
- this.removeNationalPrefixFromNationalNumber_();
- return this.attemptToChooseFormattingPattern_();
- }
- case 4:
- case 5:
- if (this.isExpectingCountryCode_) {
- if (this.attemptToExtractCountryCode_()) {
- this.isExpectingCountryCode_ = false;
- }
- return this.prefixBeforeNationalNumber_.toString() +
- this.nationalNumber_.toString();
- }
- // We make a last attempt to extract a country code at the 6th digit because
- // the maximum length of IDD and country code are both 3.
- case 6:
- if (this.isExpectingCountryCode_ && !this.attemptToExtractCountryCode_()) {
- this.ableToFormat_ = false;
+ case 0:
+ case 1:
+ case 2:
return this.accruedInput_.toString();
- }
- default:
- if (this.possibleFormats_.length > 0) {
- // The formatting pattern is already chosen.
- /** @type {string} */
- var tempNationalNumber = this.inputDigitHelper_(nextChar);
- // See if the accrued digits can be formatted properly already. If not,
- // use the results from inputDigitHelper, which does formatting based on
- // the formatting pattern chosen.
- /** @type {string} */
- var formattedNumber = this.attemptToFormatAccruedDigits_();
- if (formattedNumber.length > 0) {
- return formattedNumber;
+ case 3:
+ if (this.attemptToExtractIdd_()) {
+ this.isExpectingCountryCallingCode_ = true;
+ } else {
+ // No IDD or plus sign is found, might be entering in national format.
+ this.extractedNationalPrefix_ =
+ this.removeNationalPrefixFromNationalNumber_();
+ return this.attemptToChooseFormattingPattern_();
}
- this.narrowDownPossibleFormats_(this.nationalNumber_.toString());
- if (this.maybeCreateNewTemplate_()) {
- return this.inputAccruedNationalNumber_();
+ default:
+ if (this.isExpectingCountryCallingCode_) {
+ if (this.attemptToExtractCountryCallingCode_()) {
+ this.isExpectingCountryCallingCode_ = false;
+ }
+ return this.prefixBeforeNationalNumber_.toString() +
+ this.nationalNumber_.toString();
+ }
+ if (this.possibleFormats_.length > 0) {
+ // The formatting patterns are already chosen.
+ /** @type {string} */
+ var tempNationalNumber = this.inputDigitHelper_(nextChar);
+ // See if the accrued digits can be formatted properly already. If not,
+ // use the results from inputDigitHelper, which does formatting based on
+ // the formatting pattern chosen.
+ /** @type {string} */
+ var formattedNumber = this.attemptToFormatAccruedDigits_();
+ if (formattedNumber.length > 0) {
+ return formattedNumber;
+ }
+ this.narrowDownPossibleFormats_(this.nationalNumber_.toString());
+ if (this.maybeCreateNewTemplate_()) {
+ return this.inputAccruedNationalNumber_();
+ }
+ return this.ableToFormat_ ?
+ this.appendNationalNumber_(tempNationalNumber) :
+ this.accruedInput_.toString();
+ } else {
+ return this.attemptToChooseFormattingPattern_();
}
- return this.ableToFormat_ ?
- this.prefixBeforeNationalNumber_.toString() + tempNationalNumber :
- tempNationalNumber;
- } else {
- return this.attemptToChooseFormattingPattern_();
- }
}
};
+
/**
* @return {string}
* @private
*/
+i18n.phonenumbers.AsYouTypeFormatter.prototype.
+ attemptToChoosePatternWithPrefixExtracted_ = function() {
+
+ this.ableToFormat_ = true;
+ this.isExpectingCountryCallingCode_ = false;
+ this.possibleFormats_ = [];
+ this.lastMatchPosition_ = 0;
+ this.formattingTemplate_.clear();
+ this.currentFormattingPattern_ = '';
+ return this.attemptToChooseFormattingPattern_();
+};
+
+
+/**
+ * @return {string}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.getExtractedNationalPrefix_ =
+ function() {
+ return this.extractedNationalPrefix_;
+};
+
+
+/**
+ * Some national prefixes are a substring of others. If extracting the shorter
+ * NDD doesn't result in a number we can format, we try to see if we can extract
+ * a longer version here.
+ * @return {boolean}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.ableToExtractLongerNdd_ =
+ function() {
+ if (this.extractedNationalPrefix_.length > 0) {
+ // Put the extracted NDD back to the national number before attempting to
+ // extract a new NDD.
+ /** @type {string} */
+ var nationalNumberStr = this.nationalNumber_.toString();
+ this.nationalNumber_.clear();
+ this.nationalNumber_.append(this.extractedNationalPrefix_);
+ this.nationalNumber_.append(nationalNumberStr);
+ // Remove the previously extracted NDD from prefixBeforeNationalNumber. We
+ // cannot simply set it to empty string because people sometimes incorrectly
+ // enter national prefix after the country code, e.g. +44 (0)20-1234-5678.
+ /** @type {string} */
+ var prefixBeforeNationalNumberStr =
+ this.prefixBeforeNationalNumber_.toString();
+ /** @type {number} */
+ var indexOfPreviousNdd = prefixBeforeNationalNumberStr.lastIndexOf(
+ this.extractedNationalPrefix_);
+ this.prefixBeforeNationalNumber_.clear();
+ this.prefixBeforeNationalNumber_.append(
+ prefixBeforeNationalNumberStr.substring(0, indexOfPreviousNdd));
+ }
+ return this.extractedNationalPrefix_ !=
+ this.removeNationalPrefixFromNationalNumber_();
+};
+
+
+/**
+ * @param {string} nextChar
+ * @return {boolean}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.isDigitOrLeadingPlusSign_ =
+ function(nextChar) {
+ return i18n.phonenumbers.PhoneNumberUtil.CAPTURING_DIGIT_PATTERN
+ .test(nextChar) ||
+ (this.accruedInput_.getLength() == 1 &&
+ i18n.phonenumbers.PhoneNumberUtil.PLUS_CHARS_PATTERN.test(nextChar));
+};
+
+
+/**
+ * Check to see if there is an exact pattern match for these digits. If so, we
+ * should use this instead of any other formatting template whose
+ * leadingDigitsPattern also matches the input.
+ * @return {string}
+ * @private
+ */
i18n.phonenumbers.AsYouTypeFormatter.prototype.attemptToFormatAccruedDigits_ =
- function() {
+ function() {
/** @type {string} */
var nationalNumber = this.nationalNumber_.toString();
var possibleFormatsLength = this.possibleFormats_.length;
for (var i = 0; i < possibleFormatsLength; ++i) {
/** @type {i18n.phonenumbers.NumberFormat} */
- var numFormat = this.possibleFormats_[i];
+ var numberFormat = this.possibleFormats_[i];
/** @type {string} */
- var pattern = numFormat.getPatternOrDefault();
+ var pattern = numberFormat.getPatternOrDefault();
/** @type {RegExp} */
- var patternRegExp = new RegExp('^(' + pattern + ')$');
+ var patternRegExp = new RegExp('^(?:' + pattern + ')$');
if (patternRegExp.test(nationalNumber)) {
+ this.shouldAddSpaceAfterNationalPrefix_ =
+ i18n.phonenumbers.AsYouTypeFormatter.
+ NATIONAL_PREFIX_SEPARATORS_PATTERN_.test(
+ numberFormat.getNationalPrefixFormattingRule());
/** @type {string} */
var formattedNumber = nationalNumber.replace(new RegExp(pattern, 'g'),
- numFormat.getFormat());
- return this.prefixBeforeNationalNumber_.toString() + formattedNumber;
+ numberFormat.getFormat());
+ return this.appendNationalNumber_(formattedNumber);
}
}
return '';
};
+
+/**
+ * Combines the national number with any prefix (IDD/+ and country code or
+ * national prefix) that was collected. A space will be inserted between them if
+ * the current formatting template indicates this to be suitable.
+ * @param {string} nationalNumber The number to be appended.
+ * @return {string} The combined number.
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.appendNationalNumber_ =
+ function(nationalNumber) {
+ /** @type {number} */
+ var prefixBeforeNationalNumberLength =
+ this.prefixBeforeNationalNumber_.getLength();
+ if (this.shouldAddSpaceAfterNationalPrefix_ &&
+ prefixBeforeNationalNumberLength > 0 &&
+ this.prefixBeforeNationalNumber_.toString().charAt(
+ prefixBeforeNationalNumberLength - 1) !=
+ i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_) {
+ // We want to add a space after the national prefix if the national prefix
+ // formatting rule indicates that this would normally be done, with the
+ // exception of the case where we already appended a space because the NDD
+ // was surprisingly long.
+ return this.prefixBeforeNationalNumber_ +
+ i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_ +
+ nationalNumber;
+ } else {
+ return this.prefixBeforeNationalNumber_ + nationalNumber;
+ }
+};
+
+
/**
* Returns the current position in the partially formatted phone number of the
* character which was previously passed in as the parameter of
- * inputDigitAndRememberPosition().
+ * {@link #inputDigitAndRememberPosition}.
*
* @return {number}
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.getRememberedPosition =
- function() {
+ function() {
if (!this.ableToFormat_) {
return this.originalPosition_;
this.accruedInputWithoutFormatting_.toString();
/** @type {string} */
var currentOutput = this.currentOutput_.toString();
- while (accruedInputIndex < this.positionToRemember_) {
+ while (accruedInputIndex < this.positionToRemember_ &&
+ currentOutputIndex < currentOutput.length) {
if (accruedInputWithoutFormatting.charAt(accruedInputIndex) ==
currentOutput.charAt(currentOutputIndex)) {
accruedInputIndex++;
- currentOutputIndex++;
- } else {
- currentOutputIndex++;
}
+ currentOutputIndex++;
}
return currentOutputIndex;
};
+
/**
* Attempts to set the formatting template and returns a string which contains
* the formatted version of the digits entered so far.
/** @type {string} */
var nationalNumber = this.nationalNumber_.toString();
- // We start to attempt to format only when as least MIN_LEADING_DIGITS_LENGTH
+ // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH
// digits of national number (excluding national prefix) have been entered.
- if (nationalNumber.length >= this.MIN_LEADING_DIGITS_LENGTH_) {
- this.getAvailableFormats_(
- nationalNumber.substring(0, this.MIN_LEADING_DIGITS_LENGTH_));
- this.maybeCreateNewTemplate_();
- return this.inputAccruedNationalNumber_();
+ if (nationalNumber.length >=
+ i18n.phonenumbers.AsYouTypeFormatter.MIN_LEADING_DIGITS_LENGTH_) {
+ this.getAvailableFormats_(nationalNumber);
+ // See if the accrued digits can be formatted properly already.
+ var formattedNumber = this.attemptToFormatAccruedDigits_();
+ if (formattedNumber.length > 0) {
+ return formattedNumber;
+ }
+ return this.maybeCreateNewTemplate_() ?
+ this.inputAccruedNationalNumber_() : this.accruedInput_.toString();
} else {
- return this.prefixBeforeNationalNumber_.toString() + nationalNumber;
+ return this.appendNationalNumber_(nationalNumber);
}
};
+
/**
* Invokes inputDigitHelper on each digit of the national number accrued, and
* returns a formatted string in the end.
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.inputAccruedNationalNumber_ =
- function() {
+ function() {
/** @type {string} */
var nationalNumber = this.nationalNumber_.toString();
this.inputDigitHelper_(nationalNumber.charAt(i));
}
return this.ableToFormat_ ?
- this.prefixBeforeNationalNumber_.toString() + tempNationalNumber :
- tempNationalNumber;
+ this.appendNationalNumber_(tempNationalNumber) :
+ this.accruedInput_.toString();
} else {
return this.prefixBeforeNationalNumber_.toString();
}
};
+
/**
+ * @return {boolean} true if the current country is a NANPA country and the
+ * national number begins with the national prefix.
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.
- removeNationalPrefixFromNationalNumber_ = function() {
+ isNanpaNumberWithNationalPrefix_ = function() {
+ // For NANPA numbers beginning with 1[2-9], treat the 1 as the national
+ // prefix. The reason is that national significant numbers in NANPA always
+ // start with [2-9] after the national prefix. Numbers beginning with 1[01]
+ // can only be short/emergency numbers, which don't need the national prefix.
+ if (this.currentMetadata_.getCountryCode() != 1) {
+ return false;
+ }
+ /** @type {string} */
+ var nationalNumber = this.nationalNumber_.toString();
+ return (nationalNumber.charAt(0) == '1') &&
+ (nationalNumber.charAt(1) != '0') &&
+ (nationalNumber.charAt(1) != '1');
+};
+
+
+/**
+ * Returns the national prefix extracted, or an empty string if it is not
+ * present.
+ * @return {string}
+ * @private
+ */
+i18n.phonenumbers.AsYouTypeFormatter.prototype.
+ removeNationalPrefixFromNationalNumber_ = function() {
/** @type {string} */
var nationalNumber = this.nationalNumber_.toString();
/** @type {number} */
var startOfNationalNumber = 0;
- if (this.currentMetaData_.getCountryCode() == 1 &&
- nationalNumber.charAt(0) == '1') {
+ if (this.isNanpaNumberWithNationalPrefix_()) {
startOfNationalNumber = 1;
- this.prefixBeforeNationalNumber_.append('1 ');
- this.isInternationalFormatting_ = true;
- } else if (this.currentMetaData_.hasNationalPrefix()) {
+ this.prefixBeforeNationalNumber_.append('1').append(
+ i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_);
+ this.isCompleteNumber_ = true;
+ } else if (this.currentMetadata_.hasNationalPrefixForParsing()) {
+ /** @type {RegExp} */
+ var nationalPrefixForParsing = new RegExp(
+ '^(?:' + this.currentMetadata_.getNationalPrefixForParsing() + ')');
/** @type {Array.<string>} */
- var m = nationalNumber.match(this.nationalPrefixForParsing_);
+ var m = nationalNumber.match(nationalPrefixForParsing);
+ // Since some national prefix patterns are entirely optional, check that a
+ // national prefix could actually be extracted.
if (m != null && m[0] != null && m[0].length > 0) {
// When the national prefix is detected, we use international formatting
// rules instead of national ones, because national formatting rules could
// contain local formatting rules for numbers entered without area code.
- this.isInternationalFormatting_ = true;
+ this.isCompleteNumber_ = true;
startOfNationalNumber = m[0].length;
this.prefixBeforeNationalNumber_.append(nationalNumber.substring(0,
startOfNationalNumber));
}
this.nationalNumber_.clear();
this.nationalNumber_.append(nationalNumber.substring(startOfNationalNumber));
+ return nationalNumber.substring(0, startOfNationalNumber);
};
+
/**
* Extracts IDD and plus sign to prefixBeforeNationalNumber when they are
* available, and places the remaining input into nationalNumber.
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.attemptToExtractIdd_ =
- function() {
+ function() {
/** @type {string} */
var accruedInputWithoutFormatting =
this.accruedInputWithoutFormatting_.toString();
+ /** @type {RegExp} */
+ var internationalPrefix = new RegExp(
+ '^(?:' + '\\' + i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN + '|' +
+ this.currentMetadata_.getInternationalPrefix() + ')');
/** @type {Array.<string>} */
- var m = accruedInputWithoutFormatting.match(this.internationalPrefix_);
+ var m = accruedInputWithoutFormatting.match(internationalPrefix);
if (m != null && m[0] != null && m[0].length > 0) {
- this.isInternationalFormatting_ = true;
+ this.isCompleteNumber_ = true;
/** @type {number} */
- var startOfCountryCode = m[0].length;
+ var startOfCountryCallingCode = m[0].length;
this.nationalNumber_.clear();
this.nationalNumber_.append(
- accruedInputWithoutFormatting.substring(startOfCountryCode));
+ accruedInputWithoutFormatting.substring(startOfCountryCallingCode));
+ this.prefixBeforeNationalNumber_.clear();
this.prefixBeforeNationalNumber_.append(
- accruedInputWithoutFormatting.substring(0, startOfCountryCode));
+ accruedInputWithoutFormatting.substring(0, startOfCountryCallingCode));
if (accruedInputWithoutFormatting.charAt(0) !=
i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN) {
- this.prefixBeforeNationalNumber_.append(' ');
+ this.prefixBeforeNationalNumber_.append(
+ i18n.phonenumbers.AsYouTypeFormatter.
+ SEPARATOR_BEFORE_NATIONAL_NUMBER_);
}
return true;
}
return false;
};
+
/**
- * Extracts country code from the beginning of nationalNumber to
+ * Extracts the country calling code from the beginning of nationalNumber to
* prefixBeforeNationalNumber when they are available, and places the remaining
* input into nationalNumber.
*
- * @return {boolean} true when a valid country code can be found.
+ * @return {boolean} true when a valid country calling code can be found.
* @private
*/
-i18n.phonenumbers.AsYouTypeFormatter.prototype.attemptToExtractCountryCode_ =
- function() {
+i18n.phonenumbers.AsYouTypeFormatter.prototype.
+ attemptToExtractCountryCallingCode_ = function() {
- if (this.nationalNumber_.getLength() == 0) {
- return false;
- }
- /** @type {!goog.string.StringBuffer} */
- var numberWithoutCountryCode = new goog.string.StringBuffer();
- /** @type {number} */
- var countryCode = this.phoneUtil_.extractCountryCode(
- this.nationalNumber_, numberWithoutCountryCode);
- if (countryCode == 0) {
- return false;
- } else {
- this.nationalNumber_.clear();
- this.nationalNumber_.append(numberWithoutCountryCode.toString());
- /** @type {string} */
- var newRegionCode =
- this.phoneUtil_.getRegionCodeForCountryCode(countryCode);
- if (newRegionCode != this.defaultCountry_) {
- this.initializeCountrySpecificInfo_(newRegionCode);
- }
- /** @type {string} */
- var countryCodeString = '' + countryCode;
- this.prefixBeforeNationalNumber_.append(countryCodeString).append(' ');
- }
+ if (this.nationalNumber_.getLength() == 0) {
+ return false;
+ }
+ /** @type {!goog.string.StringBuffer} */
+ var numberWithoutCountryCallingCode = new goog.string.StringBuffer();
+ /** @type {number} */
+ var countryCode = this.phoneUtil_.extractCountryCode(
+ this.nationalNumber_, numberWithoutCountryCallingCode);
+ if (countryCode == 0) {
+ return false;
+ }
+ this.nationalNumber_.clear();
+ this.nationalNumber_.append(numberWithoutCountryCallingCode.toString());
+ /** @type {string} */
+ var newRegionCode = this.phoneUtil_.getRegionCodeForCountryCode(countryCode);
+ if (i18n.phonenumbers.PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY ==
+ newRegionCode) {
+ this.currentMetadata_ =
+ this.phoneUtil_.getMetadataForNonGeographicalRegion(countryCode);
+ } else if (newRegionCode != this.defaultCountry_) {
+ this.currentMetadata_ = this.getMetadataForRegion_(newRegionCode);
+ }
+ /** @type {string} */
+ var countryCodeString = '' + countryCode;
+ this.prefixBeforeNationalNumber_.append(countryCodeString).append(
+ i18n.phonenumbers.AsYouTypeFormatter.SEPARATOR_BEFORE_NATIONAL_NUMBER_);
+ // When we have successfully extracted the IDD, the previously extracted NDD
+ // should be cleared because it is no longer valid.
+ this.extractedNationalPrefix_ = '';
return true;
};
+
/**
* Accrues digits and the plus sign to accruedInputWithoutFormatting for later
* use. If nextChar contains a digit in non-ASCII format (e.g. the full-width
* version of digits), it is first normalized to the ASCII version. The return
* value is nextChar itself, or its normalized version, if nextChar is a digit
- * in non-ASCII format.
+ * in non-ASCII format. This method assumes its input is either a digit or the
+ * plus sign.
*
* @param {string} nextChar
* @param {boolean} rememberPosition
normalizeAndAccrueDigitsAndPlusSign_ = function(nextChar,
rememberPosition) {
+ /** @type {string} */
+ var normalizedChar;
if (nextChar == i18n.phonenumbers.PhoneNumberUtil.PLUS_SIGN) {
+ normalizedChar = nextChar;
this.accruedInputWithoutFormatting_.append(nextChar);
- }
- if (nextChar in i18n.phonenumbers.PhoneNumberUtil.DIGIT_MAPPINGS) {
- nextChar = i18n.phonenumbers.PhoneNumberUtil.DIGIT_MAPPINGS[nextChar];
- this.accruedInputWithoutFormatting_.append(nextChar);
- this.nationalNumber_.append(nextChar);
+ } else {
+ normalizedChar = i18n.phonenumbers.PhoneNumberUtil.DIGIT_MAPPINGS[nextChar];
+ this.accruedInputWithoutFormatting_.append(normalizedChar);
+ this.nationalNumber_.append(normalizedChar);
}
if (rememberPosition) {
this.positionToRemember_ = this.accruedInputWithoutFormatting_.getLength();
}
- return nextChar;
+ return normalizedChar;
};
+
/**
* @param {string} nextChar
* @return {string}
* @private
*/
i18n.phonenumbers.AsYouTypeFormatter.prototype.inputDigitHelper_ =
- function(nextChar) {
+ function(nextChar) {
+ // Note that formattingTemplate is not guaranteed to have a value, it could be
+ // empty, e.g. when the next digit is entered after extracting an IDD or NDD.
/** @type {string} */
var formattingTemplate = this.formattingTemplate_.toString();
if (formattingTemplate.substring(this.lastMatchPosition_)
- .search(this.digitPattern_) >= 0) {
+ .search(this.DIGIT_PATTERN_) >= 0) {
/** @type {number} */
- var digitPatternStart = formattingTemplate.search(this.digitPattern_);
+ var digitPatternStart = formattingTemplate.search(this.DIGIT_PATTERN_);
/** @type {string} */
- var tempTemplate = formattingTemplate.replace(this.digitPattern_, nextChar);
+ var tempTemplate =
+ formattingTemplate.replace(this.DIGIT_PATTERN_, nextChar);
this.formattingTemplate_.clear();
this.formattingTemplate_.append(tempTemplate);
this.lastMatchPosition_ = digitPatternStart;
return tempTemplate.substring(0, this.lastMatchPosition_ + 1);
} else {
- // More digits are entered than we could handle.
- this.ableToFormat_ = false;
+ if (this.possibleFormats_.length == 1) {
+ // More digits are entered than we could handle, and there are no other
+ // valid patterns to try.
+ this.ableToFormat_ = false;
+ } // else, we just reset the formatting pattern.
+ this.currentFormattingPattern_ = '';
return this.accruedInput_.toString();
}
};