import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Arrays;
*
* NOTE: A lot of methods in this class require Region Code strings. These must be provided using
* ISO 3166-1 two-letter country-code format. These should be in upper-case. The list of the codes
- * can be found here: http://www.iso.org/iso/english_country_names_and_code_elements
+ * can be found here:
+ * http://www.iso.org/iso/country_codes/iso_3166_code_lists/country_names_and_code_elements.htm
*
* @author Shaopeng Jia
- * @author Lara Rennie
*/
public class PhoneNumberUtil {
+ // @VisibleForTesting
+ static final MetadataLoader DEFAULT_METADATA_LOADER = new MetadataLoader() {
+ @Override
+ public InputStream loadMetadata(String metadataFileName) {
+ return PhoneNumberUtil.class.getResourceAsStream(metadataFileName);
+ }
+ };
+
+ private static final Logger logger = Logger.getLogger(PhoneNumberUtil.class.getName());
+
/** Flags to use when compiling regular expressions for phone numbers. */
static final int REGEX_FLAGS = Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE;
// The minimum and maximum length of the national significant number.
- private static final int MIN_LENGTH_FOR_NSN = 3;
+ private static final int MIN_LENGTH_FOR_NSN = 2;
// The ITU says the maximum length should be 15, but we have found longer numbers in Germany.
- static final int MAX_LENGTH_FOR_NSN = 16;
+ static final int MAX_LENGTH_FOR_NSN = 17;
// The maximum length of the country calling code.
static final int MAX_LENGTH_COUNTRY_CODE = 3;
// We don't allow input strings for parsing to be longer than 250 chars. This prevents malicious
// input from overflowing the regular-expression engine.
private static final int MAX_INPUT_STRING_LENGTH = 250;
- static final String META_DATA_FILE_PREFIX =
- "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto";
- private String currentFilePrefix = META_DATA_FILE_PREFIX;
- private static final Logger LOGGER = Logger.getLogger(PhoneNumberUtil.class.getName());
- // A mapping from a country calling code to the region codes which denote the region represented
- // by that country calling code. In the case of multiple regions sharing a calling code, such as
- // the NANPA regions, the one indicated with "isMainCountryForCode" in the metadata should be
- // first.
- private Map<Integer, List<String>> countryCallingCodeToRegionCodeMap = null;
-
- // The set of regions the library supports.
- // There are roughly 240 of them and we set the initial capacity of the HashSet to 320 to offer a
- // load factor of roughly 0.75.
- private final Set<String> supportedRegions = new HashSet<String>(320);
+ private static final String META_DATA_FILE_PREFIX =
+ "/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto";
// Region-code for the unknown region.
private static final String UNKNOWN_REGION = "ZZ";
- // The set of regions that share country calling code 1.
- // There are roughly 26 regions and we set the initial capacity of the HashSet to 35 to offer a
- // load factor of roughly 0.75.
- private final Set<String> nanpaRegions = new HashSet<String>(35);
private static final int NANPA_COUNTRY_CODE = 1;
// The prefix that needs to be inserted in front of a Colombian landline number when dialed from
// a mobile phone in Colombia.
private static final String COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX = "3";
+ // Map of country calling codes that use a mobile token before the area code. One example of when
+ // this is relevant is when determining the length of the national destination code, which should
+ // be the length of the area code plus the length of the mobile token.
+ private static final Map<Integer, String> MOBILE_TOKEN_MAPPINGS;
+
// The PLUS_SIGN signifies the international prefix.
static final char PLUS_SIGN = '+';
private static final char STAR_SIGN = '*';
private static final String RFC3966_EXTN_PREFIX = ";ext=";
+ private static final String RFC3966_PREFIX = "tel:";
+ private static final String RFC3966_PHONE_CONTEXT = ";phone-context=";
+ private static final String RFC3966_ISDN_SUBADDRESS = ";isub=";
// A map that contains characters that are essential when dialling. That means any of the
- // characters in this map must not be removed from a number when dialing, otherwise the call will
- // not reach the intended destination.
+ // characters in this map must not be removed from a number when dialling, otherwise the call
+ // will not reach the intended destination.
private static final Map<Character, Character> DIALLABLE_CHAR_MAPPINGS;
// Only upper-case variants of alpha characters are stored.
private static final Map<Character, Character> ALL_PLUS_NUMBER_GROUPING_SYMBOLS;
static {
+ HashMap<Integer, String> mobileTokenMap = new HashMap<Integer, String>();
+ mobileTokenMap.put(52, "1");
+ mobileTokenMap.put(54, "9");
+ MOBILE_TOKEN_MAPPINGS = Collections.unmodifiableMap(mobileTokenMap);
+
// Simple ASCII digits map used to populate ALPHA_PHONE_MAPPINGS and
// ALL_PLUS_NUMBER_GROUPING_SYMBOLS.
HashMap<Character, Character> asciiDigitMappings = new HashMap<Character, Character>();
HashMap<Character, Character> diallableCharMap = new HashMap<Character, Character>();
diallableCharMap.putAll(asciiDigitMappings);
- diallableCharMap.put('+', '+');
+ diallableCharMap.put(PLUS_SIGN, PLUS_SIGN);
diallableCharMap.put('*', '*');
DIALLABLE_CHAR_MAPPINGS = Collections.unmodifiableMap(diallableCharMap);
// placeholder for carrier information in some phone numbers. Full-width variants are also
// present.
static final String VALID_PUNCTUATION = "-x\u2010-\u2015\u2212\u30FC\uFF0D-\uFF0F " +
- "\u00A0\u200B\u2060\u3000()\uFF08\uFF09\uFF3B\uFF3D.\\[\\]/~\u2053\u223C\uFF5E";
+ "\u00A0\u00AD\u200B\u2060\u3000()\uFF08\uFF09\uFF3B\uFF3D.\\[\\]/~\u2053\u223C\uFF5E";
private static final String DIGITS = "\\p{Nd}";
// We accept alpha characters in phone numbers, ASCII only, upper and lower case.
// carrier codes, for example in Brazilian phone numbers. We also allow multiple "+" characters at
// the start.
// Corresponds to the following:
+ // [digits]{minLengthNsn}|
// plus_sign*(([punctuation]|[star])*[digits]){3,}([punctuation]|[star]|[digits]|[alpha])*
+ //
+ // The first reg-ex is to allow short numbers (two digits long) to be parsed if they are entered
+ // as "15" etc, but only if there is no punctuation in them. The second expression restricts the
+ // number of digits to three or more, but then allows them to be in international form, and to
+ // have alpha-characters and punctuation.
+ //
// Note VALID_PUNCTUATION starts with a -, so must be the first in the range.
private static final String VALID_PHONE_NUMBER =
+ DIGITS + "{" + MIN_LENGTH_FOR_NSN + "}" + "|" +
"[" + PLUS_CHARS + "]*+(?:[" + VALID_PUNCTUATION + STAR_SIGN + "]*" + DIGITS + "){3,}[" +
VALID_PUNCTUATION + STAR_SIGN + VALID_ALPHA + DIGITS + "]*";
private static final Pattern VALID_PHONE_NUMBER_PATTERN =
Pattern.compile(VALID_PHONE_NUMBER + "(?:" + EXTN_PATTERNS_FOR_PARSING + ")?", REGEX_FLAGS);
- private static final Pattern NON_DIGITS_PATTERN = Pattern.compile("(\\D+)");
+ static final Pattern NON_DIGITS_PATTERN = Pattern.compile("(\\D+)");
// The FIRST_GROUP_PATTERN was originally set to $1 but there are some countries for which the
// first group is not used in the national pattern (e.g. Argentina) so the $1 group does not match
private static final Pattern FG_PATTERN = Pattern.compile("\\$FG");
private static final Pattern CC_PATTERN = Pattern.compile("\\$CC");
- private static PhoneNumberUtil instance = null;
+ // A pattern that is used to determine if the national prefix formatting rule has the first group
+ // only, i.e., does not start with the national prefix. Note that the pattern explicitly allows
+ // for unbalanced parentheses.
+ private static final Pattern FIRST_GROUP_ONLY_PREFIX_PATTERN = Pattern.compile("\\(?\\$1\\)?");
- // A mapping from a region code to the PhoneMetadata for that region.
- private final Map<String, PhoneMetadata> regionToMetadataMap =
- Collections.synchronizedMap(new HashMap<String, PhoneMetadata>());
-
- // A mapping from a country calling code for a non-geographical entity to the PhoneMetadata for
- // that country calling code. Examples of the country calling codes include 800 (International
- // Toll Free Service) and 808 (International Shared Cost Service).
- private final Map<Integer, PhoneMetadata> countryCodeToNonGeographicalMetadataMap =
- Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>());
-
- // A cache for frequently used region-specific regular expressions.
- // As most people use phone numbers primarily from one to two countries, and there are roughly 60
- // regular expressions needed, the initial capacity of 100 offers a rough load factor of 0.75.
- private RegexCache regexCache = new RegexCache(100);
+ private static PhoneNumberUtil instance = null;
public static final String REGION_CODE_FOR_NON_GEO_ENTITY = "001";
* INTERNATIONAL and NATIONAL formats are consistent with the definition in ITU-T Recommendation
* E123. For example, the number of the Google Switzerland 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.
- * RFC3966 is as per INTERNATIONAL format, but with all spaces and other separating symbols
- * replaced with a hyphen, and with any phone number extension appended with ";ext=".
+ * E164 format is as per INTERNATIONAL format but with no formatting applied, e.g.
+ * "+41446681800". RFC3966 is as per INTERNATIONAL format, but with all spaces and other
+ * separating symbols replaced with a hyphen, and with any phone number extension appended with
+ * ";ext=". It also will have a prefix of "tel:" added, e.g. "tel:+41-44-668-1800".
*
* Note: If you are considering storing the number in a neutral format, you are highly advised to
* use the PhoneNumber class.
@Override
boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util) {
if (!util.isValidNumber(number) ||
- !containsOnlyValidXChars(number, candidate, util)) {
+ !PhoneNumberMatcher.containsOnlyValidXChars(number, candidate, util)) {
return false;
}
- return isNationalPrefixPresentIfRequired(number, util);
+ return PhoneNumberMatcher.isNationalPrefixPresentIfRequired(number, util);
}
},
/**
* are grouped in a possible way for this locale. For example, a US number written as
* "65 02 53 00 00" and "650253 0000" are not accepted at this leniency level, whereas
* "650 253 0000", "650 2530000" or "6502530000" are.
- * Numbers with more than one '/' symbol are also dropped at this level.
+ * Numbers with more than one '/' symbol in the national significant number are also dropped at
+ * this level.
* <p>
* Warning: This level might result in lower coverage especially for regions outside of country
* code "+1". If you are not sure about which level to use, email the discussion group
@Override
boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util) {
if (!util.isValidNumber(number) ||
- !containsOnlyValidXChars(number, candidate, util) ||
- containsMoreThanOneSlash(candidate) ||
- !isNationalPrefixPresentIfRequired(number, util)) {
+ !PhoneNumberMatcher.containsOnlyValidXChars(number, candidate, util) ||
+ PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate) ||
+ !PhoneNumberMatcher.isNationalPrefixPresentIfRequired(number, util)) {
return false;
}
- // TODO: Evaluate how this works for other locales (testing has been
- // limited to NANPA regions) and optimise if necessary.
- String[] formattedNumberGroups = getNationalNumberGroups(util, number);
- StringBuilder normalizedCandidate = normalizeDigits(candidate,
- true /* keep strip non-digits */);
- int fromIndex = 0;
- // Check each group of consecutive digits are not broken into separate groups in the
- // {@code candidate} string.
- for (int i = 0; i < formattedNumberGroups.length; i++) {
- // Fails if the substring of {@code candidate} starting from {@code fromIndex} doesn't
- // contain the consecutive digits in formattedNumberGroups[i].
- fromIndex = normalizedCandidate.indexOf(formattedNumberGroups[i], fromIndex);
- if (fromIndex < 0) {
- return false;
- }
- // Moves {@code fromIndex} forward.
- fromIndex += formattedNumberGroups[i].length();
- if (i == 0 && fromIndex < normalizedCandidate.length()) {
- // We are at the position right after the NDC.
- if (Character.isDigit(normalizedCandidate.charAt(fromIndex))) {
- // This means there is no formatting symbol after the NDC. In this case, we only
- // accept the number if there is no formatting symbol at all in the number, except
- // for extensions.
- String nationalSignificantNumber = util.getNationalSignificantNumber(number);
- return normalizedCandidate.substring(fromIndex - formattedNumberGroups[i].length())
- .startsWith(nationalSignificantNumber);
- }
- }
- }
- // The check here makes sure that we haven't mistakenly already used the extension to
- // match the last group of the subscriber number. Note the extension cannot have
- // formatting in-between digits.
- return normalizedCandidate.substring(fromIndex).contains(number.getExtension());
+ return PhoneNumberMatcher.checkNumberGroupingIsValid(
+ number, candidate, util, new PhoneNumberMatcher.NumberGroupingChecker() {
+ @Override
+ public boolean checkGroups(PhoneNumberUtil util, PhoneNumber number,
+ StringBuilder normalizedCandidate,
+ String[] expectedNumberGroups) {
+ return PhoneNumberMatcher.allNumberGroupsRemainGrouped(
+ util, number, normalizedCandidate, expectedNumberGroups);
+ }
+ });
}
},
/**
@Override
boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util) {
if (!util.isValidNumber(number) ||
- !containsOnlyValidXChars(number, candidate, util) ||
- containsMoreThanOneSlash(candidate) ||
- !isNationalPrefixPresentIfRequired(number, util)) {
+ !PhoneNumberMatcher.containsOnlyValidXChars(number, candidate, util) ||
+ PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate) ||
+ !PhoneNumberMatcher.isNationalPrefixPresentIfRequired(number, util)) {
return false;
}
- // TODO: Evaluate how this works for other locales (testing has been
- // limited to NANPA regions) and optimise if necessary.
- StringBuilder normalizedCandidate = normalizeDigits(candidate,
- true /* keep strip non-digits */);
- String[] candidateGroups =
- NON_DIGITS_PATTERN.split(normalizedCandidate.toString());
- // Set this to the last group, skipping it if the number has an extension.
- int candidateNumberGroupIndex =
- number.hasExtension() ? candidateGroups.length - 2 : candidateGroups.length - 1;
- // First we check if the national significant number is formatted as a block.
- // We use contains and not equals, since the national significant number may be present with
- // a prefix such as a national number prefix, or the country code itself.
- if (candidateGroups.length == 1 ||
- candidateGroups[candidateNumberGroupIndex].contains(
- util.getNationalSignificantNumber(number))) {
- return true;
- }
- String[] formattedNumberGroups = getNationalNumberGroups(util, number);
- // Starting from the end, go through in reverse, excluding the first group, and check the
- // candidate and number groups are the same.
- for (int formattedNumberGroupIndex = (formattedNumberGroups.length - 1);
- formattedNumberGroupIndex > 0 && candidateNumberGroupIndex >= 0;
- formattedNumberGroupIndex--, candidateNumberGroupIndex--) {
- if (!candidateGroups[candidateNumberGroupIndex].equals(
- formattedNumberGroups[formattedNumberGroupIndex])) {
- return false;
- }
- }
- // Now check the first group. There may be a national prefix at the start, so we only check
- // that the candidate group ends with the formatted number group.
- return (candidateNumberGroupIndex >= 0 &&
- candidateGroups[candidateNumberGroupIndex].endsWith(formattedNumberGroups[0]));
+ return PhoneNumberMatcher.checkNumberGroupingIsValid(
+ number, candidate, util, new PhoneNumberMatcher.NumberGroupingChecker() {
+ @Override
+ public boolean checkGroups(PhoneNumberUtil util, PhoneNumber number,
+ StringBuilder normalizedCandidate,
+ String[] expectedNumberGroups) {
+ return PhoneNumberMatcher.allNumberGroupsAreExactlyPresent(
+ util, number, normalizedCandidate, expectedNumberGroups);
+ }
+ });
}
};
- /**
- * Helper method to get the national-number part of a number, formatted without any national
- * prefix, and return it as a set of digit blocks that would be formatted together.
- */
- private static String[] getNationalNumberGroups(PhoneNumberUtil util, PhoneNumber number) {
- // This will be in the format +CC-DG;ext=EXT where DG represents groups of digits.
- String rfc3966Format = util.format(number, PhoneNumberFormat.RFC3966);
- // We remove the extension part from the formatted string before splitting it into different
- // groups.
- int endIndex = rfc3966Format.indexOf(';');
- if (endIndex < 0) {
- endIndex = rfc3966Format.length();
- }
- // The country-code will have a '-' following it.
- int startIndex = rfc3966Format.indexOf('-') + 1;
- return rfc3966Format.substring(startIndex, endIndex).split("-");
- }
-
- private static boolean containsMoreThanOneSlash(String candidate) {
- int firstSlashIndex = candidate.indexOf('/');
- return (firstSlashIndex > 0 && candidate.substring(firstSlashIndex + 1).contains("/"));
- }
-
- private static boolean containsOnlyValidXChars(
- PhoneNumber number, String candidate, PhoneNumberUtil util) {
- // The characters 'x' and 'X' can be (1) a carrier code, in which case they always precede the
- // national significant number or (2) an extension sign, in which case they always precede the
- // extension number. We assume a carrier code is more than 1 digit, so the first case has to
- // have more than 1 consecutive 'x' or 'X', whereas the second case can only have exactly 1
- // 'x' or 'X'. We ignore the character if it appears as the last character of the string.
- for (int index = 0; index < candidate.length() - 1; index++) {
- char charAtIndex = candidate.charAt(index);
- if (charAtIndex == 'x' || charAtIndex == 'X') {
- char charAtNextIndex = candidate.charAt(index + 1);
- if (charAtNextIndex == 'x' || charAtNextIndex == 'X') {
- // This is the carrier code case, in which the 'X's always precede the national
- // significant number.
- index++;
- if (util.isNumberMatch(number, candidate.substring(index)) != MatchType.NSN_MATCH) {
- return false;
- }
- // This is the extension sign case, in which the 'x' or 'X' should always precede the
- // extension number.
- } else if (!PhoneNumberUtil.normalizeDigitsOnly(candidate.substring(index)).equals(
- number.getExtension())) {
- return false;
- }
- }
- }
- return true;
- }
-
- private static boolean isNationalPrefixPresentIfRequired(
- PhoneNumber number, PhoneNumberUtil util) {
- // First, check how we deduced the country code. If it was written in international format,
- // then the national prefix is not required.
- if (number.getCountryCodeSource() != CountryCodeSource.FROM_DEFAULT_COUNTRY) {
- return true;
- }
- String phoneNumberRegion =
- util.getRegionCodeForCountryCode(number.getCountryCode());
- PhoneMetadata metadata = util.getMetadataForRegion(phoneNumberRegion);
- if (metadata == null) {
- return true;
- }
- // Check if a national prefix should be present when formatting this number.
- String nationalNumber = util.getNationalSignificantNumber(number);
- NumberFormat formatRule =
- util.chooseFormattingPatternForNumber(metadata.numberFormats(), nationalNumber);
- // To do this, we check that a national prefix formatting rule was present and that it wasn't
- // just the first-group symbol ($1) with punctuation.
- if ((formatRule != null) && formatRule.getNationalPrefixFormattingRule().length() > 0) {
- if (formatRule.isNationalPrefixOptionalWhenFormatting()) {
- // The national-prefix is optional in these cases, so we don't need to check if it was
- // present.
- return true;
- }
- // Remove the first-group symbol.
- String candidateNationalPrefixRule = formatRule.getNationalPrefixFormattingRule();
- // We assume that the first-group symbol will never be _before_ the national prefix.
- candidateNationalPrefixRule =
- candidateNationalPrefixRule.substring(0, candidateNationalPrefixRule.indexOf("$1"));
- candidateNationalPrefixRule = util.normalizeDigitsOnly(candidateNationalPrefixRule);
- if (candidateNationalPrefixRule.length() == 0) {
- // National Prefix not needed for this number.
- return true;
- }
- // Normalize the remainder.
- String rawInputCopy = util.normalizeDigitsOnly(number.getRawInput());
- StringBuilder rawInput = new StringBuilder(rawInputCopy);
- // Check if we found a national prefix and/or carrier code at the start of the raw input,
- // and return the result.
- return util.maybeStripNationalPrefixAndCarrierCode(rawInput, metadata, null);
- }
- return true;
- }
-
/** Returns true if {@code number} is a verified number according to this leniency. */
abstract boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util);
}
+ // A mapping from a country calling code to the region codes which denote the region represented
+ // by that country calling code. In the case of multiple regions sharing a calling code, such as
+ // the NANPA regions, the one indicated with "isMainCountryForCode" in the metadata should be
+ // first.
+ private final Map<Integer, List<String>> countryCallingCodeToRegionCodeMap;
+
+ // The set of regions that share country calling code 1.
+ // There are roughly 26 regions.
+ // We set the initial capacity of the HashSet to 35 to offer a load factor of roughly 0.75.
+ private final Set<String> nanpaRegions = new HashSet<String>(35);
+
+ // A mapping from a region code to the PhoneMetadata for that region.
+ // Note: Synchronization, though only needed for the Android version of the library, is used in
+ // all versions for consistency.
+ private final Map<String, PhoneMetadata> regionToMetadataMap =
+ Collections.synchronizedMap(new HashMap<String, PhoneMetadata>());
+
+ // A mapping from a country calling code for a non-geographical entity to the PhoneMetadata for
+ // that country calling code. Examples of the country calling codes include 800 (International
+ // Toll Free Service) and 808 (International Shared Cost Service).
+ // Note: Synchronization, though only needed for the Android version of the library, is used in
+ // all versions for consistency.
+ private final Map<Integer, PhoneMetadata> countryCodeToNonGeographicalMetadataMap =
+ Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>());
+
+ // A cache for frequently used region-specific regular expressions.
+ // The initial capacity is set to 100 as this seems to be an optimal value for Android, based on
+ // performance measurements.
+ private final RegexCache regexCache = new RegexCache(100);
+
+ // The set of regions the library supports.
+ // There are roughly 240 of them and we set the initial capacity of the HashSet to 320 to offer a
+ // load factor of roughly 0.75.
+ private final Set<String> supportedRegions = new HashSet<String>(320);
+
+ // The set of county calling codes that map to the non-geo entity region ("001"). This set
+ // currently contains < 12 elements so the default capacity of 16 (load factor=0.75) is fine.
+ private final Set<Integer> countryCodesForNonGeographicalRegion = new HashSet<Integer>();
+
+ // The prefix of the metadata files from which region data is loaded.
+ private final String currentFilePrefix;
+ // The metadata loader used to inject alternative metadata sources.
+ private final MetadataLoader metadataLoader;
+
/**
- * This class implements a singleton, so the only constructor is private.
+ * This class implements a singleton, the constructor is only visible to facilitate testing.
*/
- private PhoneNumberUtil() {
- }
-
- private void init(String filePrefix) {
- currentFilePrefix = filePrefix;
- for (List<String> regionCodes : countryCallingCodeToRegionCodeMap.values()) {
- supportedRegions.addAll(regionCodes);
+ // @VisibleForTesting
+ PhoneNumberUtil(String filePrefix, MetadataLoader metadataLoader,
+ Map<Integer, List<String>> countryCallingCodeToRegionCodeMap) {
+ this.currentFilePrefix = filePrefix;
+ this.metadataLoader = metadataLoader;
+ this.countryCallingCodeToRegionCodeMap = countryCallingCodeToRegionCodeMap;
+ for (Map.Entry<Integer, List<String>> entry : countryCallingCodeToRegionCodeMap.entrySet()) {
+ List<String> regionCodes = entry.getValue();
+ // We can assume that if the county calling code maps to the non-geo entity region code then
+ // that's the only region code it maps to.
+ if (regionCodes.size() == 1 && REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCodes.get(0))) {
+ // This is the subset of all country codes that map to the non-geo entity region code.
+ countryCodesForNonGeographicalRegion.add(entry.getKey());
+ } else {
+ // The supported regions set does not include the "001" non-geo entity region code.
+ supportedRegions.addAll(regionCodes);
+ }
+ }
+ // If the non-geo entity still got added to the set of supported regions it must be because
+ // there are entries that list the non-geo entity alongside normal regions (which is wrong).
+ // If we discover this, remove the non-geo entity from the set of supported regions and log.
+ if (supportedRegions.remove(REGION_CODE_FOR_NON_GEO_ENTITY)) {
+ logger.log(Level.WARNING, "invalid metadata " +
+ "(country calling code was mapped to the non-geo entity as well as specific region(s))");
}
- supportedRegions.remove(REGION_CODE_FOR_NON_GEO_ENTITY);
nanpaRegions.addAll(countryCallingCodeToRegionCodeMap.get(NANPA_COUNTRY_CODE));
}
- private void loadMetadataFromFile(String filePrefix, String regionCode, int countryCallingCode) {
+ // @VisibleForTesting
+ void loadMetadataFromFile(String filePrefix, String regionCode, int countryCallingCode,
+ MetadataLoader metadataLoader) {
boolean isNonGeoRegion = REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode);
- InputStream source = isNonGeoRegion
- ? PhoneNumberUtil.class.getResourceAsStream(filePrefix + "_" + countryCallingCode)
- : PhoneNumberUtil.class.getResourceAsStream(filePrefix + "_" + regionCode);
+ String fileName = filePrefix + "_" +
+ (isNonGeoRegion ? String.valueOf(countryCallingCode) : regionCode);
+ InputStream source = metadataLoader.loadMetadata(fileName);
+ if (source == null) {
+ logger.log(Level.SEVERE, "missing metadata: " + fileName);
+ throw new IllegalStateException("missing metadata: " + fileName);
+ }
ObjectInputStream in = null;
try {
in = new ObjectInputStream(source);
- PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection();
- metadataCollection.readExternal(in);
- for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
- if (isNonGeoRegion) {
- countryCodeToNonGeographicalMetadataMap.put(countryCallingCode, metadata);
- } else {
- regionToMetadataMap.put(regionCode, metadata);
- }
+ PhoneMetadataCollection metadataCollection = loadMetadataAndCloseInput(in);
+ List<PhoneMetadata> metadataList = metadataCollection.getMetadataList();
+ if (metadataList.isEmpty()) {
+ logger.log(Level.SEVERE, "empty metadata: " + fileName);
+ throw new IllegalStateException("empty metadata: " + fileName);
+ }
+ if (metadataList.size() > 1) {
+ logger.log(Level.WARNING, "invalid metadata (too many entries): " + fileName);
+ }
+ PhoneMetadata metadata = metadataList.get(0);
+ if (isNonGeoRegion) {
+ countryCodeToNonGeographicalMetadataMap.put(countryCallingCode, metadata);
+ } else {
+ regionToMetadataMap.put(regionCode, metadata);
}
} catch (IOException e) {
- LOGGER.log(Level.WARNING, e.toString());
- } finally {
- close(in);
+ logger.log(Level.SEVERE, "cannot load/parse metadata: " + fileName, e);
+ throw new RuntimeException("cannot load/parse metadata: " + fileName, e);
}
}
- private static void close(InputStream in) {
- if (in != null) {
+ /**
+ * Loads the metadata protocol buffer from the given stream and closes the stream afterwards. Any
+ * exceptions that occur while reading the stream are propagated (though exceptions that occur
+ * when the stream is closed will be ignored).
+ *
+ * @param source the non-null stream from which metadata is to be read.
+ * @return the loaded metadata protocol buffer.
+ */
+ private static PhoneMetadataCollection loadMetadataAndCloseInput(ObjectInputStream source) {
+ PhoneMetadataCollection metadataCollection = new PhoneMetadataCollection();
+ try {
+ metadataCollection.readExternal(source);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "error reading input (ignored)", e);
+ } finally {
try {
- in.close();
+ source.close();
} catch (IOException e) {
- LOGGER.log(Level.WARNING, e.toString());
+ logger.log(Level.WARNING, "error closing input stream (ignored)", e);
}
}
+ return metadataCollection;
}
/**
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);
+ logger.log(Level.FINER, "Stripped trailing characters: " + number);
}
// Check for extra numbers at the end.
Matcher secondNumber = SECOND_NUMBER_START_PATTERN.matcher(number);
/**
* 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
+ * moment, checks to see that the string begins with at least 2 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.
return normalizeDigits(number, false /* strip non-digits */).toString();
}
- private static StringBuilder normalizeDigits(String number, boolean keepNonDigits) {
+ static StringBuilder normalizeDigits(String number, boolean keepNonDigits) {
StringBuilder normalizedDigits = new StringBuilder(number.length());
for (char c : number.toCharArray()) {
int digit = Character.digit(c, 10);
}
/**
+ * Normalizes a string of characters representing a phone number. This strips all characters which
+ * are not diallable on a mobile phone keypad (including all non-ASCII digits).
+ *
+ * @param number a string of characters representing a phone number
+ * @return the normalized string version of the phone number
+ */
+ static String normalizeDiallableCharsOnly(String number) {
+ return normalizeHelper(number, DIALLABLE_CHAR_MAPPINGS, true /* remove non matches */);
+ }
+
+ /**
* Converts all alpha characters in a number to their respective digits on a keypad, but retains
* existing formatting.
*/
}
/**
- * Gets the length of the geographical area code in the {@code nationalNumber_} 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:
+ * Gets the length of the geographical area code from 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:
*
* <pre>
* PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
* entities
* <li> some geographical numbers have no area codes.
* </ul>
- * @param number the PhoneNumber object for which clients want to know the length of the area
- * code.
- * @return the length of area code of the PhoneNumber object passed in.
+ * @param number the PhoneNumber object for which clients
+ * want to know the length of the area code.
+ * @return the length of area code of the PhoneNumber object
+ * passed in.
*/
public int getLengthOfGeographicalAreaCode(PhoneNumber number) {
- String regionCode = getRegionCodeForNumber(number);
- if (!isValidRegionCode(regionCode)) {
+ PhoneMetadata metadata = getMetadataForRegion(getRegionCodeForNumber(number));
+ if (metadata == null) {
return 0;
}
- PhoneMetadata metadata = getMetadataForRegion(regionCode);
- if (!metadata.hasNationalPrefix()) {
+ // If a country doesn't use a national prefix, and this number doesn't have an Italian leading
+ // zero, we assume it is a closed dialling plan with no area codes.
+ if (!metadata.hasNationalPrefix() && !number.isItalianLeadingZero()) {
return 0;
}
- PhoneNumberType type = getNumberTypeHelper(getNationalSignificantNumber(number),
- 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) {
+ if (!isNumberGeographical(number)) {
return 0;
}
}
/**
- * Gets the length of the national destination code (NDC) from the PhoneNumber object passed in,
- * so that clients could use it to split a national significant number into NDC and subscriber
- * number. The NDC of a phone number is normally the first group of digit(s) right after the
- * country calling code when the number is formatted in the international format, if there is a
- * subscriber number part that follows. An example of how this could be used:
+ * Gets the length of the national destination code (NDC) from the
+ * PhoneNumber object passed in, so that clients could use it
+ * to split a national significant number into NDC and subscriber number. The NDC of a phone
+ * number is normally the first group of digit(s) right after the country calling code when the
+ * number is formatted in the international format, if there is a subscriber number part that
+ * follows. An example of how this could be used:
*
* <pre>
* PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
* Refer to the unittests to see the difference between this function and
* {@link #getLengthOfGeographicalAreaCode}.
*
- * @param number the PhoneNumber object for which clients want to know the length of the NDC.
- * @return the length of NDC of the PhoneNumber object passed in.
+ * @param number the PhoneNumber object for which clients
+ * want to know the length of the NDC.
+ * @return the length of NDC of the PhoneNumber object
+ * passed in.
*/
public int getLengthOfNationalDestinationCode(PhoneNumber number) {
PhoneNumber copiedProto;
return 0;
}
- if (getRegionCodeForCountryCode(number.getCountryCode()).equals("AR") &&
- getNumberType(number) == PhoneNumberType.MOBILE) {
- // Argentinian mobile numbers, when formatted in the international format, are in the form of
- // +54 9 NDC XXXX.... As a result, we take the length of the third group (NDC) and add 1 for
- // the digit 9, which also forms part of the national significant number.
- //
- // TODO: Investigate the possibility of better modeling the metadata to make it
- // easier to obtain the NDC.
- return numberGroups[3].length() + 1;
+ if (getNumberType(number) == PhoneNumberType.MOBILE) {
+ // For example Argentinian mobile numbers, when formatted in the international format, are in
+ // the form of +54 9 NDC XXXX.... As a result, we take the length of the third group (NDC) and
+ // add the length of the second group (which is the mobile token), which also forms part of
+ // the national significant number. This assumes that the mobile token is always formatted
+ // separately from the rest of the phone number.
+ String mobileToken = getCountryMobileToken(number.getCountryCode());
+ if (!mobileToken.equals("")) {
+ return numberGroups[2].length() + numberGroups[3].length();
+ }
}
return numberGroups[2].length();
}
/**
+ * Returns the mobile token for the provided country calling code if it has one, otherwise
+ * returns an empty string. A mobile token is a number inserted before the area code when dialing
+ * a mobile number from that country from abroad.
+ *
+ * @param countryCallingCode the country calling code for which we want the mobile token
+ * @return the mobile token, as a string, for the given country calling code
+ */
+ public static String getCountryMobileToken(int countryCallingCode) {
+ if (MOBILE_TOKEN_MAPPINGS.containsKey(countryCallingCode)) {
+ return MOBILE_TOKEN_MAPPINGS.get(countryCallingCode);
+ }
+ return "";
+ }
+
+ /**
* 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.
Map<Character, Character> normalizationReplacements,
boolean removeNonMatches) {
StringBuilder normalizedNumber = new StringBuilder(number.length());
- char[] numberAsCharArray = number.toCharArray();
- for (char character : numberAsCharArray) {
+ for (int i = 0; i < number.length(); i++) {
+ char character = number.charAt(i);
Character newDigit = normalizationReplacements.get(Character.toUpperCase(character));
if (newDigit != null) {
normalizedNumber.append(newDigit);
return normalizedNumber.toString();
}
- // @VisibleForTesting
- static synchronized PhoneNumberUtil getInstance(
- String baseFileLocation,
- Map<Integer, List<String>> countryCallingCodeToRegionCodeMap) {
- if (instance == null) {
- instance = new PhoneNumberUtil();
- instance.countryCallingCodeToRegionCodeMap = countryCallingCodeToRegionCodeMap;
- instance.init(baseFileLocation);
- }
- return instance;
- }
-
/**
- * Used for testing purposes only to reset the PhoneNumberUtil singleton to null.
+ * Sets or resets the PhoneNumberUtil singleton instance. If set to null, the next call to
+ * {@code getInstance()} will load (and return) the default instance.
*/
// @VisibleForTesting
- static synchronized void resetInstance() {
- instance = null;
+ static synchronized void setInstance(PhoneNumberUtil util) {
+ instance = util;
}
/**
* Convenience method to get a list of what regions the library has metadata for.
*/
public Set<String> getSupportedRegions() {
- return supportedRegions;
+ return Collections.unmodifiableSet(supportedRegions);
}
/**
* for.
*/
public Set<Integer> getSupportedGlobalNetworkCallingCodes() {
- return countryCodeToNonGeographicalMetadataMap.keySet();
+ return Collections.unmodifiableSet(countryCodesForNonGeographicalRegion);
}
/**
*/
public static synchronized PhoneNumberUtil getInstance() {
if (instance == null) {
- return getInstance(META_DATA_FILE_PREFIX,
- CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap());
+ setInstance(createInstance(DEFAULT_METADATA_LOADER));
}
return instance;
}
/**
+ * Create a new {@link PhoneNumberUtil} instance to carry out international phone number
+ * formatting, parsing, or validation. The instance is loaded with all metadata by
+ * using the metadataLoader specified.
+ *
+ * This method should only be used in the rare case in which you want to manage your own
+ * metadata loading. Calling this method multiple times is very expensive, as each time
+ * a new instance is created from scratch. When in doubt, use {@link #getInstance}.
+ *
+ * @param metadataLoader Customized metadata loader. If null, default metadata loader will
+ * be used. This should not be null.
+ * @return a PhoneNumberUtil instance
+ */
+ public static PhoneNumberUtil createInstance(MetadataLoader metadataLoader) {
+ if (metadataLoader == null) {
+ throw new IllegalArgumentException("metadataLoader could not be null.");
+ }
+ return new PhoneNumberUtil(META_DATA_FILE_PREFIX, metadataLoader,
+ CountryCodeToRegionCodeMap.getCountryCodeToRegionCodeMap());
+ }
+
+ /**
+ * Helper function to check if the national prefix formatting rule has the first group only, i.e.,
+ * does not start with the national prefix.
+ */
+ static boolean formattingRuleHasFirstGroupOnly(String nationalPrefixFormattingRule) {
+ return nationalPrefixFormattingRule.length() == 0 ||
+ FIRST_GROUP_ONLY_PREFIX_PATTERN.matcher(nationalPrefixFormattingRule).matches();
+ }
+
+ /**
+ * Tests whether a phone number has a geographical association. It checks if the number is
+ * associated to a certain region in the country where it belongs to. Note that this doesn't
+ * verify if the number is actually in use.
+ *
+ * A similar method is implemented as PhoneNumberOfflineGeocoder.canBeGeocoded, which performs a
+ * looser check, since it only prevents cases where prefixes overlap for geocodable and
+ * non-geocodable numbers. Also, if new phone number types were added, we should check if this
+ * other method should be updated too.
+ */
+ boolean isNumberGeographical(PhoneNumber phoneNumber) {
+ PhoneNumberType numberType = getNumberType(phoneNumber);
+ // TODO: Include mobile phone numbers from countries like Indonesia, which has some
+ // mobile numbers that are geographical.
+ return numberType == PhoneNumberType.FIXED_LINE ||
+ numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE;
+ }
+
+ /**
* Helper function to check region code is not unknown or null.
*/
private boolean isValidRegionCode(String regionCode) {
*/
public String format(PhoneNumber number, PhoneNumberFormat numberFormat) {
if (number.getNationalNumber() == 0 && number.hasRawInput()) {
+ // Unparseable numbers that kept their raw input just use that.
+ // This is the only case where a number can be formatted as E164 without a
+ // leading '+' symbol (but the original number wasn't parseable anyway).
+ // TODO: Consider removing the 'if' above so that unparseable
+ // strings without raw input format to the empty string instead of "+00"
String rawInput = number.getRawInput();
if (rawInput.length() > 0) {
return rawInput;
formattedNumber.setLength(0);
int countryCallingCode = 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.
+ // Early exit for E164 case (even if the country calling code is invalid) since no formatting
+ // of the national number needs to be applied. Extensions are not formatted.
formattedNumber.append(nationalSignificantNumber);
prefixNumberWithCountryCallingCode(countryCallingCode, PhoneNumberFormat.E164,
formattedNumber);
return;
}
- // Note getRegionCodeForCountryCode() is used because formatting information for regions which
- // share a country calling code is contained by only one region for performance reasons. For
- // example, for NANPA regions it will be contained in the metadata for US.
- String regionCode = getRegionCodeForCountryCode(countryCallingCode);
if (!hasValidCountryCallingCode(countryCallingCode)) {
formattedNumber.append(nationalSignificantNumber);
return;
}
-
+ // Note getRegionCodeForCountryCode() is used because formatting information for regions which
+ // share a country calling code is contained by only one region for performance reasons. For
+ // example, for NANPA regions it will be contained in the metadata for US.
+ String regionCode = getRegionCodeForCountryCode(countryCallingCode);
+ // Metadata cannot be null because the country calling code is valid (which means that the
+ // region code cannot be ZZ and must be one of our supported region codes).
PhoneMetadata metadata =
getMetadataForRegionOrCallingCode(countryCallingCode, regionCode);
formattedNumber.append(formatNsn(nationalSignificantNumber, metadata, numberFormat));
List<NumberFormat> userDefinedFormats) {
int countryCallingCode = number.getCountryCode();
String nationalSignificantNumber = getNationalSignificantNumber(number);
+ if (!hasValidCountryCallingCode(countryCallingCode)) {
+ return nationalSignificantNumber;
+ }
// Note getRegionCodeForCountryCode() is used because formatting information for regions which
// share a country calling code is contained by only one region for performance reasons. For
// example, for NANPA regions it will be contained in the metadata for US.
String regionCode = getRegionCodeForCountryCode(countryCallingCode);
- if (!hasValidCountryCallingCode(countryCallingCode)) {
- return nationalSignificantNumber;
- }
+ // Metadata cannot be null because the country calling code is valid
PhoneMetadata metadata =
getMetadataForRegionOrCallingCode(countryCallingCode, regionCode);
public String formatNationalNumberWithCarrierCode(PhoneNumber number, String carrierCode) {
int countryCallingCode = number.getCountryCode();
String nationalSignificantNumber = getNationalSignificantNumber(number);
+ if (!hasValidCountryCallingCode(countryCallingCode)) {
+ return nationalSignificantNumber;
+ }
+
// Note getRegionCodeForCountryCode() is used because formatting information for regions which
// share a country calling code is contained by only one region for performance reasons. For
// example, for NANPA regions it will be contained in the metadata for US.
String regionCode = getRegionCodeForCountryCode(countryCallingCode);
- if (!hasValidCountryCallingCode(countryCallingCode)) {
- return nationalSignificantNumber;
- }
+ // Metadata cannot be null because the country calling code is valid.
+ PhoneMetadata metadata = getMetadataForRegionOrCallingCode(countryCallingCode, regionCode);
StringBuilder formattedNumber = new StringBuilder(20);
- PhoneMetadata metadata = getMetadataForRegionOrCallingCode(countryCallingCode, regionCode);
formattedNumber.append(formatNsn(nationalSignificantNumber, metadata,
PhoneNumberFormat.NATIONAL, carrierCode));
maybeAppendFormattedExtension(number, metadata, PhoneNumberFormat.NATIONAL, formattedNumber);
return number.hasRawInput() ? number.getRawInput() : "";
}
- String formattedNumber;
+ String formattedNumber = "";
// Clear the extension, as that part cannot normally be dialed together with the main number.
PhoneNumber numberNoExt = new PhoneNumber().mergeFrom(number).clearExtension();
- PhoneNumberType numberType = getNumberType(numberNoExt);
String regionCode = getRegionCodeForCountryCode(countryCallingCode);
- if (regionCode.equals("CO") && regionCallingFrom.equals("CO")) {
- if (numberType == PhoneNumberType.FIXED_LINE) {
+ PhoneNumberType numberType = getNumberType(numberNoExt);
+ boolean isValidNumber = (numberType != PhoneNumberType.UNKNOWN);
+ if (regionCallingFrom.equals(regionCode)) {
+ boolean isFixedLineOrMobile =
+ (numberType == PhoneNumberType.FIXED_LINE) || (numberType == PhoneNumberType.MOBILE) ||
+ (numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
+ // Carrier codes may be needed in some countries. We handle this here.
+ if (regionCode.equals("CO") && numberType == PhoneNumberType.FIXED_LINE) {
formattedNumber =
formatNationalNumberWithCarrierCode(numberNoExt, COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX);
+ } else if (regionCode.equals("BR") && isFixedLineOrMobile) {
+ formattedNumber = numberNoExt.hasPreferredDomesticCarrierCode()
+ ? formattedNumber = formatNationalNumberWithPreferredCarrierCode(numberNoExt, "")
+ // Brazilian fixed line and mobile numbers need to be dialed with a carrier code when
+ // called within Brazil. Without that, most of the carriers won't connect the call.
+ // Because of that, we return an empty string here.
+ : "";
+ } else if (isValidNumber && regionCode.equals("HU")) {
+ // The national format for HU numbers doesn't contain the national prefix, because that is
+ // how numbers are normally written down. However, the national prefix is obligatory when
+ // dialing from a mobile phone, except for short numbers. As a result, we add it back here
+ // if it is a valid regular length phone number.
+ formattedNumber =
+ getNddPrefixForRegion(regionCode, true /* strip non-digits */) +
+ " " + format(numberNoExt, PhoneNumberFormat.NATIONAL);
+ } else if (countryCallingCode == NANPA_COUNTRY_CODE) {
+ // For NANPA countries, we output international format for numbers that can be dialed
+ // internationally, since that always works, except for numbers which might potentially be
+ // short numbers, which are always dialled in national format.
+ PhoneMetadata regionMetadata = getMetadataForRegion(regionCallingFrom);
+ if (canBeInternationallyDialled(numberNoExt) &&
+ !isShorterThanPossibleNormalNumber(regionMetadata,
+ getNationalSignificantNumber(numberNoExt))) {
+ formattedNumber = format(numberNoExt, PhoneNumberFormat.INTERNATIONAL);
+ } else {
+ formattedNumber = format(numberNoExt, PhoneNumberFormat.NATIONAL);
+ }
} else {
- // E164 doesn't work at all when dialing within Colombia.
- formattedNumber = format(numberNoExt, PhoneNumberFormat.NATIONAL);
+ // For non-geographical countries, and Mexican and Chilean fixed line and mobile numbers, we
+ // output international format for numbers that can be dialed internationally as that always
+ // works.
+ if ((regionCode.equals(REGION_CODE_FOR_NON_GEO_ENTITY) ||
+ // MX fixed line and mobile numbers should always be formatted in international format,
+ // even when dialed within MX. For national format to work, a carrier code needs to be
+ // used, and the correct carrier code depends on if the caller and callee are from the
+ // same local area. It is trickier to get that to work correctly than using
+ // international format, which is tested to work fine on all carriers.
+ // CL fixed line numbers need the national prefix when dialing in the national format,
+ // but don't have it when used for display. The reverse is true for mobile numbers.
+ // As a result, we output them in the international format to make it work.
+ ((regionCode.equals("MX") || regionCode.equals("CL")) &&
+ isFixedLineOrMobile)) &&
+ canBeInternationallyDialled(numberNoExt)) {
+ formattedNumber = format(numberNoExt, PhoneNumberFormat.INTERNATIONAL);
+ } else {
+ formattedNumber = format(numberNoExt, PhoneNumberFormat.NATIONAL);
+ }
}
- } else if (regionCode.equals("PE") && regionCallingFrom.equals("PE")) {
- // In Peru, numbers cannot be dialled using E164 format from a mobile phone for Movistar.
- // Instead they must be dialled in national format.
- formattedNumber = format(numberNoExt, PhoneNumberFormat.NATIONAL);
- } else if (regionCode.equals("BR") && regionCallingFrom.equals("BR") &&
- ((numberType == PhoneNumberType.FIXED_LINE) || (numberType == PhoneNumberType.MOBILE) ||
- (numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE))) {
- formattedNumber = numberNoExt.hasPreferredDomesticCarrierCode()
- ? formatNationalNumberWithPreferredCarrierCode(numberNoExt, "")
- // Brazilian fixed line and mobile numbers need to be dialed with a carrier code when
- // called within Brazil. Without that, most of the carriers won't connect the call.
- // Because of that, we return an empty string here.
- : "";
- } else if (canBeInternationallyDialled(numberNoExt)) {
+ } else if (isValidNumber && canBeInternationallyDialled(numberNoExt)) {
+ // We assume that short numbers are not diallable from outside their region, so if a number
+ // is not a valid regular length phone number, we treat it as if it cannot be internationally
+ // dialled.
return withFormatting ? format(numberNoExt, PhoneNumberFormat.INTERNATIONAL)
: format(numberNoExt, PhoneNumberFormat.E164);
- } else {
- formattedNumber = (regionCallingFrom.equals(regionCode))
- ? format(numberNoExt, PhoneNumberFormat.NATIONAL) : "";
}
return withFormatting ? formattedNumber
- : normalizeHelper(formattedNumber, DIALLABLE_CHAR_MAPPINGS,
- true /* remove non matches */);
+ : normalizeDiallableCharsOnly(formattedNumber);
}
/**
public String formatOutOfCountryCallingNumber(PhoneNumber number,
String regionCallingFrom) {
if (!isValidRegionCode(regionCallingFrom)) {
- LOGGER.log(Level.WARNING,
+ logger.log(Level.WARNING,
"Trying to format number from invalid region "
+ regionCallingFrom
+ ". International formatting applied.");
return countryCallingCode + " " + format(number, PhoneNumberFormat.NATIONAL);
}
} else if (countryCallingCode == getCountryCodeForValidRegion(regionCallingFrom)) {
- // For regions that share a country calling code, the country calling code need not be dialled.
- // This also applies when dialling within a region, so this if clause covers both these cases.
- // 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 calling code.
- // Details here: http://www.petitfute.com/voyage/225-info-pratiques-reunion
+ // If regions share a country calling code, the country calling code need not be dialled.
+ // This also applies when dialling within a region, so this if clause covers both these cases.
+ // 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 calling code.
+ // Details here: http://www.petitfute.com/voyage/225-info-pratiques-reunion
return format(number, PhoneNumberFormat.NATIONAL);
}
+ // Metadata cannot be null because we checked 'isValidRegionCode()' above.
PhoneMetadata metadataForRegionCallingFrom = getMetadataForRegion(regionCallingFrom);
String internationalPrefix = metadataForRegionCallingFrom.getInternationalPrefix();
}
String regionCode = getRegionCodeForCountryCode(countryCallingCode);
+ // Metadata cannot be null because the country calling code is valid.
PhoneMetadata metadataForRegion =
getMetadataForRegionOrCallingCode(countryCallingCode, regionCode);
String formattedNationalNumber =
formattedNumber = nationalFormat;
break;
}
+ // Metadata cannot be null here because getNddPrefixForRegion() (above) returns null if
+ // there is no metadata for the region.
PhoneMetadata metadata = getMetadataForRegion(regionCode);
String nationalNumber = getNationalSignificantNumber(number);
NumberFormat formatRule =
chooseFormattingPatternForNumber(metadata.numberFormats(), nationalNumber);
+ // The format rule could still be null here if the national number was 0 and there was no
+ // raw input (this should not be possible for numbers generated by the phonenumber library
+ // as they would also not have a country calling code and we would have exited earlier).
+ if (formatRule == null) {
+ formattedNumber = nationalFormat;
+ break;
+ }
// When the format we apply to this number doesn't contain national prefix, we can just
// return the national format.
- // TODO: Refactor the code below with the code in isNationalPrefixPresentIfRequired.
+ // TODO: Refactor the code below with the code in
+ // isNationalPrefixPresentIfRequired.
String candidateNationalPrefixRule = formatRule.getNationalPrefixFormattingRule();
// We assume that the first-group symbol will never be _before_ the national prefix.
int indexOfFirstGroup = candidateNationalPrefixRule.indexOf("$1");
String rawInput = number.getRawInput();
// If no digit is inserted/removed/modified as a result of our formatting, we return the
// formatted phone number; otherwise we return the raw input the user entered.
- return (formattedNumber != null &&
- normalizeDigitsOnly(formattedNumber).equals(normalizeDigitsOnly(rawInput)))
- ? formattedNumber
- : rawInput;
+ if (formattedNumber != null && rawInput.length() > 0) {
+ String normalizedFormattedNumber = normalizeDiallableCharsOnly(formattedNumber);
+ String normalizedRawInput = normalizeDiallableCharsOnly(rawInput);
+ if (!normalizedFormattedNumber.equals(normalizedRawInput)) {
+ formattedNumber = rawInput;
+ }
+ }
+ return formattedNumber;
}
// Check if rawInput, which is assumed to be in the national format, has a national prefix. The
if (isNANPACountry(regionCallingFrom)) {
return countryCode + " " + rawInput;
}
- } else if (isValidRegionCode(regionCallingFrom) &&
+ } else if (metadataForRegionCallingFrom != null &&
countryCode == getCountryCodeForValidRegion(regionCallingFrom)) {
NumberFormat formattingPattern =
chooseFormattingPatternForNumber(metadataForRegionCallingFrom.numberFormats(),
}
StringBuilder formattedNumber = new StringBuilder(rawInput);
String regionCode = getRegionCodeForCountryCode(countryCode);
+ // Metadata cannot be null because the country calling code is valid.
PhoneMetadata metadataForRegion = getMetadataForRegionOrCallingCode(countryCode, regionCode);
maybeAppendFormattedExtension(number, metadataForRegion,
PhoneNumberFormat.INTERNATIONAL, formattedNumber);
} else {
// Invalid region entered as country-calling-from (so no metadata was found for it) or the
// region chosen has multiple international dialling prefixes.
- LOGGER.log(Level.WARNING,
+ logger.log(Level.WARNING,
"Trying to format number from invalid region "
+ regionCallingFrom
+ ". International formatting applied.");
* @return the national significant number of the PhoneNumber object passed in
*/
public String getNationalSignificantNumber(PhoneNumber number) {
- // If a leading zero has been set, we prefix this now. Note this is not a national prefix.
- StringBuilder nationalNumber = new StringBuilder(number.isItalianLeadingZero() ? "0" : "");
+ // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix.
+ StringBuilder nationalNumber = new StringBuilder();
+ if (number.isItalianLeadingZero()) {
+ char[] zeros = new char[number.getNumberOfLeadingZeros()];
+ Arrays.fill(zeros, '0');
+ nationalNumber.append(new String(zeros));
+ }
nationalNumber.append(number.getNationalNumber());
return nationalNumber.toString();
}
formattedNumber.insert(0, " ").insert(0, countryCallingCode).insert(0, PLUS_SIGN);
return;
case RFC3966:
- formattedNumber.insert(0, "-").insert(0, countryCallingCode).insert(0, PLUS_SIGN);
+ formattedNumber.insert(0, "-").insert(0, countryCallingCode).insert(0, PLUS_SIGN)
+ .insert(0, RFC3966_PREFIX);
return;
case NATIONAL:
default:
: formatNsnUsingPattern(number, formattingPattern, numberFormat, carrierCode);
}
- private NumberFormat chooseFormattingPatternForNumber(List<NumberFormat> availableFormats,
- String nationalNumber) {
+ NumberFormat chooseFormattingPatternForNumber(List<NumberFormat> availableFormats,
+ String nationalNumber) {
for (NumberFormat numFormat : availableFormats) {
int size = numFormat.leadingDigitsPatternSize();
if (size == 0 || regexCache.getPatternForRegex(
}
// Simple wrapper of formatNsnUsingPattern for the common case of no carrier code.
- private String formatNsnUsingPattern(String nationalNumber,
- NumberFormat formattingPattern,
- PhoneNumberFormat numberFormat) {
+ String formatNsnUsingPattern(String nationalNumber,
+ NumberFormat formattingPattern,
+ PhoneNumberFormat numberFormat) {
return formatNsnUsingPattern(nationalNumber, formattingPattern, numberFormat, null);
}
- // Note that carrierCode is optional - if NULL or an empty string, no carrier code replacement
+ // Note that carrierCode is optional - if null or an empty string, no carrier code replacement
// will take place.
private String formatNsnUsingPattern(String nationalNumber,
NumberFormat formattingPattern,
public PhoneNumber getExampleNumberForType(String regionCode, PhoneNumberType type) {
// Check the region code is valid.
if (!isValidRegionCode(regionCode)) {
- LOGGER.log(Level.WARNING, "Invalid or unknown region code provided: " + regionCode);
+ logger.log(Level.WARNING, "Invalid or unknown region code provided: " + regionCode);
return null;
}
PhoneNumberDesc desc = getNumberDescByType(getMetadataForRegion(regionCode), type);
return parse(desc.getExampleNumber(), regionCode);
}
} catch (NumberParseException e) {
- LOGGER.log(Level.SEVERE, e.toString());
+ logger.log(Level.SEVERE, e.toString());
}
return null;
}
return parse("+" + countryCallingCode + desc.getExampleNumber(), "ZZ");
}
} catch (NumberParseException e) {
- LOGGER.log(Level.SEVERE, e.toString());
+ logger.log(Level.SEVERE, e.toString());
}
} else {
- LOGGER.log(Level.WARNING,
+ logger.log(Level.WARNING,
"Invalid or unknown country calling code provided: " + countryCallingCode);
}
return null;
*/
public PhoneNumberType getNumberType(PhoneNumber number) {
String regionCode = getRegionCodeForNumber(number);
- if (!isValidRegionCode(regionCode) && !REGION_CODE_FOR_NON_GEO_ENTITY.equals(regionCode)) {
+ PhoneMetadata metadata = getMetadataForRegionOrCallingCode(number.getCountryCode(), regionCode);
+ if (metadata == null) {
return PhoneNumberType.UNKNOWN;
}
String nationalSignificantNumber = getNationalSignificantNumber(number);
- PhoneMetadata metadata = getMetadataForRegionOrCallingCode(number.getCountryCode(), regionCode);
return getNumberTypeHelper(nationalSignificantNumber, metadata);
}
private PhoneNumberType getNumberTypeHelper(String nationalNumber, PhoneMetadata metadata) {
- PhoneNumberDesc generalNumberDesc = metadata.getGeneralDesc();
- if (!generalNumberDesc.hasNationalNumberPattern() ||
- !isNumberMatchingDesc(nationalNumber, generalNumberDesc)) {
+ if (!isNumberMatchingDesc(nationalNumber, metadata.getGeneralDesc())) {
return PhoneNumberType.UNKNOWN;
}
return PhoneNumberType.UNKNOWN;
}
+ /**
+ * Returns the metadata for the given region code or {@code null} if the region code is invalid
+ * or unknown.
+ */
PhoneMetadata getMetadataForRegion(String regionCode) {
if (!isValidRegionCode(regionCode)) {
return null;
if (!regionToMetadataMap.containsKey(regionCode)) {
// The regionCode here will be valid and won't be '001', so we don't need to worry about
// what to pass in for the country calling code.
- loadMetadataFromFile(currentFilePrefix, regionCode, 0);
+ loadMetadataFromFile(currentFilePrefix, regionCode, 0, metadataLoader);
}
}
return regionToMetadataMap.get(regionCode);
return null;
}
if (!countryCodeToNonGeographicalMetadataMap.containsKey(countryCallingCode)) {
- loadMetadataFromFile(currentFilePrefix, REGION_CODE_FOR_NON_GEO_ENTITY, countryCallingCode);
+ loadMetadataFromFile(
+ currentFilePrefix, REGION_CODE_FOR_NON_GEO_ENTITY, countryCallingCode, metadataLoader);
}
}
return countryCodeToNonGeographicalMetadataMap.get(countryCallingCode);
}
- private boolean isNumberMatchingDesc(String nationalNumber, PhoneNumberDesc numberDesc) {
+ boolean isNumberPossibleForDesc(String nationalNumber, PhoneNumberDesc numberDesc) {
Matcher possibleNumberPatternMatcher =
regexCache.getPatternForRegex(numberDesc.getPossibleNumberPattern())
.matcher(nationalNumber);
+ return possibleNumberPatternMatcher.matches();
+ }
+
+ boolean isNumberMatchingDesc(String nationalNumber, PhoneNumberDesc numberDesc) {
Matcher nationalNumberPatternMatcher =
regexCache.getPatternForRegex(numberDesc.getNationalNumberPattern())
.matcher(nationalNumber);
- return possibleNumberPatternMatcher.matches() && nationalNumberPatternMatcher.matches();
+ return isNumberPossibleForDesc(nationalNumber, numberDesc) &&
+ nationalNumberPatternMatcher.matches();
}
/**
* 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.
+ * Warning: In most cases, you want to use {@link #isValidNumber} instead. For example, this
+ * method will mark numbers from British Crown dependencies such as the Isle of Man as invalid for
+ * the region "GB" (United Kingdom), since it has its own region code, "IM", which may be
+ * undesirable.
*
* @param number the phone number that we want to validate
* @param regionCode the region that we want to validate the phone number for
// match that of the region code.
return false;
}
- PhoneNumberDesc generalNumDesc = metadata.getGeneralDesc();
String nationalSignificantNumber = getNationalSignificantNumber(number);
-
- // For regions 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()) {
- int numberLength = nationalSignificantNumber.length();
- return numberLength > MIN_LENGTH_FOR_NSN && numberLength <= MAX_LENGTH_FOR_NSN;
- }
return getNumberTypeHelper(nationalSignificantNumber, metadata) != PhoneNumberType.UNKNOWN;
}
List<String> regions = countryCallingCodeToRegionCodeMap.get(countryCode);
if (regions == null) {
String numberString = getNationalSignificantNumber(number);
- LOGGER.log(Level.WARNING,
+ logger.log(Level.WARNING,
"Missing/invalid country_code (" + countryCode + ") for number " + numberString);
return null;
}
String nationalNumber = getNationalSignificantNumber(number);
for (String regionCode : regionCodes) {
// If leadingDigits is present, use this. Otherwise, do full validation.
+ // Metadata cannot be null because the region codes come from the country calling code map.
PhoneMetadata metadata = getMetadataForRegion(regionCode);
if (metadata.hasLeadingDigits()) {
if (regexCache.getPatternForRegex(metadata.getLeadingDigits())
/**
* Returns the region code that matches the specific country calling code. In the case of no
* region code being found, ZZ will be returned. In the case of multiple regions, the one
- * designated in the metadata as the "main" region for this calling code will be returned.
+ * designated in the metadata as the "main" region for this calling code will be returned. If the
+ * countryCallingCode entered is valid but doesn't match a specific region (such as in the case of
+ * non-geographical calling codes like 800) the value "001" will be returned (corresponding to
+ * the value for World in the UN M.49 schema).
*/
public String getRegionCodeForCountryCode(int countryCallingCode) {
List<String> regionCodes = countryCallingCodeToRegionCodeMap.get(countryCallingCode);
}
/**
+ * Returns a list with the region codes that match the specific country calling code. For
+ * non-geographical country calling codes, the region code 001 is returned. Also, in the case
+ * of no region code being found, an empty list is returned.
+ */
+ public List<String> getRegionCodesForCountryCode(int countryCallingCode) {
+ List<String> regionCodes = countryCallingCodeToRegionCodeMap.get(countryCallingCode);
+ return Collections.unmodifiableList(regionCodes == null ? new ArrayList<String>(0)
+ : regionCodes);
+ }
+
+ /**
* Returns the country calling code for a specific region. For example, this would be 1 for the
* United States, and 64 for New Zealand.
*
*/
public int getCountryCodeForRegion(String regionCode) {
if (!isValidRegionCode(regionCode)) {
- LOGGER.log(Level.WARNING,
+ logger.log(Level.WARNING,
"Invalid or missing region code ("
+ ((regionCode == null) ? "null" : regionCode)
+ ") provided.");
*
* @param regionCode the region that we want to get the country calling code for
* @return the country calling code for the region denoted by regionCode
+ * @throws IllegalArgumentException if the region is invalid
*/
private int getCountryCodeForValidRegion(String regionCode) {
PhoneMetadata metadata = getMetadataForRegion(regionCode);
+ if (metadata == null) {
+ throw new IllegalArgumentException("Invalid region code: " + regionCode);
+ }
return metadata.getCountryCode();
}
* @return the dialling prefix for the region denoted by regionCode
*/
public String getNddPrefixForRegion(String regionCode, boolean stripNonDigits) {
- if (!isValidRegionCode(regionCode)) {
- LOGGER.log(Level.WARNING,
+ PhoneMetadata metadata = getMetadataForRegion(regionCode);
+ if (metadata == null) {
+ logger.log(Level.WARNING,
"Invalid or missing region code ("
+ ((regionCode == null) ? "null" : regionCode)
+ ") provided.");
return null;
}
- PhoneMetadata metadata = getMetadataForRegion(regionCode);
String nationalPrefix = metadata.getNationalPrefix();
// If no national prefix was found, we return null.
if (nationalPrefix.length() == 0) {
* metadata for the country is found.
*/
boolean isLeadingZeroPossible(int countryCallingCode) {
- PhoneMetadata mainMetadataForCallingCode = getMetadataForRegion(
- getRegionCodeForCountryCode(countryCallingCode));
+ PhoneMetadata mainMetadataForCallingCode =
+ getMetadataForRegionOrCallingCode(countryCallingCode,
+ getRegionCodeForCountryCode(countryCallingCode));
if (mainMetadataForCallingCode == null) {
return false;
}
}
/**
+ * Helper method to check whether a number is too short to be a regular length phone number in a
+ * region.
+ */
+ private boolean isShorterThanPossibleNormalNumber(PhoneMetadata regionMetadata, String number) {
+ Pattern possibleNumberPattern = regexCache.getPatternForRegex(
+ regionMetadata.getGeneralDesc().getPossibleNumberPattern());
+ return testNumberLengthAgainstPattern(possibleNumberPattern, number) ==
+ ValidationResult.TOO_SHORT;
+ }
+
+ /**
* Check whether a phone number is a possible number. It provides a more lenient check than
* {@link #isValidNumber} in the following sense:
*<ol>
* 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.
- * </ol
+ * </ol>
* @param number the number that needs to be checked
* @return a ValidationResult object which indicates whether the number is possible
*/
return ValidationResult.INVALID_COUNTRY_CODE;
}
String regionCode = getRegionCodeForCountryCode(countryCode);
+ // Metadata cannot be null because the country calling code is valid.
PhoneMetadata metadata = getMetadataForRegionOrCallingCode(countryCode, regionCode);
- PhoneNumberDesc generalNumDesc = metadata.getGeneralDesc();
- // Handling case of numbers with no metadata.
- if (!generalNumDesc.hasNationalNumberPattern()) {
- LOGGER.log(Level.FINER, "Checking if number is possible with incomplete metadata.");
- int numberLength = nationalNumber.length();
- if (numberLength < MIN_LENGTH_FOR_NSN) {
- return ValidationResult.TOO_SHORT;
- } else if (numberLength > MAX_LENGTH_FOR_NSN) {
- return ValidationResult.TOO_LONG;
- } else {
- return ValidationResult.IS_POSSIBLE;
- }
- }
Pattern possibleNumberPattern =
- regexCache.getPatternForRegex(generalNumDesc.getPossibleNumberPattern());
+ regexCache.getPatternForRegex(metadata.getGeneralDesc().getPossibleNumberPattern());
return testNumberLengthAgainstPattern(possibleNumberPattern, nationalNumber);
}
phoneNumber.setCountryCodeSource(countryCodeSource);
}
if (countryCodeSource != CountryCodeSource.FROM_DEFAULT_COUNTRY) {
- if (fullNumber.length() < MIN_LENGTH_FOR_NSN) {
+ 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.");
private boolean checkRegionForParsing(String numberToParse, String defaultRegion) {
if (!isValidRegionCode(defaultRegion)) {
// If the number is null or empty, we can't infer the region.
- if (numberToParse == null || numberToParse.length() == 0 ||
+ if ((numberToParse == null) || (numberToParse.length() == 0) ||
!PLUS_CHARS_PATTERN.matcher(numberToParse).lookingAt()) {
return false;
}
* particular region is not performed. This can be done separately with {@link #isValidNumber}.
*
* @param numberToParse number that we are attempting to parse. This can contain formatting
- * such as +, ( and -, as well as a phone number extension.
+ * such as +, ( and -, as well as a phone number extension. It can also
+ * be provided in RFC3966 format.
* @param defaultRegion region 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
final long maxTries) {
return new Iterable<PhoneNumberMatch>() {
+ @Override
public Iterator<PhoneNumberMatch> iterator() {
return new PhoneNumberMatcher(
PhoneNumberUtil.this, text, defaultRegion, leniency, maxTries);
}
/**
+ * A helper function to set the values related to leading zeros in a PhoneNumber.
+ */
+ static void setItalianLeadingZerosForPhoneNumber(String nationalNumber, PhoneNumber phoneNumber) {
+ if (nationalNumber.length() > 1 && nationalNumber.charAt(0) == '0') {
+ phoneNumber.setItalianLeadingZero(true);
+ int numberOfLeadingZeros = 1;
+ // Note that if the national number is all "0"s, the last "0" is not counted as a leading
+ // zero.
+ while (numberOfLeadingZeros < nationalNumber.length() - 1 &&
+ nationalNumber.charAt(numberOfLeadingZeros) == '0') {
+ numberOfLeadingZeros++;
+ }
+ if (numberOfLeadingZeros != 1) {
+ phoneNumber.setNumberOfLeadingZeros(numberOfLeadingZeros);
+ }
+ }
+ }
+
+ /**
* Parses a string and fills up the phoneNumber. This method is the same as the public
* parse() method, with the exception that it allows the default region to be null, for use by
* isNumberMatch(). checkRegion should be set to false if it is permitted for the default region
throw new NumberParseException(NumberParseException.ErrorType.TOO_LONG,
"The string supplied was too long to parse.");
}
- // 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)) {
+
+ StringBuilder nationalNumber = new StringBuilder();
+ buildNationalNumberForParsing(numberToParse, nationalNumber);
+
+ if (!isViablePhoneNumber(nationalNumber.toString())) {
throw new NumberParseException(NumberParseException.ErrorType.NOT_A_NUMBER,
"The string supplied did not seem to be a phone number.");
}
// Check the region supplied is valid, or that the extracted number starts with some sort of +
// sign so the number's region can be determined.
- if (checkRegion && !checkRegionForParsing(number, defaultRegion)) {
+ if (checkRegion && !checkRegionForParsing(nationalNumber.toString(), defaultRegion)) {
throw new NumberParseException(NumberParseException.ErrorType.INVALID_COUNTRY_CODE,
"Missing or invalid default region.");
}
if (keepRawInput) {
phoneNumber.setRawInput(numberToParse);
}
- StringBuilder nationalNumber = new StringBuilder(number);
// Attempt to parse extension first, since it doesn't require region-specific data and we want
// to have the non-normalised number here.
String extension = maybeStripExtension(nationalNumber);
if (countryCode != 0) {
String phoneNumberRegion = getRegionCodeForCountryCode(countryCode);
if (!phoneNumberRegion.equals(defaultRegion)) {
+ // Metadata cannot be null because the country calling code is valid.
regionMetadata = getMetadataForRegionOrCallingCode(countryCode, phoneNumberRegion);
}
} else {
}
if (regionMetadata != null) {
StringBuilder carrierCode = new StringBuilder();
- maybeStripNationalPrefixAndCarrierCode(normalizedNationalNumber, regionMetadata, carrierCode);
- if (keepRawInput) {
- phoneNumber.setPreferredDomesticCarrierCode(carrierCode.toString());
+ StringBuilder potentialNationalNumber = new StringBuilder(normalizedNationalNumber);
+ maybeStripNationalPrefixAndCarrierCode(potentialNationalNumber, regionMetadata, carrierCode);
+ // We require that the NSN remaining after stripping the national prefix and carrier code be
+ // of a possible length for the region. Otherwise, we don't do the stripping, since the
+ // original number could be a valid short number.
+ if (!isShorterThanPossibleNormalNumber(regionMetadata, potentialNationalNumber.toString())) {
+ normalizedNationalNumber = potentialNationalNumber;
+ if (keepRawInput) {
+ phoneNumber.setPreferredDomesticCarrierCode(carrierCode.toString());
+ }
}
}
int lengthOfNationalNumber = normalizedNationalNumber.length();
throw new NumberParseException(NumberParseException.ErrorType.TOO_LONG,
"The string supplied is too long to be a phone number.");
}
- if (normalizedNationalNumber.charAt(0) == '0') {
- phoneNumber.setItalianLeadingZero(true);
- }
+ setItalianLeadingZerosForPhoneNumber(normalizedNationalNumber.toString(), phoneNumber);
phoneNumber.setNationalNumber(Long.parseLong(normalizedNationalNumber.toString()));
}
/**
+ * Converts numberToParse to a form that we can parse and write it to nationalNumber if it is
+ * written in RFC3966; otherwise extract a possible number out of it and write to nationalNumber.
+ */
+ private void buildNationalNumberForParsing(String numberToParse, StringBuilder nationalNumber) {
+ int indexOfPhoneContext = numberToParse.indexOf(RFC3966_PHONE_CONTEXT);
+ if (indexOfPhoneContext > 0) {
+ int phoneContextStart = indexOfPhoneContext + RFC3966_PHONE_CONTEXT.length();
+ // If the phone context contains a phone number prefix, we need to capture it, whereas domains
+ // will be ignored.
+ if (numberToParse.charAt(phoneContextStart) == PLUS_SIGN) {
+ // Additional parameters might follow the phone context. If so, we will remove them here
+ // because the parameters after phone context are not important for parsing the
+ // phone number.
+ int phoneContextEnd = numberToParse.indexOf(';', phoneContextStart);
+ if (phoneContextEnd > 0) {
+ nationalNumber.append(numberToParse.substring(phoneContextStart, phoneContextEnd));
+ } else {
+ nationalNumber.append(numberToParse.substring(phoneContextStart));
+ }
+ }
+
+ // Now append everything between the "tel:" prefix and the phone-context. This should include
+ // the national number, an optional extension or isdn-subaddress component. Note we also
+ // handle the case when "tel:" is missing, as we have seen in some of the phone number inputs.
+ // In that case, we append everything from the beginning.
+ int indexOfRfc3966Prefix = numberToParse.indexOf(RFC3966_PREFIX);
+ int indexOfNationalNumber = (indexOfRfc3966Prefix >= 0) ?
+ indexOfRfc3966Prefix + RFC3966_PREFIX.length() : 0;
+ nationalNumber.append(numberToParse.substring(indexOfNationalNumber, indexOfPhoneContext));
+ } else {
+ // Extract a possible number from the string passed in (this strips leading characters that
+ // could not be the start of a phone number.)
+ nationalNumber.append(extractPossibleNumber(numberToParse));
+ }
+
+ // Delete the isdn-subaddress and everything after it if it is present. Note extension won't
+ // appear at the same time with isdn-subaddress according to paragraph 5.3 of the RFC3966 spec,
+ int indexOfIsdn = nationalNumber.indexOf(RFC3966_ISDN_SUBADDRESS);
+ if (indexOfIsdn > 0) {
+ nationalNumber.delete(indexOfIsdn, nationalNumber.length());
+ }
+ // If both phone context and isdn-subaddress are absent but other parameters are present, the
+ // parameters are left in nationalNumber. This is because we are concerned about deleting
+ // content from a potential number string when there is no strong evidence that the number is
+ // actually written in RFC3966.
+ }
+
+ /**
* Takes two phone numbers and compares them for equality.
*
* <p>Returns EXACT_MATCH if the country_code, NSN, presence of a leading zero for Italian numbers
/**
* Returns true if the number can be dialled from outside the region, or unknown. If the number
* can only be dialled from within the region, returns false. Does not check the number is a valid
- * number.
+ * number. Note that, at the moment, this method does not handle short numbers.
* TODO: Make this method public when we have enough metadata to make it worthwhile.
*
* @param number the phone-number for which we want to know whether it is diallable from
*/
// @VisibleForTesting
boolean canBeInternationallyDialled(PhoneNumber number) {
- String regionCode = getRegionCodeForNumber(number);
- if (!isValidRegionCode(regionCode)) {
+ PhoneMetadata metadata = getMetadataForRegion(getRegionCodeForNumber(number));
+ if (metadata == null) {
// Note numbers belonging to non-geographical entities (e.g. +800 numbers) are always
// internationally diallable, and will be caught here.
return true;
}
- PhoneMetadata metadata = getMetadataForRegion(regionCode);
String nationalSignificantNumber = getNationalSignificantNumber(number);
return !isNumberMatchingDesc(nationalSignificantNumber, metadata.getNoInternationalDialling());
}
+
+ /**
+ * Returns true if the supplied region supports mobile number portability. Returns false for
+ * invalid, unknown or regions that don't support mobile number portability.
+ *
+ * @param regionCode the region for which we want to know whether it supports mobile number
+ * portability or not.
+ */
+ public boolean isMobileNumberPortableRegion(String regionCode) {
+ PhoneMetadata metadata = getMetadataForRegion(regionCode);
+ if (metadata == null) {
+ logger.log(Level.WARNING, "Invalid or unknown region code provided: " + regionCode);
+ return false;
+ }
+ return metadata.isMobileNumberPortableRegion();
+ }
}