/* * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.i18n.phonenumbers; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat; import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata; import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection; import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource; import com.google.protobuf.MessageLite; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Utility for international phone numbers. Functionality includes formatting, parsing and * validation. * * @author Shaopeng Jia * @author Lara Rennie */ public class PhoneNumberUtil { // The minimum and maximum length of the national significant number. private static final int MIN_LENGTH_FOR_NSN = 3; private static final int MAX_LENGTH_FOR_NSN = 15; private static final String META_DATA_FILE = "/com/google/i18n/phonenumbers/src/generated_files/PhoneNumberMetadataProto"; private static final Logger LOGGER = Logger.getLogger(PhoneNumberUtil.class.getName()); // A mapping from a country code to a region code which denotes the country/region // represented by that country code. Note countries under NANPA share the country code 1, // Russia and Kazakhstan share the country code 7, and many French territories in the Indian // Ocean share the country code 262. Under this map, 1 is mapped to US, 7 is mapped to RU, // and 262 is mapped to RE. private final Map countryCodeToRegionCodeMap = new HashMap(); // The set of countries that share country code 1. private final Set nanpaCountries = new HashSet(); private static final int NANPA_COUNTRY_CODE = 1; // The set of countries that share country code 7. private final Set russiaFederationCountries = new HashSet(2); private static final int RUSSIAN_FED_COUNTRY_CODE = 7; // The set of countries that share country code 262. private final Set frenchIndianOceanTerritories = new HashSet(6); private static final int FRENCH_INDIAN_OCEAN_COUNTRY_CODE = 262; // The PLUS_SIGN signifies the international prefix. static final char PLUS_SIGN = '+'; // These mappings map a character (key) to a specific digit that should replace it for // normalization purposes. Non-European digits that may be used in phone numbers are mapped to a // European equivalent. static final Map DIGIT_MAPPINGS = new ImmutableMap.Builder() .put('0', '0') .put('\uFF10', '0') // Fullwidth digit 0 .put('\u0660', '0') // Arabic-indic digit 0 .put('1', '1') .put('\uFF11', '1') // Fullwidth digit 1 .put('\u0661', '1') // Arabic-indic digit 1 .put('2', '2') .put('\uFF12', '2') // Fullwidth digit 2 .put('\u0662', '2') // Arabic-indic digit 2 .put('3', '3') .put('\uFF13', '3') // Fullwidth digit 3 .put('\u0663', '3') // Arabic-indic digit 3 .put('4', '4') .put('\uFF14', '4') // Fullwidth digit 4 .put('\u0664', '4') // Arabic-indic digit 4 .put('5', '5') .put('\uFF15', '5') // Fullwidth digit 5 .put('\u0665', '5') // Arabic-indic digit 5 .put('6', '6') .put('\uFF16', '6') // Fullwidth digit 6 .put('\u0666', '6') // Arabic-indic digit 6 .put('7', '7') .put('\uFF17', '7') // Fullwidth digit 7 .put('\u0667', '7') // Arabic-indic digit 7 .put('8', '8') .put('\uFF18', '8') // Fullwidth digit 8 .put('\u0668', '8') // Arabic-indic digit 8 .put('9', '9') .put('\uFF19', '9') // Fullwidth digit 9 .put('\u0669', '9') // Arabic-indic digit 9 .build(); // Only upper-case variants of alpha characters are stored. private static final Map ALPHA_MAPPINGS = new ImmutableMap.Builder() .put('A', '2') .put('B', '2') .put('C', '2') .put('D', '3') .put('E', '3') .put('F', '3') .put('G', '4') .put('H', '4') .put('I', '4') .put('J', '5') .put('K', '5') .put('L', '5') .put('M', '6') .put('N', '6') .put('O', '6') .put('P', '7') .put('Q', '7') .put('R', '7') .put('S', '7') .put('T', '8') .put('U', '8') .put('V', '8') .put('W', '9') .put('X', '9') .put('Y', '9') .put('Z', '9') .build(); // For performance reasons, amalgamate both into one map. private static final Map ALL_NORMALIZATION_MAPPINGS = new ImmutableMap.Builder() .putAll(ALPHA_MAPPINGS) .putAll(DIGIT_MAPPINGS) .build(); // A list of all country codes where national significant numbers (excluding any national prefix) // exist that start with a leading zero. private static final Set LEADING_ZERO_COUNTRIES = new ImmutableSet.Builder() .add(39) // Italy .add(225) // Cote d'Ivoire .add(227) // Niger .add(228) // Togo .add(240) // Equatorial Guinea .add(241) // Gabon .build(); // Pattern that makes it easy to distinguish whether a country has a unique international dialing // prefix or not. If a country has a unique international prefix (e.g. 011 in USA), it will be // represented as a string that contains a sequence of ASCII digits. If there are multiple // available international prefixes in a country, they will be represented as a regex string that // always contains character(s) other than ASCII digits. // Note this regex also includes tilde, which signals waiting for the tone. private static final Pattern UNIQUE_INTERNATIONAL_PREFIX = Pattern.compile("[\\d]+(?:[~\u2053\u223C\uFF5E][\\d]+)?"); // Regular expression of acceptable punctuation found in phone numbers. This excludes punctuation // found as a leading character only. // This consists of dash characters, white space characters, full stops, slashes, // square brackets, parentheses and tildes. It also includes the letter 'x' as that is found as a // placeholder for carrier information in some phone numbers. private static final String VALID_PUNCTUATION = "-x\u2010-\u2015\u2212\uFF0D-\uFF0F " + "\u00A0\u200B\u2060\u3000()\uFF08\uFF09\uFF3B\uFF3D.\\[\\]/~\u2053\u223C\uFF5E"; // Digits accepted in phone numbers private static final String VALID_DIGITS = Arrays.toString(DIGIT_MAPPINGS.keySet().toArray()).replaceAll(", ", ""); // We accept alpha characters in phone numbers, ASCII only, upper and lower case. private static final String VALID_ALPHA = Arrays.toString(ALPHA_MAPPINGS.keySet().toArray()).replaceAll(", ", "") + Arrays.toString(ALPHA_MAPPINGS.keySet().toArray()).toLowerCase().replaceAll(", ", ""); private static final String PLUS_CHARS = "+\uFF0B"; private static final Pattern CAPTURING_DIGIT_PATTERN = Pattern.compile("([" + VALID_DIGITS + "])"); // Regular expression of acceptable characters that may start a phone number for the purposes of // parsing. This allows us to strip away meaningless prefixes to phone numbers that may be // mistakenly given to us. This consists of digits, the plus symbol and arabic-indic digits. This // does not contain alpha characters, although they may be used later in the number. It also does // not include other punctuation, as this will be stripped later during parsing and is of no // information value when parsing a number. private static final String VALID_START_CHAR = "[" + PLUS_CHARS + VALID_DIGITS + "]"; private static final Pattern VALID_START_CHAR_PATTERN = Pattern.compile(VALID_START_CHAR); // Regular expression of characters typically used to start a second phone number for the purposes // of parsing. This allows us to strip off parts of the number that are actually the start of // another number, such as for: (530) 583-6985 x302/x2303 -> the second extension here makes this // actually two phone numbers, (530) 583-6985 x302 and (530) 583-6985 x2303. We remove the second // extension so that the first number is parsed correctly. private static final String SECOND_NUMBER_START = "[\\\\/] *x"; private static final Pattern SECOND_NUMBER_START_PATTERN = Pattern.compile(SECOND_NUMBER_START); // Regular expression of trailing characters that we want to remove. We remove all characters that // are not alpha or numerical characters. The hash character is retained here, as it may signify // the previous block was an extension. private static final String UNWANTED_END_CHARS = "[[\\P{N}&&\\P{L}]&&[^#]]+$"; private static final Pattern UNWANTED_END_CHAR_PATTERN = Pattern.compile(UNWANTED_END_CHARS); // We use this pattern to check if the phone number has at least three letters in it - if so, then // we treat it as a number where some phone-number digits are represented by letters. private static final Pattern VALID_ALPHA_PHONE_PATTERN = Pattern.compile("(?:.*?[A-Za-z]){3}.*"); // Regular expression of viable phone numbers. This is location independent. Checks we have at // least three leading digits, and only valid punctuation, alpha characters and // digits in the phone number. Does not include extension data. // The symbol 'x' is allowed here as valid punctuation since it is often used as a placeholder for // carrier codes, for example in Brazilian phone numbers. // Corresponds to the following: // plus_sign?([punctuation]*[digits]){3,}([punctuation]|[digits]|[alpha])* private static final String VALID_PHONE_NUMBER = "[" + PLUS_CHARS + "]?(?:[" + VALID_PUNCTUATION + "]*[" + VALID_DIGITS + "]){3,}[" + VALID_ALPHA + VALID_PUNCTUATION + VALID_DIGITS + "]*"; // Default extension prefix to use when formatting. This will be put in front of any extension // component of the number, after the main national number is formatted. For example, if you wish // the default extension formatting to be " extn: 3456", then you should specify " extn: " here // as the default extension prefix. This can be overridden by country-specific preferences. private static final String DEFAULT_EXTN_PREFIX = " ext. "; // Regexp of all possible ways to write extensions, for use when parsing. This will be run as a // case-insensitive regexp match. Wide character versions are also provided after each ascii // version. There are two regular expressions here: the more generic one starts with optional // white space and ends with an optional full stop (.), followed by zero or more spaces/tabs and // then the numbers themselves. The other one covers the special case of American numbers where // the extension is written with a hash at the end, such as "- 503#". // Note that the only capturing groups should be around the digits that you want to capture as // part of the extension, or else parsing will fail! private static final String KNOWN_EXTN_PATTERNS = "[ \u00A0\\t,]*(?:ext(?:ensio)?n?|" + "\uFF45\uFF58\uFF54\uFF4E?|[,x\uFF58#\uFF03~\uFF5E]|int|\uFF49\uFF4E\uFF54)" + "[:\\.\uFF0E]?[ \u00A0\\t,]*([" + VALID_DIGITS + "]{1,7})|[- ]+([" + VALID_DIGITS + "]{1,5})#"; // Regexp of all known extension prefixes used by different countries followed by 1 or more valid // digits, for use when parsing. private static final Pattern EXTN_PATTERN = Pattern.compile("(?:" + KNOWN_EXTN_PATTERNS + ")$", Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE); // We append optionally the extension pattern to the end here, as a valid phone number may // have an extension prefix appended, followed by 1 or more digits. private static final Pattern VALID_PHONE_NUMBER_PATTERN = Pattern.compile(VALID_PHONE_NUMBER + "(?:" + KNOWN_EXTN_PATTERNS + ")?", Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE); private static PhoneNumberUtil instance = null; // A mapping from a region code to the PhoneMetadata for that region. private Map countryToMetadataMap = Collections.synchronizedMap(new HashMap()); /** * INTERNATIONAL and NATIONAL formats are consistent with the definition in ITU-T Recommendation * E. 123. For example, the number of the Google Zurich office will be written as * "+41 44 668 1800" in INTERNATIONAL format, and as "044 668 1800" in NATIONAL format. * E164 format is as per INTERNATIONAL format but with no formatting applied, e.g. +41446681800. * * Note: If you are considering storing the number in a neutral format, you are highly advised to * use the phonenumber.proto. */ public enum PhoneNumberFormat { E164, INTERNATIONAL, NATIONAL } /** * Type of phone numbers. */ public enum PhoneNumberType { FIXED_LINE, MOBILE, // In some countries (e.g. the USA), it is impossible to distinguish between fixed-line and // mobile numbers by looking at the phone number itself. FIXED_LINE_OR_MOBILE, // Freephone lines TOLL_FREE, PREMIUM_RATE, // The cost of this call is shared between the caller and the recipient, and is hence typically // less than PREMIUM_RATE calls. See // http://en.wikipedia.org/wiki/Shared_Cost_Service for // more information. SHARED_COST, // Voice over IP numbers. This includes TSoIP (Telephony Service over IP). VOIP, // A personal number is associated with a particular person, and may be routed to either a // MOBILE or FIXED_LINE number. Some more information can be found here: // http://en.wikipedia.org/wiki/Personal_Numbers PERSONAL_NUMBER, // A phone number is of type UNKNOWN when it does not fit any of the known patterns for a // specific country. UNKNOWN } /** * Types of phone number matches. See detailed description beside the isNumberMatch() method. */ public enum MatchType { NO_MATCH, SHORT_NSN_MATCH, NSN_MATCH, EXACT_MATCH, } /** * Possible outcomes when testing if a PhoneNumber is possible. */ public enum ValidationResult { IS_POSSIBLE, INVALID_COUNTRY_CODE, TOO_SHORT, TOO_LONG, } /** * This class implements a singleton, so the only constructor is private. */ private PhoneNumberUtil() { } private void init(InputStream source) { // Read in metadata for each country. try { PhoneMetadataCollection metadataCollection = PhoneMetadataCollection.parseFrom(source); for (PhoneMetadata metadata : metadataCollection.getMetadataList()) { String regionCode = metadata.getId(); countryToMetadataMap.put(regionCode, metadata); int countryCode = metadata.getCountryCode(); switch (countryCode) { case NANPA_COUNTRY_CODE: nanpaCountries.add(regionCode); break; case RUSSIAN_FED_COUNTRY_CODE: russiaFederationCountries.add(regionCode); break; case FRENCH_INDIAN_OCEAN_COUNTRY_CODE: frenchIndianOceanTerritories.add(regionCode); break; default: countryCodeToRegionCodeMap.put(countryCode, regionCode); break; } } // Override the value, so that 1 is always mapped to US, 7 is always mapped to RU, and 262 to // RE. countryCodeToRegionCodeMap.put(NANPA_COUNTRY_CODE, "US"); countryCodeToRegionCodeMap.put(RUSSIAN_FED_COUNTRY_CODE, "RU"); countryCodeToRegionCodeMap.put(FRENCH_INDIAN_OCEAN_COUNTRY_CODE, "RE"); } catch (IOException e) { LOGGER.log(Level.WARNING, e.toString()); } } /** * Attempts to extract a possible number from the string passed in. This currently strips all * leading characters that could not be used to start a phone number. Characters that can be used * to start a phone number are defined in the VALID_START_CHAR_PATTERN. If none of these * characters are found in the number passed in, an empty string is returned. This function also * attempts to strip off any alternative extensions or endings if two or more are present, such as * in the case of: (530) 583-6985 x302/x2303. The second extension here makes this actually two * phone numbers, (530) 583-6985 x302 and (530) 583-6985 x2303. We remove the second extension so * that the first number is parsed correctly. * * @param number the string that might contain a phone number * @return the number, stripped of any non-phone-number prefix (such as "Tel:") or an empty * string if no character used to start phone numbers (such as + or any digit) is * found in the number */ @VisibleForTesting static String extractPossibleNumber(String number) { // Remove trailing non-alpha non-numerical characters. Matcher trailingCharsMatcher = UNWANTED_END_CHAR_PATTERN.matcher(number); if (trailingCharsMatcher.find()) { number = number.substring(0, trailingCharsMatcher.start()); LOGGER.log(Level.FINER, "Stripped trailing characters: " + number); } Matcher m = VALID_START_CHAR_PATTERN.matcher(number); if (m.find()) { number = number.substring(m.start()); // Check for extra numbers at the end. Matcher secondNumber = SECOND_NUMBER_START_PATTERN.matcher(number); if (secondNumber.find()) { number = number.substring(0, secondNumber.start()); } return number; } else { return ""; } } /** * Checks to see if the string of characters could possibly be a phone number at all. At the * moment, checks to see that the string begins with at least 3 digits, ignoring any punctuation * commonly found in phone numbers. * This method does not require the number to be normalized in advance - but does assume that * leading non-number symbols have been removed, such as by the method extractPossibleNumber. * * @param number string to be checked for viability as a phone number * @return true if the number could be a phone number of some sort, otherwise false */ @VisibleForTesting static boolean isViablePhoneNumber(String number) { if (number.length() < MIN_LENGTH_FOR_NSN) { return false; } Matcher m = VALID_PHONE_NUMBER_PATTERN.matcher(number); return m.matches(); } /** * Normalizes a string of characters representing a phone number. This performs the following * conversions: * Wide-ascii digits are converted to normal ASCII (European) digits. * Letters are converted to their numeric representation on a telephone keypad. The keypad * used here is the one defined in ITU Recommendation E.161. This is only done if there are * 3 or more letters in the number, to lessen the risk that such letters are typos - * otherwise alpha characters are stripped. * Punctuation is stripped. * Arabic-Indic numerals are converted to European numerals. * * @param number a string of characters representing a phone number * @return the normalized string version of the phone number */ static String normalize(String number) { Matcher m = VALID_ALPHA_PHONE_PATTERN.matcher(number); if (m.matches()) { return normalizeHelper(number, ALL_NORMALIZATION_MAPPINGS, true); } else { return normalizeHelper(number, DIGIT_MAPPINGS, true); } } /** * Normalizes a string of characters representing a phone number. This is a wrapper for * normalize(String number) but does in-place normalization of the StringBuffer provided. * * @param number a StringBuffer of characters representing a phone number that will be normalized * in place */ static void normalize(StringBuffer number) { String normalizedNumber = normalize(number.toString()); number.replace(0, number.length(), normalizedNumber); } /** * Normalizes a string of characters representing a phone number. This converts wide-ascii and * arabic-indic numerals to European numerals, and strips punctuation and alpha characters. * * @param number a string of characters representing a phone number * @return the normalized string version of the phone number */ public static String normalizeDigitsOnly(String number) { return normalizeHelper(number, DIGIT_MAPPINGS, true); } /** * Converts all alpha characters in a number to their respective digits on a keypad, but retains * existing formatting. Also converts wide-ascii digits to normal ascii digits, and converts * Arabic-Indic numerals to European numerals. */ public static String convertAlphaCharactersInNumber(String number) { return normalizeHelper(number, ALL_NORMALIZATION_MAPPINGS, false); } /** * Gets the length of the geographical area code from the national_number field of the PhoneNumber * object passed in, so that clients could use it to split a national significant number into * geographical area code and subscriber number. It works in such a way that the resultant * subscriber number should be diallable, at least on some devices. An example of how this could * be used: * * PhoneNumberUtil phoneUtil.PhoneNumberUtil.getInstance(); * PhoneNumber number = phoneUtil.parse("16502530000", RegionCode.US); * String nationalSignificantNumber = PhoneNumberUtil.getNationalSignificantNumber(number); * String areaCode; * String subscriberNumber; * * int areaCodeLength = phoneUtil.getLengthOfGeographicalAreaCode(number); * if (areaCodeLength > 0) { * areaCode = nationalSignificantNumber.substring(0, areaCodeLength); * subscriberNumber = nationalSignificantNumber.substring(areaCodeLength); * } else { * areaCode = ""; * subscriberNumber = nationalSignificantNumber; * } * * N.B.: area code is a very ambiguous concept, so the I18N team generally recommends against * using it for most purposes, but recommends using the more general national_number instead. Read * the following carefully before deciding to use this method: * * - geographical area codes change over time, and this method honors those changes; therefore, * it doesn't guarantee the stability of the result it produces. * - subscriber numbers may not be diallable from all devices (notably mobile devices, which * typically requires the full national_number to be dialled in most countries). * - most non-geographical numbers have no area codes. * - some geographical numbers have no area codes. * * @param number the PhoneNumber object for which clients want to know the length of the area * code in the national_number field. * @return the length of area code of the PhoneNumber object passed in. */ public int getLengthOfGeographicalAreaCode(PhoneNumber number) { String regionCode = getRegionCodeForNumber(number); if (regionCode == null || regionCode.equalsIgnoreCase("ZZ")) { return 0; } PhoneMetadata metadata = getMetadataForRegion(regionCode); // For NANPA countries, national prefix is the same as country code, but it is not stored in // the metadata. if (!metadata.hasNationalPrefix() && !isNANPACountry(regionCode)) { return 0; } PhoneNumberType type = getNumberTypeHelper(String.valueOf(number.getNationalNumber()), metadata); // Most numbers other than the two types below have to be dialled in full. if (type != PhoneNumberType.FIXED_LINE && type != PhoneNumberType.FIXED_LINE_OR_MOBILE) { return 0; } PhoneNumber copiedProto; if (number.hasExtension()) { // We don't want to alter the proto given to us, but we don't want to include the extension // when we format it, so we copy it and clear the extension here. PhoneNumber.Builder protoBuilder = PhoneNumber.newBuilder(); protoBuilder.mergeFrom(number); protoBuilder.clearExtension(); copiedProto = protoBuilder.build(); } else { copiedProto = number; } String nationalSignificantNumber = format(copiedProto, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); Pattern nonDigitPattern = Pattern.compile("(\\D+)"); String[] numberGroups = nonDigitPattern.split(nationalSignificantNumber); // The pattern will start with "+COUNTRY_CODE " so the first group will always be the empty // string (before the + symbol) and the second group will be the country code. The third group // will be area code if it is not the last group. if (numberGroups.length <= 3) { return 0; } // Note all countries that use leading zero in national number don't use national prefix, so // they won't have an area code, which means clients don't need to worry about appending the // leading zero to the geographical area code they derive from the length we return here. return numberGroups[2].length(); } /** * Normalizes a string of characters representing a phone number by replacing all characters found * in the accompanying map with the values therein, and stripping all other characters if * removeNonMatches is true. * * @param number a string of characters representing a phone number * @param normalizationReplacements a mapping of characters to what they should be replaced by in * the normalized version of the phone number * @param removeNonMatches indicates whether characters that are not able to be replaced * should be stripped from the number. If this is false, they * will be left unchanged in the number. * @return the normalized string version of the phone number */ private static String normalizeHelper(String number, Map normalizationReplacements, boolean removeNonMatches) { StringBuffer normalizedNumber = new StringBuffer(number.length()); char[] numberAsCharArray = number.toCharArray(); for (char character : numberAsCharArray) { Character newDigit = normalizationReplacements.get(Character.toUpperCase(character)); if (newDigit != null) { normalizedNumber.append(newDigit); } else if (!removeNonMatches) { normalizedNumber.append(character); } // If neither of the above are true, we remove this character. } return normalizedNumber.toString(); } @VisibleForTesting static synchronized PhoneNumberUtil getInstance(InputStream source) { if (instance == null) { instance = new PhoneNumberUtil(); instance.init(source); } return instance; } /** * Used for testing purposes only to reset the PhoneNumberUtil singleton to null. */ @VisibleForTesting static synchronized void resetInstance() { instance = null; } /** * Convenience method to enable tests to get a list of what countries the library has metadata * for. */ @VisibleForTesting Set getSupportedCountries() { return countryToMetadataMap.keySet(); } /** * Gets a PhoneNumberUtil instance to carry out international phone number formatting, parsing, * or validation. The instance is loaded with phone number metadata for a number of most commonly * used countries/regions. * * The PhoneNumberUtil is implemented as a singleton. Therefore, calling getInstance multiple * times will only result in one instance being created. * * @return a PhoneNumberUtil instance */ public static synchronized PhoneNumberUtil getInstance() { if (instance == null) { instance = new PhoneNumberUtil(); InputStream in = PhoneNumberUtil.class.getResourceAsStream(META_DATA_FILE); instance.init(in); } return instance; } /** * Helper function to check region code is not unknown or null. The countryCode and number * supplied is used only for the resultant log message. */ private boolean isValidRegionCode(String regionCode, int countryCode, String number) { if (regionCode == null || regionCode.equalsIgnoreCase("ZZ")) { LOGGER.log(Level.WARNING, "Number " + number + "has invalid or missing country code (" + countryCode + ")"); return false; } return true; } /** * Formats a phone number in the specified format using default rules. Note that this does not * promise to produce a phone number that the user can dial from where they are - although we do * format in either 'national' or 'international' format depending on what the client asks for, we * do not currently support a more abbreviated format, such as for users in the same "area" who * could potentially dial the number without area code. Note that if the phone number has a * country code of 0 or an otherwise invalid country code, we cannot work out which formatting * rules to apply so we return the national significant number with no formatting applied. * * @param number the phone number to be formatted * @param numberFormat the format the phone number should be formatted into * @return the formatted phone number */ public String format(PhoneNumber number, PhoneNumberFormat numberFormat) { int countryCode = number.getCountryCode(); String nationalSignificantNumber = getNationalSignificantNumber(number); if (numberFormat == PhoneNumberFormat.E164) { // Early exit for E164 case since no formatting of the national number needs to be applied. // Extensions are not formatted. return formatNumberByFormat(countryCode, PhoneNumberFormat.E164, nationalSignificantNumber, ""); } // Note here that all NANPA formatting rules are contained by US, so we use that to format NANPA // numbers. The same applies to Russian Fed countries - rules are contained by Russia. French // Indian Ocean country rules are contained by Reunion. String regionCode = getRegionCodeForCountryCode(countryCode); if (!isValidRegionCode(regionCode, countryCode, nationalSignificantNumber)) { return nationalSignificantNumber; } String formattedExtension = maybeGetFormattedExtension(number, regionCode); return formatNumberByFormat(countryCode, numberFormat, formatNationalNumber(nationalSignificantNumber, regionCode, numberFormat), formattedExtension); } /** * Formats a phone number in the specified format using client-defined formatting rules. Note that * if the phone number has a country code of zero or an otherwise invalid country code, we cannot * work out things like whether there should be a national prefix applied, or how to format * extensions, so we return the national significant number with no formatting applied. * * @param number the phone number to be formatted * @param numberFormat the format the phone number should be formatted into * @param userDefinedFormats formatting rules specified by clients * @return the formatted phone number */ public String formatByPattern(PhoneNumber number, PhoneNumberFormat numberFormat, List userDefinedFormats) { int countryCode = number.getCountryCode(); // Note getRegionCodeForCountryCode() is used because formatting information for countries which // share a country code is contained by only one country for performance reasons. For example, // for NANPA countries it will be contained in the metadata for US. String regionCode = getRegionCodeForCountryCode(countryCode); String nationalSignificantNumber = getNationalSignificantNumber(number); if (!isValidRegionCode(regionCode, countryCode, nationalSignificantNumber)) { return nationalSignificantNumber; } int size = userDefinedFormats.size(); for (int i = 0; i < size; i++) { NumberFormat numFormat = userDefinedFormats.get(i); String nationalPrefixFormattingRule = numFormat.getNationalPrefixFormattingRule(); if (nationalPrefixFormattingRule.length() > 0) { String nationalPrefix = getMetadataForRegion(regionCode).getNationalPrefix(); // Replace $NP with national prefix and $FG with the first group ($1). nationalPrefixFormattingRule = nationalPrefixFormattingRule.replaceFirst("\\$NP", nationalPrefix) .replaceFirst("\\$FG", "\\$1"); userDefinedFormats.set(i, NumberFormat.newBuilder(numFormat) .setNationalPrefixFormattingRule(nationalPrefixFormattingRule).build()); } } String formattedExtension = maybeGetFormattedExtension(number, regionCode); return formatNumberByFormat(countryCode, numberFormat, formatAccordingToFormats(nationalSignificantNumber, userDefinedFormats, numberFormat), formattedExtension); } /** * Formats a phone number for out-of-country dialing purpose. If no countryCallingFrom * is supplied, we format the number in its INTERNATIONAL format. If the countryCallingFrom is * the same as the country where the number is from, then NATIONAL formatting will be applied. * * If the number itself has a country code of zero or an otherwise invalid country code, then we * return the number with no formatting applied. * * Note this function takes care of the case for calling inside of NANPA and between Russia and * Kazakhstan (who share the same country code). In those cases, no international prefix is used. * For countries which have multiple international prefixes, the number in its INTERNATIONAL * format will be returned instead. * * @param number the phone number to be formatted * @param countryCallingFrom the ISO 3166-1 two-letter country code that denotes the foreign * country where the call is being placed * @return the formatted phone number */ public String formatOutOfCountryCallingNumber(PhoneNumber number, String countryCallingFrom) { if (countryCallingFrom == null || countryCallingFrom.equalsIgnoreCase("ZZ")) { LOGGER.log(Level.WARNING, "Trying to format number from invalid region. International formatting applied."); return format(number, PhoneNumberFormat.INTERNATIONAL); } countryCallingFrom = countryCallingFrom.toUpperCase(); int countryCode = number.getCountryCode(); if (countryCode == NANPA_COUNTRY_CODE && isNANPACountry(countryCallingFrom)) { // For NANPA countries, return the national format for these countries but prefix it with the // country code. return countryCode + " " + format(number, PhoneNumberFormat.NATIONAL); } if (countryCode == FRENCH_INDIAN_OCEAN_COUNTRY_CODE && frenchIndianOceanTerritories.contains(countryCallingFrom)) { // For dialling between FRENCH_INDIAN_OCEAN countries, the 10 digit number is all we need. // Technically this is the case for dialling from la Reunion to other overseas departments of // France (French Guiana, Martinique, Guadeloupe), but not vice versa - so we don't cover this // edge case for now and for those cases return the version including country code. // Details here: http://www.petitfute.com/voyage/225-info-pratiques-reunion return format(number, PhoneNumberFormat.NATIONAL); } // If the country code is the Russian Fed country code, we check the number itself to determine // which region code it is for. We don't do this for NANPA countries because of performance // reasons, and instead use US rules for all NANPA numbers. Also, NANPA countries share the // same national and international prefixes, which is not the case for Russian Fed countries. // There is also a special case for toll-free and premium rate numbers dialled within Russian // Fed countries. String regionCode; if (countryCode == RUSSIAN_FED_COUNTRY_CODE) { if (russiaFederationCountries.contains(countryCallingFrom)) { // For toll-free numbers and premium rate numbers dialled from within Russian Fed countries, // we should format them as if they are local numbers. // A toll-free number would be dialled from KZ as 8-800-080-7777 but from Russia as // 0-800-080-7777. (Confirmation on government websites such as e.gov.kz). PhoneNumberType numberType = getNumberType(number); if (numberType == PhoneNumberType.TOLL_FREE || numberType == PhoneNumberType.PREMIUM_RATE) { return format(number, PhoneNumberFormat.NATIONAL); } } // Otherwise, we should find out what region the number really belongs to before continuing, // since they have different formatting rules. regionCode = getRegionCodeForNumber(number); } else { regionCode = getRegionCodeForCountryCode(countryCode); } String nationalSignificantNumber = getNationalSignificantNumber(number); if (!isValidRegionCode(regionCode, countryCode, nationalSignificantNumber)) { return nationalSignificantNumber; } if (regionCode.equals(countryCallingFrom)) { return format(number, PhoneNumberFormat.NATIONAL); } String formattedNationalNumber = formatNationalNumber(nationalSignificantNumber, regionCode, PhoneNumberFormat.INTERNATIONAL); PhoneMetadata metadata = getMetadataForRegion(countryCallingFrom); String internationalPrefix = metadata.getInternationalPrefix(); String formattedExtension = maybeGetFormattedExtension(number, regionCode); // For countries that have multiple international prefixes, the international format of the // number is returned, unless there is a preferred international prefix. String internationalPrefixForFormatting = ""; if (UNIQUE_INTERNATIONAL_PREFIX.matcher(internationalPrefix).matches()) { internationalPrefixForFormatting = internationalPrefix; } else if (metadata.hasPreferredInternationalPrefix()) { internationalPrefixForFormatting = metadata.getPreferredInternationalPrefix(); } return !internationalPrefixForFormatting.equals("") ? internationalPrefixForFormatting + " " + countryCode + " " + formattedNationalNumber + formattedExtension : formatNumberByFormat(countryCode, PhoneNumberFormat.INTERNATIONAL, formattedNationalNumber, formattedExtension); } /** * Gets the national significant number of the a phone number. Note a national significant number * doesn't contain a national prefix or any formatting. * * @param number the PhoneNumber object for which the national significant number is needed * @return the national significant number of the PhoneNumber object passed in */ public static String getNationalSignificantNumber(PhoneNumber number) { // The leading zero in the national (significant) number of an Italian phone number has a // special meaning. Unlike the rest of the world, it indicates the number is a landline // number. There have been plans to migrate landline numbers to start with the digit two since // December 2000, but it has not yet happened. // See http://en.wikipedia.org/wiki/%2B39 for more details. // Other countries such as Cote d'Ivoire and Gabon use this for their mobile numbers. StringBuffer nationalNumber = new StringBuffer( (isLeadingZeroCountry(number.getCountryCode()) && number.hasItalianLeadingZero() && number.getItalianLeadingZero()) ? "0" : "" ); nationalNumber.append(number.getNationalNumber()); return nationalNumber.toString(); } /** * A helper function that is used by format and formatByPattern. */ private String formatNumberByFormat(int countryCode, PhoneNumberFormat numberFormat, String formattedNationalNumber, String formattedExtension) { switch (numberFormat) { case E164: return String.valueOf(PLUS_SIGN) + countryCode + formattedNationalNumber + formattedExtension; case INTERNATIONAL: return String.valueOf(PLUS_SIGN) + countryCode + " " + formattedNationalNumber + formattedExtension; case NATIONAL: default: return formattedNationalNumber + formattedExtension; } } // Note in some countries, the national number can be written in two completely different ways // depending on whether it forms part of the NATIONAL format or INTERNATIONAL format. The // numberFormat parameter here is used to specify which format to use for those cases. private String formatNationalNumber(String number, String regionCode, PhoneNumberFormat numberFormat) { PhoneMetadata metadata = getMetadataForRegion(regionCode); List intlNumberFormats = metadata.getIntlNumberFormatList(); // When the intlNumberFormats exists, we use that to format national number for the // INTERNATIONAL format instead of using the numberDesc.numberFormats. List availableFormats = (intlNumberFormats.size() == 0 || numberFormat == PhoneNumberFormat.NATIONAL) ? metadata.getNumberFormatList() : metadata.getIntlNumberFormatList(); return formatAccordingToFormats(number, availableFormats, numberFormat); } private String formatAccordingToFormats(String nationalNumber, List availableFormats, PhoneNumberFormat numberFormat) { for (NumberFormat numFormat : availableFormats) { if (!numFormat.hasLeadingDigits() || Pattern.compile(numFormat.getLeadingDigits()).matcher(nationalNumber).lookingAt()) { String patternToMatch = numFormat.getPattern(); if (nationalNumber.matches(patternToMatch)) { String nationalPrefixFormattingRule = numFormat.getNationalPrefixFormattingRule(); if (numberFormat == PhoneNumberFormat.NATIONAL && nationalPrefixFormattingRule != null && nationalPrefixFormattingRule.length() > 0) { return nationalNumber.replaceAll( patternToMatch, numFormat.getFormat().replaceFirst("(\\$1)", nationalPrefixFormattingRule)); } else { return nationalNumber.replaceAll(patternToMatch, numFormat.getFormat()); } } } } // If no pattern above is matched, we format the number as a whole. return nationalNumber; } /** * Gets a valid number for the specified country. * * @param regionCode the ISO 3166-1 two-letter country code that denotes the country for which * an example number is needed * @return a valid fixed-line number for the specified country. Returns null when the metadata * does not contain such information. */ public PhoneNumber getExampleNumber(String regionCode) { regionCode = regionCode.toUpperCase(); return getExampleNumberForType(regionCode, PhoneNumberType.FIXED_LINE); } /** * Gets a valid number for the specified country and number type. * * @param regionCode the ISO 3166-1 two-letter country code that denotes the country for which * an example number is needed * @param type the type of number that is needed * @return a valid number for the specified country and type. Returns null when the metadata * does not contain such information. */ public PhoneNumber getExampleNumberForType(String regionCode, PhoneNumberType type) { regionCode = regionCode.toUpperCase(); PhoneNumberDesc desc = getNumberDescByType(getMetadataForRegion(regionCode), type); try { if (desc.hasExampleNumber()) { return parse(desc.getExampleNumber(), regionCode); } } catch (NumberParseException e) { LOGGER.log(Level.SEVERE, e.toString()); } return null; } /** * Gets the formatted extension of a phone number, if the phone number had an extension specified. * If not, it returns an empty string. */ private String maybeGetFormattedExtension(PhoneNumber number, String regionCode) { if (!number.hasExtension()) { return ""; } else { return formatExtension(number.getExtension(), regionCode); } } /** * Formats the extension part of the phone number by prefixing it with the appropriate extension * prefix. This will be the default extension prefix, unless overridden by a preferred * extension prefix for this country. */ private String formatExtension(String extensionDigits, String regionCode) { PhoneMetadata metadata = getMetadataForRegion(regionCode); if (metadata.hasPreferredExtnPrefix()) { return metadata.getPreferredExtnPrefix() + extensionDigits; } else { return DEFAULT_EXTN_PREFIX + extensionDigits; } } PhoneNumberDesc getNumberDescByType(PhoneMetadata metadata, PhoneNumberType type) { switch (type) { case PREMIUM_RATE: return metadata.getPremiumRate(); case TOLL_FREE: return metadata.getTollFree(); case MOBILE: return metadata.getMobile(); case FIXED_LINE: case FIXED_LINE_OR_MOBILE: return metadata.getFixedLine(); case SHARED_COST: return metadata.getSharedCost(); case VOIP: return metadata.getVoip(); case PERSONAL_NUMBER: return metadata.getPersonalNumber(); default: return metadata.getGeneralDesc(); } } /** * Gets the type of a phone number. * * @param number the phone number that we want to know the type * @return the type of the phone number */ public PhoneNumberType getNumberType(PhoneNumber number) { String regionCode = getRegionCodeForNumber(number); String nationalSignificantNumber = getNationalSignificantNumber(number); if (!isValidRegionCode(regionCode, number.getCountryCode(), nationalSignificantNumber)) { return PhoneNumberType.UNKNOWN; } return getNumberTypeHelper(nationalSignificantNumber, getMetadataForRegion(regionCode)); } private PhoneNumberType getNumberTypeHelper(String nationalNumber, PhoneMetadata metadata) { PhoneNumberDesc generalNumberDesc = metadata.getGeneralDesc(); if (!generalNumberDesc.hasNationalNumberPattern() || !isNumberMatchingDesc(nationalNumber, generalNumberDesc)) { LOGGER.log(Level.FINEST, "Number type unknown - doesn't match general national number pattern."); return PhoneNumberType.UNKNOWN; } if (isNumberMatchingDesc(nationalNumber, metadata.getPremiumRate())) { LOGGER.log(Level.FINEST, "Number is a premium number."); return PhoneNumberType.PREMIUM_RATE; } if (isNumberMatchingDesc(nationalNumber, metadata.getTollFree())) { LOGGER.log(Level.FINEST, "Number is a toll-free number."); return PhoneNumberType.TOLL_FREE; } if (isNumberMatchingDesc(nationalNumber, metadata.getSharedCost())) { LOGGER.log(Level.FINEST, "Number is a shared cost number."); return PhoneNumberType.SHARED_COST; } if (isNumberMatchingDesc(nationalNumber, metadata.getVoip())) { LOGGER.log(Level.FINEST, "Number is a VOIP (Voice over IP) number."); return PhoneNumberType.VOIP; } if (isNumberMatchingDesc(nationalNumber, metadata.getPersonalNumber())) { LOGGER.log(Level.FINEST, "Number is a personal number."); return PhoneNumberType.PERSONAL_NUMBER; } boolean isFixedLine = isNumberMatchingDesc(nationalNumber, metadata.getFixedLine()); if (isFixedLine) { if (metadata.getSameMobileAndFixedLinePattern()) { LOGGER.log(Level.FINEST, "Fixed-line and mobile patterns equal, number is fixed-line or mobile"); return PhoneNumberType.FIXED_LINE_OR_MOBILE; } else if (isNumberMatchingDesc(nationalNumber, metadata.getMobile())) { LOGGER.log(Level.FINEST, "Fixed-line and mobile patterns differ, but number is " + "still fixed-line or mobile"); return PhoneNumberType.FIXED_LINE_OR_MOBILE; } LOGGER.log(Level.FINEST, "Number is a fixed line number."); return PhoneNumberType.FIXED_LINE; } // Otherwise, test to see if the number is mobile. Only do this if certain that the patterns for // mobile and fixed line aren't the same. if (!metadata.getSameMobileAndFixedLinePattern() && isNumberMatchingDesc(nationalNumber, metadata.getMobile())) { LOGGER.log(Level.FINEST, "Number is a mobile number."); return PhoneNumberType.MOBILE; } LOGGER.log(Level.FINEST, "Number type unknown - doesn't match any specific number type pattern."); return PhoneNumberType.UNKNOWN; } PhoneMetadata getMetadataForRegion(String regionCode) { if (regionCode == null) { return null; } return countryToMetadataMap.get(regionCode); } private boolean isNumberMatchingDesc(String nationalNumber, PhoneNumberDesc numberDesc) { return nationalNumber.matches(numberDesc.getPossibleNumberPattern()) && nationalNumber.matches(numberDesc.getNationalNumberPattern()); } /** * Tests whether a phone number matches a valid pattern. Note this doesn't verify the number * is actually in use, which is impossible to tell by just looking at a number itself. * * @param number the phone number that we want to validate * @return a boolean that indicates whether the number is of a valid pattern */ public boolean isValidNumber(PhoneNumber number) { String regionCode = getRegionCodeForNumber(number); return isValidRegionCode(regionCode, number.getCountryCode(), getNationalSignificantNumber(number)) && isValidNumberForRegion(number, regionCode); } /** * Tests whether a phone number is valid for a certain region. Note this doesn't verify the number * is actually in use, which is impossible to tell by just looking at a number itself. If the * country code is not the same as the country code for the region, this immediately exits with * false. After this, the specific number pattern rules for the region are examined. This is * useful for determining for example whether a particular number is valid for Canada, rather than * just a valid NANPA number. * * @param number the phone number that we want to validate * @param regionCode the ISO 3166-1 two-letter country code that denotes the region/country * that we want to validate the phone number for * @return a boolean that indicates whether the number is of a valid pattern */ public boolean isValidNumberForRegion(PhoneNumber number, String regionCode) { regionCode = regionCode.toUpperCase(); if (number.getCountryCode() != getCountryCodeForRegion(regionCode)) { return false; } PhoneMetadata metadata = getMetadataForRegion(regionCode); PhoneNumberDesc generalNumDesc = metadata.getGeneralDesc(); String nationalSignificantNumber = getNationalSignificantNumber(number); // For countries where we don't have metadata for PhoneNumberDesc, we treat any number passed // in as a valid number if its national significant number is between the minimum and maximum // lengths defined by ITU for a national significant number. if (!generalNumDesc.hasNationalNumberPattern()) { LOGGER.log(Level.FINER, "Validating number with incomplete metadata."); int numberLength = nationalSignificantNumber.length(); return numberLength > MIN_LENGTH_FOR_NSN && numberLength <= MAX_LENGTH_FOR_NSN; } return isNumberMatchingDesc(nationalSignificantNumber, generalNumDesc) && getNumberTypeHelper(nationalSignificantNumber, metadata) != PhoneNumberType.UNKNOWN; } /** * Returns the country/region where a phone number is from. This could be used for geo-coding in * the country/region level. * * @param number the phone number whose origin we want to know * @return the country/region where the phone number is from */ public String getRegionCodeForNumber(PhoneNumber number) { int countryCode = number.getCountryCode(); switch (countryCode) { case NANPA_COUNTRY_CODE: // Override this and try the US case first, since it is more likely than other countries, // for performance reasons. String nationalNumber = getNationalSignificantNumber(number); if (getNumberTypeHelper(nationalNumber, getMetadataForRegion("US")) != PhoneNumberType.UNKNOWN) { return "US"; } Set nanpaExceptUS = new HashSet(nanpaCountries); nanpaExceptUS.remove("US"); return getRegionCodeForNumberFromRegionList(number, nanpaExceptUS); case RUSSIAN_FED_COUNTRY_CODE: return getRegionCodeForNumberFromRegionList(number, russiaFederationCountries); case FRENCH_INDIAN_OCEAN_COUNTRY_CODE: return getRegionCodeForNumberFromRegionList(number, frenchIndianOceanTerritories); default: return getRegionCodeForCountryCode(countryCode); } } private String getRegionCodeForNumberFromRegionList(PhoneNumber number, Set regionCodes) { String nationalNumber = String.valueOf(number.getNationalNumber()); for (String regionCode : regionCodes) { if (getNumberTypeHelper(nationalNumber, getMetadataForRegion(regionCode)) != PhoneNumberType.UNKNOWN) { return regionCode; } } return null; } /** * Returns the region code that matches the specific country code. In the case of no region code * being found, ZZ will be returned. */ String getRegionCodeForCountryCode(int countryCode) { String regionCode = countryCodeToRegionCodeMap.get(countryCode); return regionCode == null ? "ZZ" : regionCode; } /** * Returns the country calling code for a specific region. For example, this would be 1 for the * United States, and 64 for New Zealand. * * @param regionCode the ISO 3166-1 two-letter country code that denotes the country/region that * we want to get the country code for * @return the country calling code for the country/region denoted by regionCode */ public int getCountryCodeForRegion(String regionCode) { if (regionCode == null || regionCode.equalsIgnoreCase("ZZ")) { LOGGER.log(Level.SEVERE, "Invalid or missing country code provided."); return 0; } PhoneMetadata metadata = getMetadataForRegion(regionCode.toUpperCase()); if (metadata == null) { LOGGER.log(Level.SEVERE, "Unsupported country code provided."); return 0; } return metadata.getCountryCode(); } /** * Check if a country is one of the countries under the North American Numbering Plan * Administration (NANPA). * * @return true if regionCode is one of the countries under NANPA */ public boolean isNANPACountry(String regionCode) { regionCode = regionCode.toUpperCase(); return nanpaCountries.contains(regionCode); } /** * Convenience wrapper around isPossibleNumberWithReason. Instead of returning the reason for * failure, this method returns a boolean value. * @param number the number that needs to be checked * @return true if the number is possible */ public boolean isPossibleNumber(PhoneNumber number) { return isPossibleNumberWithReason(number) == ValidationResult.IS_POSSIBLE; } /** * Check whether countryCode represents the country calling code from a country whose national * significant number could contain a leading zero. An example of such a country is Italy. */ public static boolean isLeadingZeroCountry(int countryCode) { return LEADING_ZERO_COUNTRIES.contains(countryCode); } /** * Check whether a phone number is a possible number. It provides a more lenient check than * isValidNumber in the following sense: * 1. It only checks the length of phone numbers. In particular, it doesn't check starting * digits of the number. * 2. It doesn't attempt to figure out the type of the number, but uses general rules which * applies to all types of phone numbers in a country. Therefore, it is much faster than * isValidNumber. * 3. For fixed line numbers, many countries have the concept of area code, which together with * subscriber number constitute the national significant number. It is sometimes okay to dial * the subscriber number only when dialing in the same area. This function will return * true if the subscriber-number-only version is passed in. On the other hand, because * isValidNumber validates using information on both starting digits (for fixed line * numbers, that would most likely be area codes) and length (obviously includes the * length of area codes for fixed line numbers), it will return false for the * subscriber-number-only version. * * @param number the number that needs to be checked * @return a ValidationResult object which indicates whether the number is possible */ public ValidationResult isPossibleNumberWithReason(PhoneNumber number) { String nationalNumber = getNationalSignificantNumber(number); int countryCode = number.getCountryCode(); // Note: For Russian Fed and NANPA numbers, we just use the rules from the default region (US or // Russia) since the getRegionCodeForNumber will not work if the number is possible but not // valid. This would need to be revisited if the possible number pattern ever differed between // various countries within those plans. String regionCode = getRegionCodeForCountryCode(countryCode); if (!isValidRegionCode(regionCode, countryCode, nationalNumber)) { return ValidationResult.INVALID_COUNTRY_CODE; } PhoneNumberDesc generalNumDesc = getMetadataForRegion(regionCode).getGeneralDesc(); String possibleNumberPattern = generalNumDesc.getPossibleNumberPattern(); Matcher m = Pattern.compile(possibleNumberPattern).matcher(nationalNumber); if (m.lookingAt()) { return (m.end() == nationalNumber.length()) ? ValidationResult.IS_POSSIBLE : ValidationResult.TOO_LONG; } else { return ValidationResult.TOO_SHORT; } } /** * Check whether a phone number is a possible number given a number in the form of a string, and * the country where the number could be dialed from. It provides a more lenient check than * isValidNumber. See isPossibleNumber(PhoneNumber number) for details. * * This method first parses the number, then invokes isPossibleNumber(PhoneNumber number) with the * resultant PhoneNumber object. * * @param number the number that needs to be checked, in the form of a string * @param countryDialingFrom the ISO 3166-1 two-letter country code that denotes * the country that we are expecting the number to be dialed from. * Note this is different from the country where the number belongs. * For example, the number +1 650 253 0000 is a number that belongs to US. * When written in this form, it could be dialed from any country. * When it is written as 00 1 650 253 0000, it could be dialed from * any country which has international prefix 00. When it is written as * 650 253 0000, it could only be dialed from US, and when written as * 253 0000, it could only be dialed from US (Mountain View, CA, to be * more specific). * @return true if the number is possible */ public boolean isPossibleNumber(String number, String countryDialingFrom) { countryDialingFrom = countryDialingFrom.toUpperCase(); try { return isPossibleNumber(parse(number, countryDialingFrom)); } catch (NumberParseException e) { return false; } } /** * Gets an AsYouTypeFormatter for the specific country. Note this function doesn't attempt to * figure out the types of phone number being entered on the fly due to performance reasons. * Instead, it tries to apply a standard format to all types of phone numbers. For countries * where different types of phone numbers follow different formats, the formatter returned * will do no formatting but output exactly what is fed into the inputDigit method. * * If the type of the phone number being entered is known beforehand, use * getAsYouTypeFormatterByType instead. * * @param regionCode the ISO 3166-1 two-letter country code that denotes the country/region * where the phone number is being entered * @return an AsYouTypeFormatter object, which could be used to format phone numbers in the * specific country "as you type" */ public AsYouTypeFormatter getAsYouTypeFormatter(String regionCode) { regionCode = regionCode.toUpperCase(); return new AsYouTypeFormatter(regionCode); } // Extracts country code from fullNumber, returns it and places the remaining number in // nationalNumber. It assumes that the leading plus sign or IDD has already been removed. Returns // 0 if fullNumber doesn't start with a valid country code, and leaves nationalNumber unmodified. int extractCountryCode(StringBuffer fullNumber, StringBuffer nationalNumber) { int potentialCountryCode; for (int i = 1; i <= 3; i++) { potentialCountryCode = Integer.parseInt(fullNumber.substring(0, i)); if (countryCodeToRegionCodeMap.containsKey(potentialCountryCode)) { nationalNumber.append(fullNumber.substring(i)); return potentialCountryCode; } } return 0; } /** * Tries to extract a country code from a number. This method will return zero if no country code * is considered to be present. Country codes are extracted in the following ways: * - by stripping the international dialing prefix of the country the person is dialing from, * if this is present in the number, and looking at the next digits * - by stripping the '+' sign if present and then looking at the next digits * - by comparing the start of the number and the country code of the default region. If the * number is not considered possible for the numbering plan of the default region initially, * but starts with the country code of this region, validation will be reattempted after * stripping this country code. If this number is considered a possible number, then the * first digits will be considered the country code and removed as such. * * It will throw a NumberParseException if the number starts with a '+' but the country code * supplied after this does not match that of any known country. * * @param number non-normalized telephone number that we wish to extract a country * code from - may begin with '+' * @param defaultRegionMetadata metadata about the region this number may be from * @param nationalNumber a string buffer to store the national significant number in, in the case * that a country code was extracted. The number is appended to any existing contents. If no * country code was extracted, this will be left unchanged. * @param storeCountryCodeSource true if the country_code_source field of phoneNumber should be * populated. * @param phoneNumber the PhoneNumber.Builder object that needs to be populated with country code * and country code source. Note the country code is always populated, whereas country code * source is only populated when keepCountryCodeSource is true. * @return the country code extracted or 0 if none could be extracted */ @VisibleForTesting int maybeExtractCountryCode(String number, PhoneMetadata defaultRegionMetadata, StringBuffer nationalNumber, boolean storeCountryCodeSource, PhoneNumber.Builder phoneNumber) throws NumberParseException { if (number.length() == 0) { return 0; } StringBuffer fullNumber = new StringBuffer(number); // Set the default prefix to be something that will never match. String possibleCountryIddPrefix = "NonMatch"; if (defaultRegionMetadata != null) { possibleCountryIddPrefix = defaultRegionMetadata.getInternationalPrefix(); } CountryCodeSource countryCodeSource = maybeStripInternationalPrefixAndNormalize(fullNumber, possibleCountryIddPrefix); if (storeCountryCodeSource) { phoneNumber.setCountryCodeSource(countryCodeSource); } if (countryCodeSource != CountryCodeSource.FROM_DEFAULT_COUNTRY) { if (fullNumber.length() < MIN_LENGTH_FOR_NSN) { throw new NumberParseException(NumberParseException.ErrorType.TOO_SHORT_AFTER_IDD, "Phone number had an IDD, but after this was not " + "long enough to be a viable phone number."); } int potentialCountryCode = extractCountryCode(fullNumber, nationalNumber); if (potentialCountryCode != 0) { phoneNumber.setCountryCode(potentialCountryCode); return potentialCountryCode; } // If this fails, they must be using a strange country code that we don't recognize, or // that doesn't exist. throw new NumberParseException(NumberParseException.ErrorType.INVALID_COUNTRY_CODE, "Country code supplied was not recognised."); } else if (defaultRegionMetadata != null) { // Check to see if the number is valid for the default region already. If not, we check to // see if the country code for the default region is present at the start of the number. Pattern validNumberPattern = Pattern.compile(defaultRegionMetadata.getGeneralDesc().getNationalNumberPattern()); if (!validNumberPattern.matcher(fullNumber).matches()) { int defaultCountryCode = defaultRegionMetadata.getCountryCode(); String defaultCountryCodeString = String.valueOf(defaultCountryCode); String normalizedNumber = fullNumber.toString(); if (normalizedNumber.startsWith(defaultCountryCodeString)) { // If so, strip this, and see if the resultant number is valid. StringBuffer potentialNationalNumber = new StringBuffer(normalizedNumber.substring(defaultCountryCodeString.length())); maybeStripNationalPrefix( potentialNationalNumber, defaultRegionMetadata.getNationalPrefixForParsing(), defaultRegionMetadata.getNationalPrefixTransformRule(), validNumberPattern); if (validNumberPattern.matcher(potentialNationalNumber).matches()) { nationalNumber.append(potentialNationalNumber); if (storeCountryCodeSource) { phoneNumber.setCountryCodeSource(CountryCodeSource.FROM_NUMBER_WITHOUT_PLUS_SIGN); } phoneNumber.setCountryCode(defaultCountryCode); return defaultCountryCode; } } } } // No country code present. phoneNumber.setCountryCode(0); return 0; } /** * Strips the IDD from the start of the number if present. Helper function used by * maybeStripInternationalPrefixAndNormalize. */ private boolean parsePrefixAsIdd(Pattern iddPattern, StringBuffer number) { Matcher m = iddPattern.matcher(number); if (m.lookingAt()) { int matchEnd = m.end(); // Only strip this if the first digit after the match is not a 0, since country codes cannot // begin with 0. Matcher digitMatcher = CAPTURING_DIGIT_PATTERN.matcher(number.substring(matchEnd)); if (digitMatcher.find()) { String normalizedGroup = normalizeHelper(digitMatcher.group(1), DIGIT_MAPPINGS, true); if (normalizedGroup.equals("0")) { return false; } } number.delete(0, matchEnd); return true; } return false; } /** * Strips any international prefix (such as +, 00, 011) present in the number provided, normalizes * the resulting number, and indicates if an international prefix was present. * * @param number the non-normalized telephone number that we wish to strip any international * dialing prefix from * @param possibleIddPrefix the international direct dialing prefix from the country we * think this number may be dialed in * @return the corresponding CountryCodeSource if an international dialing prefix could be * removed from the number, otherwise CountryCodeSource.FROM_DEFAULT_COUNTRY if the number * did not seem to be in international format. */ @VisibleForTesting CountryCodeSource maybeStripInternationalPrefixAndNormalize( StringBuffer number, String possibleIddPrefix) { if (number.length() == 0) { return CountryCodeSource.FROM_DEFAULT_COUNTRY; } if (number.charAt(0) == PLUS_SIGN) { number.deleteCharAt(0); // Can now normalize the rest of the number since we've consumed the "+" sign at the start. normalize(number); return CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN; } // Attempt to parse the first digits as an international prefix. Pattern iddPattern = Pattern.compile(possibleIddPrefix); if (parsePrefixAsIdd(iddPattern, number)) { normalize(number); return CountryCodeSource.FROM_NUMBER_WITH_IDD; } // If still not found, then try and normalize the number and then try again. This shouldn't be // done before, since non-numeric characters (+ and ~) may legally be in the international // prefix. normalize(number); return parsePrefixAsIdd(iddPattern, number) ? CountryCodeSource.FROM_NUMBER_WITH_IDD : CountryCodeSource.FROM_DEFAULT_COUNTRY; } /** * Strips any national prefix (such as 0, 1) present in the number provided. * * @param number the normalized telephone number that we wish to strip any national * dialing prefix from * @param possibleNationalPrefix a regex that represents the national direct dialing prefix * from the country we think this number may be dialed in * @param transformRule the string that specifies how number should be transformed according * to the regex specified in possibleNationalPrefix * @param nationalNumberRule a regular expression that specifies what a valid phonenumber from * this region should look like after any national prefix was stripped or transformed */ @VisibleForTesting void maybeStripNationalPrefix(StringBuffer number, String possibleNationalPrefix, String transformRule, Pattern nationalNumberRule) { int numberLength = number.length(); if (numberLength == 0 || possibleNationalPrefix.equals("")) { // Early return for numbers of zero length. return; } // Attempt to parse the first digits as a national prefix. Matcher m = Pattern.compile(possibleNationalPrefix).matcher(number); if (m.lookingAt()) { // m.group(1) == null implies nothing was captured by the capturing groups in // possibleNationalPrefix; therefore, no transformation is necessary, and we // just remove the national prefix. if (transformRule == null || transformRule.equals("") || m.group(1) == null) { // Check that the resultant number is viable. If not, return. Matcher nationalNumber = nationalNumberRule.matcher(number.substring(m.end())); if (!nationalNumber.matches()) { return; } number.delete(0, m.end()); } else { // Check that the resultant number is viable. If not, return. Check this by copying the // string buffer and making the transformation on the copy first. StringBuffer transformedNumber = new StringBuffer(number); transformedNumber.replace(0, numberLength, m.replaceFirst(transformRule)); Matcher nationalNumber = nationalNumberRule.matcher(transformedNumber.toString()); if (!nationalNumber.matches()) { return; } number.replace(0, number.length(), transformedNumber.toString()); } } } /** * Strips any extension (as in, the part of the number dialled after the call is connected, * usually indicated with extn, ext, x or similar) from the end of the number, and returns it. * * @param number the non-normalized telephone number that we wish to strip the extension from * @return the phone extension */ @VisibleForTesting String maybeStripExtension(StringBuffer number) { Matcher m = EXTN_PATTERN.matcher(number); // If we find a potential extension, and the number preceding this is a viable number, we assume // it is an extension. if (m.find() && isViablePhoneNumber(number.substring(0, m.start()))) { // The numbers are captured into groups in the regular expression. for (int i = 1; i <= m.groupCount(); i++) { if (m.group(i) != null) { // We go through the capturing groups until we find one that captured some digits. If none // did, then we will return the empty string. String extension = m.group(i); number.delete(m.start(), number.length()); return extension; } } } return ""; } /** * Parses a string and returns it in proto buffer format. This method will throw a * NumberParseException exception if the number is not considered to be a possible number. Note * that validation of whether the number is actually a valid number for a particular * country/region is not performed. This can be done separately with isValidNumber. * * @param numberToParse number that we are attempting to parse. This can contain formatting * such as +, ( and -, as well as a phone number extension. * @param defaultCountry the ISO 3166-1 two-letter country code that denotes the country that * we are expecting the number to be from. This is only used * if the number being parsed is not written in international format. * The country code for the number in this case would be stored as that * of the default country supplied. * @return a phone number proto buffer filled with the parsed number * @throws NumberParseException if the string is not considered to be a viable phone number or if * no default country was supplied */ public PhoneNumber parse(String numberToParse, String defaultCountry) throws NumberParseException { if (defaultCountry == null || defaultCountry.equalsIgnoreCase("ZZ")) { throw new NumberParseException(NumberParseException.ErrorType.INVALID_COUNTRY_CODE, "No default country was supplied."); } return parseHelper(numberToParse, defaultCountry.toUpperCase(), false); } /** * Parses a string and returns it in proto buffer format. This method differs from parse() in that * it always populates the raw_input field of the protocol buffer with numberToParse as well as * the country_code_source field. * * @param numberToParse number that we are attempting to parse. This can contain formatting * such as +, ( and -, as well as a phone number extension. * @param defaultCountry the ISO 3166-1 two-letter country code that denotes the country that * we are expecting the number to be from. This is only used * if the number being parsed is not written in international format. * The country code for the number in this case would be stored as that * of the default country supplied. * @return a phone number proto buffer filled with the parsed number * @throws NumberParseException if the string is not considered to be a viable phone number or if * no default country was supplied */ public PhoneNumber parseAndKeepRawInput(String numberToParse, String defaultCountry) throws NumberParseException { if (defaultCountry == null || defaultCountry.equalsIgnoreCase("ZZ")) { throw new NumberParseException(NumberParseException.ErrorType.INVALID_COUNTRY_CODE, "No default country was supplied."); } return parseHelper(numberToParse, defaultCountry.toUpperCase(), true); } /** * As no equals method is implemented for MessageLite, we implement our own equals method here * to compare the serialized data. */ @VisibleForTesting static Boolean areSameMessages(MessageLite message1, MessageLite message2) { if (message1 == null && message2 == null) { return true; } if (message1 == null || message2 == null) { return false; } OutputStream output1 = new ByteArrayOutputStream(); OutputStream output2 = new ByteArrayOutputStream(); try { message1.writeTo(output1); message2.writeTo(output2); } catch (IOException e) { LOGGER.log(Level.WARNING, e.toString()); } return output1.toString().equals(output2.toString()); } /** * Parses a string and returns it in proto buffer format. This method is the same as the public * parse() method, with the exception that it allows the default country to be null, for use by * isNumberMatch(). */ private PhoneNumber parseHelper(String numberToParse, String defaultCountry, Boolean keepRawInput) throws NumberParseException { // Extract a possible number from the string passed in (this strips leading characters that // could not be the start of a phone number.) String number = extractPossibleNumber(numberToParse); if (!isViablePhoneNumber(number)) { throw new NumberParseException(NumberParseException.ErrorType.NOT_A_NUMBER, "The string supplied did not seem to be a phone number."); } PhoneNumber.Builder phoneNumber = PhoneNumber.newBuilder(); if (keepRawInput) { phoneNumber.setRawInput(numberToParse); } StringBuffer nationalNumber = new StringBuffer(number); // Attempt to parse extension first, since it doesn't require country-specific data and we want // to have the non-normalised number here. String extension = maybeStripExtension(nationalNumber); if (!extension.equals("")) { phoneNumber.setExtension(extension); } PhoneMetadata countryMetadata = getMetadataForRegion(defaultCountry); // Check to see if the number is given in international format so we know whether this number is // from the default country or not. StringBuffer normalizedNationalNumber = new StringBuffer(); // been created, and just remove the prefix, rather than taking in a string and then outputting // a string buffer. int countryCode = maybeExtractCountryCode(nationalNumber.toString(), countryMetadata, normalizedNationalNumber, keepRawInput, phoneNumber); if (countryCode != 0) { String phoneNumberRegion = getRegionCodeForCountryCode(countryCode); if (!phoneNumberRegion.equals(defaultCountry)) { countryMetadata = getMetadataForRegion(phoneNumberRegion); } } else { // If no extracted country code, use the region supplied instead. The national number is just // the normalized version of the number we were given to parse. normalize(nationalNumber); normalizedNationalNumber.append(nationalNumber); if (defaultCountry != null) { countryCode = countryMetadata.getCountryCode(); phoneNumber.setCountryCode(countryCode); } else if (keepRawInput) { phoneNumber.clearCountryCodeSource(); } } if (normalizedNationalNumber.length() < MIN_LENGTH_FOR_NSN) { throw new NumberParseException(NumberParseException.ErrorType.TOO_SHORT_NSN, "The string supplied is too short to be a phone number."); } if (countryMetadata != null) { Pattern validNumberPattern = Pattern.compile(countryMetadata.getGeneralDesc().getNationalNumberPattern()); maybeStripNationalPrefix(normalizedNationalNumber, countryMetadata.getNationalPrefixForParsing(), countryMetadata.getNationalPrefixTransformRule(), validNumberPattern); } int lengthOfNationalNumber = normalizedNationalNumber.length(); if (lengthOfNationalNumber < MIN_LENGTH_FOR_NSN) { throw new NumberParseException(NumberParseException.ErrorType.TOO_SHORT_NSN, "The string supplied is too short to be a " + "phone number."); } if (lengthOfNationalNumber > MAX_LENGTH_FOR_NSN) { throw new NumberParseException(NumberParseException.ErrorType.TOO_LONG, "The string supplied is too long to be a " + "phone number."); } if (isLeadingZeroCountry(countryCode) && normalizedNationalNumber.charAt(0) == '0') { phoneNumber.setItalianLeadingZero(true); } phoneNumber.setNationalNumber(Long.parseLong(normalizedNationalNumber.toString())); return phoneNumber.build(); } /** * Takes two phone numbers and compares them for equality. * * Returns EXACT_MATCH if the country code, NSN, presence of a leading zero for Italian numbers * and any extension present are the same. * Returns NSN_MATCH if either or both has no country specified, and the NSNs and extensions are * the same. * Returns SHORT_NSN_MATCH if either or both has no country specified, or the country specified * is the same, and one NSN could be a shorter version of the other number. This includes the case * where one has an extension specified, and the other does not. * Returns NO_MATCH otherwise. * For example, the numbers +1 345 657 1234 and 657 1234 are a SHORT_NSN_MATCH. * The numbers +1 345 657 1234 and 345 657 are a NO_MATCH. * * @param firstNumberIn first number to compare * @param secondNumberIn second number to compare * * @return NO_MATCH, SHORT_NSN_MATCH, NSN_MATCH or EXACT_MATCH depending on the level of equality * of the two numbers, described in the method definition. */ public MatchType isNumberMatch(PhoneNumber firstNumberIn, PhoneNumber secondNumberIn) { // Make copies of the phone number so that the numbers passed in are not edited. PhoneNumber.Builder firstNumber = PhoneNumber.newBuilder(); firstNumber.mergeFrom(firstNumberIn); PhoneNumber.Builder secondNumber = PhoneNumber.newBuilder(); secondNumber.mergeFrom(secondNumberIn); // First clear raw_input and country_code_source field and any empty-string extensions so that // we can use the proto-buffer equality method. firstNumber.clearRawInput(); firstNumber.clearCountryCodeSource(); secondNumber.clearRawInput(); secondNumber.clearCountryCodeSource(); if (firstNumber.hasExtension() && firstNumber.getExtension().equals("")) { firstNumber.clearExtension(); } if (secondNumber.hasExtension() && secondNumber.getExtension().equals("")) { secondNumber.clearExtension(); } PhoneNumber number1 = firstNumber.build(); PhoneNumber number2 = secondNumber.build(); // Early exit if both had extensions and these are different. if (number1.hasExtension() && number2.hasExtension() && !number1.getExtension().equals(number2.getExtension())) { return MatchType.NO_MATCH; } int firstNumberCountryCode = number1.getCountryCode(); int secondNumberCountryCode = number2.getCountryCode(); // Both had country code specified. if (firstNumberCountryCode != 0 && secondNumberCountryCode != 0) { if (areSameMessages(number1, number2)) { return MatchType.EXACT_MATCH; } else if (firstNumberCountryCode == secondNumberCountryCode && isNationalNumberSuffixOfTheOther(number1, number2)) { // A SHORT_NSN_MATCH occurs if there is a difference because of the presence or absence of // an 'Italian leading zero', the presence or absence of an extension, or one NSN being a // shorter variant of the other. return MatchType.SHORT_NSN_MATCH; } // This is not a match. return MatchType.NO_MATCH; } // Checks cases where one or both country codes were not specified. To make equality checks // easier, we first set the country codes to be equal. PhoneNumber newNumber = PhoneNumber.newBuilder(number1).setCountryCode(secondNumberCountryCode).build(); // If all else was the same, then this is an NSN_MATCH. if (areSameMessages(newNumber, number2)) { return MatchType.NSN_MATCH; } if (isNationalNumberSuffixOfTheOther(newNumber, number2)) { return MatchType.SHORT_NSN_MATCH; } return MatchType.NO_MATCH; } // Returns true when one national number is the suffix of the other or both are the same. private boolean isNationalNumberSuffixOfTheOther(PhoneNumber firstNumber, PhoneNumber secondNumber) { String firstNumberNationalNumber = String.valueOf(firstNumber.getNationalNumber()); String secondNumberNationalNumber = String.valueOf(secondNumber.getNationalNumber()); // Note that endsWith returns true if the numbers are equal. return firstNumberNationalNumber.endsWith(secondNumberNationalNumber) || secondNumberNationalNumber.endsWith(firstNumberNationalNumber); } /** * Takes two phone numbers as strings and compares them for equality. This is a convenience * wrapper for isNumberMatch(PhoneNumber firstNumber, PhoneNumber secondNumber). No default region * is known. * * @param firstNumber first number to compare. Can contain formatting, and can have country code * specified with + at the start. * @param secondNumber second number to compare. Can contain formatting, and can have country * code specified with + at the start. * @return NO_MATCH, SHORT_NSN_MATCH, NSN_MATCH, EXACT_MATCH. See isNumberMatch(PhoneNumber * firstNumber, PhoneNumber secondNumber) for more details. * @throws NumberParseException if either number is not considered to be a viable phone * number */ public MatchType isNumberMatch(String firstNumber, String secondNumber) throws NumberParseException { return isNumberMatch(parseHelper(firstNumber, null, false), parseHelper(secondNumber, null, false)); } /** * Takes two phone numbers and compares them for equality. This is a convenience wrapper for * isNumberMatch(PhoneNumber firstNumber, PhoneNumber secondNumber). No default region is known. * * @param firstNumber first number to compare in proto buffer format. * @param secondNumber second number to compare. Can contain formatting, and can have country * code specified with + at the start. * @return NO_MATCH, SHORT_NSN_MATCH, NSN_MATCH, EXACT_MATCH. See isNumberMatch(PhoneNumber * firstNumber, PhoneNumber secondNumber) for more details. * @throws NumberParseException if the second number is not considered to be a viable phone * number */ public MatchType isNumberMatch(PhoneNumber firstNumber, String secondNumber) throws NumberParseException { return isNumberMatch(firstNumber, parseHelper(secondNumber, null, false)); } }