+libphonenumber (5.7) precise; urgency=low
+
+ * Code changes:
+ - Improve phone number extraction recall.
+ - Add support for loading short number metadata.
+
+ -- David Yonge-Mallo <davinci@google.com> Wed, 17 Jul 2013 11:25:45 +0200
+
libphonenumber (5.6.2) precise; urgency=low
* Metadata changes:
- Updated geocoding data for country calling code(s):
49 (en), 98 (en, fa), 241 (en)
- -- Lara Rennie <lararennie@google.com> Tue, Jul 16 2013 10:01:53 +0200
+ -- Lara Rennie <lararennie@google.com> Tue, 16 Jul 2013 10:01:53 +0200
libphonenumber (5.6.1) precise; urgency=low
* Code changes:
- Rename all instances of PhoneNumberMetadata to PhoneNumberMetadata for consistency.
- -- David Yonge-Mallo <davinci@google.com> Fri, Jul 12 2013 10:26:53 +0200
+ -- David Yonge-Mallo <davinci@google.com> Fri, 12 Jul 2013 10:26:53 +0200
libphonenumber (5.6) precise; urgency=low
</fileset>
</path>
- <target name="build-metadata">
+ <target name="build-phone-metadata">
<exec executable="java">
<arg value="-jar" />
<arg value="${build.tools.jar}"/>
</target>
<target name="compile" description="Compile Java source."
- depends="build-metadata,build-alternate-metadata,build-geo-data">
+ depends="build-phone-metadata,build-short-metadata,build-alternate-metadata,build-geo-data">
<mkdir dir="${classes.dir}"/>
<javac srcdir="${libphonenumber.src.dir};${geocoder.src.dir}"
destdir="${classes.dir}" classpathref="classpath" includeAntRuntime="false"/>
<fileset dir="${libphonenumber.src.dir}">
<include name="**/PhoneNumberMetadataProto*"/>
<include name="**/PhoneNumberAlternateFormatsProto*"/>
+ <include name="**/ShortNumberMetadataProto*"/>
</fileset>
</jar>
<jar destfile="${jar.dir}/offline-geocoder.jar">
<fileset dir="${libphonenumber.src.dir}">
<include name="**/PhoneNumberMetadataProto*"/>
<include name="**/PhoneNumberAlternateFormatsProto*"/>
+ <include name="**/ShortNumberMetadataProto*"/>
</fileset>
<fileset dir="${libphonenumber.test.dir}">
<include name="**/PhoneNumberMetadataProtoForTesting*"/>
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
class MetadataManager {
private static final String ALTERNATE_FORMATS_FILE_PREFIX =
"/com/google/i18n/phonenumbers/data/PhoneNumberAlternateFormatsProto";
+ private static final String SHORT_NUMBER_METADATA_FILE_PREFIX =
+ "/com/google/i18n/phonenumbers/data/ShortNumberMetadataProto";
private static final Logger LOGGER = Logger.getLogger(MetadataManager.class.getName());
private static final Map<Integer, PhoneMetadata> callingCodeToAlternateFormatsMap =
Collections.synchronizedMap(new HashMap<Integer, PhoneMetadata>());
+ private static final Map<String, PhoneMetadata> regionCodeToShortNumberMetadataMap =
+ Collections.synchronizedMap(new HashMap<String, PhoneMetadata>());
// A set of which country calling codes there are alternate format data for. If the set has an
// entry for a code, then there should be data for that code linked into the resources.
private static final Set<Integer> countryCodeSet =
AlternateFormatsCountryCodeSet.getCountryCodeSet();
+ // A set of which region codes there are short number data for. If the set has an entry for a
+ // code, then there should be data for that code linked into the resources.
+ private static final Set<String> regionCodeSet = ShortNumbersRegionCodeSet.getRegionCodeSet();
+
private MetadataManager() {
}
}
}
- private static void loadMetadataFromFile(int countryCallingCode) {
+ private static void loadAlternateFormatsMetadataFromFile(int countryCallingCode) {
InputStream source = PhoneNumberMatcher.class.getResourceAsStream(
ALTERNATE_FORMATS_FILE_PREFIX + "_" + countryCallingCode);
ObjectInputStream in = null;
}
synchronized (callingCodeToAlternateFormatsMap) {
if (!callingCodeToAlternateFormatsMap.containsKey(countryCallingCode)) {
- loadMetadataFromFile(countryCallingCode);
+ loadAlternateFormatsMetadataFromFile(countryCallingCode);
}
}
return callingCodeToAlternateFormatsMap.get(countryCallingCode);
}
+
+ private static void loadShortNumberMetadataFromFile(String regionCode) {
+ InputStream source = PhoneNumberMatcher.class.getResourceAsStream(
+ SHORT_NUMBER_METADATA_FILE_PREFIX + "_" + regionCode);
+ ObjectInputStream in = null;
+ try {
+ in = new ObjectInputStream(source);
+ PhoneMetadataCollection shortNumberMetadata = new PhoneMetadataCollection();
+ shortNumberMetadata.readExternal(in);
+ for (PhoneMetadata metadata : shortNumberMetadata.getMetadataList()) {
+ regionCodeToShortNumberMetadataMap.put(regionCode, metadata);
+ }
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, e.toString());
+ } finally {
+ close(in);
+ }
+ }
+
+ static PhoneMetadata getShortNumberMetadataForRegion(String regionCode) {
+ if (!regionCodeSet.contains(regionCode)) {
+ return null;
+ }
+ synchronized (regionCodeToShortNumberMetadataMap) {
+ if (!regionCodeToShortNumberMetadataMap.containsKey(regionCode)) {
+ loadShortNumberMetadataFromFile(regionCode);
+ }
+ }
+ return regionCodeToShortNumberMetadataMap.get(regionCode);
+ }
+
+ // @VisibleForTesting
+ static Set<String> getShortNumberMetadataSupportedRegions() {
+ return regionCodeSet;
+ }
}
return false;
}
- static boolean containsMoreThanOneSlash(String candidate) {
- int firstSlashIndex = candidate.indexOf('/');
- return (firstSlashIndex > 0 && candidate.substring(firstSlashIndex + 1).contains("/"));
+ static boolean containsMoreThanOneSlashInNationalNumber(PhoneNumber number, String candidate) {
+ int firstSlashInBodyIndex = candidate.indexOf('/');
+ if (firstSlashInBodyIndex < 0) {
+ // No slashes, this is okay.
+ return false;
+ }
+ // Now look for a second one.
+ int secondSlashInBodyIndex = candidate.indexOf('/', firstSlashInBodyIndex + 1);
+ if (secondSlashInBodyIndex < 0) {
+ // Only one slash, this is okay.
+ return false;
+ }
+
+ // If the first slash is after the country calling code, this is permitted.
+ boolean candidateHasCountryCode =
+ (number.getCountryCodeSource() == CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN ||
+ number.getCountryCodeSource() == CountryCodeSource.FROM_NUMBER_WITHOUT_PLUS_SIGN);
+ if (candidateHasCountryCode &&
+ PhoneNumberUtil.normalizeDigitsOnly(candidate.substring(0, firstSlashInBodyIndex))
+ .equals(Integer.toString(number.getCountryCode()))) {
+ // Any more slashes and this is illegal.
+ return candidate.substring(secondSlashInBodyIndex + 1).contains("/");
+ }
+ return true;
}
static boolean containsOnlyValidXChars(
* 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
boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util) {
if (!util.isValidNumber(number) ||
!PhoneNumberMatcher.containsOnlyValidXChars(number, candidate, util) ||
- PhoneNumberMatcher.containsMoreThanOneSlash(candidate) ||
+ PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate) ||
!PhoneNumberMatcher.isNationalPrefixPresentIfRequired(number, util)) {
return false;
}
boolean verify(PhoneNumber number, String candidate, PhoneNumberUtil util) {
if (!util.isValidNumber(number) ||
!PhoneNumberMatcher.containsOnlyValidXChars(number, candidate, util) ||
- PhoneNumberMatcher.containsMoreThanOneSlash(candidate) ||
+ PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate) ||
!PhoneNumberMatcher.isNationalPrefixPresentIfRequired(number, util)) {
return false;
}
}
/**
+ * 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.
*/
: format(numberNoExt, PhoneNumberFormat.E164);
}
return withFormatting ? formattedNumber
- : normalizeHelper(formattedNumber, DIALLABLE_CHAR_MAPPINGS,
- true /* remove non matches */);
+ : normalizeDiallableCharsOnly(formattedNumber);
}
/**
// 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.
if (formattedNumber != null && rawInput.length() > 0) {
- String normalizedFormattedNumber =
- normalizeHelper(formattedNumber, DIALLABLE_CHAR_MAPPINGS, true /* remove non matches */);
- String normalizedRawInput =
- normalizeHelper(rawInput, DIALLABLE_CHAR_MAPPINGS, true /* remove non matches */);
+ String normalizedFormattedNumber = normalizeDiallableCharsOnly(formattedNumber);
+ String normalizedRawInput = normalizeDiallableCharsOnly(rawInput);
if (!normalizedFormattedNumber.equals(normalizedRawInput)) {
formattedNumber = rawInput;
}
return countryCodeToNonGeographicalMetadataMap.get(countryCallingCode);
}
- private boolean isNumberMatchingDesc(String nationalNumber, PhoneNumberDesc numberDesc) {
+ // @VisibleForTesting
+ boolean isNumberPossibleForDesc(String nationalNumber, PhoneNumberDesc numberDesc) {
Matcher possibleNumberPatternMatcher =
regexCache.getPatternForRegex(numberDesc.getPossibleNumberPattern())
.matcher(nationalNumber);
+ return possibleNumberPatternMatcher.matches();
+ }
+
+ // @VisibleForTesting
+ 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();
}
/**
return this;
}
+ // required PhoneNumberDesc emergency = 27;
+ private boolean hasEmergency;
+ private PhoneNumberDesc emergency_ = null;
+ public boolean hasEmergency() { return hasEmergency; }
+ public PhoneNumberDesc getEmergency() { return emergency_; }
+ public PhoneMetadata setEmergency(PhoneNumberDesc value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ hasEmergency = true;
+ emergency_ = value;
+ return this;
+ }
+
// required PhoneNumberDesc voicemail = 28;
private boolean hasVoicemail;
private PhoneNumberDesc voicemail_ = null;
return this;
}
- // required PhoneNumberDesc emergency = 27;
- private boolean hasEmergency;
- private PhoneNumberDesc emergency_ = null;
- public boolean hasEmergency() { return hasEmergency; }
- public PhoneNumberDesc getEmergency() { return emergency_; }
- public PhoneMetadata setEmergency(PhoneNumberDesc value) {
+ // required PhoneNumberDesc short_code = 29;
+ private boolean hasShortCode;
+ private PhoneNumberDesc shortCode_ = null;
+ public boolean hasShortCode() { return hasShortCode; }
+ public PhoneNumberDesc getShortCode() { return shortCode_; }
+ public PhoneMetadata setShortCode(PhoneNumberDesc value) {
if (value == null) {
throw new NullPointerException();
}
- hasEmergency = true;
- emergency_ = value;
+ hasShortCode = true;
+ shortCode_ = value;
+ return this;
+ }
+
+ // required PhoneNumberDesc standard_rate = 30;
+ private boolean hasStandardRate;
+ private PhoneNumberDesc standardRate_ = null;
+ public boolean hasStandardRate() { return hasStandardRate; }
+ public PhoneNumberDesc getStandardRate() { return standardRate_; }
+ public PhoneMetadata setStandardRate(PhoneNumberDesc value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ hasStandardRate = true;
+ standardRate_ = value;
return this;
}
if (hasUan) {
uan_.writeExternal(objectOutput);
}
+ objectOutput.writeBoolean(hasEmergency);
+ if (hasEmergency) {
+ emergency_.writeExternal(objectOutput);
+ }
objectOutput.writeBoolean(hasVoicemail);
if (hasVoicemail) {
voicemail_.writeExternal(objectOutput);
}
- objectOutput.writeBoolean(hasEmergency);
- if (hasEmergency) {
- emergency_.writeExternal(objectOutput);
+ objectOutput.writeBoolean(hasShortCode);
+ if (hasShortCode) {
+ shortCode_.writeExternal(objectOutput);
+ }
+ objectOutput.writeBoolean(hasStandardRate);
+ if (hasStandardRate) {
+ standardRate_.writeExternal(objectOutput);
}
objectOutput.writeBoolean(hasNoInternationalDialling);
if (hasNoInternationalDialling) {
if (hasDesc) {
PhoneNumberDesc desc = new PhoneNumberDesc();
desc.readExternal(objectInput);
+ setEmergency(desc);
+ }
+ hasDesc = objectInput.readBoolean();
+ if (hasDesc) {
+ PhoneNumberDesc desc = new PhoneNumberDesc();
+ desc.readExternal(objectInput);
setVoicemail(desc);
}
hasDesc = objectInput.readBoolean();
if (hasDesc) {
PhoneNumberDesc desc = new PhoneNumberDesc();
desc.readExternal(objectInput);
- setEmergency(desc);
+ setShortCode(desc);
+ }
+ hasDesc = objectInput.readBoolean();
+ if (hasDesc) {
+ PhoneNumberDesc desc = new PhoneNumberDesc();
+ desc.readExternal(objectInput);
+ setStandardRate(desc);
}
hasDesc = objectInput.readBoolean();
if (hasDesc) {
package com.google.i18n.phonenumbers;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+import java.util.Collections;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import java.util.regex.Pattern;
/*
* most commercial short numbers are not handled here, but by the PhoneNumberUtil.
*
* @author Shaopeng Jia
+ * @author David Yonge-Mallo
*/
public class ShortNumberUtil {
private final PhoneNumberUtil phoneUtil;
+ private static final Logger LOGGER = Logger.getLogger(ShortNumberUtil.class.getName());
+
+ /**
+ * Cost categories of short numbers.
+ */
+ public enum ShortNumberCost {
+ TOLL_FREE,
+ STANDARD_RATE,
+ PREMIUM_RATE,
+ UNKNOWN_COST
+ }
public ShortNumberUtil() {
phoneUtil = PhoneNumberUtil.getInstance();
}
/**
+ * Convenience method to get a list of what regions the library has metadata for.
+ */
+ public Set<String> getSupportedRegions() {
+ return Collections.unmodifiableSet(MetadataManager.getShortNumberMetadataSupportedRegions());
+ }
+
+ /**
+ * Gets a valid short number for the specified region.
+ *
+ * @param regionCode the region for which an example short number is needed
+ * @return a valid short number for the specified region. Returns an empty string when the
+ * metadata does not contain such information.
+ */
+ // @VisibleForTesting
+ String getExampleShortNumber(String regionCode) {
+ PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
+ if (null == phoneMetadata) {
+ LOGGER.log(Level.WARNING, "Unable to get short number metadata for region: " + regionCode);
+ return "";
+ }
+ PhoneNumberDesc desc = phoneMetadata.getShortCode();
+ if (desc.hasExampleNumber()) {
+ return desc.getExampleNumber();
+ }
+ return "";
+ }
+
+ /**
+ * Gets a valid short number for the specified cost category.
+ *
+ * @param regionCode the region for which an example short number is needed
+ * @param cost the cost category of number that is needed
+ * @return a valid short number for the specified region and cost category. Returns an empty
+ * string when the metadata does not contain such information, or the cost is UNKNOWN_COST.
+ */
+ // @VisibleForTesting
+ String getExampleShortNumberForCost(String regionCode, ShortNumberCost cost) {
+ PhoneMetadata phoneMetadata = MetadataManager.getShortNumberMetadataForRegion(regionCode);
+ if (null == phoneMetadata) {
+ LOGGER.log(Level.WARNING, "Unable to get short number metadata for region: " + regionCode);
+ return "";
+ }
+ PhoneNumberDesc desc = getShortNumberDescByCost(phoneMetadata, cost);
+ if (desc != null && desc.hasExampleNumber()) {
+ return desc.getExampleNumber();
+ }
+ return "";
+ }
+
+ private PhoneNumberDesc getShortNumberDescByCost(PhoneMetadata metadata, ShortNumberCost cost) {
+ switch (cost) {
+ case TOLL_FREE:
+ return metadata.getTollFree();
+ case STANDARD_RATE:
+ return metadata.getStandardRate();
+ case PREMIUM_RATE:
+ return metadata.getPremiumRate();
+ default:
+ // UNKNOWN_COST numbers are computed by the process of elimination from the other cost
+ // categories.
+ return null;
+ }
+ }
+
+ /**
* Returns true if the number might be used to connect to an emergency service in the given
* region.
*
public class ShortNumbersRegionCodeSet {
// A set of all region codes for which data is available.
static Set<String> getRegionCodeSet() {
- // The capacity is set to 1 as there are 50 different entries,
+ // The capacity is set to 66 as there are 50 different entries,
// and this offers a load factor of roughly 0.75.
- Set<String> regionCodeSet = new HashSet<String>(1);
+ Set<String> regionCodeSet = new HashSet<String>(66);
regionCodeSet.add("AM");
regionCodeSet.add("AR");
// countries sharing a calling code, such as the NANPA countries, the one
// indicated with "isMainCountryForCode" in the metadata should be first.
static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {
- // The capacity is set to 26 as there are 20 different country codes,
+ // The capacity is set to 26 as there are 20 different entries,
// and this offers a load factor of roughly 0.75.
Map<Integer, List<String>> countryCodeToRegionCodeMap =
new HashMap<Integer, List<String>>(26);
public class ExampleNumbersTest extends TestCase {
private static final Logger LOGGER = Logger.getLogger(ExampleNumbersTest.class.getName());
private PhoneNumberUtil phoneNumberUtil;
+ private ShortNumberUtil shortNumberUtil;
private List<PhoneNumber> invalidCases = new ArrayList<PhoneNumber>();
private List<PhoneNumber> wrongTypeCases = new ArrayList<PhoneNumber>();
public ExampleNumbersTest() {
PhoneNumberUtil.resetInstance();
phoneNumberUtil = PhoneNumberUtil.getInstance();
+ shortNumberUtil = new ShortNumberUtil(phoneNumberUtil);
}
@Override
}
public void testEmergency() throws Exception {
- ShortNumberUtil shortUtil = new ShortNumberUtil(phoneNumberUtil);
int wrongTypeCounter = 0;
for (String regionCode : phoneNumberUtil.getSupportedRegions()) {
PhoneNumberDesc desc =
if (desc.hasExampleNumber()) {
String exampleNumber = desc.getExampleNumber();
if (!exampleNumber.matches(desc.getPossibleNumberPattern()) ||
- !shortUtil.isEmergencyNumber(exampleNumber, regionCode)) {
+ !shortNumberUtil.isEmergencyNumber(exampleNumber, regionCode)) {
wrongTypeCounter++;
LOGGER.log(Level.SEVERE, "Emergency example number test failed for " + regionCode);
}
PhoneNumber exampleNumber = phoneNumberUtil.getExampleNumber(regionCode);
assertNotNull("None found for region " + regionCode, exampleNumber);
}
+
+ for (String regionCode : shortNumberUtil.getSupportedRegions()) {
+ String exampleShortNumber = shortNumberUtil.getExampleShortNumber(regionCode);
+ assertFalse("No example short number found for region " + regionCode,
+ exampleShortNumber.equals(""));
+ }
}
}
assertTrue(germanyAlternateFormats.numberFormats().size() > 0);
}
+ public void testShortNumberMetadataContainsData() throws Exception {
+ // We should have some data for France.
+ PhoneMetadata franceShortNumberMetadata = MetadataManager.getShortNumberMetadataForRegion("FR");
+ assertNotNull(franceShortNumberMetadata);
+ assertTrue(franceShortNumberMetadata.hasShortCode());
+ }
+
public void testAlternateFormatsFailsGracefully() throws Exception {
PhoneMetadata noAlternateFormats = MetadataManager.getAlternateFormatsForCountry(999);
assertNull(noAlternateFormats);
}
+
+ public void testShortNumberMetadataFailsGracefully() throws Exception {
+ PhoneMetadata noShortNumberMetadata = MetadataManager.getShortNumberMetadataForRegion("XXX");
+ assertNull(noShortNumberMetadata);
+ }
}
import com.google.i18n.phonenumbers.PhoneNumberUtil.Leniency;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource;
import java.util.ArrayList;
import java.util.Arrays;
*/
public class PhoneNumberMatcherTest extends TestMetadataTestCase {
+ public void testContainsMoreThanOneSlashInNationalNumber() throws Exception {
+ // A date should return true.
+ PhoneNumber number = new PhoneNumber();
+ number.setCountryCode(1);
+ number.setCountryCodeSource(CountryCodeSource.FROM_DEFAULT_COUNTRY);
+ String candidate = "1/05/2013";
+ assertTrue(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ // Here, the country code source thinks it started with a country calling code, but this is not
+ // the same as the part before the slash, so it's still true.
+ number = new PhoneNumber();
+ number.setCountryCode(274);
+ number.setCountryCodeSource(CountryCodeSource.FROM_NUMBER_WITHOUT_PLUS_SIGN);
+ candidate = "27/4/2013";
+ assertTrue(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ // Now it should be false, because the first slash is after the country calling code.
+ number = new PhoneNumber();
+ number.setCountryCode(49);
+ number.setCountryCodeSource(CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN);
+ candidate = "49/69/2013";
+ assertFalse(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ number = new PhoneNumber();
+ number.setCountryCode(49);
+ number.setCountryCodeSource(CountryCodeSource.FROM_NUMBER_WITHOUT_PLUS_SIGN);
+ candidate = "+49/69/2013";
+ assertFalse(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ candidate = "+ 49/69/2013";
+ assertFalse(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ candidate = "+ 49/69/20/13";
+ assertTrue(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+
+ // Here, the first group is not assumed to be the country calling code, even though it is the
+ // same as it, so this should return true.
+ number = new PhoneNumber();
+ number.setCountryCode(49);
+ number.setCountryCodeSource(CountryCodeSource.FROM_DEFAULT_COUNTRY);
+ candidate = "49/69/2013";
+ assertTrue(PhoneNumberMatcher.containsMoreThanOneSlashInNationalNumber(number, candidate));
+ }
+
/** See {@link PhoneNumberUtilTest#testParseNationalNumber()}. */
public void testFindNationalNumber() throws Exception {
// same cases as in testParseNationalNumber
doTestNumberNonMatchesForLeniency(testCases, Leniency.EXACT_GROUPING);
}
- private void doTestNumberMatchesForLeniency(List<NumberTest> testCases,
- PhoneNumberUtil.Leniency leniency) {
+ private void doTestNumberMatchesForLeniency(List<NumberTest> testCases, Leniency leniency) {
int noMatchFoundCount = 0;
int wrongMatchFoundCount = 0;
for (NumberTest test : testCases) {
assertEquals(0, wrongMatchFoundCount);
}
- private void doTestNumberNonMatchesForLeniency(List<NumberTest> testCases,
- PhoneNumberUtil.Leniency leniency) {
+ private void doTestNumberNonMatchesForLeniency(List<NumberTest> testCases, Leniency leniency) {
int matchFoundCount = 0;
for (NumberTest test : testCases) {
Iterator<PhoneNumberMatch> iterator =
}
private Iterator<PhoneNumberMatch> findNumbersForLeniency(
- String text, String defaultCountry, PhoneNumberUtil.Leniency leniency) {
+ String text, String defaultCountry, Leniency leniency) {
return phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator();
}
PhoneNumberUtil.normalizeDigitsOnly(inputNumber));
}
+ public void testNormaliseStripNonDiallableCharacters() {
+ String inputNumber = "03*4-56&+a#234";
+ String expectedOutput = "03*456+234";
+ assertEquals("Conversion did not correctly remove non-diallable characters",
+ expectedOutput,
+ PhoneNumberUtil.normalizeDiallableCharsOnly(inputNumber));
+ }
+
public void testFormatUSNumber() {
assertEquals("650 253 0000", phoneUtil.format(US_NUMBER, PhoneNumberFormat.NATIONAL));
assertEquals("+1 650 253 0000", phoneUtil.format(US_NUMBER, PhoneNumberFormat.INTERNATIONAL));
static final String UN001 = "001";
static final String AD = "AD";
static final String AE = "AE";
+ static final String AM = "AM";
static final String AO = "AO";
static final String AQ = "AQ";
static final String AR = "AR";
static final String CN = "CN";
static final String CS = "CS";
static final String DE = "DE";
+ static final String FR = "FR";
static final String GB = "GB";
static final String IT = "IT";
static final String JP = "JP";
shortUtil = new ShortNumberUtil(phoneUtil);
}
+ public void testGetExampleShortNumber() {
+ assertEquals("8711", shortUtil.getExampleShortNumber(RegionCode.AM));
+ assertEquals("1010", shortUtil.getExampleShortNumber(RegionCode.FR));
+ assertEquals("", shortUtil.getExampleShortNumber(RegionCode.UN001));
+ assertEquals("", shortUtil.getExampleShortNumber(null));
+ }
+
+ public void testGetExampleShortNumberForCost() {
+ assertEquals("3010", shortUtil.getExampleShortNumberForCost(RegionCode.FR,
+ ShortNumberUtil.ShortNumberCost.TOLL_FREE));
+ assertEquals("118777", shortUtil.getExampleShortNumberForCost(RegionCode.FR,
+ ShortNumberUtil.ShortNumberCost.STANDARD_RATE));
+ assertEquals("3200", shortUtil.getExampleShortNumberForCost(RegionCode.FR,
+ ShortNumberUtil.ShortNumberCost.PREMIUM_RATE));
+ assertEquals("", shortUtil.getExampleShortNumberForCost(RegionCode.FR,
+ ShortNumberUtil.ShortNumberCost.UNKNOWN_COST));
+ }
+
public void testConnectsToEmergencyNumber_US() {
assertTrue(shortUtil.connectsToEmergencyNumber("911", RegionCode.US));
assertTrue(shortUtil.connectsToEmergencyNumber("119", RegionCode.US));
+Jul 17, 2013: libphonenumber-5.7
+* Code changes:
+ - Improve phone number extraction recall.
+ - Add support for loading short number metadata.
+
Jul 16, 2013: libphonenumber-5.6.2
* Metadata changes:
- Updated alternate formatting data for country calling code(s): 49, 61
required PhoneNumberDesc uan = 25;
required PhoneNumberDesc emergency = 27;
required PhoneNumberDesc voicemail = 28;
+ required PhoneNumberDesc short_code = 29;
+ required PhoneNumberDesc standard_rate = 30;
// The rules here distinguish the numbers that are only able to be dialled
// nationally.
required PhoneNumberDesc no_international_dialling = 24;
private static final String PREFERRED_INTERNATIONAL_PREFIX = "preferredInternationalPrefix";
private static final String PREMIUM_RATE = "premiumRate";
private static final String SHARED_COST = "sharedCost";
+ private static final String SHORT_CODE = "shortCode";
+ private static final String STANDARD_RATE = "standardRate";
private static final String TOLL_FREE = "tollFree";
private static final String UAN = "uan";
private static final String VOICEMAIL = "voicemail";
processPhoneNumberDescElement(generalDesc, element, MOBILE, liteBuild));
metadata.setTollFree(
processPhoneNumberDescElement(generalDesc, element, TOLL_FREE, liteBuild));
+ metadata.setStandardRate(
+ processPhoneNumberDescElement(generalDesc, element, STANDARD_RATE, liteBuild));
metadata.setPremiumRate(
processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE, liteBuild));
+ metadata.setShortCode(
+ processPhoneNumberDescElement(generalDesc, element, SHORT_CODE, liteBuild));
metadata.setSharedCost(
processPhoneNumberDescElement(generalDesc, element, SHARED_COST, liteBuild));
metadata.setVoip(
" <mobile><nationalNumberPattern>\\d{2}</nationalNumberPattern></mobile>" +
" <pager><nationalNumberPattern>\\d{3}</nationalNumberPattern></pager>" +
" <tollFree><nationalNumberPattern>\\d{4}</nationalNumberPattern></tollFree>" +
- " <premiumRate><nationalNumberPattern>\\d{5}</nationalNumberPattern></premiumRate>" +
- " <sharedCost><nationalNumberPattern>\\d{6}</nationalNumberPattern></sharedCost>" +
- " <personalNumber><nationalNumberPattern>\\d{7}</nationalNumberPattern></personalNumber>" +
- " <voip><nationalNumberPattern>\\d{8}</nationalNumberPattern></voip>" +
- " <uan><nationalNumberPattern>\\d{9}</nationalNumberPattern></uan>" +
- " <shortCode><nationalNumberPattern>\\d{10}</nationalNumberPattern></shortCode>" +
+ " <standardRate><nationalNumberPattern>\\d{5}</nationalNumberPattern></standardRate>" +
+ " <premiumRate><nationalNumberPattern>\\d{6}</nationalNumberPattern></premiumRate>" +
+ " <shortCode><nationalNumberPattern>\\d{7}</nationalNumberPattern></shortCode>" +
+ " <sharedCost><nationalNumberPattern>\\d{8}</nationalNumberPattern></sharedCost>" +
+ " <personalNumber><nationalNumberPattern>\\d{9}</nationalNumberPattern></personalNumber>" +
+ " <voip><nationalNumberPattern>\\d{10}</nationalNumberPattern></voip>" +
+ " <uan><nationalNumberPattern>\\d{11}</nationalNumberPattern></uan>" +
"</territory>";
Element territoryElement = parseXmlString(xmlInput);
PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
assertEquals("\\d{2}", metadata.getMobile().getNationalNumberPattern());
assertEquals("\\d{3}", metadata.getPager().getNationalNumberPattern());
assertEquals("\\d{4}", metadata.getTollFree().getNationalNumberPattern());
- assertEquals("\\d{5}", metadata.getPremiumRate().getNationalNumberPattern());
- assertEquals("\\d{6}", metadata.getSharedCost().getNationalNumberPattern());
- assertEquals("\\d{7}", metadata.getPersonalNumber().getNationalNumberPattern());
- assertEquals("\\d{8}", metadata.getVoip().getNationalNumberPattern());
- assertEquals("\\d{9}", metadata.getUan().getNationalNumberPattern());
+ assertEquals("\\d{5}", metadata.getStandardRate().getNationalNumberPattern());
+ assertEquals("\\d{6}", metadata.getPremiumRate().getNationalNumberPattern());
+ assertEquals("\\d{7}", metadata.getShortCode().getNationalNumberPattern());
+ assertEquals("\\d{8}", metadata.getSharedCost().getNationalNumberPattern());
+ assertEquals("\\d{9}", metadata.getPersonalNumber().getNationalNumberPattern());
+ assertEquals("\\d{10}", metadata.getVoip().getNationalNumberPattern());
+ assertEquals("\\d{11}", metadata.getUan().getNationalNumberPattern());
}
}
private static void writeCountryCallingCodeMappingToJavaFile(
Map<Integer, List<String>> countryCodeToRegionCodeMap,
String outputDir, String mappingClass, String copyright) throws IOException {
- int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR);
-
- // Find out whether the countryCodeToRegionCodeMap has any region codes listed in it.
+ // Find out whether the countryCodeToRegionCodeMap has any region codes or country
+ // calling codes listed in it.
boolean hasRegionCodes = false;
for (List<String> listWithRegionCode : countryCodeToRegionCodeMap.values()) {
if (!listWithRegionCode.isEmpty()) {
ClassWriter writer = new ClassWriter(outputDir, mappingClass, copyright);
+ int capacity = (int) (countryCodeToRegionCodeMap.size() / CAPACITY_FACTOR);
if (hasRegionCodes && hasCountryCodes) {
writeMap(writer, capacity, countryCodeToRegionCodeMap);
} else if (hasCountryCodes) {
writeCountryCodeSet(writer, capacity, countryCodeToRegionCodeMap.keySet());
} else {
- writeRegionCodeSet(writer, capacity, countryCodeToRegionCodeMap.get(0));
+ List<String> regionCodeList = countryCodeToRegionCodeMap.get(0);
+ capacity = (int) (regionCodeList.size() / CAPACITY_FACTOR);
+ writeRegionCodeSet(writer, capacity, regionCodeList);
}
writer.writeToFile();
}
private static void writeRegionCodeSet(ClassWriter writer, int capacity,
- List<String> regionCodeSet) {
+ List<String> regionCodeList) {
writer.addToBody(REGION_CODE_SET_COMMENT);
writer.addToImports("java.util.HashSet");
writer.addToImports("java.util.Set");
writer.addToBody(" static Set<String> getRegionCodeSet() {\n");
- writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeSet.size());
+ writer.formatToBody(CAPACITY_COMMENT, capacity, regionCodeList.size());
writer.addToBody(" Set<String> regionCodeSet = new HashSet<String>(" + capacity + ");\n");
writer.addToBody("\n");
- for (String regionCode : regionCodeSet) {
+ for (String regionCode : regionCodeList) {
writer.addToBody(" regionCodeSet.add(\"" + regionCode + "\");\n");
}