/* * Copyright (C) 2011 The Libphonenumber Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.i18n.phonenumbers; import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency; import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; /** * Tests for {@link PhoneNumberMatcher}. This only tests basic functionality based on test metadata. * * @author Tom Hofmann * @see PhoneNumberUtilTest {@link PhoneNumberUtilTest} for the origin of the test data */ public class PhoneNumberMatcherTest extends TestMetadataTestCase { /** See {@link PhoneNumberUtilTest#testParseNationalNumber()}. */ public void testFindNationalNumber() throws Exception { // same cases as in testParseNationalNumber doTestFindInContext("033316005", RegionCode.NZ); // ("33316005", RegionCode.NZ) is omitted since the national prefix is obligatory for these // types of numbers in New Zealand. // National prefix attached and some formatting present. doTestFindInContext("03-331 6005", RegionCode.NZ); doTestFindInContext("03 331 6005", RegionCode.NZ); // Testing international prefixes. // Should strip country code. doTestFindInContext("0064 3 331 6005", RegionCode.NZ); // Try again, but this time we have an international number with Region Code US. It should // recognize the country code and parse accordingly. doTestFindInContext("01164 3 331 6005", RegionCode.US); doTestFindInContext("+64 3 331 6005", RegionCode.US); doTestFindInContext("64(0)64123456", RegionCode.NZ); // Check that using a "/" is fine in a phone number. doTestFindInContext("123/45678", RegionCode.DE); doTestFindInContext("123-456-7890", RegionCode.US); } /** See {@link PhoneNumberUtilTest#testParseWithInternationalPrefixes()}. */ public void testFindWithInternationalPrefixes() throws Exception { doTestFindInContext("+1 (650) 333-6000", RegionCode.NZ); doTestFindInContext("1-650-333-6000", RegionCode.US); // Calling the US number from Singapore by using different service providers // 1st test: calling using SingTel IDD service (IDD is 001) doTestFindInContext("0011-650-333-6000", RegionCode.SG); // 2nd test: calling using StarHub IDD service (IDD is 008) doTestFindInContext("0081-650-333-6000", RegionCode.SG); // 3rd test: calling using SingTel V019 service (IDD is 019) doTestFindInContext("0191-650-333-6000", RegionCode.SG); // Calling the US number from Poland doTestFindInContext("0~01-650-333-6000", RegionCode.PL); // Using "++" at the start. doTestFindInContext("++1 (650) 333-6000", RegionCode.PL); // Using a full-width plus sign. doTestFindInContext("\uFF0B1 (650) 333-6000", RegionCode.SG); // The whole number, including punctuation, is here represented in full-width form. doTestFindInContext("\uFF0B\uFF11\u3000\uFF08\uFF16\uFF15\uFF10\uFF09" + "\u3000\uFF13\uFF13\uFF13\uFF0D\uFF16\uFF10\uFF10\uFF10", RegionCode.SG); } /** See {@link PhoneNumberUtilTest#testParseWithLeadingZero()}. */ public void testFindWithLeadingZero() throws Exception { doTestFindInContext("+39 02-36618 300", RegionCode.NZ); doTestFindInContext("02-36618 300", RegionCode.IT); doTestFindInContext("312 345 678", RegionCode.IT); } /** See {@link PhoneNumberUtilTest#testParseNationalNumberArgentina()}. */ public void testFindNationalNumberArgentina() throws Exception { // Test parsing mobile numbers of Argentina. doTestFindInContext("+54 9 343 555 1212", RegionCode.AR); doTestFindInContext("0343 15 555 1212", RegionCode.AR); doTestFindInContext("+54 9 3715 65 4320", RegionCode.AR); doTestFindInContext("03715 15 65 4320", RegionCode.AR); // Test parsing fixed-line numbers of Argentina. doTestFindInContext("+54 11 3797 0000", RegionCode.AR); doTestFindInContext("011 3797 0000", RegionCode.AR); doTestFindInContext("+54 3715 65 4321", RegionCode.AR); doTestFindInContext("03715 65 4321", RegionCode.AR); doTestFindInContext("+54 23 1234 0000", RegionCode.AR); doTestFindInContext("023 1234 0000", RegionCode.AR); } /** See {@link PhoneNumberUtilTest#testParseWithXInNumber()}. */ public void testFindWithXInNumber() throws Exception { doTestFindInContext("(0xx) 123456789", RegionCode.AR); // A case where x denotes both carrier codes and extension symbol. doTestFindInContext("(0xx) 123456789 x 1234", RegionCode.AR); // This test is intentionally constructed such that the number of digit after xx is larger than // 7, so that the number won't be mistakenly treated as an extension, as we allow extensions up // to 7 digits. This assumption is okay for now as all the countries where a carrier selection // code is written in the form of xx have a national significant number of length larger than 7. doTestFindInContext("011xx5481429712", RegionCode.US); } /** See {@link PhoneNumberUtilTest#testParseNumbersMexico()}. */ public void testFindNumbersMexico() throws Exception { // Test parsing fixed-line numbers of Mexico. doTestFindInContext("+52 (449)978-0001", RegionCode.MX); doTestFindInContext("01 (449)978-0001", RegionCode.MX); doTestFindInContext("(449)978-0001", RegionCode.MX); // Test parsing mobile numbers of Mexico. doTestFindInContext("+52 1 33 1234-5678", RegionCode.MX); doTestFindInContext("044 (33) 1234-5678", RegionCode.MX); doTestFindInContext("045 33 1234-5678", RegionCode.MX); } /** See {@link PhoneNumberUtilTest#testParseNumbersWithPlusWithNoRegion()}. */ public void testFindNumbersWithPlusWithNoRegion() throws Exception { // RegionCode.ZZ is allowed only if the number starts with a '+' - then the country code can be // calculated. doTestFindInContext("+64 3 331 6005", RegionCode.ZZ); // Null is also allowed for the region code in these cases. doTestFindInContext("+64 3 331 6005", null); } /** See {@link PhoneNumberUtilTest#testParseExtensions()}. */ public void testFindExtensions() throws Exception { doTestFindInContext("03 331 6005 ext 3456", RegionCode.NZ); doTestFindInContext("03-3316005x3456", RegionCode.NZ); doTestFindInContext("03-3316005 int.3456", RegionCode.NZ); doTestFindInContext("03 3316005 #3456", RegionCode.NZ); doTestFindInContext("0~0 1800 7493 524", RegionCode.PL); doTestFindInContext("(1800) 7493.524", RegionCode.US); // Check that the last instance of an extension token is matched. doTestFindInContext("0~0 1800 7493 524 ~1234", RegionCode.PL); // Verifying bug-fix where the last digit of a number was previously omitted if it was a 0 when // extracting the extension. Also verifying a few different cases of extensions. doTestFindInContext("+44 2034567890x456", RegionCode.NZ); doTestFindInContext("+44 2034567890x456", RegionCode.GB); doTestFindInContext("+44 2034567890 x456", RegionCode.GB); doTestFindInContext("+44 2034567890 X456", RegionCode.GB); doTestFindInContext("+44 2034567890 X 456", RegionCode.GB); doTestFindInContext("+44 2034567890 X 456", RegionCode.GB); doTestFindInContext("+44 2034567890 X 456", RegionCode.GB); doTestFindInContext("(800) 901-3355 x 7246433", RegionCode.US); doTestFindInContext("(800) 901-3355 , ext 7246433", RegionCode.US); doTestFindInContext("(800) 901-3355 ,extension 7246433", RegionCode.US); // The next test differs from PhoneNumberUtil -> when matching we don't consider a lone comma to // indicate an extension, although we accept it when parsing. doTestFindInContext("(800) 901-3355 ,x 7246433", RegionCode.US); doTestFindInContext("(800) 901-3355 ext: 7246433", RegionCode.US); } public void testFindInterspersedWithSpace() throws Exception { doTestFindInContext("0 3 3 3 1 6 0 0 5", RegionCode.NZ); } /** * Test matching behavior when starting in the middle of a phone number. */ public void testIntermediateParsePositions() throws Exception { String text = "Call 033316005 or 032316005!"; // | | | | | | // 0 5 10 15 20 25 // Iterate over all possible indices. for (int i = 0; i <= 5; i++) { assertEqualRange(text, i, 5, 14); } // 7 and 8 digits in a row are still parsed as number. assertEqualRange(text, 6, 6, 14); assertEqualRange(text, 7, 7, 14); // Anything smaller is skipped to the second instance. for (int i = 8; i <= 19; i++) { assertEqualRange(text, i, 19, 28); } } public void testMatchWithSurroundingZipcodes() throws Exception { String number = "415-666-7777"; String zipPreceding = "My address is CA 34215 - " + number + " is my number."; PhoneNumber expectedResult = phoneUtil.parse(number, RegionCode.US); Iterator iterator = phoneUtil.findNumbers(zipPreceding, RegionCode.US).iterator(); PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; assertNotNull("Did not find a number in '" + zipPreceding + "'; expected " + number, match); assertEquals(expectedResult, match.number()); assertEquals(number, match.rawString()); // Now repeat, but this time the phone number has spaces in it. It should still be found. number = "(415) 666 7777"; String zipFollowing = "My number is " + number + ". 34215 is my zip-code."; iterator = phoneUtil.findNumbers(zipFollowing, RegionCode.US).iterator(); PhoneNumberMatch matchWithSpaces = iterator.hasNext() ? iterator.next() : null; assertNotNull("Did not find a number in '" + zipFollowing + "'; expected " + number, matchWithSpaces); assertEquals(expectedResult, matchWithSpaces.number()); assertEquals(number, matchWithSpaces.rawString()); } public void testIsLatinLetter() throws Exception { assertTrue(PhoneNumberMatcher.isLatinLetter('c')); assertTrue(PhoneNumberMatcher.isLatinLetter('C')); assertTrue(PhoneNumberMatcher.isLatinLetter('\u00C9')); assertTrue(PhoneNumberMatcher.isLatinLetter('\u0301')); // Combining acute accent // Punctuation, digits and white-space are not considered "latin letters". assertFalse(PhoneNumberMatcher.isLatinLetter(':')); assertFalse(PhoneNumberMatcher.isLatinLetter('5')); assertFalse(PhoneNumberMatcher.isLatinLetter('-')); assertFalse(PhoneNumberMatcher.isLatinLetter('.')); assertFalse(PhoneNumberMatcher.isLatinLetter(' ')); assertFalse(PhoneNumberMatcher.isLatinLetter('\u6211')); // Chinese character assertFalse(PhoneNumberMatcher.isLatinLetter('\u306E')); // Hiragana letter no } public void testMatchesWithSurroundingLatinChars() throws Exception { ArrayList possibleOnlyContexts = new ArrayList(); possibleOnlyContexts.add(new NumberContext("abc", "def")); possibleOnlyContexts.add(new NumberContext("abc", "")); possibleOnlyContexts.add(new NumberContext("", "def")); // Latin capital letter e with an acute accent. possibleOnlyContexts.add(new NumberContext("\u00C9", "")); // e with an acute accent decomposed (with combining mark). possibleOnlyContexts.add(new NumberContext("e\u0301", "")); // Numbers should not be considered valid, if they are surrounded by Latin characters, but // should be considered possible. findMatchesInContexts(possibleOnlyContexts, false, true); } public void testMoneyNotSeenAsPhoneNumber() throws Exception { ArrayList possibleOnlyContexts = new ArrayList(); possibleOnlyContexts.add(new NumberContext("$", "")); possibleOnlyContexts.add(new NumberContext("", "$")); possibleOnlyContexts.add(new NumberContext("\u00A3", "")); // Pound sign possibleOnlyContexts.add(new NumberContext("\u00A5", "")); // Yen sign findMatchesInContexts(possibleOnlyContexts, false, true); } public void testPercentageNotSeenAsPhoneNumber() throws Exception { ArrayList possibleOnlyContexts = new ArrayList(); possibleOnlyContexts.add(new NumberContext("", "%")); // Numbers followed by % should be dropped. findMatchesInContexts(possibleOnlyContexts, false, true); } public void testPhoneNumberWithLeadingOrTrailingMoneyMatches() throws Exception { // Because of the space after the 20 (or before the 100) these dollar amounts should not stop // the actual number from being found. ArrayList contexts = new ArrayList(); contexts.add(new NumberContext("$20 ", "")); contexts.add(new NumberContext("", " 100$")); findMatchesInContexts(contexts, true, true); } public void testMatchesWithSurroundingLatinCharsAndLeadingPunctuation() throws Exception { // Contexts with trailing characters. Leading characters are okay here since the numbers we will // insert start with punctuation, but trailing characters are still not allowed. ArrayList possibleOnlyContexts = new ArrayList(); possibleOnlyContexts.add(new NumberContext("abc", "def")); possibleOnlyContexts.add(new NumberContext("", "def")); possibleOnlyContexts.add(new NumberContext("", "\u00C9")); // Numbers should not be considered valid, if they have trailing Latin characters, but should be // considered possible. String numberWithPlus = "+14156667777"; String numberWithBrackets = "(415)6667777"; findMatchesInContexts(possibleOnlyContexts, false, true, RegionCode.US, numberWithPlus); findMatchesInContexts(possibleOnlyContexts, false, true, RegionCode.US, numberWithBrackets); ArrayList validContexts = new ArrayList(); validContexts.add(new NumberContext("abc", "")); validContexts.add(new NumberContext("\u00C9", "")); validContexts.add(new NumberContext("\u00C9", ".")); // Trailing punctuation. validContexts.add(new NumberContext("\u00C9", " def")); // Trailing white-space. // Numbers should be considered valid, since they start with punctuation. findMatchesInContexts(validContexts, true, true, RegionCode.US, numberWithPlus); findMatchesInContexts(validContexts, true, true, RegionCode.US, numberWithBrackets); } public void testMatchesWithSurroundingChineseChars() throws Exception { ArrayList validContexts = new ArrayList(); validContexts.add(new NumberContext("\u6211\u7684\u7535\u8BDD\u53F7\u7801\u662F", "")); validContexts.add(new NumberContext("", "\u662F\u6211\u7684\u7535\u8BDD\u53F7\u7801")); validContexts.add(new NumberContext("\u8BF7\u62E8\u6253", "\u6211\u5728\u660E\u5929")); // Numbers should be considered valid, since they are surrounded by Chinese. findMatchesInContexts(validContexts, true, true); } public void testMatchesWithSurroundingPunctuation() throws Exception { ArrayList validContexts = new ArrayList(); validContexts.add(new NumberContext("My number-", "")); // At end of text. validContexts.add(new NumberContext("", ".Nice day.")); // At start of text. validContexts.add(new NumberContext("Tel:", ".")); // Punctuation surrounds number. validContexts.add(new NumberContext("Tel: ", " on Saturdays.")); // White-space is also fine. // Numbers should be considered valid, since they are surrounded by punctuation. findMatchesInContexts(validContexts, true, true); } public void testMatchesMultiplePhoneNumbersSeparatedByPhoneNumberPunctuation() throws Exception { String text = "Call 650-253-4561 -- 455-234-3451"; String region = RegionCode.US; PhoneNumber number1 = new PhoneNumber(); number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); number1.setNationalNumber(6502534561L); PhoneNumberMatch match1 = new PhoneNumberMatch(5, "650-253-4561", number1); PhoneNumber number2 = new PhoneNumber(); number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); number2.setNationalNumber(4552343451L); PhoneNumberMatch match2 = new PhoneNumberMatch(21, "455-234-3451", number2); Iterator matches = phoneUtil.findNumbers(text, region).iterator(); assertEquals(match1, matches.next()); assertEquals(match2, matches.next()); } public void testDoesNotMatchMultiplePhoneNumbersSeparatedWithNoWhiteSpace() throws Exception { // No white-space found between numbers - neither is found. String text = "Call 650-253-4561--455-234-3451"; String region = RegionCode.US; assertTrue(hasNoMatches(phoneUtil.findNumbers(text, region))); } /** * Strings with number-like things that shouldn't be found under any level. */ private static final NumberTest[] IMPOSSIBLE_CASES = { new NumberTest("12345", RegionCode.US), new NumberTest("23456789", RegionCode.US), new NumberTest("234567890112", RegionCode.US), new NumberTest("650+253+1234", RegionCode.US), new NumberTest("3/10/1984", RegionCode.CA), new NumberTest("03/27/2011", RegionCode.US), new NumberTest("31/8/2011", RegionCode.US), new NumberTest("1/12/2011", RegionCode.US), new NumberTest("10/12/82", RegionCode.DE), new NumberTest("650x2531234", RegionCode.US), new NumberTest("2012-01-02 08:00", RegionCode.US), new NumberTest("2012/01/02 08:00", RegionCode.US), new NumberTest("20120102 08:00", RegionCode.US), }; /** * Strings with number-like things that should only be found under "possible". */ private static final NumberTest[] POSSIBLE_ONLY_CASES = { // US numbers cannot start with 7 in the test metadata to be valid. new NumberTest("7121115678", RegionCode.US), // 'X' should not be found in numbers at leniencies stricter than POSSIBLE, unless it represents // a carrier code or extension. new NumberTest("1650 x 253 - 1234", RegionCode.US), new NumberTest("650 x 253 - 1234", RegionCode.US), new NumberTest("6502531x234", RegionCode.US), new NumberTest("(20) 3346 1234", RegionCode.GB), // Non-optional NP omitted }; /** * Strings with number-like things that should only be found up to and including the "valid" * leniency level. */ private static final NumberTest[] VALID_CASES = { new NumberTest("65 02 53 00 00", RegionCode.US), new NumberTest("6502 538365", RegionCode.US), new NumberTest("650//253-1234", RegionCode.US), // 2 slashes are illegal at higher levels new NumberTest("650/253/1234", RegionCode.US), new NumberTest("9002309. 158", RegionCode.US), new NumberTest("12 7/8 - 14 12/34 - 5", RegionCode.US), new NumberTest("12.1 - 23.71 - 23.45", RegionCode.US), new NumberTest("800 234 1 111x1111", RegionCode.US), new NumberTest("1979-2011 100", RegionCode.US), new NumberTest("+494949-4-94", RegionCode.DE), // National number in wrong format new NumberTest("\uFF14\uFF11\uFF15\uFF16\uFF16\uFF16\uFF16-\uFF17\uFF17\uFF17", RegionCode.US), new NumberTest("2012-0102 08", RegionCode.US), // Very strange formatting. new NumberTest("2012-01-02 08", RegionCode.US), // Breakdown assistance number with unexpected formatting. new NumberTest("1800-1-0-10 22", RegionCode.AU), new NumberTest("030-3-2 23 12 34", RegionCode.DE), new NumberTest("03 0 -3 2 23 12 34", RegionCode.DE), new NumberTest("(0)3 0 -3 2 23 12 34", RegionCode.DE), new NumberTest("0 3 0 -3 2 23 12 34", RegionCode.DE), }; /** * Strings with number-like things that should only be found up to and including the * "strict_grouping" leniency level. */ private static final NumberTest[] STRICT_GROUPING_CASES = { new NumberTest("(415) 6667777", RegionCode.US), new NumberTest("415-6667777", RegionCode.US), // Should be found by strict grouping but not exact grouping, as the last two groups are // formatted together as a block. new NumberTest("0800-2491234", RegionCode.DE), // Doesn't match any formatting in the test file, but almost matches an alternate format (the // last two groups have been squashed together here). new NumberTest("0900-1 123123", RegionCode.DE), new NumberTest("(0)900-1 123123", RegionCode.DE), new NumberTest("0 900-1 123123", RegionCode.DE), }; /** * Strings with number-like things that should be found at all levels. */ private static final NumberTest[] EXACT_GROUPING_CASES = { new NumberTest("\uFF14\uFF11\uFF15\uFF16\uFF16\uFF16\uFF17\uFF17\uFF17\uFF17", RegionCode.US), new NumberTest("\uFF14\uFF11\uFF15-\uFF16\uFF16\uFF16-\uFF17\uFF17\uFF17\uFF17", RegionCode.US), new NumberTest("4156667777", RegionCode.US), new NumberTest("4156667777 x 123", RegionCode.US), new NumberTest("415-666-7777", RegionCode.US), new NumberTest("415/666-7777", RegionCode.US), new NumberTest("415-666-7777 ext. 503", RegionCode.US), new NumberTest("1 415 666 7777 x 123", RegionCode.US), new NumberTest("+1 415-666-7777", RegionCode.US), new NumberTest("+494949 49", RegionCode.DE), new NumberTest("+49-49-34", RegionCode.DE), new NumberTest("+49-4931-49", RegionCode.DE), new NumberTest("04931-49", RegionCode.DE), // With National Prefix new NumberTest("+49-494949", RegionCode.DE), // One group with country code new NumberTest("+49-494949 ext. 49", RegionCode.DE), new NumberTest("+49494949 ext. 49", RegionCode.DE), new NumberTest("0494949", RegionCode.DE), new NumberTest("0494949 ext. 49", RegionCode.DE), new NumberTest("01 (33) 3461 2234", RegionCode.MX), // Optional NP present new NumberTest("(33) 3461 2234", RegionCode.MX), // Optional NP omitted new NumberTest("1800-10-10 22", RegionCode.AU), // Breakdown assistance number. // Doesn't match any formatting in the test file, but matches an alternate format exactly. new NumberTest("0900-1 123 123", RegionCode.DE), new NumberTest("(0)900-1 123 123", RegionCode.DE), new NumberTest("0 900-1 123 123", RegionCode.DE), }; public void testMatchesWithPossibleLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); testCases.addAll(Arrays.asList(VALID_CASES)); testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); doTestNumberMatchesForLeniency(testCases, Leniency.POSSIBLE); } public void testNonMatchesWithPossibleLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); doTestNumberNonMatchesForLeniency(testCases, Leniency.POSSIBLE); } public void testMatchesWithValidLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); testCases.addAll(Arrays.asList(VALID_CASES)); doTestNumberMatchesForLeniency(testCases, Leniency.VALID); } public void testNonMatchesWithValidLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); doTestNumberNonMatchesForLeniency(testCases, Leniency.VALID); } public void testMatchesWithStrictGroupingLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); doTestNumberMatchesForLeniency(testCases, Leniency.STRICT_GROUPING); } public void testNonMatchesWithStrictGroupLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); testCases.addAll(Arrays.asList(VALID_CASES)); doTestNumberNonMatchesForLeniency(testCases, Leniency.STRICT_GROUPING); } public void testMatchesWithExactGroupingLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); doTestNumberMatchesForLeniency(testCases, Leniency.EXACT_GROUPING); } public void testNonMatchesExactGroupLeniency() throws Exception { List testCases = new ArrayList(); testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); testCases.addAll(Arrays.asList(VALID_CASES)); testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); doTestNumberNonMatchesForLeniency(testCases, Leniency.EXACT_GROUPING); } private void doTestNumberMatchesForLeniency(List testCases, PhoneNumberUtil.Leniency leniency) { int noMatchFoundCount = 0; int wrongMatchFoundCount = 0; for (NumberTest test : testCases) { Iterator iterator = findNumbersForLeniency(test.rawString, test.region, leniency); PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; if (match == null) { noMatchFoundCount++; System.err.println("No match found in " + test.toString() + " for leniency: " + leniency); } else { if (!test.rawString.equals(match.rawString())) { wrongMatchFoundCount++; System.err.println("Found wrong match in test " + test.toString() + ". Found " + match.rawString()); } } } assertEquals(0, noMatchFoundCount); assertEquals(0, wrongMatchFoundCount); } private void doTestNumberNonMatchesForLeniency(List testCases, PhoneNumberUtil.Leniency leniency) { int matchFoundCount = 0; for (NumberTest test : testCases) { Iterator iterator = findNumbersForLeniency(test.rawString, test.region, leniency); PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; if (match != null) { matchFoundCount++; System.err.println("Match found in " + test.toString() + " for leniency: " + leniency); } } assertEquals(0, matchFoundCount); } /** * Helper method which tests the contexts provided and ensures that: * -- if isValid is true, they all find a test number inserted in the middle when leniency of * matching is set to VALID; else no test number should be extracted at that leniency level * -- if isPossible is true, they all find a test number inserted in the middle when leniency of * matching is set to POSSIBLE; else no test number should be extracted at that leniency level */ private void findMatchesInContexts(List contexts, boolean isValid, boolean isPossible, String region, String number) { if (isValid) { doTestInContext(number, region, contexts, Leniency.VALID); } else { for (NumberContext context : contexts) { String text = context.leadingText + number + context.trailingText; assertTrue("Should not have found a number in " + text, hasNoMatches(phoneUtil.findNumbers(text, region))); } } if (isPossible) { doTestInContext(number, region, contexts, Leniency.POSSIBLE); } else { for (NumberContext context : contexts) { String text = context.leadingText + number + context.trailingText; assertTrue("Should not have found a number in " + text, hasNoMatches(phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, Long.MAX_VALUE))); } } } /** * Variant of findMatchesInContexts that uses a default number and region. */ private void findMatchesInContexts(List contexts, boolean isValid, boolean isPossible) { String region = RegionCode.US; String number = "415-666-7777"; findMatchesInContexts(contexts, isValid, isPossible, region, number); } public void testNonMatchingBracketsAreInvalid() throws Exception { // The digits up to the ", " form a valid US number, but it shouldn't be matched as one since // there was a non-matching bracket present. assertTrue(hasNoMatches(phoneUtil.findNumbers( "80.585 [79.964, 81.191]", RegionCode.US))); // The trailing "]" is thrown away before parsing, so the resultant number, while a valid US // number, does not have matching brackets. assertTrue(hasNoMatches(phoneUtil.findNumbers( "80.585 [79.964]", RegionCode.US))); assertTrue(hasNoMatches(phoneUtil.findNumbers( "80.585 ((79.964)", RegionCode.US))); // This case has too many sets of brackets to be valid. assertTrue(hasNoMatches(phoneUtil.findNumbers( "(80).(585) (79).(9)64", RegionCode.US))); } public void testNoMatchIfRegionIsNull() throws Exception { // Fail on non-international prefix if region code is null. assertTrue(hasNoMatches(phoneUtil.findNumbers( "Random text body - number is 0331 6005, see you there", null))); } public void testNoMatchInEmptyString() throws Exception { assertTrue(hasNoMatches(phoneUtil.findNumbers("", RegionCode.US))); assertTrue(hasNoMatches(phoneUtil.findNumbers(" ", RegionCode.US))); } public void testNoMatchIfNoNumber() throws Exception { assertTrue(hasNoMatches(phoneUtil.findNumbers( "Random text body - number is foobar, see you there", RegionCode.US))); } public void testSequences() throws Exception { // Test multiple occurrences. String text = "Call 033316005 or 032316005!"; String region = RegionCode.NZ; PhoneNumber number1 = new PhoneNumber(); number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); number1.setNationalNumber(33316005); PhoneNumberMatch match1 = new PhoneNumberMatch(5, "033316005", number1); PhoneNumber number2 = new PhoneNumber(); number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); number2.setNationalNumber(32316005); PhoneNumberMatch match2 = new PhoneNumberMatch(19, "032316005", number2); Iterator matches = phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, Long.MAX_VALUE).iterator(); assertEquals(match1, matches.next()); assertEquals(match2, matches.next()); } public void testNullInput() throws Exception { assertTrue(hasNoMatches(phoneUtil.findNumbers(null, RegionCode.US))); assertTrue(hasNoMatches(phoneUtil.findNumbers(null, null))); } public void testMaxMatches() throws Exception { // Set up text with 100 valid phone numbers. StringBuilder numbers = new StringBuilder(); for (int i = 0; i < 100; i++) { numbers.append("My info: 415-666-7777,"); } // Matches all 100. Max only applies to failed cases. List expected = new ArrayList(100); PhoneNumber number = phoneUtil.parse("+14156667777", null); for (int i = 0; i < 100; i++) { expected.add(number); } Iterable iterable = phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); List actual = new ArrayList(100); for (PhoneNumberMatch match : iterable) { actual.add(match.number()); } assertEquals(expected, actual); } public void testMaxMatchesInvalid() throws Exception { // Set up text with 10 invalid phone numbers followed by 100 valid. StringBuilder numbers = new StringBuilder(); for (int i = 0; i < 10; i++) { numbers.append("My address 949-8945-0"); } for (int i = 0; i < 100; i++) { numbers.append("My info: 415-666-7777,"); } Iterable iterable = phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); assertFalse(iterable.iterator().hasNext()); } public void testMaxMatchesMixed() throws Exception { // Set up text with 100 valid numbers inside an invalid number. StringBuilder numbers = new StringBuilder(); for (int i = 0; i < 100; i++) { numbers.append("My info: 415-666-7777 123 fake street"); } // Only matches the first 10 despite there being 100 numbers due to max matches. List expected = new ArrayList(100); PhoneNumber number = phoneUtil.parse("+14156667777", null); for (int i = 0; i < 10; i++) { expected.add(number); } Iterable iterable = phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); List actual = new ArrayList(100); for (PhoneNumberMatch match : iterable) { actual.add(match.number()); } assertEquals(expected, actual); } public void testNonPlusPrefixedNumbersNotFoundForInvalidRegion() throws Exception { // Does not start with a "+", we won't match it. Iterable iterable = phoneUtil.findNumbers("1 456 764 156", RegionCode.ZZ); Iterator iterator = iterable.iterator(); assertFalse(iterator.hasNext()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } assertFalse(iterator.hasNext()); } public void testEmptyIteration() throws Exception { Iterable iterable = phoneUtil.findNumbers("", RegionCode.ZZ); Iterator iterator = iterable.iterator(); assertFalse(iterator.hasNext()); assertFalse(iterator.hasNext()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } assertFalse(iterator.hasNext()); } public void testSingleIteration() throws Exception { Iterable iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ); // With hasNext() -> next(). Iterator iterator = iterable.iterator(); // Double hasNext() to ensure it does not advance. assertTrue(iterator.hasNext()); assertTrue(iterator.hasNext()); assertNotNull(iterator.next()); assertFalse(iterator.hasNext()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } assertFalse(iterator.hasNext()); // With next() only. iterator = iterable.iterator(); assertNotNull(iterator.next()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } } public void testDoubleIteration() throws Exception { Iterable iterable = phoneUtil.findNumbers("+14156667777 foobar +14156667777 ", RegionCode.ZZ); // With hasNext() -> next(). Iterator iterator = iterable.iterator(); // Double hasNext() to ensure it does not advance. assertTrue(iterator.hasNext()); assertTrue(iterator.hasNext()); assertNotNull(iterator.next()); assertTrue(iterator.hasNext()); assertTrue(iterator.hasNext()); assertNotNull(iterator.next()); assertFalse(iterator.hasNext()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } assertFalse(iterator.hasNext()); // With next() only. iterator = iterable.iterator(); assertNotNull(iterator.next()); assertNotNull(iterator.next()); try { iterator.next(); fail("Violation of the Iterator contract."); } catch (NoSuchElementException e) { /* Success */ } } /** * Ensures that {@link Iterator#remove()} is not supported and that calling it does not * change iteration behavior. */ public void testRemovalNotSupported() throws Exception { Iterable iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ); Iterator iterator = iterable.iterator(); try { iterator.remove(); fail("Iterator must not support remove."); } catch (UnsupportedOperationException e) { /* success */ } assertTrue(iterator.hasNext()); try { iterator.remove(); fail("Iterator must not support remove."); } catch (UnsupportedOperationException e) { /* success */ } assertNotNull(iterator.next()); try { iterator.remove(); fail("Iterator must not support remove."); } catch (UnsupportedOperationException e) { /* success */ } assertFalse(iterator.hasNext()); } /** * Asserts that another number can be found in {@code text} starting at {@code index}, and that * its corresponding range is {@code [start, end)}. */ private void assertEqualRange(CharSequence text, int index, int start, int end) { CharSequence sub = text.subSequence(index, text.length()); Iterator matches = phoneUtil.findNumbers(sub, RegionCode.NZ, Leniency.POSSIBLE, Long.MAX_VALUE).iterator(); assertTrue(matches.hasNext()); PhoneNumberMatch match = matches.next(); assertEquals(start - index, match.start()); assertEquals(end - index, match.end()); assertEquals(sub.subSequence(match.start(), match.end()).toString(), match.rawString()); } /** * Tests numbers found by {@link PhoneNumberUtil#findNumbers(CharSequence, String)} in various * textual contexts. * * @param number the number to test and the corresponding region code to use */ private void doTestFindInContext(String number, String defaultCountry) throws Exception { findPossibleInContext(number, defaultCountry); PhoneNumber parsed = phoneUtil.parse(number, defaultCountry); if (phoneUtil.isValidNumber(parsed)) { findValidInContext(number, defaultCountry); } } /** * Tests valid numbers in contexts that should pass for {@link Leniency#POSSIBLE}. */ private void findPossibleInContext(String number, String defaultCountry) { ArrayList contextPairs = new ArrayList(); contextPairs.add(new NumberContext("", "")); // no context contextPairs.add(new NumberContext(" ", "\t")); // whitespace only contextPairs.add(new NumberContext("Hello ", "")); // no context at end contextPairs.add(new NumberContext("", " to call me!")); // no context at start contextPairs.add(new NumberContext("Hi there, call ", " to reach me!")); // no context at start contextPairs.add(new NumberContext("Hi there, call ", ", or don't")); // with commas // Three examples without whitespace around the number. contextPairs.add(new NumberContext("Hi call", "")); contextPairs.add(new NumberContext("", "forme")); contextPairs.add(new NumberContext("Hi call", "forme")); // With other small numbers. contextPairs.add(new NumberContext("It's cheap! Call ", " before 6:30")); // With a second number later. contextPairs.add(new NumberContext("Call ", " or +1800-123-4567!")); contextPairs.add(new NumberContext("Call me on June 2 at", "")); // with a Month-Day date // With publication pages. contextPairs.add(new NumberContext( "As quoted by Alfonso 12-15 (2009), you may call me at ", "")); contextPairs.add(new NumberContext( "As quoted by Alfonso et al. 12-15 (2009), you may call me at ", "")); // With dates, written in the American style. contextPairs.add(new NumberContext( "As I said on 03/10/2011, you may call me at ", "")); // With trailing numbers after a comma. The 45 should not be considered an extension. contextPairs.add(new NumberContext("", ", 45 days a year")); // With a postfix stripped off as it looks like the start of another number. contextPairs.add(new NumberContext("Call ", "/x12 more")); doTestInContext(number, defaultCountry, contextPairs, Leniency.POSSIBLE); } /** * Tests valid numbers in contexts that fail for {@link Leniency#POSSIBLE} but are valid for * {@link Leniency#VALID}. */ private void findValidInContext(String number, String defaultCountry) { ArrayList contextPairs = new ArrayList(); // With other small numbers. contextPairs.add(new NumberContext("It's only 9.99! Call ", " to buy")); // With a number Day.Month.Year date. contextPairs.add(new NumberContext("Call me on 21.6.1984 at ", "")); // With a number Month/Day date. contextPairs.add(new NumberContext("Call me on 06/21 at ", "")); // With a number Day.Month date. contextPairs.add(new NumberContext("Call me on 21.6. at ", "")); // With a number Month/Day/Year date. contextPairs.add(new NumberContext("Call me on 06/21/84 at ", "")); doTestInContext(number, defaultCountry, contextPairs, Leniency.VALID); } private void doTestInContext(String number, String defaultCountry, List contextPairs, Leniency leniency) { for (NumberContext context : contextPairs) { String prefix = context.leadingText; String text = prefix + number + context.trailingText; int start = prefix.length(); int end = start + number.length(); Iterator iterator = phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator(); PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; assertNotNull("Did not find a number in '" + text + "'; expected '" + number + "'", match); CharSequence extracted = text.subSequence(match.start(), match.end()); assertTrue("Unexpected phone region in '" + text + "'; extracted '" + extracted + "'", start == match.start() && end == match.end()); assertTrue(number.contentEquals(extracted)); assertTrue(match.rawString().contentEquals(extracted)); ensureTermination(text, defaultCountry, leniency); } } /** * Exhaustively searches for phone numbers from each index within {@code text} to test that * finding matches always terminates. */ private void ensureTermination(String text, String defaultCountry, Leniency leniency) { for (int index = 0; index <= text.length(); index++) { String sub = text.substring(index); StringBuilder matches = new StringBuilder(); // Iterates over all matches. for (PhoneNumberMatch match : phoneUtil.findNumbers(sub, defaultCountry, leniency, Long.MAX_VALUE)) { matches.append(", ").append(match.toString()); } } } private Iterator findNumbersForLeniency( String text, String defaultCountry, PhoneNumberUtil.Leniency leniency) { return phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator(); } private boolean hasNoMatches(Iterable iterable) { return !iterable.iterator().hasNext(); } /** * Small class that holds the context of the number we are testing against. The test will * insert the phone number to be found between leadingText and trailingText. */ private static class NumberContext { final String leadingText; final String trailingText; NumberContext(String leadingText, String trailingText) { this.leadingText = leadingText; this.trailingText = trailingText; } } /** * Small class that holds the number we want to test and the region for which it should be valid. */ private static class NumberTest { final String rawString; final String region; NumberTest(String rawString, String regionCode) { this.rawString = rawString; this.region = regionCode; } @Override public String toString() { return rawString + " (" + region.toString() + ")"; } } }