Home | History | Annotate | Download | only in phonenumbers
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.i18n.phonenumbers;
     18 
     19 import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency;
     20 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
     21 
     22 import junit.framework.TestCase;
     23 
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.Iterator;
     27 import java.util.List;
     28 import java.util.NoSuchElementException;
     29 
     30 /**
     31  * Tests for {@link PhoneNumberMatcher}. This only tests basic functionality based on test metadata.
     32  *
     33  * @author Tom Hofmann
     34  * @see PhoneNumberUtilTest {@link PhoneNumberUtilTest} for the origin of the test data
     35  */
     36 public class PhoneNumberMatcherTest extends TestCase {
     37   private PhoneNumberUtil phoneUtil;
     38 
     39   @Override
     40   protected void setUp() throws Exception {
     41     phoneUtil = PhoneNumberUtilTest.initializePhoneUtilForTesting();
     42   }
     43 
     44   /** See {@link PhoneNumberUtilTest#testParseNationalNumber()}. */
     45   public void testFindNationalNumber() throws Exception {
     46     // same cases as in testParseNationalNumber
     47     doTestFindInContext("033316005", "NZ");
     48     doTestFindInContext("33316005", "NZ");
     49     // National prefix attached and some formatting present.
     50     doTestFindInContext("03-331 6005", "NZ");
     51     doTestFindInContext("03 331 6005", "NZ");
     52     // Testing international prefixes.
     53     // Should strip country code.
     54     doTestFindInContext("0064 3 331 6005", "NZ");
     55     // Try again, but this time we have an international number with Region Code US. It should
     56     // recognize the country code and parse accordingly.
     57     doTestFindInContext("01164 3 331 6005", "US");
     58     doTestFindInContext("+64 3 331 6005", "US");
     59 
     60     doTestFindInContext("64(0)64123456", "NZ");
     61     // Check that using a "/" is fine in a phone number.
     62     doTestFindInContext("123/45678", "DE");
     63     doTestFindInContext("123-456-7890", "US");
     64   }
     65 
     66   /** See {@link PhoneNumberUtilTest#testParseWithInternationalPrefixes()}. */
     67   public void testFindWithInternationalPrefixes() throws Exception {
     68     doTestFindInContext("+1 (650) 333-6000", "NZ");
     69     doTestFindInContext("1-650-333-6000", "US");
     70     // Calling the US number from Singapore by using different service providers
     71     // 1st test: calling using SingTel IDD service (IDD is 001)
     72     doTestFindInContext("0011-650-333-6000", "SG");
     73     // 2nd test: calling using StarHub IDD service (IDD is 008)
     74     doTestFindInContext("0081-650-333-6000", "SG");
     75     // 3rd test: calling using SingTel V019 service (IDD is 019)
     76     doTestFindInContext("0191-650-333-6000", "SG");
     77     // Calling the US number from Poland
     78     doTestFindInContext("0~01-650-333-6000", "PL");
     79     // Using "++" at the start.
     80     doTestFindInContext("++1 (650) 333-6000", "PL");
     81     // Using a full-width plus sign.
     82     doTestFindInContext("\uFF0B1 (650) 333-6000", "SG");
     83     // The whole number, including punctuation, is here represented in full-width form.
     84     doTestFindInContext("\uFF0B\uFF11\u3000\uFF08\uFF16\uFF15\uFF10\uFF09" +
     85         "\u3000\uFF13\uFF13\uFF13\uFF0D\uFF16\uFF10\uFF10\uFF10",
     86         "SG");
     87   }
     88 
     89   /** See {@link PhoneNumberUtilTest#testParseWithLeadingZero()}. */
     90   public void testFindWithLeadingZero() throws Exception {
     91     doTestFindInContext("+39 02-36618 300", "NZ");
     92     doTestFindInContext("02-36618 300", "IT");
     93     doTestFindInContext("312 345 678", "IT");
     94   }
     95 
     96   /** See {@link PhoneNumberUtilTest#testParseNationalNumberArgentina()}. */
     97   public void testFindNationalNumberArgentina() throws Exception {
     98     // Test parsing mobile numbers of Argentina.
     99     doTestFindInContext("+54 9 343 555 1212", "AR");
    100     doTestFindInContext("0343 15 555 1212", "AR");
    101 
    102     doTestFindInContext("+54 9 3715 65 4320", "AR");
    103     doTestFindInContext("03715 15 65 4320", "AR");
    104 
    105     // Test parsing fixed-line numbers of Argentina.
    106     doTestFindInContext("+54 11 3797 0000", "AR");
    107     doTestFindInContext("011 3797 0000", "AR");
    108 
    109     doTestFindInContext("+54 3715 65 4321", "AR");
    110     doTestFindInContext("03715 65 4321", "AR");
    111 
    112     doTestFindInContext("+54 23 1234 0000", "AR");
    113     doTestFindInContext("023 1234 0000", "AR");
    114   }
    115 
    116   /** See {@link PhoneNumberUtilTest#testParseWithXInNumber()}. */
    117   public void testFindWithXInNumber() throws Exception {
    118     doTestFindInContext("(0xx) 123456789", "AR");
    119     // A case where x denotes both carrier codes and extension symbol.
    120     doTestFindInContext("(0xx) 123456789 x 1234", "AR");
    121 
    122     // This test is intentionally constructed such that the number of digit after xx is larger than
    123     // 7, so that the number won't be mistakenly treated as an extension, as we allow extensions up
    124     // to 7 digits. This assumption is okay for now as all the countries where a carrier selection
    125     // code is written in the form of xx have a national significant number of length larger than 7.
    126     doTestFindInContext("011xx5481429712", "US");
    127   }
    128 
    129   /** See {@link PhoneNumberUtilTest#testParseNumbersMexico()}. */
    130   public void testFindNumbersMexico() throws Exception {
    131     // Test parsing fixed-line numbers of Mexico.
    132     doTestFindInContext("+52 (449)978-0001", "MX");
    133     doTestFindInContext("01 (449)978-0001", "MX");
    134     doTestFindInContext("(449)978-0001", "MX");
    135 
    136     // Test parsing mobile numbers of Mexico.
    137     doTestFindInContext("+52 1 33 1234-5678", "MX");
    138     doTestFindInContext("044 (33) 1234-5678", "MX");
    139     doTestFindInContext("045 33 1234-5678", "MX");
    140   }
    141 
    142   /** See {@link PhoneNumberUtilTest#testParseNumbersWithPlusWithNoRegion()}. */
    143   public void testFindNumbersWithPlusWithNoRegion() throws Exception {
    144     // "ZZ" is allowed only if the number starts with a '+' - then the country code can be
    145     // calculated.
    146     doTestFindInContext("+64 3 331 6005", "ZZ");
    147     // Null is also allowed for the region code in these cases.
    148     doTestFindInContext("+64 3 331 6005", null);
    149   }
    150 
    151   /** See {@link PhoneNumberUtilTest#testParseExtensions()}. */
    152   public void testFindExtensions() throws Exception {
    153     doTestFindInContext("03 331 6005 ext 3456", "NZ");
    154     doTestFindInContext("03-3316005x3456", "NZ");
    155     doTestFindInContext("03-3316005 int.3456", "NZ");
    156     doTestFindInContext("03 3316005 #3456", "NZ");
    157     doTestFindInContext("0~0 1800 7493 524", "PL");
    158     doTestFindInContext("(1800) 7493.524", "US");
    159     // Check that the last instance of an extension token is matched.
    160     doTestFindInContext("0~0 1800 7493 524 ~1234", "PL");
    161     // Verifying bug-fix where the last digit of a number was previously omitted if it was a 0 when
    162     // extracting the extension. Also verifying a few different cases of extensions.
    163     doTestFindInContext("+44 2034567890x456", "NZ");
    164     doTestFindInContext("+44 2034567890x456", "GB");
    165     doTestFindInContext("+44 2034567890 x456", "GB");
    166     doTestFindInContext("+44 2034567890 X456", "GB");
    167     doTestFindInContext("+44 2034567890 X 456", "GB");
    168     doTestFindInContext("+44 2034567890 X  456", "GB");
    169     doTestFindInContext("+44 2034567890  X 456", "GB");
    170 
    171     doTestFindInContext("(800) 901-3355 x 7246433", "US");
    172     doTestFindInContext("(800) 901-3355 , ext 7246433", "US");
    173     doTestFindInContext("(800) 901-3355 ,extension 7246433", "US");
    174     // The next test differs from PhoneNumberUtil -> when matching we don't consider a lone comma to
    175     // indicate an extension, although we accept it when parsing.
    176     doTestFindInContext("(800) 901-3355 ,x 7246433", "US");
    177     doTestFindInContext("(800) 901-3355 ext: 7246433", "US");
    178   }
    179 
    180   public void testFindInterspersedWithSpace() throws Exception {
    181     doTestFindInContext("0 3   3 3 1   6 0 0 5", "NZ");
    182   }
    183 
    184   /**
    185    * Test matching behavior when starting in the middle of a phone number.
    186    */
    187   public void testIntermediateParsePositions() throws Exception {
    188     String text = "Call 033316005  or 032316005!";
    189     //             |    |    |    |    |    |
    190     //             0    5   10   15   20   25
    191 
    192     // Iterate over all possible indices.
    193     for (int i = 0; i <= 5; i++) {
    194       assertEqualRange(text, i, 5, 14);
    195     }
    196     // 7 and 8 digits in a row are still parsed as number.
    197     assertEqualRange(text, 6, 6, 14);
    198     assertEqualRange(text, 7, 7, 14);
    199     // Anything smaller is skipped to the second instance.
    200     for (int i = 8; i <= 19; i++) {
    201       assertEqualRange(text, i, 19, 28);
    202     }
    203   }
    204 
    205   public void testMatchWithSurroundingZipcodes() throws Exception {
    206     String number = "415-666-7777";
    207     String zipPreceding = "My address is CA 34215 - " + number + " is my number.";
    208     PhoneNumber expectedResult = phoneUtil.parse(number, "US");
    209 
    210     Iterator<PhoneNumberMatch> iterator =
    211         phoneUtil.findNumbers(zipPreceding, "US").iterator();
    212     PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
    213     assertNotNull("Did not find a number in '" + zipPreceding + "'; expected " + number, match);
    214     assertEquals(expectedResult, match.number());
    215     assertEquals(number, match.rawString());
    216 
    217     // Now repeat, but this time the phone number has spaces in it. It should still be found.
    218     number = "(415) 666 7777";
    219 
    220     String zipFollowing = "My number is " + number + ". 34215 is my zip-code.";
    221     iterator = phoneUtil.findNumbers(zipFollowing, "US").iterator();
    222 
    223     PhoneNumberMatch matchWithSpaces = iterator.hasNext() ? iterator.next() : null;
    224     assertNotNull("Did not find a number in '" + zipFollowing + "'; expected " + number,
    225                   matchWithSpaces);
    226     assertEquals(expectedResult, matchWithSpaces.number());
    227     assertEquals(number, matchWithSpaces.rawString());
    228   }
    229 
    230   public void testIsLatinLetter() throws Exception {
    231     assertTrue(PhoneNumberMatcher.isLatinLetter('c'));
    232     assertTrue(PhoneNumberMatcher.isLatinLetter('C'));
    233     assertTrue(PhoneNumberMatcher.isLatinLetter('\u00C9'));
    234     assertTrue(PhoneNumberMatcher.isLatinLetter('\u0301'));  // Combining acute accent
    235     // Punctuation, digits and white-space are not considered "latin letters".
    236     assertFalse(PhoneNumberMatcher.isLatinLetter(':'));
    237     assertFalse(PhoneNumberMatcher.isLatinLetter('5'));
    238     assertFalse(PhoneNumberMatcher.isLatinLetter('-'));
    239     assertFalse(PhoneNumberMatcher.isLatinLetter('.'));
    240     assertFalse(PhoneNumberMatcher.isLatinLetter(' '));
    241     assertFalse(PhoneNumberMatcher.isLatinLetter('\u6211'));  // Chinese character
    242   }
    243 
    244   public void testMatchesWithSurroundingLatinChars() throws Exception {
    245     ArrayList<NumberContext> possibleOnlyContexts = new ArrayList<NumberContext>();
    246     possibleOnlyContexts.add(new NumberContext("abc", "def"));
    247     possibleOnlyContexts.add(new NumberContext("abc", ""));
    248     possibleOnlyContexts.add(new NumberContext("", "def"));
    249     // Latin capital letter e with an acute accent.
    250     possibleOnlyContexts.add(new NumberContext("\u00C9", ""));
    251     // e with an acute accent decomposed (with combining mark).
    252     possibleOnlyContexts.add(new NumberContext("e\u0301", ""));
    253 
    254     // Numbers should not be considered valid, if they are surrounded by Latin characters, but
    255     // should be considered possible.
    256     findMatchesInContexts(possibleOnlyContexts, false, true);
    257   }
    258 
    259   public void testMoneyNotSeenAsPhoneNumber() throws Exception {
    260     ArrayList<NumberContext> possibleOnlyContexts = new ArrayList<NumberContext>();
    261     possibleOnlyContexts.add(new NumberContext("$", ""));
    262     possibleOnlyContexts.add(new NumberContext("", "$"));
    263     possibleOnlyContexts.add(new NumberContext("\u00A3", ""));  // Pound sign
    264     possibleOnlyContexts.add(new NumberContext("\u00A5", ""));  // Yen sign
    265     findMatchesInContexts(possibleOnlyContexts, false, true);
    266   }
    267 
    268   public void testPhoneNumberWithLeadingOrTrailingMoneyMatches() throws Exception {
    269     // Because of the space after the 20 (or before the 100) these dollar amounts should not stop
    270     // the actual number from being found.
    271     ArrayList<NumberContext> contexts = new ArrayList<NumberContext>();
    272     contexts.add(new NumberContext("$20 ", ""));
    273     contexts.add(new NumberContext("", " 100$"));
    274     findMatchesInContexts(contexts, true, true);
    275   }
    276 
    277   public void testMatchesWithSurroundingLatinCharsAndLeadingPunctuation() throws Exception {
    278     // Contexts with trailing characters. Leading characters are okay here since the numbers we will
    279     // insert start with punctuation, but trailing characters are still not allowed.
    280     ArrayList<NumberContext> possibleOnlyContexts = new ArrayList<NumberContext>();
    281     possibleOnlyContexts.add(new NumberContext("abc", "def"));
    282     possibleOnlyContexts.add(new NumberContext("", "def"));
    283     possibleOnlyContexts.add(new NumberContext("", "\u00C9"));
    284 
    285     // Numbers should not be considered valid, if they have trailing Latin characters, but should be
    286     // considered possible.
    287     String numberWithPlus = "+14156667777";
    288     String numberWithBrackets = "(415)6667777";
    289     findMatchesInContexts(possibleOnlyContexts, false, true, "US", numberWithPlus);
    290     findMatchesInContexts(possibleOnlyContexts, false, true, "US", numberWithBrackets);
    291 
    292     ArrayList<NumberContext> validContexts = new ArrayList<NumberContext>();
    293     validContexts.add(new NumberContext("abc", ""));
    294     validContexts.add(new NumberContext("\u00C9", ""));
    295     validContexts.add(new NumberContext("\u00C9", "."));  // Trailing punctuation.
    296     validContexts.add(new NumberContext("\u00C9", " def"));  // Trailing white-space.
    297 
    298     // Numbers should be considered valid, since they start with punctuation.
    299     findMatchesInContexts(validContexts, true, true, "US", numberWithPlus);
    300     findMatchesInContexts(validContexts, true, true, "US", numberWithBrackets);
    301   }
    302 
    303   public void testMatchesWithSurroundingChineseChars() throws Exception {
    304     ArrayList<NumberContext> validContexts = new ArrayList<NumberContext>();
    305     validContexts.add(new NumberContext("\u6211\u7684\u7535\u8BDD\u53F7\u7801\u662F", ""));
    306     validContexts.add(new NumberContext("", "\u662F\u6211\u7684\u7535\u8BDD\u53F7\u7801"));
    307     validContexts.add(new NumberContext("\u8BF7\u62E8\u6253", "\u6211\u5728\u660E\u5929"));
    308 
    309     // Numbers should be considered valid, since they are surrounded by Chinese.
    310     findMatchesInContexts(validContexts, true, true);
    311   }
    312 
    313   public void testMatchesWithSurroundingPunctuation() throws Exception {
    314     ArrayList<NumberContext> validContexts = new ArrayList<NumberContext>();
    315     validContexts.add(new NumberContext("My number-", ""));  // At end of text.
    316     validContexts.add(new NumberContext("", ".Nice day."));  // At start of text.
    317     validContexts.add(new NumberContext("Tel:", "."));  // Punctuation surrounds number.
    318     validContexts.add(new NumberContext("Tel: ", " on Saturdays."));  // White-space is also fine.
    319 
    320     // Numbers should be considered valid, since they are surrounded by punctuation.
    321     findMatchesInContexts(validContexts, true, true);
    322   }
    323 
    324   public void testMatchesMultiplePhoneNumbersSeparatedByPhoneNumberPunctuation() throws Exception {
    325     String text = "Call 650-253-4561 -- 455-234-3451";
    326     String region = "US";
    327 
    328     PhoneNumber number1 = new PhoneNumber();
    329     number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
    330     number1.setNationalNumber(6502534561L);
    331     PhoneNumberMatch match1 = new PhoneNumberMatch(5, "650-253-4561", number1);
    332 
    333     PhoneNumber number2 = new PhoneNumber();
    334     number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
    335     number2.setNationalNumber(4552343451L);
    336     PhoneNumberMatch match2 = new PhoneNumberMatch(21, "455-234-3451", number2);
    337 
    338     Iterator<PhoneNumberMatch> matches = phoneUtil.findNumbers(text, region).iterator();
    339     assertEquals(match1, matches.next());
    340     assertEquals(match2, matches.next());
    341   }
    342 
    343   public void testDoesNotMatchMultiplePhoneNumbersSeparatedWithNoWhiteSpace() throws Exception {
    344     // No white-space found between numbers - neither is found.
    345     String text = "Call 650-253-4561--455-234-3451";
    346     String region = "US";
    347 
    348     assertTrue(hasNoMatches(phoneUtil.findNumbers(text, region)));
    349   }
    350 
    351   /**
    352    * Strings with number-like things that shouldn't be found under any level.
    353    */
    354   private static final NumberTest[] IMPOSSIBLE_CASES = {
    355     new NumberTest("12345", "US"),
    356     new NumberTest("23456789", "US"),
    357     new NumberTest("234567890112", "US"),
    358     new NumberTest("650+253+1234", "US"),
    359     new NumberTest("3/10/1984", "CA"),
    360     new NumberTest("03/27/2011", "US"),
    361     new NumberTest("31/8/2011", "US"),
    362     new NumberTest("1/12/2011", "US"),
    363     new NumberTest("10/12/82", "DE"),
    364   };
    365 
    366   /**
    367    * Strings with number-like things that should only be found under "possible".
    368    */
    369   private static final NumberTest[] POSSIBLE_ONLY_CASES = {
    370     new NumberTest("abc8002345678", "US"),
    371     // US numbers cannot start with 7 in the test metadata to be valid.
    372     new NumberTest("7121115678", "US"),
    373     // 'X' should not be found in numbers at leniencies stricter than POSSIBLE, unless it represents
    374     // a carrier code or extension.
    375     new NumberTest("1650 x 253 - 1234", "US"),
    376     new NumberTest("650 x 253 - 1234", "US"),
    377     new NumberTest("650x2531234", "US"),
    378   };
    379 
    380   /**
    381    * Strings with number-like things that should only be found up to and including the "valid"
    382    * leniency level.
    383    */
    384   private static final NumberTest[] VALID_CASES = {
    385     new NumberTest("65 02 53 00 00.", "US"),
    386     new NumberTest("6502 538365", "US"),
    387     new NumberTest("650//253-1234", "US"),  // 2 slashes are illegal at higher levels
    388     new NumberTest("650/253/1234", "US"),
    389     new NumberTest("9002309. 158", "US"),
    390     new NumberTest("21 7/8 - 14 12/34 - 5", "US"),
    391     new NumberTest("12.1 - 23.71 - 23.45", "US"),
    392     new NumberTest("1979-2011 100%", "US"),
    393     new NumberTest("800 234 1 111x1111", "US"),
    394     new NumberTest("+494949-4-94", "DE"),  // National number in wrong format
    395   };
    396 
    397   /**
    398    * Strings with number-like things that should only be found up to and including the
    399    * "strict_grouping" leniency level.
    400    */
    401   private static final NumberTest[] STRICT_GROUPING_CASES = {
    402     new NumberTest("(415) 6667777", "US"),
    403     new NumberTest("415-6667777", "US"),
    404     // Should be found by strict grouping but not exact grouping, as the last two groups are
    405     // formatted together as a block.
    406     new NumberTest("800-2491234", "DE"),
    407   };
    408 
    409   /**
    410    * Strings with number-like things that should found at all levels.
    411    */
    412   private static final NumberTest[] EXACT_GROUPING_CASES = {
    413     new NumberTest("\uFF14\uFF11\uFF15\uFF16\uFF16\uFF16\uFF17\uFF17\uFF17\uFF17", "US"),
    414     new NumberTest("\uFF14\uFF11\uFF15-\uFF16\uFF16\uFF16-\uFF17\uFF17\uFF17\uFF17", "US"),
    415     new NumberTest("4156667777", "US"),
    416     new NumberTest("4156667777 x 123", "US"),
    417     new NumberTest("415-666-7777", "US"),
    418     new NumberTest("415/666-7777", "US"),
    419     new NumberTest("415-666-7777 ext. 503", "US"),
    420     new NumberTest("1 415 666 7777 x 123", "US"),
    421     new NumberTest("+1 415-666-7777", "US"),
    422     new NumberTest("+494949 49", "DE"),
    423     new NumberTest("+49-49-34", "DE"),
    424     new NumberTest("+49-4931-49", "DE"),
    425     new NumberTest("04931-49", "DE"),  // With National Prefix
    426     new NumberTest("+49-494949", "DE"),  // One group with country code
    427     new NumberTest("+49-494949 ext. 49", "DE"),
    428     new NumberTest("+49494949 ext. 49", "DE"),
    429     new NumberTest("0494949", "DE"),
    430     new NumberTest("0494949 ext. 49", "DE"),
    431   };
    432 
    433   public void testMatchesWithStrictGroupingLeniency() throws Exception {
    434     int noMatchFoundCount = 0;
    435     int wrongMatchFoundCount = 0;
    436     List<NumberTest> testCases = new ArrayList<NumberTest>();
    437     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
    438     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
    439     doTestNumberMatchesForLeniency(testCases, Leniency.STRICT_GROUPING);
    440   }
    441 
    442   public void testNonMatchesWithStrictGroupLeniency() throws Exception {
    443     int matchFoundCount = 0;
    444     List<NumberTest> testCases = new ArrayList<NumberTest>();
    445     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
    446     testCases.addAll(Arrays.asList(VALID_CASES));
    447     doTestNumberNonMatchesForLeniency(testCases, Leniency.STRICT_GROUPING);
    448   }
    449 
    450   public void testMatchesWithExactGroupingLeniency() throws Exception {
    451     List<NumberTest> testCases = new ArrayList<NumberTest>();
    452     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
    453     doTestNumberMatchesForLeniency(testCases, Leniency.EXACT_GROUPING);
    454   }
    455 
    456   public void testNonMatchesExactGroupLeniency() throws Exception {
    457     List<NumberTest> testCases = new ArrayList<NumberTest>();
    458     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
    459     testCases.addAll(Arrays.asList(VALID_CASES));
    460     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
    461     doTestNumberNonMatchesForLeniency(testCases, Leniency.EXACT_GROUPING);
    462   }
    463 
    464   private void doTestNumberMatchesForLeniency(List<NumberTest> testCases,
    465                                               PhoneNumberUtil.Leniency leniency) {
    466     int noMatchFoundCount = 0;
    467     int wrongMatchFoundCount = 0;
    468     for (NumberTest test : testCases) {
    469       Iterator<PhoneNumberMatch> iterator =
    470           findNumbersForLeniency(test.rawString, test.region, leniency);
    471       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
    472       if (match == null) {
    473         noMatchFoundCount++;
    474         System.err.println("No match found in " + test.toString() + " for leniency: " + leniency);
    475       } else {
    476         if (!test.rawString.equals(match.rawString())) {
    477           wrongMatchFoundCount++;
    478           System.err.println("Found wrong match in test " + test.toString() +
    479                              ". Found " + match.rawString());
    480         }
    481       }
    482     }
    483     assertEquals(0, noMatchFoundCount);
    484     assertEquals(0, wrongMatchFoundCount);
    485   }
    486 
    487   private void doTestNumberNonMatchesForLeniency(List<NumberTest> testCases,
    488                                                  PhoneNumberUtil.Leniency leniency) {
    489     int matchFoundCount = 0;
    490     for (NumberTest test : testCases) {
    491       Iterator<PhoneNumberMatch> iterator =
    492           findNumbersForLeniency(test.rawString, test.region, leniency);
    493       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
    494       if (match != null) {
    495         matchFoundCount++;
    496         System.err.println("Match found in " + test.toString() + " for leniency: " + leniency);
    497       }
    498     }
    499     assertEquals(0, matchFoundCount);
    500   }
    501 
    502   /**
    503    * Helper method which tests the contexts provided and ensures that:
    504    * -- if isValid is true, they all find a test number inserted in the middle when leniency of
    505    *  matching is set to VALID; else no test number should be extracted at that leniency level
    506    * -- if isPossible is true, they all find a test number inserted in the middle when leniency of
    507    *  matching is set to POSSIBLE; else no test number should be extracted at that leniency level
    508    */
    509   private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid,
    510                                      boolean isPossible, String region, String number) {
    511     if (isValid) {
    512       doTestInContext(number, region, contexts, Leniency.VALID);
    513     } else {
    514       for (NumberContext context : contexts) {
    515         String text = context.leadingText + number + context.trailingText;
    516         assertTrue("Should not have found a number in " + text,
    517                    hasNoMatches(phoneUtil.findNumbers(text, region)));
    518       }
    519     }
    520     if (isPossible) {
    521       doTestInContext(number, region, contexts, Leniency.POSSIBLE);
    522     } else {
    523       for (NumberContext context : contexts) {
    524         String text = context.leadingText + number + context.trailingText;
    525         assertTrue("Should not have found a number in " + text,
    526                    hasNoMatches(phoneUtil.findNumbers(text, region, Leniency.POSSIBLE,
    527                                                       Long.MAX_VALUE)));
    528       }
    529     }
    530   }
    531 
    532   /**
    533    * Variant of findMatchesInContexts that uses a default number and region.
    534    */
    535   private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid,
    536                                      boolean isPossible) {
    537     String region = "US";
    538     String number = "415-666-7777";
    539 
    540     findMatchesInContexts(contexts, isValid, isPossible, region, number);
    541   }
    542 
    543   public void testNonMatchingBracketsAreInvalid() throws Exception {
    544     // The digits up to the ", " form a valid US number, but it shouldn't be matched as one since
    545     // there was a non-matching bracket present.
    546     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    547         "80.585 [79.964, 81.191]", "US")));
    548 
    549     // The trailing "]" is thrown away before parsing, so the resultant number, while a valid US
    550     // number, does not have matching brackets.
    551     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    552         "80.585 [79.964]", "US")));
    553 
    554     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    555         "80.585 ((79.964)", "US")));
    556 
    557     // This case has too many sets of brackets to be valid.
    558     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    559         "(80).(585) (79).(9)64", "US")));
    560   }
    561 
    562   public void testNoMatchIfRegionIsNull() throws Exception {
    563     // Fail on non-international prefix if region code is null.
    564     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    565         "Random text body - number is 0331 6005, see you there", null)));
    566   }
    567 
    568   public void testNoMatchInEmptyString() throws Exception {
    569     assertTrue(hasNoMatches(phoneUtil.findNumbers("", "US")));
    570     assertTrue(hasNoMatches(phoneUtil.findNumbers("  ", "US")));
    571   }
    572 
    573   public void testNoMatchIfNoNumber() throws Exception {
    574     assertTrue(hasNoMatches(phoneUtil.findNumbers(
    575         "Random text body - number is foobar, see you there", "US")));
    576   }
    577 
    578   public void testSequences() throws Exception {
    579     // Test multiple occurrences.
    580     String text = "Call 033316005  or 032316005!";
    581     String region = "NZ";
    582 
    583     PhoneNumber number1 = new PhoneNumber();
    584     number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
    585     number1.setNationalNumber(33316005);
    586     PhoneNumberMatch match1 = new PhoneNumberMatch(5, "033316005", number1);
    587 
    588     PhoneNumber number2 = new PhoneNumber();
    589     number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
    590     number2.setNationalNumber(32316005);
    591     PhoneNumberMatch match2 = new PhoneNumberMatch(19, "032316005", number2);
    592 
    593     Iterator<PhoneNumberMatch> matches =
    594         phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, Long.MAX_VALUE).iterator();
    595 
    596     assertEquals(match1, matches.next());
    597     assertEquals(match2, matches.next());
    598   }
    599 
    600   public void testNullInput() throws Exception {
    601     assertTrue(hasNoMatches(phoneUtil.findNumbers(null, "US")));
    602     assertTrue(hasNoMatches(phoneUtil.findNumbers(null, null)));
    603   }
    604 
    605   public void testMaxMatches() throws Exception {
    606     // Set up text with 100 valid phone numbers.
    607     StringBuilder numbers = new StringBuilder();
    608     for (int i = 0; i < 100; i++) {
    609       numbers.append("My info: 415-666-7777,");
    610     }
    611 
    612     // Matches all 100. Max only applies to failed cases.
    613     List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100);
    614     PhoneNumber number = phoneUtil.parse("+14156667777", null);
    615     for (int i = 0; i < 100; i++) {
    616       expected.add(number);
    617     }
    618 
    619     Iterable<PhoneNumberMatch> iterable =
    620         phoneUtil.findNumbers(numbers.toString(), "US", Leniency.VALID, 10);
    621     List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100);
    622     for (PhoneNumberMatch match : iterable) {
    623       actual.add(match.number());
    624     }
    625     assertEquals(expected, actual);
    626   }
    627 
    628   public void testMaxMatchesInvalid() throws Exception {
    629     // Set up text with 10 invalid phone numbers followed by 100 valid.
    630     StringBuilder numbers = new StringBuilder();
    631     for (int i = 0; i < 10; i++) {
    632       numbers.append("My address 949-8945-0");
    633     }
    634     for (int i = 0; i < 100; i++) {
    635       numbers.append("My info: 415-666-7777,");
    636     }
    637 
    638     Iterable<PhoneNumberMatch> iterable =
    639         phoneUtil.findNumbers(numbers.toString(), "US", Leniency.VALID, 10);
    640     assertFalse(iterable.iterator().hasNext());
    641   }
    642 
    643   public void testMaxMatchesMixed() throws Exception {
    644     // Set up text with 100 valid numbers inside an invalid number.
    645     StringBuilder numbers = new StringBuilder();
    646     for (int i = 0; i < 100; i++) {
    647       numbers.append("My info: 415-666-7777 123 fake street");
    648     }
    649 
    650     // Only matches the first 10 despite there being 100 numbers due to max matches.
    651     List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100);
    652     PhoneNumber number = phoneUtil.parse("+14156667777", null);
    653     for (int i = 0; i < 10; i++) {
    654       expected.add(number);
    655     }
    656 
    657     Iterable<PhoneNumberMatch> iterable =
    658         phoneUtil.findNumbers(numbers.toString(), "US", Leniency.VALID, 10);
    659     List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100);
    660     for (PhoneNumberMatch match : iterable) {
    661       actual.add(match.number());
    662     }
    663     assertEquals(expected, actual);
    664   }
    665 
    666   public void testEmptyIteration() throws Exception {
    667     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("", "ZZ");
    668     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
    669 
    670     assertFalse(iterator.hasNext());
    671     assertFalse(iterator.hasNext());
    672     try {
    673       iterator.next();
    674       fail("Violation of the Iterator contract.");
    675     } catch (NoSuchElementException e) { /* Success */ }
    676     assertFalse(iterator.hasNext());
    677   }
    678 
    679   public void testSingleIteration() throws Exception {
    680     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", "ZZ");
    681 
    682     // With hasNext() -> next().
    683     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
    684     // Double hasNext() to ensure it does not advance.
    685     assertTrue(iterator.hasNext());
    686     assertTrue(iterator.hasNext());
    687     assertNotNull(iterator.next());
    688     assertFalse(iterator.hasNext());
    689     try {
    690       iterator.next();
    691       fail("Violation of the Iterator contract.");
    692     } catch (NoSuchElementException e) { /* Success */ }
    693     assertFalse(iterator.hasNext());
    694 
    695     // With next() only.
    696     iterator = iterable.iterator();
    697     assertNotNull(iterator.next());
    698     try {
    699       iterator.next();
    700       fail("Violation of the Iterator contract.");
    701     } catch (NoSuchElementException e) { /* Success */ }
    702   }
    703 
    704   public void testDoubleIteration() throws Exception {
    705     Iterable<PhoneNumberMatch> iterable =
    706         phoneUtil.findNumbers("+14156667777 foobar +14156667777 ", "ZZ");
    707 
    708     // With hasNext() -> next().
    709     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
    710     // Double hasNext() to ensure it does not advance.
    711     assertTrue(iterator.hasNext());
    712     assertTrue(iterator.hasNext());
    713     assertNotNull(iterator.next());
    714     assertTrue(iterator.hasNext());
    715     assertTrue(iterator.hasNext());
    716     assertNotNull(iterator.next());
    717     assertFalse(iterator.hasNext());
    718     try {
    719       iterator.next();
    720       fail("Violation of the Iterator contract.");
    721     } catch (NoSuchElementException e) { /* Success */ }
    722     assertFalse(iterator.hasNext());
    723 
    724     // With next() only.
    725     iterator = iterable.iterator();
    726     assertNotNull(iterator.next());
    727     assertNotNull(iterator.next());
    728     try {
    729       iterator.next();
    730       fail("Violation of the Iterator contract.");
    731     } catch (NoSuchElementException e) { /* Success */ }
    732   }
    733 
    734   /**
    735    * Ensures that {@link Iterator#remove()} is not supported and that calling it does not
    736    * change iteration behavior.
    737    */
    738   public void testRemovalNotSupported() throws Exception {
    739     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", "ZZ");
    740 
    741     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
    742     try {
    743       iterator.remove();
    744       fail("Iterator must not support remove.");
    745     } catch (UnsupportedOperationException e) { /* success */ }
    746 
    747     assertTrue(iterator.hasNext());
    748 
    749     try {
    750       iterator.remove();
    751       fail("Iterator must not support remove.");
    752     } catch (UnsupportedOperationException e) { /* success */ }
    753 
    754     assertNotNull(iterator.next());
    755 
    756     try {
    757       iterator.remove();
    758       fail("Iterator must not support remove.");
    759     } catch (UnsupportedOperationException e) { /* success */ }
    760 
    761     assertFalse(iterator.hasNext());
    762   }
    763 
    764   /**
    765    * Asserts that another number can be found in {@code text} starting at {@code index}, and that
    766    * its corresponding range is {@code [start, end)}.
    767    */
    768   private void assertEqualRange(CharSequence text, int index, int start, int end) {
    769     CharSequence sub = text.subSequence(index, text.length());
    770     Iterator<PhoneNumberMatch> matches =
    771       phoneUtil.findNumbers(sub, "NZ", Leniency.POSSIBLE, Long.MAX_VALUE).iterator();
    772     assertTrue(matches.hasNext());
    773     PhoneNumberMatch match = matches.next();
    774     assertEquals(start - index, match.start());
    775     assertEquals(end - index, match.end());
    776     assertEquals(sub.subSequence(match.start(), match.end()).toString(), match.rawString());
    777   }
    778 
    779   /**
    780    * Tests numbers found by {@link PhoneNumberUtil#findNumbers(CharSequence, String)} in various
    781    * textual contexts.
    782    *
    783    * @param number the number to test and the corresponding region code to use
    784    */
    785   private void doTestFindInContext(String number, String defaultCountry) throws Exception {
    786     findPossibleInContext(number, defaultCountry);
    787 
    788     PhoneNumber parsed = phoneUtil.parse(number, defaultCountry);
    789     if (phoneUtil.isValidNumber(parsed)) {
    790       findValidInContext(number, defaultCountry);
    791     }
    792   }
    793 
    794   /**
    795    * Tests valid numbers in contexts that should pass for {@link Leniency#POSSIBLE}.
    796    */
    797   private void findPossibleInContext(String number, String defaultCountry) {
    798     ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>();
    799     contextPairs.add(new NumberContext("", ""));  // no context
    800     contextPairs.add(new NumberContext("   ", "\t"));  // whitespace only
    801     contextPairs.add(new NumberContext("Hello ", ""));  // no context at end
    802     contextPairs.add(new NumberContext("", " to call me!"));  // no context at start
    803     contextPairs.add(new NumberContext("Hi there, call ", " to reach me!"));  // no context at start
    804     contextPairs.add(new NumberContext("Hi there, call ", ", or don't"));  // with commas
    805     // Three examples without whitespace around the number.
    806     contextPairs.add(new NumberContext("Hi call", ""));
    807     contextPairs.add(new NumberContext("", "forme"));
    808     contextPairs.add(new NumberContext("Hi call", "forme"));
    809     // With other small numbers.
    810     contextPairs.add(new NumberContext("It's cheap! Call ", " before 6:30"));
    811     // With a second number later.
    812     contextPairs.add(new NumberContext("Call ", " or +1800-123-4567!"));
    813     contextPairs.add(new NumberContext("Call me on June 21 at", ""));  // with a Month-Day date
    814     // With publication pages.
    815     contextPairs.add(new NumberContext(
    816         "As quoted by Alfonso 12-15 (2009), you may call me at ", ""));
    817     contextPairs.add(new NumberContext(
    818         "As quoted by Alfonso et al. 12-15 (2009), you may call me at ", ""));
    819     // With dates, written in the American style.
    820     contextPairs.add(new NumberContext(
    821         "As I said on 03/10/2011, you may call me at ", ""));
    822     // With trailing numbers after a comma. The 45 should not be considered an extension.
    823     contextPairs.add(new NumberContext("", ", 45 days a year"));
    824      // With a postfix stripped off as it looks like the start of another number.
    825     contextPairs.add(new NumberContext("Call ", "/x12 more"));
    826 
    827     doTestInContext(number, defaultCountry, contextPairs, Leniency.POSSIBLE);
    828   }
    829 
    830   /**
    831    * Tests valid numbers in contexts that fail for {@link Leniency#POSSIBLE} but are valid for
    832    * {@link Leniency#VALID}.
    833    */
    834   private void findValidInContext(String number, String defaultCountry) {
    835     ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>();
    836     // With other small numbers.
    837     contextPairs.add(new NumberContext("It's only 9.99! Call ", " to buy"));
    838     // With a number Day.Month.Year date.
    839     contextPairs.add(new NumberContext("Call me on 21.6.1984 at ", ""));
    840     // With a number Month/Day date.
    841     contextPairs.add(new NumberContext("Call me on 06/21 at ", ""));
    842     // With a number Day.Month date.
    843     contextPairs.add(new NumberContext("Call me on 21.6. at ", ""));
    844     // With a number Month/Day/Year date.
    845     contextPairs.add(new NumberContext("Call me on 06/21/84 at ", ""));
    846 
    847     doTestInContext(number, defaultCountry, contextPairs, Leniency.VALID);
    848   }
    849 
    850   private void doTestInContext(String number, String defaultCountry,
    851       List<NumberContext> contextPairs, Leniency leniency) {
    852     for (NumberContext context : contextPairs) {
    853       String prefix = context.leadingText;
    854       String text = prefix + number + context.trailingText;
    855 
    856       int start = prefix.length();
    857       int end = start + number.length();
    858       Iterator<PhoneNumberMatch> iterator =
    859           phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator();
    860 
    861       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
    862       assertNotNull("Did not find a number in '" + text + "'; expected '" + number + "'", match);
    863 
    864       CharSequence extracted = text.subSequence(match.start(), match.end());
    865       assertTrue("Unexpected phone region in '" + text + "'; extracted '" + extracted + "'",
    866           start == match.start() && end == match.end());
    867       assertTrue(number.contentEquals(extracted));
    868       assertTrue(match.rawString().contentEquals(extracted));
    869 
    870       ensureTermination(text, defaultCountry, leniency);
    871     }
    872   }
    873 
    874   /**
    875    * Exhaustively searches for phone numbers from each index within {@code text} to test that
    876    * finding matches always terminates.
    877    */
    878   private void ensureTermination(String text, String defaultCountry, Leniency leniency) {
    879     for (int index = 0; index <= text.length(); index++) {
    880       String sub = text.substring(index);
    881       StringBuilder matches = new StringBuilder();
    882       // Iterates over all matches.
    883       for (PhoneNumberMatch match :
    884            phoneUtil.findNumbers(sub, defaultCountry, leniency, Long.MAX_VALUE)) {
    885         matches.append(", ").append(match.toString());
    886       }
    887     }
    888   }
    889 
    890   private Iterator<PhoneNumberMatch> findNumbersForLeniency(
    891       String text, String defaultCountry, PhoneNumberUtil.Leniency leniency) {
    892     return phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator();
    893   }
    894 
    895   /**
    896    * Returns true if there were no matches found.
    897    */
    898   private boolean hasNoMatches(Iterator<PhoneNumberMatch> iterator) {
    899     return !iterator.hasNext();
    900   }
    901 
    902   private boolean hasNoMatches(Iterable<PhoneNumberMatch> iterable) {
    903     return !iterable.iterator().hasNext();
    904   }
    905 
    906   /**
    907    * Small class that holds the context of the number we are testing against. The test will
    908    * insert the phone number to be found between leadingText and trailingText.
    909    */
    910   private static class NumberContext {
    911     final String leadingText;
    912     final String trailingText;
    913 
    914     NumberContext(String leadingText, String trailingText) {
    915       this.leadingText = leadingText;
    916       this.trailingText = trailingText;
    917     }
    918   }
    919 
    920   /**
    921    * Small class that holds the number we want to test and the region for which it should be valid.
    922    */
    923   private static class NumberTest {
    924     final String rawString;
    925     final String region;
    926 
    927     NumberTest(String rawString, String regionCode) {
    928       this.rawString = rawString;
    929       this.region = regionCode;
    930     }
    931 
    932     @Override
    933     public String toString() {
    934       return rawString + " (" + region.toString() + ")";
    935     }
    936   }
    937 }
    938