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 : accruedInput.toString(); 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 : accruedInput.toString(); 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