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.google.i18n.phonenumbers;
18 
19 import com.google.i18n.phonenumbers.PhoneNumberUtil.Leniency;
20 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
21 import com.google.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 
testContainsMoreThanOneSlashInNationalNumber()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()}. */
testFindNationalNumber()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()}. */
testFindWithInternationalPrefixes()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()}. */
testFindWithLeadingZero()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()}. */
testFindNationalNumberArgentina()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()}. */
testFindWithXInNumber()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()}. */
testFindNumbersMexico()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()}. */
testFindNumbersWithPlusWithNoRegion()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()}. */
testFindExtensions()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 
testFindInterspersedWithSpace()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    */
testIntermediateParsePositions()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 
testFourMatchesInARow()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 
testMatchesFoundWithMultipleSpaces()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 
testMatchWithSurroundingZipcodes()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 
testIsLatinLetter()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 
testMatchesWithSurroundingLatinChars()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 
testMoneyNotSeenAsPhoneNumber()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 
testPercentageNotSeenAsPhoneNumber()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 
testPhoneNumberWithLeadingOrTrailingMoneyMatches()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 
testMatchesWithSurroundingLatinCharsAndLeadingPunctuation()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 
testMatchesWithSurroundingChineseChars()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 
testMatchesWithSurroundingPunctuation()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 
testMatchesMultiplePhoneNumbersSeparatedByPhoneNumberPunctuation()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 
testDoesNotMatchMultiplePhoneNumbersSeparatedWithNoWhiteSpace()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     // Fits an alternate pattern, but the leading digits don't match.
488     new NumberTest("+52 332 123 23 23", RegionCode.MX),
489   };
490 
491   /**
492    * Strings with number-like things that should only be found up to and including the
493    * "strict_grouping" leniency level.
494    */
495   private static final NumberTest[] STRICT_GROUPING_CASES = {
496     new NumberTest("(415) 6667777", RegionCode.US),
497     new NumberTest("415-6667777", RegionCode.US),
498     // Should be found by strict grouping but not exact grouping, as the last two groups are
499     // formatted together as a block.
500     new NumberTest("0800-2491234", RegionCode.DE),
501     // Doesn't match any formatting in the test file, but almost matches an alternate format (the
502     // last two groups have been squashed together here).
503     new NumberTest("0900-1 123123", RegionCode.DE),
504     new NumberTest("(0)900-1 123123", RegionCode.DE),
505     new NumberTest("0 900-1 123123", RegionCode.DE),
506     // NDC also found as part of the country calling code; this shouldn't ruin the grouping
507     // expectations.
508     new NumberTest("+33 3 34 2312", RegionCode.FR),
509   };
510 
511   /**
512    * Strings with number-like things that should be found at all levels.
513    */
514   private static final NumberTest[] EXACT_GROUPING_CASES = {
515     new NumberTest("\uFF14\uFF11\uFF15\uFF16\uFF16\uFF16\uFF17\uFF17\uFF17\uFF17", RegionCode.US),
516     new NumberTest("\uFF14\uFF11\uFF15-\uFF16\uFF16\uFF16-\uFF17\uFF17\uFF17\uFF17", RegionCode.US),
517     new NumberTest("4156667777", RegionCode.US),
518     new NumberTest("4156667777 x 123", RegionCode.US),
519     new NumberTest("415-666-7777", RegionCode.US),
520     new NumberTest("415/666-7777", RegionCode.US),
521     new NumberTest("415-666-7777 ext. 503", RegionCode.US),
522     new NumberTest("1 415 666 7777 x 123", RegionCode.US),
523     new NumberTest("+1 415-666-7777", RegionCode.US),
524     new NumberTest("+494949 49", RegionCode.DE),
525     new NumberTest("+49-49-34", RegionCode.DE),
526     new NumberTest("+49-4931-49", RegionCode.DE),
527     new NumberTest("04931-49", RegionCode.DE),  // With National Prefix
528     new NumberTest("+49-494949", RegionCode.DE),  // One group with country code
529     new NumberTest("+49-494949 ext. 49", RegionCode.DE),
530     new NumberTest("+49494949 ext. 49", RegionCode.DE),
531     new NumberTest("0494949", RegionCode.DE),
532     new NumberTest("0494949 ext. 49", RegionCode.DE),
533     new NumberTest("01 (33) 3461 2234", RegionCode.MX),  // Optional NP present
534     new NumberTest("(33) 3461 2234", RegionCode.MX),  // Optional NP omitted
535     new NumberTest("1800-10-10 22", RegionCode.AU),  // Breakdown assistance number.
536     // Doesn't match any formatting in the test file, but matches an alternate format exactly.
537     new NumberTest("0900-1 123 123", RegionCode.DE),
538     new NumberTest("(0)900-1 123 123", RegionCode.DE),
539     new NumberTest("0 900-1 123 123", RegionCode.DE),
540     new NumberTest("+33 3 34 23 12", RegionCode.FR),
541   };
542 
testMatchesWithPossibleLeniency()543   public void testMatchesWithPossibleLeniency() throws Exception {
544     List<NumberTest> testCases = new ArrayList<NumberTest>();
545     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
546     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
547     testCases.addAll(Arrays.asList(VALID_CASES));
548     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
549     doTestNumberMatchesForLeniency(testCases, Leniency.POSSIBLE);
550   }
551 
testNonMatchesWithPossibleLeniency()552   public void testNonMatchesWithPossibleLeniency() throws Exception {
553     List<NumberTest> testCases = new ArrayList<NumberTest>();
554     testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES));
555     doTestNumberNonMatchesForLeniency(testCases, Leniency.POSSIBLE);
556   }
557 
testMatchesWithValidLeniency()558   public void testMatchesWithValidLeniency() throws Exception {
559     List<NumberTest> testCases = new ArrayList<NumberTest>();
560     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
561     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
562     testCases.addAll(Arrays.asList(VALID_CASES));
563     doTestNumberMatchesForLeniency(testCases, Leniency.VALID);
564   }
565 
testNonMatchesWithValidLeniency()566   public void testNonMatchesWithValidLeniency() throws Exception {
567     List<NumberTest> testCases = new ArrayList<NumberTest>();
568     testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES));
569     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
570     doTestNumberNonMatchesForLeniency(testCases, Leniency.VALID);
571   }
572 
testMatchesWithStrictGroupingLeniency()573   public void testMatchesWithStrictGroupingLeniency() throws Exception {
574     List<NumberTest> testCases = new ArrayList<NumberTest>();
575     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
576     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
577     doTestNumberMatchesForLeniency(testCases, Leniency.STRICT_GROUPING);
578   }
579 
testNonMatchesWithStrictGroupLeniency()580   public void testNonMatchesWithStrictGroupLeniency() throws Exception {
581     List<NumberTest> testCases = new ArrayList<NumberTest>();
582     testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES));
583     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
584     testCases.addAll(Arrays.asList(VALID_CASES));
585     doTestNumberNonMatchesForLeniency(testCases, Leniency.STRICT_GROUPING);
586   }
587 
testMatchesWithExactGroupingLeniency()588   public void testMatchesWithExactGroupingLeniency() throws Exception {
589     List<NumberTest> testCases = new ArrayList<NumberTest>();
590     testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES));
591     doTestNumberMatchesForLeniency(testCases, Leniency.EXACT_GROUPING);
592   }
593 
testNonMatchesExactGroupLeniency()594   public void testNonMatchesExactGroupLeniency() throws Exception {
595     List<NumberTest> testCases = new ArrayList<NumberTest>();
596     testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES));
597     testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES));
598     testCases.addAll(Arrays.asList(VALID_CASES));
599     testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES));
600     doTestNumberNonMatchesForLeniency(testCases, Leniency.EXACT_GROUPING);
601   }
602 
doTestNumberMatchesForLeniency(List<NumberTest> testCases, Leniency leniency)603   private void doTestNumberMatchesForLeniency(List<NumberTest> testCases, Leniency leniency) {
604     int noMatchFoundCount = 0;
605     int wrongMatchFoundCount = 0;
606     for (NumberTest test : testCases) {
607       Iterator<PhoneNumberMatch> iterator =
608           findNumbersForLeniency(test.rawString, test.region, leniency);
609       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
610       if (match == null) {
611         noMatchFoundCount++;
612         System.err.println("No match found in " + test.toString() + " for leniency: " + leniency);
613       } else {
614         if (!test.rawString.equals(match.rawString())) {
615           wrongMatchFoundCount++;
616           System.err.println("Found wrong match in test " + test.toString()
617               + ". Found " + match.rawString());
618         }
619       }
620     }
621     assertEquals(0, noMatchFoundCount);
622     assertEquals(0, wrongMatchFoundCount);
623   }
624 
doTestNumberNonMatchesForLeniency(List<NumberTest> testCases, Leniency leniency)625   private void doTestNumberNonMatchesForLeniency(List<NumberTest> testCases, Leniency leniency) {
626     int matchFoundCount = 0;
627     for (NumberTest test : testCases) {
628       Iterator<PhoneNumberMatch> iterator =
629           findNumbersForLeniency(test.rawString, test.region, leniency);
630       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
631       if (match != null) {
632         matchFoundCount++;
633         System.err.println("Match found in " + test.toString() + " for leniency: " + leniency);
634       }
635     }
636     assertEquals(0, matchFoundCount);
637   }
638 
639   /**
640    * Helper method which tests the contexts provided and ensures that:
641    * -- if isValid is true, they all find a test number inserted in the middle when leniency of
642    *  matching is set to VALID; else no test number should be extracted at that leniency level
643    * -- if isPossible is true, they all find a test number inserted in the middle when leniency of
644    *  matching is set to POSSIBLE; else no test number should be extracted at that leniency level
645    */
findMatchesInContexts(List<NumberContext> contexts, boolean isValid, boolean isPossible, String region, String number)646   private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid,
647                                      boolean isPossible, String region, String number) {
648     if (isValid) {
649       doTestInContext(number, region, contexts, Leniency.VALID);
650     } else {
651       for (NumberContext context : contexts) {
652         String text = context.leadingText + number + context.trailingText;
653         assertTrue("Should not have found a number in " + text,
654                    hasNoMatches(phoneUtil.findNumbers(text, region)));
655       }
656     }
657     if (isPossible) {
658       doTestInContext(number, region, contexts, Leniency.POSSIBLE);
659     } else {
660       for (NumberContext context : contexts) {
661         String text = context.leadingText + number + context.trailingText;
662         assertTrue("Should not have found a number in " + text,
663                    hasNoMatches(phoneUtil.findNumbers(text, region, Leniency.POSSIBLE,
664                                                       Long.MAX_VALUE)));
665       }
666     }
667   }
668 
669   /**
670    * Variant of findMatchesInContexts that uses a default number and region.
671    */
findMatchesInContexts(List<NumberContext> contexts, boolean isValid, boolean isPossible)672   private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid,
673                                      boolean isPossible) {
674     String region = RegionCode.US;
675     String number = "415-666-7777";
676 
677     findMatchesInContexts(contexts, isValid, isPossible, region, number);
678   }
679 
testNonMatchingBracketsAreInvalid()680   public void testNonMatchingBracketsAreInvalid() throws Exception {
681     // The digits up to the ", " form a valid US number, but it shouldn't be matched as one since
682     // there was a non-matching bracket present.
683     assertTrue(hasNoMatches(phoneUtil.findNumbers(
684         "80.585 [79.964, 81.191]", RegionCode.US)));
685 
686     // The trailing "]" is thrown away before parsing, so the resultant number, while a valid US
687     // number, does not have matching brackets.
688     assertTrue(hasNoMatches(phoneUtil.findNumbers(
689         "80.585 [79.964]", RegionCode.US)));
690 
691     assertTrue(hasNoMatches(phoneUtil.findNumbers(
692         "80.585 ((79.964)", RegionCode.US)));
693 
694     // This case has too many sets of brackets to be valid.
695     assertTrue(hasNoMatches(phoneUtil.findNumbers(
696         "(80).(585) (79).(9)64", RegionCode.US)));
697   }
698 
testNoMatchIfRegionIsNull()699   public void testNoMatchIfRegionIsNull() throws Exception {
700     // Fail on non-international prefix if region code is null.
701     assertTrue(hasNoMatches(phoneUtil.findNumbers(
702         "Random text body - number is 0331 6005, see you there", null)));
703   }
704 
testNoMatchInEmptyString()705   public void testNoMatchInEmptyString() throws Exception {
706     assertTrue(hasNoMatches(phoneUtil.findNumbers("", RegionCode.US)));
707     assertTrue(hasNoMatches(phoneUtil.findNumbers("  ", RegionCode.US)));
708   }
709 
testNoMatchIfNoNumber()710   public void testNoMatchIfNoNumber() throws Exception {
711     assertTrue(hasNoMatches(phoneUtil.findNumbers(
712         "Random text body - number is foobar, see you there", RegionCode.US)));
713   }
714 
testSequences()715   public void testSequences() throws Exception {
716     // Test multiple occurrences.
717     String text = "Call 033316005  or 032316005!";
718     String region = RegionCode.NZ;
719 
720     PhoneNumber number1 = new PhoneNumber();
721     number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
722     number1.setNationalNumber(33316005);
723     PhoneNumberMatch match1 = new PhoneNumberMatch(5, "033316005", number1);
724 
725     PhoneNumber number2 = new PhoneNumber();
726     number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region));
727     number2.setNationalNumber(32316005);
728     PhoneNumberMatch match2 = new PhoneNumberMatch(19, "032316005", number2);
729 
730     Iterator<PhoneNumberMatch> matches =
731         phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, Long.MAX_VALUE).iterator();
732 
733     assertEquals(match1, matches.next());
734     assertEquals(match2, matches.next());
735   }
736 
testNullInput()737   public void testNullInput() throws Exception {
738     assertTrue(hasNoMatches(phoneUtil.findNumbers(null, RegionCode.US)));
739     assertTrue(hasNoMatches(phoneUtil.findNumbers(null, null)));
740   }
741 
testMaxMatches()742   public void testMaxMatches() throws Exception {
743     // Set up text with 100 valid phone numbers.
744     StringBuilder numbers = new StringBuilder();
745     for (int i = 0; i < 100; i++) {
746       numbers.append("My info: 415-666-7777,");
747     }
748 
749     // Matches all 100. Max only applies to failed cases.
750     List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100);
751     PhoneNumber number = phoneUtil.parse("+14156667777", null);
752     for (int i = 0; i < 100; i++) {
753       expected.add(number);
754     }
755 
756     Iterable<PhoneNumberMatch> iterable =
757         phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10);
758     List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100);
759     for (PhoneNumberMatch match : iterable) {
760       actual.add(match.number());
761     }
762     assertEquals(expected, actual);
763   }
764 
testMaxMatchesInvalid()765   public void testMaxMatchesInvalid() throws Exception {
766     // Set up text with 10 invalid phone numbers followed by 100 valid.
767     StringBuilder numbers = new StringBuilder();
768     for (int i = 0; i < 10; i++) {
769       numbers.append("My address 949-8945-0");
770     }
771     for (int i = 0; i < 100; i++) {
772       numbers.append("My info: 415-666-7777,");
773     }
774 
775     Iterable<PhoneNumberMatch> iterable =
776         phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10);
777     assertFalse(iterable.iterator().hasNext());
778   }
779 
testMaxMatchesMixed()780   public void testMaxMatchesMixed() throws Exception {
781     // Set up text with 100 valid numbers inside an invalid number.
782     StringBuilder numbers = new StringBuilder();
783     for (int i = 0; i < 100; i++) {
784       numbers.append("My info: 415-666-7777 123 fake street");
785     }
786 
787     // Only matches the first 10 despite there being 100 numbers due to max matches.
788     List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100);
789     PhoneNumber number = phoneUtil.parse("+14156667777", null);
790     for (int i = 0; i < 10; i++) {
791       expected.add(number);
792     }
793 
794     Iterable<PhoneNumberMatch> iterable =
795         phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10);
796     List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100);
797     for (PhoneNumberMatch match : iterable) {
798       actual.add(match.number());
799     }
800     assertEquals(expected, actual);
801   }
802 
testNonPlusPrefixedNumbersNotFoundForInvalidRegion()803   public void testNonPlusPrefixedNumbersNotFoundForInvalidRegion() throws Exception {
804     // Does not start with a "+", we won't match it.
805     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("1 456 764 156", RegionCode.ZZ);
806     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
807 
808     assertFalse(iterator.hasNext());
809     try {
810       iterator.next();
811       fail("Violation of the Iterator contract.");
812     } catch (NoSuchElementException e) { /* Success */ }
813     assertFalse(iterator.hasNext());
814   }
815 
testEmptyIteration()816   public void testEmptyIteration() throws Exception {
817     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("", RegionCode.ZZ);
818     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
819 
820     assertFalse(iterator.hasNext());
821     assertFalse(iterator.hasNext());
822     try {
823       iterator.next();
824       fail("Violation of the Iterator contract.");
825     } catch (NoSuchElementException e) { /* Success */ }
826     assertFalse(iterator.hasNext());
827   }
828 
testSingleIteration()829   public void testSingleIteration() throws Exception {
830     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ);
831 
832     // With hasNext() -> next().
833     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
834     // Double hasNext() to ensure it does not advance.
835     assertTrue(iterator.hasNext());
836     assertTrue(iterator.hasNext());
837     assertNotNull(iterator.next());
838     assertFalse(iterator.hasNext());
839     try {
840       iterator.next();
841       fail("Violation of the Iterator contract.");
842     } catch (NoSuchElementException e) { /* Success */ }
843     assertFalse(iterator.hasNext());
844 
845     // With next() only.
846     iterator = iterable.iterator();
847     assertNotNull(iterator.next());
848     try {
849       iterator.next();
850       fail("Violation of the Iterator contract.");
851     } catch (NoSuchElementException e) { /* Success */ }
852   }
853 
testDoubleIteration()854   public void testDoubleIteration() throws Exception {
855     Iterable<PhoneNumberMatch> iterable =
856         phoneUtil.findNumbers("+14156667777 foobar +14156667777 ", RegionCode.ZZ);
857 
858     // With hasNext() -> next().
859     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
860     // Double hasNext() to ensure it does not advance.
861     assertTrue(iterator.hasNext());
862     assertTrue(iterator.hasNext());
863     assertNotNull(iterator.next());
864     assertTrue(iterator.hasNext());
865     assertTrue(iterator.hasNext());
866     assertNotNull(iterator.next());
867     assertFalse(iterator.hasNext());
868     try {
869       iterator.next();
870       fail("Violation of the Iterator contract.");
871     } catch (NoSuchElementException e) { /* Success */ }
872     assertFalse(iterator.hasNext());
873 
874     // With next() only.
875     iterator = iterable.iterator();
876     assertNotNull(iterator.next());
877     assertNotNull(iterator.next());
878     try {
879       iterator.next();
880       fail("Violation of the Iterator contract.");
881     } catch (NoSuchElementException e) { /* Success */ }
882   }
883 
884   /**
885    * Ensures that {@link Iterator#remove()} is not supported and that calling it does not
886    * change iteration behavior.
887    */
testRemovalNotSupported()888   public void testRemovalNotSupported() throws Exception {
889     Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ);
890 
891     Iterator<PhoneNumberMatch> iterator = iterable.iterator();
892     try {
893       iterator.remove();
894       fail("Iterator must not support remove.");
895     } catch (UnsupportedOperationException e) { /* success */ }
896 
897     assertTrue(iterator.hasNext());
898 
899     try {
900       iterator.remove();
901       fail("Iterator must not support remove.");
902     } catch (UnsupportedOperationException e) { /* success */ }
903 
904     assertNotNull(iterator.next());
905 
906     try {
907       iterator.remove();
908       fail("Iterator must not support remove.");
909     } catch (UnsupportedOperationException e) { /* success */ }
910 
911     assertFalse(iterator.hasNext());
912   }
913 
914   /**
915    * Asserts that the expected match is non-null, and that the raw string and expected
916    * proto buffer are set appropriately.
917    */
assertMatchProperties( PhoneNumberMatch match, String text, String number, String region)918   private void assertMatchProperties(
919       PhoneNumberMatch match, String text, String number, String region) throws Exception {
920     PhoneNumber expectedResult = phoneUtil.parse(number, region);
921     assertNotNull("Did not find a number in '" + text + "'; expected " + number, match);
922     assertEquals(expectedResult, match.number());
923     assertEquals(number, match.rawString());
924   }
925 
926   /**
927    * Asserts that another number can be found in {@code text} starting at {@code index}, and that
928    * its corresponding range is {@code [start, end)}.
929    */
assertEqualRange(CharSequence text, int index, int start, int end)930   private void assertEqualRange(CharSequence text, int index, int start, int end) {
931     CharSequence sub = text.subSequence(index, text.length());
932     Iterator<PhoneNumberMatch> matches =
933       phoneUtil.findNumbers(sub, RegionCode.NZ, Leniency.POSSIBLE, Long.MAX_VALUE).iterator();
934     assertTrue(matches.hasNext());
935     PhoneNumberMatch match = matches.next();
936     assertEquals(start - index, match.start());
937     assertEquals(end - index, match.end());
938     assertEquals(sub.subSequence(match.start(), match.end()).toString(), match.rawString());
939   }
940 
941   /**
942    * Tests numbers found by {@link PhoneNumberUtil#findNumbers(CharSequence, String)} in various
943    * textual contexts.
944    *
945    * @param number the number to test and the corresponding region code to use
946    */
doTestFindInContext(String number, String defaultCountry)947   private void doTestFindInContext(String number, String defaultCountry) throws Exception {
948     findPossibleInContext(number, defaultCountry);
949 
950     PhoneNumber parsed = phoneUtil.parse(number, defaultCountry);
951     if (phoneUtil.isValidNumber(parsed)) {
952       findValidInContext(number, defaultCountry);
953     }
954   }
955 
956   /**
957    * Tests valid numbers in contexts that should pass for {@link Leniency#POSSIBLE}.
958    */
findPossibleInContext(String number, String defaultCountry)959   private void findPossibleInContext(String number, String defaultCountry) {
960     ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>();
961     contextPairs.add(new NumberContext("", ""));  // no context
962     contextPairs.add(new NumberContext("   ", "\t"));  // whitespace only
963     contextPairs.add(new NumberContext("Hello ", ""));  // no context at end
964     contextPairs.add(new NumberContext("", " to call me!"));  // no context at start
965     contextPairs.add(new NumberContext("Hi there, call ", " to reach me!"));  // no context at start
966     contextPairs.add(new NumberContext("Hi there, call ", ", or don't"));  // with commas
967     // Three examples without whitespace around the number.
968     contextPairs.add(new NumberContext("Hi call", ""));
969     contextPairs.add(new NumberContext("", "forme"));
970     contextPairs.add(new NumberContext("Hi call", "forme"));
971     // With other small numbers.
972     contextPairs.add(new NumberContext("It's cheap! Call ", " before 6:30"));
973     // With a second number later.
974     contextPairs.add(new NumberContext("Call ", " or +1800-123-4567!"));
975     contextPairs.add(new NumberContext("Call me on June 2 at", ""));  // with a Month-Day date
976     // With publication pages.
977     contextPairs.add(new NumberContext(
978         "As quoted by Alfonso 12-15 (2009), you may call me at ", ""));
979     contextPairs.add(new NumberContext(
980         "As quoted by Alfonso et al. 12-15 (2009), you may call me at ", ""));
981     // With dates, written in the American style.
982     contextPairs.add(new NumberContext(
983         "As I said on 03/10/2011, you may call me at ", ""));
984     // With trailing numbers after a comma. The 45 should not be considered an extension.
985     contextPairs.add(new NumberContext("", ", 45 days a year"));
986     // When matching we don't consider semicolon along with legitimate extension symbol to indicate
987     // an extension. The 7246433 should not be considered an extension.
988     contextPairs.add(new NumberContext("", ";x 7246433"));
989      // With a postfix stripped off as it looks like the start of another number.
990     contextPairs.add(new NumberContext("Call ", "/x12 more"));
991 
992     doTestInContext(number, defaultCountry, contextPairs, Leniency.POSSIBLE);
993   }
994 
995   /**
996    * Tests valid numbers in contexts that fail for {@link Leniency#POSSIBLE} but are valid for
997    * {@link Leniency#VALID}.
998    */
findValidInContext(String number, String defaultCountry)999   private void findValidInContext(String number, String defaultCountry) {
1000     ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>();
1001     // With other small numbers.
1002     contextPairs.add(new NumberContext("It's only 9.99! Call ", " to buy"));
1003     // With a number Day.Month.Year date.
1004     contextPairs.add(new NumberContext("Call me on 21.6.1984 at ", ""));
1005     // With a number Month/Day date.
1006     contextPairs.add(new NumberContext("Call me on 06/21 at ", ""));
1007     // With a number Day.Month date.
1008     contextPairs.add(new NumberContext("Call me on 21.6. at ", ""));
1009     // With a number Month/Day/Year date.
1010     contextPairs.add(new NumberContext("Call me on 06/21/84 at ", ""));
1011 
1012     doTestInContext(number, defaultCountry, contextPairs, Leniency.VALID);
1013   }
1014 
doTestInContext(String number, String defaultCountry, List<NumberContext> contextPairs, Leniency leniency)1015   private void doTestInContext(String number, String defaultCountry,
1016       List<NumberContext> contextPairs, Leniency leniency) {
1017     for (NumberContext context : contextPairs) {
1018       String prefix = context.leadingText;
1019       String text = prefix + number + context.trailingText;
1020 
1021       int start = prefix.length();
1022       int end = start + number.length();
1023       Iterator<PhoneNumberMatch> iterator =
1024           phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator();
1025 
1026       PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null;
1027       assertNotNull("Did not find a number in '" + text + "'; expected '" + number + "'", match);
1028 
1029       CharSequence extracted = text.subSequence(match.start(), match.end());
1030       assertTrue("Unexpected phone region in '" + text + "'; extracted '" + extracted + "'",
1031           start == match.start() && end == match.end());
1032       assertTrue(number.contentEquals(extracted));
1033       assertTrue(match.rawString().contentEquals(extracted));
1034 
1035       ensureTermination(text, defaultCountry, leniency);
1036     }
1037   }
1038 
1039   /**
1040    * Exhaustively searches for phone numbers from each index within {@code text} to test that
1041    * finding matches always terminates.
1042    */
ensureTermination(String text, String defaultCountry, Leniency leniency)1043   private void ensureTermination(String text, String defaultCountry, Leniency leniency) {
1044     for (int index = 0; index <= text.length(); index++) {
1045       String sub = text.substring(index);
1046       StringBuilder matches = new StringBuilder();
1047       // Iterates over all matches.
1048       for (PhoneNumberMatch match :
1049            phoneUtil.findNumbers(sub, defaultCountry, leniency, Long.MAX_VALUE)) {
1050         matches.append(", ").append(match.toString());
1051       }
1052     }
1053   }
1054 
findNumbersForLeniency( String text, String defaultCountry, Leniency leniency)1055   private Iterator<PhoneNumberMatch> findNumbersForLeniency(
1056       String text, String defaultCountry, Leniency leniency) {
1057     return phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator();
1058   }
1059 
hasNoMatches(Iterable<PhoneNumberMatch> iterable)1060   private boolean hasNoMatches(Iterable<PhoneNumberMatch> iterable) {
1061     return !iterable.iterator().hasNext();
1062   }
1063 
1064   /**
1065    * Small class that holds the context of the number we are testing against. The test will
1066    * insert the phone number to be found between leadingText and trailingText.
1067    */
1068   private static class NumberContext {
1069     final String leadingText;
1070     final String trailingText;
1071 
NumberContext(String leadingText, String trailingText)1072     NumberContext(String leadingText, String trailingText) {
1073       this.leadingText = leadingText;
1074       this.trailingText = trailingText;
1075     }
1076   }
1077 
1078   /**
1079    * Small class that holds the number we want to test and the region for which it should be valid.
1080    */
1081   private static class NumberTest {
1082     final String rawString;
1083     final String region;
1084 
NumberTest(String rawString, String regionCode)1085     NumberTest(String rawString, String regionCode) {
1086       this.rawString = rawString;
1087       this.region = regionCode;
1088     }
1089 
1090     @Override
toString()1091     public String toString() {
1092       return rawString + " (" + region.toString() + ")";
1093     }
1094   }
1095 }
1096