2 * Copyright (C) 2009 The Libphonenumber Authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.i18n.phonenumbers;
19 import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
20 import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
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;
29 * A formatter which formats phone numbers as they are entered.
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
37 * <p>See the unittests for more details on how the formatter is to be used.
39 * @author Shaopeng Jia
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 // This indicates whether AsYouTypeFormatter is currently doing the formatting.
49 private boolean ableToFormat = true;
50 // Set to true when users enter their own formatting. AsYouTypeFormatter will do no formatting at
51 // all when this is set to true.
52 private boolean inputHasFormatting = false;
53 private boolean isInternationalFormatting = false;
54 private boolean isExpectingCountryCallingCode = false;
55 private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
56 private String defaultCountry;
58 private static final PhoneMetadata EMPTY_METADATA =
59 new PhoneMetadata().setInternationalPrefix("NA");
60 private PhoneMetadata defaultMetaData;
61 private PhoneMetadata currentMetaData;
63 // A pattern that is used to match character classes in regular expressions. An example of a
64 // character class is [1-4].
65 private static final Pattern CHARACTER_CLASS_PATTERN = Pattern.compile("\\[([^\\[\\]])*\\]");
66 // Any digit in a regular expression that actually denotes a digit. For example, in the regular
67 // expression 80[0-2]\d{6,10}, the first 2 digits (8 and 0) are standalone digits, but the rest
69 // Two look-aheads are needed because the number following \\d could be a two-digit number, since
70 // the phone number can be as long as 15 digits.
71 private static final Pattern STANDALONE_DIGIT_PATTERN = Pattern.compile("\\d(?=[^,}][^,}])");
73 // A pattern that is used to determine if a numberFormat under availableFormats is eligible to be
74 // used by the AYTF. It is eligible when the format element under numberFormat contains groups of
75 // the dollar sign followed by a single digit, separated by valid phone number punctuation. This
76 // prevents invalid punctuation (such as the star sign in Israeli star numbers) getting into the
77 // output of the AYTF.
78 private static final Pattern ELIGIBLE_FORMAT_PATTERN =
79 Pattern.compile("[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*" +
80 "(\\$\\d" + "[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*)+");
82 // This is the minimum length of national number accrued that is required to trigger the
83 // formatter. The first element of the leadingDigitsPattern of each numberFormat contains a
84 // regular expression that matches up to this number of digits.
85 private static final int MIN_LEADING_DIGITS_LENGTH = 3;
87 // The digits that have not been entered yet will be represented by a \u2008, the punctuation
89 private String digitPlaceholder = "\u2008";
90 private Pattern digitPattern = Pattern.compile(digitPlaceholder);
91 private int lastMatchPosition = 0;
92 // The position of a digit upon which inputDigitAndRememberPosition is most recently invoked, as
93 // found in the original sequence of characters the user entered.
94 private int originalPosition = 0;
95 // The position of a digit upon which inputDigitAndRememberPosition is most recently invoked, as
96 // found in accruedInputWithoutFormatting.
97 private int positionToRemember = 0;
98 // This contains anything that has been entered so far preceding the national significant number,
99 // and it is formatted (e.g. with space inserted). For example, this can contain IDD, country
100 // code, and/or NDD, etc.
101 private StringBuilder prefixBeforeNationalNumber = new StringBuilder();
102 // This contains the national prefix that has been extracted. It contains only digits without
104 private String nationalPrefixExtracted = "";
105 private StringBuilder nationalNumber = new StringBuilder();
106 private List<NumberFormat> possibleFormats = new ArrayList<NumberFormat>();
108 // A cache for frequently used country-specific regular expressions.
109 private RegexCache regexCache = new RegexCache(64);
112 * Constructs an as-you-type formatter. Should be obtained from {@link
113 * PhoneNumberUtil#getAsYouTypeFormatter}.
115 * @param regionCode the country/region where the phone number is being entered
117 AsYouTypeFormatter(String regionCode) {
118 defaultCountry = regionCode;
119 currentMetaData = getMetadataForRegion(defaultCountry);
120 defaultMetaData = currentMetaData;
123 // The metadata needed by this class is the same for all regions sharing the same country calling
124 // code. Therefore, we return the metadata for "main" region for this country calling code.
125 private PhoneMetadata getMetadataForRegion(String regionCode) {
126 int countryCallingCode = phoneUtil.getCountryCodeForRegion(regionCode);
127 String mainCountry = phoneUtil.getRegionCodeForCountryCode(countryCallingCode);
128 PhoneMetadata metadata = phoneUtil.getMetadataForRegion(mainCountry);
129 if (metadata != null) {
132 // Set to a default instance of the metadata. This allows us to function with an incorrect
133 // region code, even if formatting only works for numbers specified with "+".
134 return EMPTY_METADATA;
137 // Returns true if a new template is created as opposed to reusing the existing template.
138 private boolean maybeCreateNewTemplate() {
139 // When there are multiple available formats, the formatter uses the first format where a
140 // formatting template could be created.
141 Iterator<NumberFormat> it = possibleFormats.iterator();
142 while (it.hasNext()) {
143 NumberFormat numberFormat = it.next();
144 String pattern = numberFormat.getPattern();
145 if (currentFormattingPattern.equals(pattern)) {
148 if (createFormattingTemplate(numberFormat)) {
149 currentFormattingPattern = pattern;
150 // With a new formatting template, the matched position using the old template needs to be
152 lastMatchPosition = 0;
154 } else { // Remove the current number format from possibleFormats.
158 ableToFormat = false;
162 private void getAvailableFormats(String leadingThreeDigits) {
163 List<NumberFormat> formatList =
164 (isInternationalFormatting && currentMetaData.intlNumberFormatSize() > 0)
165 ? currentMetaData.intlNumberFormats()
166 : currentMetaData.numberFormats();
167 for (NumberFormat format : formatList) {
168 if (isFormatEligible(format.getFormat())) {
169 possibleFormats.add(format);
172 narrowDownPossibleFormats(leadingThreeDigits);
175 private boolean isFormatEligible(String format) {
176 return ELIGIBLE_FORMAT_PATTERN.matcher(format).matches();
179 private void narrowDownPossibleFormats(String leadingDigits) {
180 int indexOfLeadingDigitsPattern = leadingDigits.length() - MIN_LEADING_DIGITS_LENGTH;
181 Iterator<NumberFormat> it = possibleFormats.iterator();
182 while (it.hasNext()) {
183 NumberFormat format = it.next();
184 if (format.leadingDigitsPatternSize() > indexOfLeadingDigitsPattern) {
185 Pattern leadingDigitsPattern =
186 regexCache.getPatternForRegex(
187 format.getLeadingDigitsPattern(indexOfLeadingDigitsPattern));
188 Matcher m = leadingDigitsPattern.matcher(leadingDigits);
189 if (!m.lookingAt()) {
192 } // else the particular format has no more specific leadingDigitsPattern, and it should be
197 private boolean createFormattingTemplate(NumberFormat format) {
198 String numberPattern = format.getPattern();
200 // The formatter doesn't format numbers when numberPattern contains "|", e.g.
201 // (20|3)\d{4}. In those cases we quickly return.
202 if (numberPattern.indexOf('|') != -1) {
206 // Replace anything in the form of [..] with \d
207 numberPattern = CHARACTER_CLASS_PATTERN.matcher(numberPattern).replaceAll("\\\\d");
209 // Replace any standalone digit (not the one in d{}) with \d
210 numberPattern = STANDALONE_DIGIT_PATTERN.matcher(numberPattern).replaceAll("\\\\d");
211 formattingTemplate.setLength(0);
212 String tempTemplate = getFormattingTemplate(numberPattern, format.getFormat());
213 if (tempTemplate.length() > 0) {
214 formattingTemplate.append(tempTemplate);
220 // Gets a formatting template which can be used to efficiently format a partial number where
221 // digits are added one by one.
222 private String getFormattingTemplate(String numberPattern, String numberFormat) {
223 // Creates a phone number consisting only of the digit 9 that matches the
224 // numberPattern by applying the pattern to the longestPhoneNumber string.
225 String longestPhoneNumber = "999999999999999";
226 Matcher m = regexCache.getPatternForRegex(numberPattern).matcher(longestPhoneNumber);
227 m.find(); // this will always succeed
228 String aPhoneNumber = m.group();
229 // No formatting template can be created if the number of digits entered so far is longer than
230 // the maximum the current formatting rule can accommodate.
231 if (aPhoneNumber.length() < nationalNumber.length()) {
234 // Formats the number according to numberFormat
235 String template = aPhoneNumber.replaceAll(numberPattern, numberFormat);
236 // Replaces each digit with character digitPlaceholder
237 template = template.replaceAll("9", digitPlaceholder);
242 * Clears the internal state of the formatter, so it can be reused.
244 public void clear() {
246 accruedInput.setLength(0);
247 accruedInputWithoutFormatting.setLength(0);
248 formattingTemplate.setLength(0);
249 lastMatchPosition = 0;
250 currentFormattingPattern = "";
251 prefixBeforeNationalNumber.setLength(0);
252 nationalPrefixExtracted = "";
253 nationalNumber.setLength(0);
255 inputHasFormatting = false;
256 positionToRemember = 0;
257 originalPosition = 0;
258 isInternationalFormatting = false;
259 isExpectingCountryCallingCode = false;
260 possibleFormats.clear();
261 if (!currentMetaData.equals(defaultMetaData)) {
262 currentMetaData = getMetadataForRegion(defaultCountry);
267 * Formats a phone number on-the-fly as each digit is entered.
269 * @param nextChar the most recently entered digit of a phone number. Formatting characters are
270 * allowed, but as soon as they are encountered this method formats the number as entered and
271 * not "as you type" anymore. Full width digits and Arabic-indic digits are allowed, and will
272 * be shown as they are.
273 * @return the partially formatted phone number.
275 public String inputDigit(char nextChar) {
276 currentOutput = inputDigitWithOptionToRememberPosition(nextChar, false);
277 return currentOutput;
281 * Same as {@link #inputDigit}, but remembers the position where {@code nextChar} is inserted, so
282 * that it can be retrieved later by using {@link #getRememberedPosition}. The remembered
283 * position will be automatically adjusted if additional formatting characters are later
284 * inserted/removed in front of {@code nextChar}.
286 public String inputDigitAndRememberPosition(char nextChar) {
287 currentOutput = inputDigitWithOptionToRememberPosition(nextChar, true);
288 return currentOutput;
291 @SuppressWarnings("fallthrough")
292 private String inputDigitWithOptionToRememberPosition(char nextChar, boolean rememberPosition) {
293 accruedInput.append(nextChar);
294 if (rememberPosition) {
295 originalPosition = accruedInput.length();
297 // We do formatting on-the-fly only when each character entered is either a digit, or a plus
298 // sign (accepted at the start of the number only).
299 if (!isDigitOrLeadingPlusSign(nextChar)) {
300 ableToFormat = false;
301 inputHasFormatting = true;
303 nextChar = normalizeAndAccrueDigitsAndPlusSign(nextChar, rememberPosition);
306 // When we are unable to format because of reasons other than that formatting chars have been
307 // entered, it can be due to really long IDDs or NDDs. If that is the case, we might be able
308 // to do formatting again after extracting them.
309 if (inputHasFormatting) {
310 return accruedInput.toString();
311 } else if (attemptToExtractIdd()) {
312 if (attemptToExtractCountryCallingCode()) {
313 return attemptToChoosePatternWithPrefixExtracted();
315 } else if (ableToExtractLongerNdd()) {
316 // Add an additional space to separate long NDD and national significant number for
318 prefixBeforeNationalNumber.append(" ");
319 return attemptToChoosePatternWithPrefixExtracted();
321 return accruedInput.toString();
324 // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH digits (the plus
325 // sign is counted as a digit as well for this purpose) have been entered.
326 switch (accruedInputWithoutFormatting.length()) {
330 return accruedInput.toString();
332 if (attemptToExtractIdd()) {
333 isExpectingCountryCallingCode = true;
334 } else { // No IDD or plus sign is found, might be entering in national format.
335 nationalPrefixExtracted = removeNationalPrefixFromNationalNumber();
336 return attemptToChooseFormattingPattern();
339 if (isExpectingCountryCallingCode) {
340 if (attemptToExtractCountryCallingCode()) {
341 isExpectingCountryCallingCode = false;
343 return prefixBeforeNationalNumber + nationalNumber.toString();
345 if (possibleFormats.size() > 0) { // The formatting pattern is already chosen.
346 String tempNationalNumber = inputDigitHelper(nextChar);
347 // See if the accrued digits can be formatted properly already. If not, use the results
348 // from inputDigitHelper, which does formatting based on the formatting pattern chosen.
349 String formattedNumber = attemptToFormatAccruedDigits();
350 if (formattedNumber.length() > 0) {
351 return formattedNumber;
353 narrowDownPossibleFormats(nationalNumber.toString());
354 if (maybeCreateNewTemplate()) {
355 return inputAccruedNationalNumber();
358 ? prefixBeforeNationalNumber + tempNationalNumber
359 : tempNationalNumber;
361 return attemptToChooseFormattingPattern();
366 private String attemptToChoosePatternWithPrefixExtracted() {
368 isExpectingCountryCallingCode = false;
369 possibleFormats.clear();
370 return attemptToChooseFormattingPattern();
373 // Some national prefixes are a substring of others. If extracting the shorter NDD doesn't result
374 // in a number we can format, we try to see if we can extract a longer version here.
375 private boolean ableToExtractLongerNdd() {
376 if (nationalPrefixExtracted.length() > 0) {
377 // Put the extracted NDD back to the national number before attempting to extract a new NDD.
378 nationalNumber.insert(0, nationalPrefixExtracted);
379 // Remove the previously extracted NDD from prefixBeforeNationalNumber. We cannot simply set
380 // it to empty string because people sometimes enter national prefix after country code, e.g
381 // +44 (0)20-1234-5678.
382 int indexOfPreviousNdd = prefixBeforeNationalNumber.lastIndexOf(nationalPrefixExtracted);
383 prefixBeforeNationalNumber.setLength(indexOfPreviousNdd);
385 return !nationalPrefixExtracted.equals(removeNationalPrefixFromNationalNumber());
388 private boolean isDigitOrLeadingPlusSign(char nextChar) {
389 return Character.isDigit(nextChar) ||
390 (accruedInput.length() == 1 &&
391 PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(Character.toString(nextChar)).matches());
394 String attemptToFormatAccruedDigits() {
395 for (NumberFormat numFormat : possibleFormats) {
396 Matcher m = regexCache.getPatternForRegex(numFormat.getPattern()).matcher(nationalNumber);
398 String formattedNumber = m.replaceAll(numFormat.getFormat());
399 return prefixBeforeNationalNumber + formattedNumber;
406 * Returns the current position in the partially formatted phone number of the character which was
407 * previously passed in as the parameter of {@link #inputDigitAndRememberPosition}.
409 public int getRememberedPosition() {
411 return originalPosition;
413 int accruedInputIndex = 0, currentOutputIndex = 0;
414 while (accruedInputIndex < positionToRemember && currentOutputIndex < currentOutput.length()) {
415 if (accruedInputWithoutFormatting.charAt(accruedInputIndex) ==
416 currentOutput.charAt(currentOutputIndex)) {
419 currentOutputIndex++;
421 return currentOutputIndex;
424 // Attempts to set the formatting template and returns a string which contains the formatted
425 // version of the digits entered so far.
426 private String attemptToChooseFormattingPattern() {
427 // We start to attempt to format only when as least MIN_LEADING_DIGITS_LENGTH digits of national
428 // number (excluding national prefix) have been entered.
429 if (nationalNumber.length() >= MIN_LEADING_DIGITS_LENGTH) {
430 getAvailableFormats(nationalNumber.substring(0, MIN_LEADING_DIGITS_LENGTH));
431 maybeCreateNewTemplate();
432 return inputAccruedNationalNumber();
434 return prefixBeforeNationalNumber + nationalNumber.toString();
438 // Invokes inputDigitHelper on each digit of the national number accrued, and returns a formatted
439 // string in the end.
440 private String inputAccruedNationalNumber() {
441 int lengthOfNationalNumber = nationalNumber.length();
442 if (lengthOfNationalNumber > 0) {
443 String tempNationalNumber = "";
444 for (int i = 0; i < lengthOfNationalNumber; i++) {
445 tempNationalNumber = inputDigitHelper(nationalNumber.charAt(i));
448 ? prefixBeforeNationalNumber + tempNationalNumber
449 : tempNationalNumber;
451 return prefixBeforeNationalNumber.toString();
455 // Returns the national prefix extracted, or an empty string if it is not present.
456 private String removeNationalPrefixFromNationalNumber() {
457 int startOfNationalNumber = 0;
458 if (currentMetaData.getCountryCode() == 1 && nationalNumber.charAt(0) == '1') {
459 startOfNationalNumber = 1;
460 prefixBeforeNationalNumber.append("1 ");
461 isInternationalFormatting = true;
462 } else if (currentMetaData.hasNationalPrefixForParsing()) {
463 Pattern nationalPrefixForParsing =
464 regexCache.getPatternForRegex(currentMetaData.getNationalPrefixForParsing());
465 Matcher m = nationalPrefixForParsing.matcher(nationalNumber);
467 // When the national prefix is detected, we use international formatting rules instead of
468 // national ones, because national formatting rules could contain local formatting rules
469 // for numbers entered without area code.
470 isInternationalFormatting = true;
471 startOfNationalNumber = m.end();
472 prefixBeforeNationalNumber.append(nationalNumber.substring(0, startOfNationalNumber));
475 String nationalPrefix = nationalNumber.substring(0, startOfNationalNumber);
476 nationalNumber.delete(0, startOfNationalNumber);
477 return nationalPrefix;
481 * Extracts IDD and plus sign to prefixBeforeNationalNumber when they are available, and places
482 * the remaining input into nationalNumber.
484 * @return true when accruedInputWithoutFormatting begins with the plus sign or valid IDD for
487 private boolean attemptToExtractIdd() {
488 Pattern internationalPrefix =
489 regexCache.getPatternForRegex("\\" + PhoneNumberUtil.PLUS_SIGN + "|" +
490 currentMetaData.getInternationalPrefix());
491 Matcher iddMatcher = internationalPrefix.matcher(accruedInputWithoutFormatting);
492 if (iddMatcher.lookingAt()) {
493 isInternationalFormatting = true;
494 int startOfCountryCallingCode = iddMatcher.end();
495 nationalNumber.setLength(0);
496 nationalNumber.append(accruedInputWithoutFormatting.substring(startOfCountryCallingCode));
497 prefixBeforeNationalNumber.setLength(0);
498 prefixBeforeNationalNumber.append(
499 accruedInputWithoutFormatting.substring(0, startOfCountryCallingCode));
500 if (accruedInputWithoutFormatting.charAt(0) != PhoneNumberUtil.PLUS_SIGN) {
501 prefixBeforeNationalNumber.append(" ");
509 * Extracts the country calling code from the beginning of nationalNumber to
510 * prefixBeforeNationalNumber when they are available, and places the remaining input into
513 * @return true when a valid country calling code can be found.
515 private boolean attemptToExtractCountryCallingCode() {
516 if (nationalNumber.length() == 0) {
519 StringBuilder numberWithoutCountryCallingCode = new StringBuilder();
520 int countryCode = phoneUtil.extractCountryCode(nationalNumber, numberWithoutCountryCallingCode);
521 if (countryCode == 0) {
524 nationalNumber.setLength(0);
525 nationalNumber.append(numberWithoutCountryCallingCode);
526 String newRegionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
527 if (!newRegionCode.equals(defaultCountry)) {
528 currentMetaData = getMetadataForRegion(newRegionCode);
530 String countryCodeString = Integer.toString(countryCode);
531 prefixBeforeNationalNumber.append(countryCodeString).append(" ");
535 // Accrues digits and the plus sign to accruedInputWithoutFormatting for later use. If nextChar
536 // contains a digit in non-ASCII format (e.g. the full-width version of digits), it is first
537 // normalized to the ASCII version. The return value is nextChar itself, or its normalized
538 // version, if nextChar is a digit in non-ASCII format. This method assumes its input is either a
539 // digit or the plus sign.
540 private char normalizeAndAccrueDigitsAndPlusSign(char nextChar, boolean rememberPosition) {
542 if (nextChar == PhoneNumberUtil.PLUS_SIGN) {
543 normalizedChar = nextChar;
544 accruedInputWithoutFormatting.append(nextChar);
547 normalizedChar = Character.forDigit(Character.digit(nextChar, radix), radix);
548 accruedInputWithoutFormatting.append(normalizedChar);
549 nationalNumber.append(normalizedChar);
551 if (rememberPosition) {
552 positionToRemember = accruedInputWithoutFormatting.length();
554 return normalizedChar;
557 private String inputDigitHelper(char nextChar) {
558 Matcher digitMatcher = digitPattern.matcher(formattingTemplate);
559 if (digitMatcher.find(lastMatchPosition)) {
560 String tempTemplate = digitMatcher.replaceFirst(Character.toString(nextChar));
561 formattingTemplate.replace(0, tempTemplate.length(), tempTemplate);
562 lastMatchPosition = digitMatcher.start();
563 return formattingTemplate.substring(0, lastMatchPosition + 1);
565 if (possibleFormats.size() == 1) {
566 // More digits are entered than we could handle, and there are no other valid patterns to
568 ableToFormat = false;
569 } // else, we just reset the formatting pattern.
570 currentFormattingPattern = "";
571 return accruedInput.toString();