Skip to content

Commit 966a707

Browse files
committed
Merge code changes from upstream r650
1 parent a507305 commit 966a707

6 files changed

Lines changed: 147 additions & 96 deletions

File tree

python/phonenumbers/asyoutypeformatter.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def _clear(self):
229229
self._should_add_space_after_national_prefix = False
230230
# This contains the national prefix that has been extracted. It
231231
# contains only digits without formatting.
232-
self._national_prefix_extracted = U_EMPTY_STRING
232+
self._extracted_national_prefix = U_EMPTY_STRING
233233
self._national_number = U_EMPTY_STRING
234234
# This indicates whether AsYouTypeFormatter is currently doing the
235235
# formatting.
@@ -328,7 +328,7 @@ def input_digit(self, next_char, remember_position=False):
328328
self._is_expecting_country_calling_code = True
329329
else:
330330
# No IDD or plus sign is found, might be entering in national format.
331-
self._national_prefix_extracted = self._remove_national_prefix_from_national_number()
331+
self._extracted_national_prefix = self._remove_national_prefix_from_national_number()
332332
self._current_output = self._attempt_to_choose_formatting_pattern()
333333
return self._current_output
334334
if self._is_expecting_country_calling_code:
@@ -370,17 +370,17 @@ def _attempt_to_choose_pattern_with_prefix_extracted(self):
370370
# shorter NDD doesn't result in a number we can format, we try to see if
371371
# we can extract a longer version here.
372372
def _able_to_extract_longer_ndd(self):
373-
if len(self._national_prefix_extracted) > 0:
373+
if len(self._extracted_national_prefix) > 0:
374374
# Put the extracted NDD back to the national number before
375375
# attempting to extract a new NDD.
376-
self._national_number = self._national_prefix_extracted + self._national_number
376+
self._national_number = self._extracted_national_prefix + self._national_number
377377
# Remove the previously extracted NDD from
378378
# prefixBeforeNationalNumber. We cannot simply set it to empty
379379
# string because people sometimes incorrectly enter national
380380
# prefix after the country code, e.g. +44 (0)20-1234-5678.
381-
index_of_previous_ndd = self._prefix_before_national_number.rfind(self._national_prefix_extracted)
381+
index_of_previous_ndd = self._prefix_before_national_number.rfind(self._extracted_national_prefix)
382382
self._prefix_before_national_number = self._prefix_before_national_number[:index_of_previous_ndd]
383-
return self._national_prefix_extracted != self._remove_national_prefix_from_national_number()
383+
return self._extracted_national_prefix != self._remove_national_prefix_from_national_number()
384384

385385
def _is_digit_or_leading_plus_sign(self, next_char):
386386
return (next_char.isdigit() or
@@ -548,6 +548,9 @@ def _attempt_to_extract_ccc(self):
548548

549549
self._prefix_before_national_number += str(country_code)
550550
self._prefix_before_national_number += _SEPARATOR_BEFORE_NATIONAL_NUMBER
551+
# When we have successfully extracted the IDD, the previously
552+
# extracted NDD should be cleared because it is no longer valid.
553+
self._extracted_national_prefix = U_EMPTY_STRING
551554
return True
552555

553556
def _normalize_and_accrue_digits_and_plus_sign(self, next_char, remember_position):

python/phonenumbers/phonenumbermatcher.py

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,36 @@ def _limit(lower, upper):
138138
_TIME_STAMPS = re.compile(u("[12]\\d{3}[-/]?[01]\\d[-/]?[0-3]\\d [0-2]\\d$"))
139139
_TIME_STAMPS_SUFFIX = re.compile(u(":[0-5]\\d"))
140140

141-
# Matches white-space, which may indicate the end of a phone number and the
142-
# start of something else (such as a neighbouring zip-code). If white-space is
143-
# found, continues to match all characters that are not typically used to
144-
# start a phone number.
145-
_GROUP_SEPARATOR = re.compile(u("(?u)\\s") + # Unicode Separator, \p{Z}
146-
u("[^") + _LEAD_CLASS_CHARS +
147-
u("\\d]*")) # Unicode Decimal Digit Number, \p{Nd}
141+
# Patterns used to extract phone numbers from a larger phone-number-like
142+
# pattern. These are ordered according to specificity. For example,
143+
# white-space is last since that is frequently used in numbers, not just to
144+
# separate two numbers. We have separate patterns since we don't want to break
145+
# up the phone-number-like text on more than one different kind of symbol at
146+
# one time, although symbols of the same type (e.g. space) can be safely
147+
# grouped together.
148+
#
149+
# Note that if there is a match, we will always check any text found up to the
150+
# first match as well.
151+
_INNER_MATCHES = (
152+
# Breaks on the slash - e.g. "651-234-2345/332-445-1234"
153+
re.compile(u("/+(.*)")),
154+
# Note that the bracket here is inside the capturing group, since we
155+
# consider it part of the phone number. Will match a pattern like "(650)
156+
# 223 3345 (754) 223 3321".
157+
re.compile(u("(\\([^(]*)")),
158+
# Breaks on a hyphen - e.g. "12345 - 332-445-1234 is my number." We
159+
# require a space on either side of the hyphen for it to be considered a
160+
# separator.
161+
re.compile(u("(?u)(?:\\p{Z}-|-\\s)\\s*(.+)")),
162+
# Various types of wide hyphens. Note we have decided not to enforce a
163+
# space here, since it's possible that it's supposed to be used to break
164+
# two numbers without spaces, and we haven't seen many instances of it
165+
# used within a number.
166+
re.compile(u("(?u)[\u2012-\u2015\uFF0D]\\s*(.+)")),
167+
# Breaks on a full stop - e.g. "12345. 332-445-1234 is my number."
168+
re.compile(u("(?u)\\.+\\s*([^.]+)")),
169+
# Breaks on space - e.g. "3324451234 8002341234"
170+
re.compile(u("(?u)\\s+(\\S+)")))
148171

149172

150173
class Leniency(object):
@@ -553,8 +576,7 @@ def _extract_match(self, candidate, offset):
553576
"""
554577
# Skip a match that is more likely a publication page reference or a
555578
# date.
556-
if (_PUB_PAGES.search(candidate) or
557-
_SLASH_SEPARATED_DATES.search(candidate)):
579+
if (_SLASH_SEPARATED_DATES.search(candidate)):
558580
return None
559581

560582
# Skip potential time-stamps.
@@ -581,50 +603,26 @@ def _extract_inner_match(self, candidate, offset):
581603
offset -- The current offset of candidate within text
582604
Returns the match found, None if none can be found
583605
"""
584-
# Try removing either the first or last "group" in the number and see
585-
# if this gives a result. We consider white space to be a possible
586-
# indication of the start or end of the phone number.
587-
group_match = _GROUP_SEPARATOR.search(candidate)
588-
if group_match:
589-
# Try the first group by itself.
590-
first_group_only = candidate[:group_match.start()]
591-
first_group_only = self._trim_after_first_match(_UNWANTED_END_CHAR_PATTERN,
592-
first_group_only)
593-
match = self._parse_and_verify(first_group_only, offset)
594-
if match is not None:
595-
return match
596-
self._max_tries -= 1
597-
598-
without_first_group_start = group_match.end()
599-
# Try the rest of the candidate without the first group.
600-
without_first_group = candidate[without_first_group_start:]
601-
without_first_group = self._trim_after_first_match(_UNWANTED_END_CHAR_PATTERN,
602-
without_first_group)
603-
match = self._parse_and_verify(without_first_group, offset + without_first_group_start)
604-
if match is not None:
605-
return match
606-
self._max_tries -= 1
607-
608-
if self._max_tries > 0:
609-
last_group_start = without_first_group_start
610-
group_match = _GROUP_SEPARATOR.search(candidate, last_group_start)
611-
while group_match:
612-
# Find the last group.
613-
last_group_start = group_match.start()
614-
group_match = _GROUP_SEPARATOR.search(candidate, group_match.end())
615-
without_last_group = candidate[:last_group_start]
616-
without_last_group = self._trim_after_first_match(_UNWANTED_END_CHAR_PATTERN,
617-
without_last_group)
618-
if without_last_group == first_group_only:
619-
# If there are only two groups, then the group "without
620-
# the last group" is the same as the first group. In these
621-
# cases, we don't want to re-check the number group, so we
622-
# exit already.
623-
return None
624-
match = self._parse_and_verify(without_last_group, offset)
606+
for possible_inner_match in _INNER_MATCHES:
607+
group_match = possible_inner_match.search(candidate)
608+
is_first_match = True
609+
while group_match and self._max_tries > 0:
610+
if is_first_match:
611+
# We should handle any group before this one too.
612+
group = self._trim_after_first_match(_UNWANTED_END_CHAR_PATTERN,
613+
candidate[:group_match.start()])
614+
match = self._parse_and_verify(group, offset)
615+
if match is not None:
616+
return match
617+
self._max_tries -= 1
618+
is_first_match = False
619+
group = self._trim_after_first_match(_UNWANTED_END_CHAR_PATTERN,
620+
group_match.group(1))
621+
match = self._parse_and_verify(group, offset + group_match.start(1))
625622
if match is not None:
626623
return match
627624
self._max_tries -= 1
625+
group_match = possible_inner_match.search(candidate, group_match.start() + 1)
628626
return None
629627

630628
def _parse_and_verify(self, candidate, offset):
@@ -640,7 +638,7 @@ def _parse_and_verify(self, candidate, offset):
640638
try:
641639
# Check the candidate doesn't contain any formatting which would
642640
# indicate that it really isn't a phone number.
643-
if not fullmatch(_MATCHING_BRACKETS, candidate):
641+
if (not fullmatch(_MATCHING_BRACKETS, candidate) or _PUB_PAGES.search(candidate)):
644642
return None
645643

646644
# If leniency is set to VALID or stricter, we also want to skip

python/phonenumbers/phonenumberutil.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
http://www.iso.org/iso/country_codes/iso_3166_code_lists/country_names_and_code_elements.htm
1111
1212
author: Shaopeng Jia (original Java version)
13-
author: Lara Rennie (original Java Version)
1413
author: David Drysdale (Python version)
1514
"""
1615
# Based on original Java code:
@@ -73,7 +72,7 @@
7372
_MIN_LENGTH_FOR_NSN = 2
7473
# The ITU says the maximum length should be 15, but we have found longer
7574
# numbers in Germany.
76-
_MAX_LENGTH_FOR_NSN = 16
75+
_MAX_LENGTH_FOR_NSN = 17
7776
# The maximum length of the country calling code.
7877
_MAX_LENGTH_COUNTRY_CODE = 3
7978
# We don't allow input strings for parsing to be longer than 250 chars. This

python/tests/asyoutypetest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,40 @@ def testAYTFShortNumberFormattingFix_US(self):
11041104
self.assertEqual("12", formatter.input_digit('2'))
11051105
self.assertEqual("1 22", formatter.input_digit('2'))
11061106

1107+
def testAYTFClearNDDAfterIDDExtraction(self):
1108+
formatter = AsYouTypeFormatter("KR")
1109+
1110+
# Check that when we have successfully extracted an IDD, the
1111+
# previously extracted NDD is cleared since it is no longer valid.
1112+
self.assertEqual("0", formatter.input_digit('0'))
1113+
self.assertEqual("00", formatter.input_digit('0'))
1114+
self.assertEqual("007", formatter.input_digit('7'))
1115+
self.assertEqual("0070", formatter.input_digit('0'))
1116+
self.assertEqual("00700", formatter.input_digit('0'))
1117+
self.assertEqual("0", formatter._extracted_national_prefix)
1118+
1119+
# Once the IDD "00700" has been extracted, it no longer makes sense
1120+
# for the initial "0" to be treated as an NDD.
1121+
self.assertEqual("00700 1 ", formatter.input_digit('1'))
1122+
self.assertEqual("", formatter._extracted_national_prefix)
1123+
1124+
self.assertEqual("00700 1 2", formatter.input_digit('2'))
1125+
self.assertEqual("00700 1 23", formatter.input_digit('3'))
1126+
self.assertEqual("00700 1 234", formatter.input_digit('4'))
1127+
self.assertEqual("00700 1 234 5", formatter.input_digit('5'))
1128+
self.assertEqual("00700 1 234 56", formatter.input_digit('6'))
1129+
self.assertEqual("00700 1 234 567", formatter.input_digit('7'))
1130+
self.assertEqual("00700 1 234 567 8", formatter.input_digit('8'))
1131+
self.assertEqual("00700 1 234 567 89", formatter.input_digit('9'))
1132+
self.assertEqual("00700 1 234 567 890", formatter.input_digit('0'))
1133+
self.assertEqual("00700 1 234 567 8901", formatter.input_digit('1'))
1134+
self.assertEqual("00700123456789012", formatter.input_digit('2'))
1135+
self.assertEqual("007001234567890123", formatter.input_digit('3'))
1136+
self.assertEqual("0070012345678901234", formatter.input_digit('4'))
1137+
self.assertEqual("00700123456789012345", formatter.input_digit('5'))
1138+
self.assertEqual("007001234567890123456", formatter.input_digit('6'))
1139+
self.assertEqual("0070012345678901234567", formatter.input_digit('7'))
1140+
11071141
def testAYTFShortNumberFormatting_AR(self):
11081142
# Python version extra test: use real metadata
11091143
formatter = AsYouTypeFormatter("AR")

python/tests/phonenumbermatchertest.py

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ def testFindNationalNumber(self):
283283

284284
self.doTestFindInContext("64(0)64123456", "NZ")
285285
# Check that using a "/" is fine in a phone number.
286-
self.doTestFindInContext("123/45678", "DE")
286+
# Note that real Polish numbers do *not* start with a 0.
287+
self.doTestFindInContext("0123/456789", "PL")
287288
self.doTestFindInContext("123-456-7890", "US")
288289

289290
# See PhoneNumberUtilTest.testParseWithInternationalPrefixes().
@@ -418,34 +419,53 @@ def testIntermediateParsePositions(self):
418419
for ii in range(8, 20):
419420
self.assertEqualRange(text, ii, 19, 28)
420421

422+
def testFourMatchesInARow(self):
423+
number1 = "415-666-7777"
424+
number2 = "800-443-1223"
425+
number3 = "212-443-1223"
426+
number4 = "650-443-1223"
427+
text = number1 + " - " + number2 + " - " + number3 + " - " + number4
428+
429+
matcher = PhoneNumberMatcher(text, "US")
430+
match = matcher.next() if matcher.has_next() else None
431+
self.assertMatchProperties(match, text, number1, "US")
432+
433+
match = matcher.next() if matcher.has_next() else None
434+
self.assertMatchProperties(match, text, number2, "US")
435+
436+
match = matcher.next() if matcher.has_next() else None
437+
self.assertMatchProperties(match, text, number3, "US")
438+
439+
match = matcher.next() if matcher.has_next() else None
440+
self.assertMatchProperties(match, text, number4, "US")
441+
442+
def testMatchesFoundWithMultipleSpaces(self):
443+
number1 = "(415) 666-7777"
444+
number2 = "(800) 443-1223"
445+
text = number1 + " " + number2
446+
447+
matcher = PhoneNumberMatcher(text, "US")
448+
match = matcher.next() if matcher.has_next() else None
449+
self.assertMatchProperties(match, text, number1, "US")
450+
451+
match = matcher.next() if matcher.has_next() else None
452+
self.assertMatchProperties(match, text, number2, "US")
453+
421454
def testMatchWithSurroundingZipcodes(self):
422455
number = "415-666-7777"
423456
zipPreceding = "My address is CA 34215 - " + number + " is my number."
424-
expectedResult = phonenumberutil.parse(number, "US")
425457

426458
matcher = PhoneNumberMatcher(zipPreceding, "US")
427-
if matcher.has_next():
428-
match = matcher.next()
429-
else:
430-
match = None
431-
self.assertTrue(match is not None,
432-
msg="Did not find a number in '" + zipPreceding + "'; expected " + number)
433-
self.assertEqual(expectedResult, match.number)
434-
self.assertEqual(number, match.raw_string)
459+
match = matcher.next() if matcher.has_next() else None
460+
self.assertMatchProperties(match, zipPreceding, number, "US")
435461

436462
# Now repeat, but this time the phone number has spaces in it. It should still be found.
437463
number = "(415) 666 7777"
438464

439465
zipFollowing = "My number is " + number + ". 34215 is my zip-code."
440466
matcher = PhoneNumberMatcher(zipFollowing, "US")
441-
if matcher.has_next():
442-
matchWithSpaces = matcher.next()
443-
else:
444-
matchWithSpaces = None
445-
self.assertTrue(matchWithSpaces is not None,
446-
msg="Did not find a number in '" + zipFollowing + "'; expected " + number)
447-
self.assertEqual(expectedResult, matchWithSpaces.number)
448-
self.assertEqual(number, matchWithSpaces.raw_string)
467+
match = matcher.next() if matcher.has_next() else None
468+
self.assertMatchProperties(match, zipFollowing, number, "US")
449469

450470
def testIsLatinLetter(self):
451471
self.assertTrue(PhoneNumberMatcher._is_latin_letter('c'))
@@ -599,10 +619,7 @@ def _doTestNumberMatchesForLeniency(self, testCases, leniency):
599619
wrongMatchFoundCount = 0
600620
for test in testCases:
601621
iterator = self.findNumbersForLeniency(test.rawString, test.region, leniency)
602-
if iterator.has_next():
603-
match = iterator.next()
604-
else:
605-
match = None
622+
match = iterator.next() if iterator.has_next() else None
606623
if match is None:
607624
noMatchFoundCount += 1
608625
prnt("No match found in %s for leniency: %s" % (test, leniency), file=sys.stderr)
@@ -617,10 +634,7 @@ def _doTestNumberNonMatchesForLeniency(self, testCases, leniency):
617634
matchFoundCount = 0
618635
for test in testCases:
619636
iterator = self.findNumbersForLeniency(test.rawString, test.region, leniency)
620-
if iterator.has_next():
621-
match = iterator.next()
622-
else:
623-
match = None
637+
match = iterator.next() if iterator.has_next() else None
624638
if match is not None:
625639
matchFoundCount += 1
626640
prnt("Match found in %s for leniency: %s" % (test, leniency), file=sys.stderr)
@@ -830,6 +844,15 @@ def assertEqualRange(self, text, index, start, end):
830844
self.assertEqual(end - index, match.end)
831845
self.assertEqual(sub[match.start:match.end], match.raw_string)
832846

847+
def assertMatchProperties(self, match, text, number, region):
848+
"""Asserts that the expected match is non-null, and that the raw string
849+
and expected proto buffer are set appropriately."""
850+
expectedResult = phonenumberutil.parse(number, region)
851+
self.assertTrue(match is not None,
852+
msg="Did not find a number in '" + text + "'; expected " + number)
853+
self.assertEqual(expectedResult, match.number)
854+
self.assertEqual(number, match.raw_string)
855+
833856
def doTestFindInContext(self, number, defaultCountry):
834857
"""Tests numbers found by PhoneNumberMatcher in various textual contexts"""
835858
self.findPossibleInContext(number, defaultCountry)
@@ -893,10 +916,7 @@ def doTestInContext(self, number, defaultCountry, contextPairs, leniency):
893916
end = start + len(number)
894917
matcher = PhoneNumberMatcher(text, defaultCountry, leniency, 65535)
895918

896-
if matcher.has_next():
897-
match = matcher.next()
898-
else:
899-
match = None
919+
match = matcher.next() if matcher.has_next() else None
900920
self.assertTrue(match is not None,
901921
msg="Did not find a number in '" + text + "'; expected '" + number + "'")
902922

@@ -934,8 +954,6 @@ def testDoubleExtensionX(self):
934954
# can't be used in a NumberTest).
935955
m0 = PhoneNumberMatcher(xx_ext, "US", leniency=Leniency.POSSIBLE).next()
936956
self.assertEqual(xx_ext, m0.raw_string)
937-
m1 = PhoneNumberMatcher(xx_ext, "US", leniency=Leniency.VALID).next()
938-
self.assertEqual("800 234 1 111", m1.raw_string)
939957
matcher2 = PhoneNumberMatcher(xx_ext, "US", leniency=Leniency.STRICT_GROUPING)
940958
self.assertFalse(matcher2.has_next())
941959

0 commit comments

Comments
 (0)