1 /* 2 * Copyright (C) 2011 The Libphonenumber Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.i18n.phonenumbers; 18 19 import com.android.i18n.phonenumbers.PhoneNumberUtil.Leniency; 20 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber; 21 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.Iterator; 25 import java.util.List; 26 import 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 */ 34 public 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 // Breakdown assistance number with unexpected formatting. 404 new NumberTest("1800-1-0-10 22", RegionCode.AU), 405 new NumberTest("030-3-2 23 12 34", RegionCode.DE), 406 new NumberTest("03 0 -3 2 23 12 34", RegionCode.DE), 407 new NumberTest("(0)3 0 -3 2 23 12 34", RegionCode.DE), 408 new NumberTest("0 3 0 -3 2 23 12 34", RegionCode.DE), 409 }; 410 411 /** 412 * Strings with number-like things that should only be found up to and including the 413 * "strict_grouping" leniency level. 414 */ 415 private static final NumberTest[] STRICT_GROUPING_CASES = { 416 new NumberTest("(415) 6667777", RegionCode.US), 417 new NumberTest("415-6667777", RegionCode.US), 418 // Should be found by strict grouping but not exact grouping, as the last two groups are 419 // formatted together as a block. 420 new NumberTest("0800-2491234", RegionCode.DE), 421 // Doesn't match any formatting in the test file, but almost matches an alternate format (the 422 // last two groups have been squashed together here). 423 new NumberTest("0900-1 123123", RegionCode.DE), 424 new NumberTest("(0)900-1 123123", RegionCode.DE), 425 new NumberTest("0 900-1 123123", RegionCode.DE), 426 }; 427 428 /** 429 * Strings with number-like things that should be found at all levels. 430 */ 431 private static final NumberTest[] EXACT_GROUPING_CASES = { 432 new NumberTest("\uFF14\uFF11\uFF15\uFF16\uFF16\uFF16\uFF17\uFF17\uFF17\uFF17", RegionCode.US), 433 new NumberTest("\uFF14\uFF11\uFF15-\uFF16\uFF16\uFF16-\uFF17\uFF17\uFF17\uFF17", RegionCode.US), 434 new NumberTest("4156667777", RegionCode.US), 435 new NumberTest("4156667777 x 123", RegionCode.US), 436 new NumberTest("415-666-7777", RegionCode.US), 437 new NumberTest("415/666-7777", RegionCode.US), 438 new NumberTest("415-666-7777 ext. 503", RegionCode.US), 439 new NumberTest("1 415 666 7777 x 123", RegionCode.US), 440 new NumberTest("+1 415-666-7777", RegionCode.US), 441 new NumberTest("+494949 49", RegionCode.DE), 442 new NumberTest("+49-49-34", RegionCode.DE), 443 new NumberTest("+49-4931-49", RegionCode.DE), 444 new NumberTest("04931-49", RegionCode.DE), // With National Prefix 445 new NumberTest("+49-494949", RegionCode.DE), // One group with country code 446 new NumberTest("+49-494949 ext. 49", RegionCode.DE), 447 new NumberTest("+49494949 ext. 49", RegionCode.DE), 448 new NumberTest("0494949", RegionCode.DE), 449 new NumberTest("0494949 ext. 49", RegionCode.DE), 450 new NumberTest("01 (33) 3461 2234", RegionCode.MX), // Optional NP present 451 new NumberTest("(33) 3461 2234", RegionCode.MX), // Optional NP omitted 452 new NumberTest("1800-10-10 22", RegionCode.AU), // Breakdown assistance number. 453 // Doesn't match any formatting in the test file, but matches an alternate format exactly. 454 new NumberTest("0900-1 123 123", RegionCode.DE), 455 new NumberTest("(0)900-1 123 123", RegionCode.DE), 456 new NumberTest("0 900-1 123 123", RegionCode.DE), 457 }; 458 459 public void testMatchesWithPossibleLeniency() 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 testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); 465 doTestNumberMatchesForLeniency(testCases, Leniency.POSSIBLE); 466 } 467 468 public void testNonMatchesWithPossibleLeniency() throws Exception { 469 List<NumberTest> testCases = new ArrayList<NumberTest>(); 470 testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); 471 doTestNumberNonMatchesForLeniency(testCases, Leniency.POSSIBLE); 472 } 473 474 public void testMatchesWithValidLeniency() 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 testCases.addAll(Arrays.asList(VALID_CASES)); 479 doTestNumberMatchesForLeniency(testCases, Leniency.VALID); 480 } 481 482 public void testNonMatchesWithValidLeniency() throws Exception { 483 List<NumberTest> testCases = new ArrayList<NumberTest>(); 484 testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); 485 testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); 486 doTestNumberNonMatchesForLeniency(testCases, Leniency.VALID); 487 } 488 489 public void testMatchesWithStrictGroupingLeniency() throws Exception { 490 List<NumberTest> testCases = new ArrayList<NumberTest>(); 491 testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); 492 testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); 493 doTestNumberMatchesForLeniency(testCases, Leniency.STRICT_GROUPING); 494 } 495 496 public void testNonMatchesWithStrictGroupLeniency() throws Exception { 497 List<NumberTest> testCases = new ArrayList<NumberTest>(); 498 testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); 499 testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); 500 testCases.addAll(Arrays.asList(VALID_CASES)); 501 doTestNumberNonMatchesForLeniency(testCases, Leniency.STRICT_GROUPING); 502 } 503 504 public void testMatchesWithExactGroupingLeniency() throws Exception { 505 List<NumberTest> testCases = new ArrayList<NumberTest>(); 506 testCases.addAll(Arrays.asList(EXACT_GROUPING_CASES)); 507 doTestNumberMatchesForLeniency(testCases, Leniency.EXACT_GROUPING); 508 } 509 510 public void testNonMatchesExactGroupLeniency() throws Exception { 511 List<NumberTest> testCases = new ArrayList<NumberTest>(); 512 testCases.addAll(Arrays.asList(IMPOSSIBLE_CASES)); 513 testCases.addAll(Arrays.asList(POSSIBLE_ONLY_CASES)); 514 testCases.addAll(Arrays.asList(VALID_CASES)); 515 testCases.addAll(Arrays.asList(STRICT_GROUPING_CASES)); 516 doTestNumberNonMatchesForLeniency(testCases, Leniency.EXACT_GROUPING); 517 } 518 519 private void doTestNumberMatchesForLeniency(List<NumberTest> testCases, 520 PhoneNumberUtil.Leniency leniency) { 521 int noMatchFoundCount = 0; 522 int wrongMatchFoundCount = 0; 523 for (NumberTest test : testCases) { 524 Iterator<PhoneNumberMatch> iterator = 525 findNumbersForLeniency(test.rawString, test.region, leniency); 526 PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; 527 if (match == null) { 528 noMatchFoundCount++; 529 System.err.println("No match found in " + test.toString() + " for leniency: " + leniency); 530 } else { 531 if (!test.rawString.equals(match.rawString())) { 532 wrongMatchFoundCount++; 533 System.err.println("Found wrong match in test " + test.toString() + 534 ". Found " + match.rawString()); 535 } 536 } 537 } 538 assertEquals(0, noMatchFoundCount); 539 assertEquals(0, wrongMatchFoundCount); 540 } 541 542 private void doTestNumberNonMatchesForLeniency(List<NumberTest> testCases, 543 PhoneNumberUtil.Leniency leniency) { 544 int matchFoundCount = 0; 545 for (NumberTest test : testCases) { 546 Iterator<PhoneNumberMatch> iterator = 547 findNumbersForLeniency(test.rawString, test.region, leniency); 548 PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; 549 if (match != null) { 550 matchFoundCount++; 551 System.err.println("Match found in " + test.toString() + " for leniency: " + leniency); 552 } 553 } 554 assertEquals(0, matchFoundCount); 555 } 556 557 /** 558 * Helper method which tests the contexts provided and ensures that: 559 * -- if isValid is true, they all find a test number inserted in the middle when leniency of 560 * matching is set to VALID; else no test number should be extracted at that leniency level 561 * -- if isPossible is true, they all find a test number inserted in the middle when leniency of 562 * matching is set to POSSIBLE; else no test number should be extracted at that leniency level 563 */ 564 private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid, 565 boolean isPossible, String region, String number) { 566 if (isValid) { 567 doTestInContext(number, region, contexts, Leniency.VALID); 568 } else { 569 for (NumberContext context : contexts) { 570 String text = context.leadingText + number + context.trailingText; 571 assertTrue("Should not have found a number in " + text, 572 hasNoMatches(phoneUtil.findNumbers(text, region))); 573 } 574 } 575 if (isPossible) { 576 doTestInContext(number, region, contexts, Leniency.POSSIBLE); 577 } else { 578 for (NumberContext context : contexts) { 579 String text = context.leadingText + number + context.trailingText; 580 assertTrue("Should not have found a number in " + text, 581 hasNoMatches(phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, 582 Long.MAX_VALUE))); 583 } 584 } 585 } 586 587 /** 588 * Variant of findMatchesInContexts that uses a default number and region. 589 */ 590 private void findMatchesInContexts(List<NumberContext> contexts, boolean isValid, 591 boolean isPossible) { 592 String region = RegionCode.US; 593 String number = "415-666-7777"; 594 595 findMatchesInContexts(contexts, isValid, isPossible, region, number); 596 } 597 598 public void testNonMatchingBracketsAreInvalid() throws Exception { 599 // The digits up to the ", " form a valid US number, but it shouldn't be matched as one since 600 // there was a non-matching bracket present. 601 assertTrue(hasNoMatches(phoneUtil.findNumbers( 602 "80.585 [79.964, 81.191]", RegionCode.US))); 603 604 // The trailing "]" is thrown away before parsing, so the resultant number, while a valid US 605 // number, does not have matching brackets. 606 assertTrue(hasNoMatches(phoneUtil.findNumbers( 607 "80.585 [79.964]", RegionCode.US))); 608 609 assertTrue(hasNoMatches(phoneUtil.findNumbers( 610 "80.585 ((79.964)", RegionCode.US))); 611 612 // This case has too many sets of brackets to be valid. 613 assertTrue(hasNoMatches(phoneUtil.findNumbers( 614 "(80).(585) (79).(9)64", RegionCode.US))); 615 } 616 617 public void testNoMatchIfRegionIsNull() throws Exception { 618 // Fail on non-international prefix if region code is null. 619 assertTrue(hasNoMatches(phoneUtil.findNumbers( 620 "Random text body - number is 0331 6005, see you there", null))); 621 } 622 623 public void testNoMatchInEmptyString() throws Exception { 624 assertTrue(hasNoMatches(phoneUtil.findNumbers("", RegionCode.US))); 625 assertTrue(hasNoMatches(phoneUtil.findNumbers(" ", RegionCode.US))); 626 } 627 628 public void testNoMatchIfNoNumber() throws Exception { 629 assertTrue(hasNoMatches(phoneUtil.findNumbers( 630 "Random text body - number is foobar, see you there", RegionCode.US))); 631 } 632 633 public void testSequences() throws Exception { 634 // Test multiple occurrences. 635 String text = "Call 033316005 or 032316005!"; 636 String region = RegionCode.NZ; 637 638 PhoneNumber number1 = new PhoneNumber(); 639 number1.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); 640 number1.setNationalNumber(33316005); 641 PhoneNumberMatch match1 = new PhoneNumberMatch(5, "033316005", number1); 642 643 PhoneNumber number2 = new PhoneNumber(); 644 number2.setCountryCode(phoneUtil.getCountryCodeForRegion(region)); 645 number2.setNationalNumber(32316005); 646 PhoneNumberMatch match2 = new PhoneNumberMatch(19, "032316005", number2); 647 648 Iterator<PhoneNumberMatch> matches = 649 phoneUtil.findNumbers(text, region, Leniency.POSSIBLE, Long.MAX_VALUE).iterator(); 650 651 assertEquals(match1, matches.next()); 652 assertEquals(match2, matches.next()); 653 } 654 655 public void testNullInput() throws Exception { 656 assertTrue(hasNoMatches(phoneUtil.findNumbers(null, RegionCode.US))); 657 assertTrue(hasNoMatches(phoneUtil.findNumbers(null, null))); 658 } 659 660 public void testMaxMatches() throws Exception { 661 // Set up text with 100 valid phone numbers. 662 StringBuilder numbers = new StringBuilder(); 663 for (int i = 0; i < 100; i++) { 664 numbers.append("My info: 415-666-7777,"); 665 } 666 667 // Matches all 100. Max only applies to failed cases. 668 List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100); 669 PhoneNumber number = phoneUtil.parse("+14156667777", null); 670 for (int i = 0; i < 100; i++) { 671 expected.add(number); 672 } 673 674 Iterable<PhoneNumberMatch> iterable = 675 phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); 676 List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100); 677 for (PhoneNumberMatch match : iterable) { 678 actual.add(match.number()); 679 } 680 assertEquals(expected, actual); 681 } 682 683 public void testMaxMatchesInvalid() throws Exception { 684 // Set up text with 10 invalid phone numbers followed by 100 valid. 685 StringBuilder numbers = new StringBuilder(); 686 for (int i = 0; i < 10; i++) { 687 numbers.append("My address 949-8945-0"); 688 } 689 for (int i = 0; i < 100; i++) { 690 numbers.append("My info: 415-666-7777,"); 691 } 692 693 Iterable<PhoneNumberMatch> iterable = 694 phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); 695 assertFalse(iterable.iterator().hasNext()); 696 } 697 698 public void testMaxMatchesMixed() throws Exception { 699 // Set up text with 100 valid numbers inside an invalid number. 700 StringBuilder numbers = new StringBuilder(); 701 for (int i = 0; i < 100; i++) { 702 numbers.append("My info: 415-666-7777 123 fake street"); 703 } 704 705 // Only matches the first 10 despite there being 100 numbers due to max matches. 706 List<PhoneNumber> expected = new ArrayList<PhoneNumber>(100); 707 PhoneNumber number = phoneUtil.parse("+14156667777", null); 708 for (int i = 0; i < 10; i++) { 709 expected.add(number); 710 } 711 712 Iterable<PhoneNumberMatch> iterable = 713 phoneUtil.findNumbers(numbers.toString(), RegionCode.US, Leniency.VALID, 10); 714 List<PhoneNumber> actual = new ArrayList<PhoneNumber>(100); 715 for (PhoneNumberMatch match : iterable) { 716 actual.add(match.number()); 717 } 718 assertEquals(expected, actual); 719 } 720 721 public void testNonPlusPrefixedNumbersNotFoundForInvalidRegion() throws Exception { 722 // Does not start with a "+", we won't match it. 723 Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("1 456 764 156", RegionCode.ZZ); 724 Iterator<PhoneNumberMatch> iterator = iterable.iterator(); 725 726 assertFalse(iterator.hasNext()); 727 try { 728 iterator.next(); 729 fail("Violation of the Iterator contract."); 730 } catch (NoSuchElementException e) { /* Success */ } 731 assertFalse(iterator.hasNext()); 732 } 733 734 public void testEmptyIteration() throws Exception { 735 Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("", RegionCode.ZZ); 736 Iterator<PhoneNumberMatch> iterator = iterable.iterator(); 737 738 assertFalse(iterator.hasNext()); 739 assertFalse(iterator.hasNext()); 740 try { 741 iterator.next(); 742 fail("Violation of the Iterator contract."); 743 } catch (NoSuchElementException e) { /* Success */ } 744 assertFalse(iterator.hasNext()); 745 } 746 747 public void testSingleIteration() throws Exception { 748 Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ); 749 750 // With hasNext() -> next(). 751 Iterator<PhoneNumberMatch> iterator = iterable.iterator(); 752 // Double hasNext() to ensure it does not advance. 753 assertTrue(iterator.hasNext()); 754 assertTrue(iterator.hasNext()); 755 assertNotNull(iterator.next()); 756 assertFalse(iterator.hasNext()); 757 try { 758 iterator.next(); 759 fail("Violation of the Iterator contract."); 760 } catch (NoSuchElementException e) { /* Success */ } 761 assertFalse(iterator.hasNext()); 762 763 // With next() only. 764 iterator = iterable.iterator(); 765 assertNotNull(iterator.next()); 766 try { 767 iterator.next(); 768 fail("Violation of the Iterator contract."); 769 } catch (NoSuchElementException e) { /* Success */ } 770 } 771 772 public void testDoubleIteration() throws Exception { 773 Iterable<PhoneNumberMatch> iterable = 774 phoneUtil.findNumbers("+14156667777 foobar +14156667777 ", RegionCode.ZZ); 775 776 // With hasNext() -> next(). 777 Iterator<PhoneNumberMatch> iterator = iterable.iterator(); 778 // Double hasNext() to ensure it does not advance. 779 assertTrue(iterator.hasNext()); 780 assertTrue(iterator.hasNext()); 781 assertNotNull(iterator.next()); 782 assertTrue(iterator.hasNext()); 783 assertTrue(iterator.hasNext()); 784 assertNotNull(iterator.next()); 785 assertFalse(iterator.hasNext()); 786 try { 787 iterator.next(); 788 fail("Violation of the Iterator contract."); 789 } catch (NoSuchElementException e) { /* Success */ } 790 assertFalse(iterator.hasNext()); 791 792 // With next() only. 793 iterator = iterable.iterator(); 794 assertNotNull(iterator.next()); 795 assertNotNull(iterator.next()); 796 try { 797 iterator.next(); 798 fail("Violation of the Iterator contract."); 799 } catch (NoSuchElementException e) { /* Success */ } 800 } 801 802 /** 803 * Ensures that {@link Iterator#remove()} is not supported and that calling it does not 804 * change iteration behavior. 805 */ 806 public void testRemovalNotSupported() throws Exception { 807 Iterable<PhoneNumberMatch> iterable = phoneUtil.findNumbers("+14156667777", RegionCode.ZZ); 808 809 Iterator<PhoneNumberMatch> iterator = iterable.iterator(); 810 try { 811 iterator.remove(); 812 fail("Iterator must not support remove."); 813 } catch (UnsupportedOperationException e) { /* success */ } 814 815 assertTrue(iterator.hasNext()); 816 817 try { 818 iterator.remove(); 819 fail("Iterator must not support remove."); 820 } catch (UnsupportedOperationException e) { /* success */ } 821 822 assertNotNull(iterator.next()); 823 824 try { 825 iterator.remove(); 826 fail("Iterator must not support remove."); 827 } catch (UnsupportedOperationException e) { /* success */ } 828 829 assertFalse(iterator.hasNext()); 830 } 831 832 /** 833 * Asserts that another number can be found in {@code text} starting at {@code index}, and that 834 * its corresponding range is {@code [start, end)}. 835 */ 836 private void assertEqualRange(CharSequence text, int index, int start, int end) { 837 CharSequence sub = text.subSequence(index, text.length()); 838 Iterator<PhoneNumberMatch> matches = 839 phoneUtil.findNumbers(sub, RegionCode.NZ, Leniency.POSSIBLE, Long.MAX_VALUE).iterator(); 840 assertTrue(matches.hasNext()); 841 PhoneNumberMatch match = matches.next(); 842 assertEquals(start - index, match.start()); 843 assertEquals(end - index, match.end()); 844 assertEquals(sub.subSequence(match.start(), match.end()).toString(), match.rawString()); 845 } 846 847 /** 848 * Tests numbers found by {@link PhoneNumberUtil#findNumbers(CharSequence, String)} in various 849 * textual contexts. 850 * 851 * @param number the number to test and the corresponding region code to use 852 */ 853 private void doTestFindInContext(String number, String defaultCountry) throws Exception { 854 findPossibleInContext(number, defaultCountry); 855 856 PhoneNumber parsed = phoneUtil.parse(number, defaultCountry); 857 if (phoneUtil.isValidNumber(parsed)) { 858 findValidInContext(number, defaultCountry); 859 } 860 } 861 862 /** 863 * Tests valid numbers in contexts that should pass for {@link Leniency#POSSIBLE}. 864 */ 865 private void findPossibleInContext(String number, String defaultCountry) { 866 ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>(); 867 contextPairs.add(new NumberContext("", "")); // no context 868 contextPairs.add(new NumberContext(" ", "\t")); // whitespace only 869 contextPairs.add(new NumberContext("Hello ", "")); // no context at end 870 contextPairs.add(new NumberContext("", " to call me!")); // no context at start 871 contextPairs.add(new NumberContext("Hi there, call ", " to reach me!")); // no context at start 872 contextPairs.add(new NumberContext("Hi there, call ", ", or don't")); // with commas 873 // Three examples without whitespace around the number. 874 contextPairs.add(new NumberContext("Hi call", "")); 875 contextPairs.add(new NumberContext("", "forme")); 876 contextPairs.add(new NumberContext("Hi call", "forme")); 877 // With other small numbers. 878 contextPairs.add(new NumberContext("It's cheap! Call ", " before 6:30")); 879 // With a second number later. 880 contextPairs.add(new NumberContext("Call ", " or +1800-123-4567!")); 881 contextPairs.add(new NumberContext("Call me on June 2 at", "")); // with a Month-Day date 882 // With publication pages. 883 contextPairs.add(new NumberContext( 884 "As quoted by Alfonso 12-15 (2009), you may call me at ", "")); 885 contextPairs.add(new NumberContext( 886 "As quoted by Alfonso et al. 12-15 (2009), you may call me at ", "")); 887 // With dates, written in the American style. 888 contextPairs.add(new NumberContext( 889 "As I said on 03/10/2011, you may call me at ", "")); 890 // With trailing numbers after a comma. The 45 should not be considered an extension. 891 contextPairs.add(new NumberContext("", ", 45 days a year")); 892 // With a postfix stripped off as it looks like the start of another number. 893 contextPairs.add(new NumberContext("Call ", "/x12 more")); 894 895 doTestInContext(number, defaultCountry, contextPairs, Leniency.POSSIBLE); 896 } 897 898 /** 899 * Tests valid numbers in contexts that fail for {@link Leniency#POSSIBLE} but are valid for 900 * {@link Leniency#VALID}. 901 */ 902 private void findValidInContext(String number, String defaultCountry) { 903 ArrayList<NumberContext> contextPairs = new ArrayList<NumberContext>(); 904 // With other small numbers. 905 contextPairs.add(new NumberContext("It's only 9.99! Call ", " to buy")); 906 // With a number Day.Month.Year date. 907 contextPairs.add(new NumberContext("Call me on 21.6.1984 at ", "")); 908 // With a number Month/Day date. 909 contextPairs.add(new NumberContext("Call me on 06/21 at ", "")); 910 // With a number Day.Month date. 911 contextPairs.add(new NumberContext("Call me on 21.6. at ", "")); 912 // With a number Month/Day/Year date. 913 contextPairs.add(new NumberContext("Call me on 06/21/84 at ", "")); 914 915 doTestInContext(number, defaultCountry, contextPairs, Leniency.VALID); 916 } 917 918 private void doTestInContext(String number, String defaultCountry, 919 List<NumberContext> contextPairs, Leniency leniency) { 920 for (NumberContext context : contextPairs) { 921 String prefix = context.leadingText; 922 String text = prefix + number + context.trailingText; 923 924 int start = prefix.length(); 925 int end = start + number.length(); 926 Iterator<PhoneNumberMatch> iterator = 927 phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator(); 928 929 PhoneNumberMatch match = iterator.hasNext() ? iterator.next() : null; 930 assertNotNull("Did not find a number in '" + text + "'; expected '" + number + "'", match); 931 932 CharSequence extracted = text.subSequence(match.start(), match.end()); 933 assertTrue("Unexpected phone region in '" + text + "'; extracted '" + extracted + "'", 934 start == match.start() && end == match.end()); 935 assertTrue(number.contentEquals(extracted)); 936 assertTrue(match.rawString().contentEquals(extracted)); 937 938 ensureTermination(text, defaultCountry, leniency); 939 } 940 } 941 942 /** 943 * Exhaustively searches for phone numbers from each index within {@code text} to test that 944 * finding matches always terminates. 945 */ 946 private void ensureTermination(String text, String defaultCountry, Leniency leniency) { 947 for (int index = 0; index <= text.length(); index++) { 948 String sub = text.substring(index); 949 StringBuilder matches = new StringBuilder(); 950 // Iterates over all matches. 951 for (PhoneNumberMatch match : 952 phoneUtil.findNumbers(sub, defaultCountry, leniency, Long.MAX_VALUE)) { 953 matches.append(", ").append(match.toString()); 954 } 955 } 956 } 957 958 private Iterator<PhoneNumberMatch> findNumbersForLeniency( 959 String text, String defaultCountry, PhoneNumberUtil.Leniency leniency) { 960 return phoneUtil.findNumbers(text, defaultCountry, leniency, Long.MAX_VALUE).iterator(); 961 } 962 963 private boolean hasNoMatches(Iterable<PhoneNumberMatch> iterable) { 964 return !iterable.iterator().hasNext(); 965 } 966 967 /** 968 * Small class that holds the context of the number we are testing against. The test will 969 * insert the phone number to be found between leadingText and trailingText. 970 */ 971 private static class NumberContext { 972 final String leadingText; 973 final String trailingText; 974 975 NumberContext(String leadingText, String trailingText) { 976 this.leadingText = leadingText; 977 this.trailingText = trailingText; 978 } 979 } 980 981 /** 982 * Small class that holds the number we want to test and the region for which it should be valid. 983 */ 984 private static class NumberTest { 985 final String rawString; 986 final String region; 987 988 NumberTest(String rawString, String regionCode) { 989 this.rawString = rawString; 990 this.region = regionCode; 991 } 992 993 @Override 994 public String toString() { 995 return rawString + " (" + region.toString() + ")"; 996 } 997 } 998 } 999