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