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