Home | History | Annotate | Download | only in phonenumbers
      1 /*
      2  * Copyright (C) 2009 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.i18n.phonenumbers;
     18 
     19 import com.android.i18n.phonenumbers.Phonemetadata.NumberFormat;
     20 import com.android.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
     21 
     22 import java.util.ArrayList;
     23 import java.util.Iterator;
     24 import java.util.List;
     25 import java.util.regex.Matcher;
     26 import java.util.regex.Pattern;
     27 
     28 /**
     29  * A formatter which formats phone numbers as they are entered.
     30  *
     31  * <p>An AsYouTypeFormatter can be created by invoking
     32  * {@link PhoneNumberUtil#getAsYouTypeFormatter}. After that, digits can be added by invoking
     33  * {@link #inputDigit} on the formatter instance, and the partially formatted phone number will be
     34  * returned each time a digit is added. {@link #clear} can be invoked before formatting a new
     35  * number.
     36  *
     37  * <p>See the unittests for more details on how the formatter is to be used.
     38  *
     39  * @author Shaopeng Jia
     40  */
     41 public class AsYouTypeFormatter {
     42   private String currentOutput = "";
     43   private StringBuilder formattingTemplate = new StringBuilder();
     44   // The pattern from numberFormat that is currently used to create formattingTemplate.
     45   private String currentFormattingPattern = "";
     46   private StringBuilder accruedInput = new StringBuilder();
     47   private StringBuilder accruedInputWithoutFormatting = new StringBuilder();
     48   private boolean ableToFormat = true;
     49   private boolean isInternationalFormatting = false;
     50   private boolean isExpectingCountryCallingCode = false;
     51   private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
     52   private String defaultCountry;
     53 
     54   private static final PhoneMetadata EMPTY_METADATA =
     55       new PhoneMetadata().setInternationalPrefix("NA");
     56   private PhoneMetadata defaultMetaData;
     57   private PhoneMetadata currentMetaData;
     58 
     59   // A pattern that is used to match character classes in regular expressions. An example of a
     60   // character class is [1-4].
     61   private static final Pattern CHARACTER_CLASS_PATTERN = Pattern.compile("\\[([^\\[\\]])*\\]");
     62   // Any digit in a regular expression that actually denotes a digit. For example, in the regular
     63   // expression 80[0-2]\d{6,10}, the first 2 digits (8 and 0) are standalone digits, but the rest
     64   // are not.
     65   // Two look-aheads are needed because the number following \\d could be a two-digit number, since
     66   // the phone number can be as long as 15 digits.
     67   private static final Pattern STANDALONE_DIGIT_PATTERN = Pattern.compile("\\d(?=[^,}][^,}])");
     68 
     69   // A pattern that is used to determine if a numberFormat under availableFormats is eligible to be
     70   // used by the AYTF. It is eligible when the format element under numberFormat contains groups of
     71   // the dollar sign followed by a single digit, separated by valid phone number punctuation. This
     72   // prevents invalid punctuation (such as the star sign in Israeli star numbers) getting into the
     73   // output of the AYTF.
     74   private static final Pattern ELIGIBLE_FORMAT_PATTERN =
     75       Pattern.compile("[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*" +
     76           "(\\$\\d" + "[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*)+");
     77 
     78   // This is the minimum length of national number accrued that is required to trigger the
     79   // formatter. The first element of the leadingDigitsPattern of each numberFormat contains a
     80   // regular expression that matches up to this number of digits.
     81   private static final int MIN_LEADING_DIGITS_LENGTH = 3;
     82 
     83   // The digits that have not been entered yet will be represented by a \u2008, the punctuation
     84   // space.
     85   private String digitPlaceholder = "\u2008";
     86   private Pattern digitPattern = Pattern.compile(digitPlaceholder);
     87   private int lastMatchPosition = 0;
     88   // The position of a digit upon which inputDigitAndRememberPosition is most recently invoked, as
     89   // found in the original sequence of characters the user entered.
     90   private int originalPosition = 0;
     91   // The position of a digit upon which inputDigitAndRememberPosition is most recently invoked, as
     92   // found in accruedInputWithoutFormatting.
     93   private int positionToRemember = 0;
     94   private StringBuilder prefixBeforeNationalNumber = new StringBuilder();
     95   private StringBuilder nationalNumber = new StringBuilder();
     96   private List<NumberFormat> possibleFormats = new ArrayList<NumberFormat>();
     97 
     98     // A cache for frequently used country-specific regular expressions.
     99   private RegexCache regexCache = new RegexCache(64);
    100 
    101   /**
    102    * Constructs an as-you-type formatter. Should be obtained from {@link
    103    * PhoneNumberUtil#getAsYouTypeFormatter}.
    104    *
    105    * @param regionCode  the country/region where the phone number is being entered
    106    */
    107   AsYouTypeFormatter(String regionCode) {
    108     defaultCountry = regionCode;
    109     currentMetaData = getMetadataForRegion(defaultCountry);
    110     defaultMetaData = currentMetaData;
    111   }
    112 
    113   // The metadata needed by this class is the same for all regions sharing the same country calling
    114   // code. Therefore, we return the metadata for "main" region for this country calling code.
    115   private PhoneMetadata getMetadataForRegion(String regionCode) {
    116     int countryCallingCode = phoneUtil.getCountryCodeForRegion(regionCode);
    117     String mainCountry = phoneUtil.getRegionCodeForCountryCode(countryCallingCode);
    118     PhoneMetadata metadata = phoneUtil.getMetadataForRegion(mainCountry);
    119     if (metadata != null) {
    120       return metadata;
    121     }
    122     // Set to a default instance of the metadata. This allows us to function with an incorrect
    123     // region code, even if formatting only works for numbers specified with "+".
    124     return EMPTY_METADATA;
    125   }
    126 
    127   // Returns true if a new template is created as opposed to reusing the existing template.
    128   private boolean maybeCreateNewTemplate() {
    129     // When there are multiple available formats, the formatter uses the first format where a
    130     // formatting template could be created.
    131     Iterator<NumberFormat> it = possibleFormats.iterator();
    132     while (it.hasNext()) {
    133       NumberFormat numberFormat = it.next();
    134       String pattern = numberFormat.getPattern();
    135       if (currentFormattingPattern.equals(pattern)) {
    136         return false;
    137       }
    138       if (createFormattingTemplate(numberFormat)) {
    139         currentFormattingPattern = pattern;
    140         return true;
    141       } else {  // Remove the current number format from possibleFormats.
    142         it.remove();
    143       }
    144     }
    145     ableToFormat = false;
    146     return false;
    147   }
    148 
    149   private void getAvailableFormats(String leadingThreeDigits) {
    150     List<NumberFormat> formatList =
    151         (isInternationalFormatting && currentMetaData.intlNumberFormatSize() > 0)
    152         ? currentMetaData.intlNumberFormats()
    153         : currentMetaData.numberFormats();
    154     for (NumberFormat format : formatList) {
    155       if (isFormatEligible(format.getFormat())) {
    156         possibleFormats.add(format);
    157       }
    158     }
    159     narrowDownPossibleFormats(leadingThreeDigits);
    160   }
    161 
    162   private boolean isFormatEligible(String format) {
    163     return ELIGIBLE_FORMAT_PATTERN.matcher(format).matches();
    164   }
    165 
    166   private void narrowDownPossibleFormats(String leadingDigits) {
    167     int indexOfLeadingDigitsPattern = leadingDigits.length() - MIN_LEADING_DIGITS_LENGTH;
    168     Iterator<NumberFormat> it = possibleFormats.iterator();
    169     while (it.hasNext()) {
    170       NumberFormat format = it.next();
    171       if (format.leadingDigitsPatternSize() > indexOfLeadingDigitsPattern) {
    172         Pattern leadingDigitsPattern =
    173             regexCache.getPatternForRegex(
    174                 format.getLeadingDigitsPattern(indexOfLeadingDigitsPattern));
    175         Matcher m = leadingDigitsPattern.matcher(leadingDigits);
    176         if (!m.lookingAt()) {
    177           it.remove();
    178         }
    179       } // else the particular format has no more specific leadingDigitsPattern, and it should be
    180         // retained.
    181     }
    182   }
    183 
    184   private boolean createFormattingTemplate(NumberFormat format) {
    185     String numberPattern = format.getPattern();
    186 
    187     // The formatter doesn't format numbers when numberPattern contains "|", e.g.
    188     // (20|3)\d{4}. In those cases we quickly return.
    189     if (numberPattern.indexOf('|') != -1) {
    190       return false;
    191     }
    192 
    193     // Replace anything in the form of [..] with \d
    194     numberPattern = CHARACTER_CLASS_PATTERN.matcher(numberPattern).replaceAll("\\\\d");
    195 
    196     // Replace any standalone digit (not the one in d{}) with \d
    197     numberPattern = STANDALONE_DIGIT_PATTERN.matcher(numberPattern).replaceAll("\\\\d");
    198     formattingTemplate.setLength(0);
    199     String tempTemplate = getFormattingTemplate(numberPattern, format.getFormat());
    200     if (tempTemplate.length() > 0) {
    201       formattingTemplate.append(tempTemplate);
    202       return true;
    203     }
    204     return false;
    205   }
    206 
    207   // Gets a formatting template which can be used to efficiently format a partial number where
    208   // digits are added one by one.
    209   private String getFormattingTemplate(String numberPattern, String numberFormat) {
    210     // Creates a phone number consisting only of the digit 9 that matches the
    211     // numberPattern by applying the pattern to the longestPhoneNumber string.
    212     String longestPhoneNumber = "999999999999999";
    213     Matcher m = regexCache.getPatternForRegex(numberPattern).matcher(longestPhoneNumber);
    214     m.find();  // this will always succeed
    215     String aPhoneNumber = m.group();
    216     // No formatting template can be created if the number of digits entered so far is longer than
    217     // the maximum the current formatting rule can accommodate.
    218     if (aPhoneNumber.length() < nationalNumber.length()) {
    219       return "";
    220     }
    221     // Formats the number according to numberFormat
    222     String template = aPhoneNumber.replaceAll(numberPattern, numberFormat);
    223     // Replaces each digit with character digitPlaceholder
    224     template = template.replaceAll("9", digitPlaceholder);
    225     return template;
    226   }
    227 
    228   /**
    229    * Clears the internal state of the formatter, so it can be reused.
    230    */
    231   public void clear() {
    232     currentOutput = "";
    233     accruedInput.setLength(0);
    234     accruedInputWithoutFormatting.setLength(0);
    235     formattingTemplate.setLength(0);
    236     lastMatchPosition = 0;
    237     currentFormattingPattern = "";
    238     prefixBeforeNationalNumber.setLength(0);
    239     nationalNumber.setLength(0);
    240     ableToFormat = true;
    241     positionToRemember = 0;
    242     originalPosition = 0;
    243     isInternationalFormatting = false;
    244     isExpectingCountryCallingCode = false;
    245     possibleFormats.clear();
    246     if (!currentMetaData.equals(defaultMetaData)) {
    247       currentMetaData = getMetadataForRegion(defaultCountry);
    248     }
    249   }
    250 
    251   /**
    252    * Formats a phone number on-the-fly as each digit is entered.
    253    *
    254    * @param nextChar  the most recently entered digit of a phone number. Formatting characters are
    255    *     allowed, but as soon as they are encountered this method formats the number as entered and
    256    *     not "as you type" anymore. Full width digits and Arabic-indic digits are allowed, and will
    257    *     be shown as they are.
    258    * @return  the partially formatted phone number.
    259    */
    260   public String inputDigit(char nextChar) {
    261     currentOutput = inputDigitWithOptionToRememberPosition(nextChar, false);
    262     return currentOutput;
    263   }
    264 
    265   /**
    266    * Same as {@link #inputDigit}, but remembers the position where {@code nextChar} is inserted, so
    267    * that it can be retrieved later by using {@link #getRememberedPosition}. The remembered
    268    * position will be automatically adjusted if additional formatting characters are later
    269    * inserted/removed in front of {@code nextChar}.
    270    */
    271   public String inputDigitAndRememberPosition(char nextChar) {
    272     currentOutput = inputDigitWithOptionToRememberPosition(nextChar, true);
    273     return currentOutput;
    274   }
    275 
    276   @SuppressWarnings("fallthrough")
    277   private String inputDigitWithOptionToRememberPosition(char nextChar, boolean rememberPosition) {
    278     accruedInput.append(nextChar);
    279     if (rememberPosition) {
    280       originalPosition = accruedInput.length();
    281     }
    282     // We do formatting on-the-fly only when each character entered is either a digit, or a plus
    283     // sign (accepted at the start of the number only).
    284     if (!isDigitOrLeadingPlusSign(nextChar)) {
    285       ableToFormat = false;
    286     }
    287     if (!ableToFormat) {
    288       return accruedInput.toString();
    289     }
    290 
    291     nextChar = normalizeAndAccrueDigitsAndPlusSign(nextChar, rememberPosition);
    292 
    293     // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH digits (the plus
    294     // sign is counted as a digit as well for this purpose) have been entered.
    295     switch (accruedInputWithoutFormatting.length()) {
    296       case 0:
    297       case 1:
    298       case 2:
    299         return accruedInput.toString();
    300       case 3:
    301         if (attemptToExtractIdd()) {
    302           isExpectingCountryCallingCode = true;
    303         } else {  // No IDD or plus sign is found, must be entering in national format.
    304           removeNationalPrefixFromNationalNumber();
    305           return attemptToChooseFormattingPattern();
    306         }
    307       case 4:
    308       case 5:
    309         if (isExpectingCountryCallingCode) {
    310           if (attemptToExtractCountryCallingCode()) {
    311             isExpectingCountryCallingCode = false;
    312           }
    313           return prefixBeforeNationalNumber + nationalNumber.toString();
    314         }
    315       // We make a last attempt to extract a country calling code at the 6th digit because the
    316       // maximum length of IDD and country calling code are both 3.
    317       case 6:
    318         if (isExpectingCountryCallingCode && !attemptToExtractCountryCallingCode()) {
    319           ableToFormat = false;
    320           return accruedInput.toString();
    321         }
    322       default:
    323         if (possibleFormats.size() > 0) {  // The formatting pattern is already chosen.
    324           String tempNationalNumber = inputDigitHelper(nextChar);
    325           // See if the accrued digits can be formatted properly already. If not, use the results
    326           // from inputDigitHelper, which does formatting based on the formatting pattern chosen.
    327           String formattedNumber = attemptToFormatAccruedDigits();
    328           if (formattedNumber.length() > 0) {
    329             return formattedNumber;
    330           }
    331           narrowDownPossibleFormats(nationalNumber.toString());
    332           if (maybeCreateNewTemplate()) {
    333             return inputAccruedNationalNumber();
    334           }
    335           return ableToFormat
    336              ? prefixBeforeNationalNumber + tempNationalNumber
    337              : tempNationalNumber;
    338         } else {
    339           return attemptToChooseFormattingPattern();
    340         }
    341     }
    342   }
    343 
    344   private boolean isDigitOrLeadingPlusSign(char nextChar) {
    345     return Character.isDigit(nextChar) ||
    346         (accruedInput.length() == 1 &&
    347          PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(Character.toString(nextChar)).matches());
    348   }
    349 
    350   String attemptToFormatAccruedDigits() {
    351     for (NumberFormat numFormat : possibleFormats) {
    352       Matcher m = regexCache.getPatternForRegex(numFormat.getPattern()).matcher(nationalNumber);
    353       if (m.matches()) {
    354         String formattedNumber = m.replaceAll(numFormat.getFormat());
    355         return prefixBeforeNationalNumber + formattedNumber;
    356       }
    357     }
    358     return "";
    359   }
    360 
    361   /**
    362    * Returns the current position in the partially formatted phone number of the character which was
    363    * previously passed in as the parameter of {@link #inputDigitAndRememberPosition}.
    364    */
    365   public int getRememberedPosition() {
    366     if (!ableToFormat) {
    367       return originalPosition;
    368     }
    369     int accruedInputIndex = 0, currentOutputIndex = 0;
    370     while (accruedInputIndex < positionToRemember && currentOutputIndex < currentOutput.length()) {
    371       if (accruedInputWithoutFormatting.charAt(accruedInputIndex) ==
    372           currentOutput.charAt(currentOutputIndex)) {
    373         accruedInputIndex++;
    374       }
    375       currentOutputIndex++;
    376     }
    377     return currentOutputIndex;
    378   }
    379 
    380   // Attempts to set the formatting template and returns a string which contains the formatted
    381   // version of the digits entered so far.
    382   private String attemptToChooseFormattingPattern() {
    383     // We start to attempt to format only when as least MIN_LEADING_DIGITS_LENGTH digits of national
    384     // number (excluding national prefix) have been entered.
    385     if (nationalNumber.length() >= MIN_LEADING_DIGITS_LENGTH) {
    386       getAvailableFormats(nationalNumber.substring(0, MIN_LEADING_DIGITS_LENGTH));
    387       maybeCreateNewTemplate();
    388       return inputAccruedNationalNumber();
    389     } else {
    390       return prefixBeforeNationalNumber + nationalNumber.toString();
    391     }
    392   }
    393 
    394   // Invokes inputDigitHelper on each digit of the national number accrued, and returns a formatted
    395   // string in the end.
    396   private String inputAccruedNationalNumber() {
    397     int lengthOfNationalNumber = nationalNumber.length();
    398     if (lengthOfNationalNumber > 0) {
    399       String tempNationalNumber = "";
    400       for (int i = 0; i < lengthOfNationalNumber; i++) {
    401         tempNationalNumber = inputDigitHelper(nationalNumber.charAt(i));
    402       }
    403       return ableToFormat
    404           ? prefixBeforeNationalNumber + tempNationalNumber
    405           : tempNationalNumber;
    406     } else {
    407       return prefixBeforeNationalNumber.toString();
    408     }
    409   }
    410 
    411   private void removeNationalPrefixFromNationalNumber() {
    412     int startOfNationalNumber = 0;
    413     if (currentMetaData.getCountryCode() == 1 && nationalNumber.charAt(0) == '1') {
    414       startOfNationalNumber = 1;
    415       prefixBeforeNationalNumber.append("1 ");
    416       isInternationalFormatting = true;
    417     } else if (currentMetaData.hasNationalPrefix()) {
    418       Pattern nationalPrefixForParsing =
    419         regexCache.getPatternForRegex(currentMetaData.getNationalPrefixForParsing());
    420       Matcher m = nationalPrefixForParsing.matcher(nationalNumber);
    421       if (m.lookingAt()) {
    422         // When the national prefix is detected, we use international formatting rules instead of
    423         // national ones, because national formatting rules could contain local formatting rules
    424         // for numbers entered without area code.
    425         isInternationalFormatting = true;
    426         startOfNationalNumber = m.end();
    427         prefixBeforeNationalNumber.append(nationalNumber.substring(0, startOfNationalNumber));
    428       }
    429     }
    430     nationalNumber.delete(0, startOfNationalNumber);
    431   }
    432 
    433   /**
    434    * Extracts IDD and plus sign to prefixBeforeNationalNumber when they are available, and places
    435    * the remaining input into nationalNumber.
    436    *
    437    * @return  true when accruedInputWithoutFormatting begins with the plus sign or valid IDD for
    438    *     defaultCountry.
    439    */
    440   private boolean attemptToExtractIdd() {
    441     Pattern internationalPrefix =
    442         regexCache.getPatternForRegex("\\" + PhoneNumberUtil.PLUS_SIGN + "|" +
    443             currentMetaData.getInternationalPrefix());
    444     Matcher iddMatcher = internationalPrefix.matcher(accruedInputWithoutFormatting);
    445     if (iddMatcher.lookingAt()) {
    446       isInternationalFormatting = true;
    447       int startOfCountryCallingCode = iddMatcher.end();
    448       nationalNumber.setLength(0);
    449       nationalNumber.append(accruedInputWithoutFormatting.substring(startOfCountryCallingCode));
    450       prefixBeforeNationalNumber.append(
    451           accruedInputWithoutFormatting.substring(0, startOfCountryCallingCode));
    452       if (accruedInputWithoutFormatting.charAt(0) != PhoneNumberUtil.PLUS_SIGN) {
    453         prefixBeforeNationalNumber.append(" ");
    454       }
    455       return true;
    456     }
    457     return false;
    458   }
    459 
    460   /**
    461    * Extracts the country calling code from the beginning of nationalNumber to
    462    * prefixBeforeNationalNumber when they are available, and places the remaining input into
    463    * nationalNumber.
    464    *
    465    * @return  true when a valid country calling code can be found.
    466    */
    467   private boolean attemptToExtractCountryCallingCode() {
    468     if (nationalNumber.length() == 0) {
    469       return false;
    470     }
    471     StringBuilder numberWithoutCountryCallingCode = new StringBuilder();
    472     int countryCode = phoneUtil.extractCountryCode(nationalNumber, numberWithoutCountryCallingCode);
    473     if (countryCode == 0) {
    474       return false;
    475     }
    476     nationalNumber.setLength(0);
    477     nationalNumber.append(numberWithoutCountryCallingCode);
    478     String newRegionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
    479     if (!newRegionCode.equals(defaultCountry)) {
    480       currentMetaData = getMetadataForRegion(newRegionCode);
    481     }
    482     String countryCodeString = Integer.toString(countryCode);
    483     prefixBeforeNationalNumber.append(countryCodeString).append(" ");
    484     return true;
    485   }
    486 
    487   // Accrues digits and the plus sign to accruedInputWithoutFormatting for later use. If nextChar
    488   // contains a digit in non-ASCII format (e.g. the full-width version of digits), it is first
    489   // normalized to the ASCII version. The return value is nextChar itself, or its normalized
    490   // version, if nextChar is a digit in non-ASCII format. This method assumes its input is either a
    491   // digit or the plus sign.
    492   private char normalizeAndAccrueDigitsAndPlusSign(char nextChar, boolean rememberPosition) {
    493     char normalizedChar;
    494     if (nextChar == PhoneNumberUtil.PLUS_SIGN) {
    495       normalizedChar = nextChar;
    496       accruedInputWithoutFormatting.append(nextChar);
    497     } else {
    498       int radix = 10;
    499       normalizedChar = Character.forDigit(Character.digit(nextChar, radix), radix);
    500       accruedInputWithoutFormatting.append(normalizedChar);
    501       nationalNumber.append(normalizedChar);
    502     }
    503     if (rememberPosition) {
    504       positionToRemember = accruedInputWithoutFormatting.length();
    505     }
    506     return normalizedChar;
    507   }
    508 
    509   private String inputDigitHelper(char nextChar) {
    510     Matcher digitMatcher = digitPattern.matcher(formattingTemplate);
    511     if (digitMatcher.find(lastMatchPosition)) {
    512       String tempTemplate = digitMatcher.replaceFirst(Character.toString(nextChar));
    513       formattingTemplate.replace(0, tempTemplate.length(), tempTemplate);
    514       lastMatchPosition = digitMatcher.start();
    515       return formattingTemplate.substring(0, lastMatchPosition + 1);
    516     } else {
    517       if (possibleFormats.size() == 1) {
    518         // More digits are entered than we could handle, and there are no other valid patterns to
    519         // try.
    520         ableToFormat = false;
    521       }  // else, we just reset the formatting pattern.
    522       currentFormattingPattern = "";
    523       return accruedInput.toString();
    524     }
    525   }
    526 }
    527