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