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