Skip to content

Commit 6b755fb

Browse files
committed
Add timezone mapping functionality as per upstream r616
1 parent 695b71e commit 6b755fb

5 files changed

Lines changed: 251 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ originally owned a phone number.
148148
u'Vodafone'
149149
```
150150

151+
You might also be able to retrieve a list of time zone names that the number potentially
152+
belongs to.
153+
154+
```pycon
155+
>>> from phonenumbers import timezone
156+
>>> gb_number = phonenumbers.parse("+447986123456", "GB")
157+
>>> str(time_zones_for_number(gb_number))
158+
"(u'Atlantic/Reykjavik', u'Europe/London')"
159+
```
160+
151161
For more information about the other functionality available from the library, look in the unit tests or in the original
152162
[libphonenumber project](http://code.google.com/p/libphonenumber/).
153163

python/phonenumbers/timezone.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Phone number to time zone mapping functionality
2+
3+
>>> import phonenumbers
4+
>>> from phonenumbers.timezone import time_zones_for_number
5+
>>> ro_number = phonenumbers.parse("+40721234567", "RO")
6+
>>> str(time_zones_for_number(ro_number))
7+
"(u'Europe/Bucharest',)"
8+
>>> gb_number = phonenumbers.parse("+447986123456", "GB")
9+
>>> str(time_zones_for_number(gb_number))
10+
"(u'Atlantic/Reykjavik', u'Europe/London')"
11+
12+
"""
13+
# Based very loosely on original Java code:
14+
# java/geocoder/src/com/google/i18n/phonenumbers/PhoneNumberToTimeZonesMapper.java
15+
# Copyright (C) 2013 The Libphonenumber Authors
16+
#
17+
# Licensed under the Apache License, Version 2.0 (the "License");
18+
# you may not use this file except in compliance with the License.
19+
# You may obtain a copy of the License at
20+
#
21+
# http://www.apache.org/licenses/LICENSE-2.0
22+
#
23+
# Unless required by applicable law or agreed to in writing, software
24+
# distributed under the License is distributed on an "AS IS" BASIS,
25+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26+
# See the License for the specific language governing permissions and
27+
# limitations under the License.
28+
29+
from .phonenumberutil import PhoneNumberType, number_type
30+
from .phonenumberutil import PhoneNumberFormat, format_number
31+
from .prefix import prefix_description_for_number
32+
try:
33+
from .tzdata import TIMEZONE_DATA, TIMEZONE_LONGEST_PREFIX
34+
except ImportError: # pragma no cover
35+
# Before the generated code exists, the carrierdata/ directory is empty.
36+
# The generation process imports this module, creating a circular
37+
# dependency. The hack below works around this.
38+
import os
39+
import sys
40+
if (os.path.basename(sys.argv[0]) == "buildmetadatafromxml.py" or
41+
os.path.basename(sys.argv[0]) == "buildprefixdata.py"):
42+
print >> sys.stderr, "Failed to import generated data (but OK as during autogeneration)"
43+
TIMEZONE_DATA = {'4411': u'Europe/London'}
44+
TIMEZONE_LONGEST_PREFIX = 4
45+
else:
46+
raise
47+
48+
# This is defined by ICU as the unknown time zone.
49+
UNKNOWN_TIMEZONE = "Etc/Unknown"
50+
_UNKNOWN_TIME_ZONE_LIST = (UNKNOWN_TIMEZONE,)
51+
52+
53+
def time_zones_for_geographical_number(numobj):
54+
"""Returns a list of time zones to which a phone number belongs.
55+
56+
This method assumes the validity of the number passed in has already been
57+
checked, and that the number is geo-localizable. We consider fixed-line
58+
and mobile numbers possible candidates for geo-localization.
59+
60+
Arguments:
61+
numobj -- a valid phone number for which we want to get the time zones
62+
to which it belongs
63+
Returns a list of the corresponding time zones or a single element list
64+
with the default unknown time zone if no other time zone was found or if
65+
the number was invalid"""
66+
e164_num = format_number(numobj, PhoneNumberFormat.E164)
67+
if not e164_num.startswith('+'): # pragma no cover
68+
# Can only hit this arm if there's an internal error in the rest of
69+
# the library
70+
raise Exception("Expect E164 number to start with +")
71+
for prefix_len in xrange(TIMEZONE_LONGEST_PREFIX, 0, -1):
72+
prefix = e164_num[1:(1 + prefix_len)]
73+
if prefix in TIMEZONE_DATA:
74+
return TIMEZONE_DATA[prefix]
75+
return _UNKNOWN_TIME_ZONE_LIST
76+
77+
78+
def time_zones_for_number(numobj):
79+
"""As time_zones_for_geographical_number() but explicitly checks the
80+
validity of the number passed in.
81+
82+
Arguments:
83+
numobj -- a valid phone number for which we want to get the time zones to which it belongs
84+
Returns a list of the corresponding time zones or a single element list with the default
85+
unknown time zone if no other time zone was found or if the number was invalid"""
86+
ntype = number_type(numobj)
87+
if ntype == PhoneNumberType.UNKNOWN:
88+
return _UNKNOWN_TIME_ZONE_LIST
89+
elif not _can_be_geocoded(ntype):
90+
return _country_level_time_zones_for_number(numobj)
91+
return time_zones_for_geographical_number(numobj)
92+
93+
94+
def _country_level_time_zones_for_number(numobj):
95+
"""Returns the list of time zones corresponding to the country calling code of a number.
96+
Arguments:
97+
numobj -- the phone number to look up
98+
Returns a list of the corresponding time zones or a single element list with the default
99+
unknown time zone if no other time zone was found or if the number was invalid"""
100+
cc = str(numobj.country_code)
101+
for prefix_len in xrange(TIMEZONE_LONGEST_PREFIX, 0, -1):
102+
prefix = cc[:(1 + prefix_len)]
103+
if prefix in TIMEZONE_DATA:
104+
return TIMEZONE_DATA[prefix]
105+
return _UNKNOWN_TIME_ZONE_LIST
106+
107+
108+
# A similar method is implemented as phonenumberutil._is_number_geographical,
109+
# which performs a stricter check, as it determines if a number has a
110+
# geographical association. Also, if new phone number types were added, we
111+
# should check if this other method should be updated too.
112+
# TODO: Remove duplication by completing the login in the method in phonenumberutil.
113+
# For more information, see the comments in that method.
114+
def _can_be_geocoded(ntype):
115+
return (ntype == PhoneNumberType.FIXED_LINE or
116+
ntype == PhoneNumberType.MOBILE or
117+
ntype == PhoneNumberType.FIXED_LINE_OR_MOBILE)
118+
119+
120+
if __name__ == '__main__': # pragma no cover
121+
import doctest
122+
doctest.testmod()

python/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .phonenumbermatchertest import PhoneNumberMatchTest, PhoneNumberMatcherTest
1010
from .geocodertest import PhoneNumberGeocoderTest
1111
from .carriertest import PhoneNumberToCarrierMapperTest
12+
from .timezonetest import PhoneNumberToTimeZonesMapperTest
1213

1314
if __name__ == '__main__':
1415
unittest.main()

python/tests/timezonetest.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python
2+
"""Unit tests for timezone.py"""
3+
4+
# Based on original Java code:
5+
# java/geocoder/test/com/google/i18n/phonenumbers/geocoding/PhoneNumberToTimeZonesMapperTest.java
6+
# Copyright (C) 2012 The Libphonenumber Authors
7+
#
8+
# Licensed under the Apache License, Version 2.0 (the "License");
9+
# you may not use this file except in compliance with the License.
10+
# You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
import unittest
21+
22+
from phonenumbers import PhoneNumber, FrozenPhoneNumber
23+
from phonenumbers import timezone
24+
from phonenumbers.timezone import time_zones_for_geographical_number
25+
from phonenumbers.timezone import time_zones_for_number
26+
from phonenumbers.timezone import _UNKNOWN_TIME_ZONE_LIST
27+
28+
# Allow override library timezone metadata with the test metadata.
29+
REAL_TIMEZONE_DATA = timezone.TIMEZONE_DATA
30+
REAL_TIMEZONE_LONGEST_PREFIX = timezone.TIMEZONE_LONGEST_PREFIX
31+
from .testtzdata import TIMEZONE_DATA as TEST_TIMEZONE_DATA
32+
from .testtzdata import TIMEZONE_LONGEST_PREFIX as TEST_TIMEZONE_LONGEST_PREFIX
33+
34+
35+
def reinstate_real_tzdata():
36+
"""Reinstate real phone number timezone metadata"""
37+
timezone.TIMEZONE_DATA = REAL_TIMEZONE_DATA
38+
timezone.TIMEZONE_LONGEST_PREFIX = REAL_TIMEZONE_LONGEST_PREFIX
39+
40+
41+
def insert_test_tzdata():
42+
"""Insert test timezone metadata into library"""
43+
timezone.TIMEZONE_DATA = TEST_TIMEZONE_DATA
44+
timezone.TIMEZONE_LONGEST_PREFIX = TEST_TIMEZONE_LONGEST_PREFIX
45+
46+
47+
# Set up some test numbers to re-use.
48+
AU_NUMBER = FrozenPhoneNumber(country_code=61, national_number=236618300L)
49+
CA_NUMBER = FrozenPhoneNumber(country_code=1, national_number=6048406565L)
50+
KO_NUMBER = FrozenPhoneNumber(country_code=82, national_number=22123456L)
51+
KO_INVALID_NUMBER = FrozenPhoneNumber(country_code=82, national_number=1234L)
52+
US_NUMBER1 = FrozenPhoneNumber(country_code=1, national_number=6509600000L)
53+
US_NUMBER2 = FrozenPhoneNumber(country_code=1, national_number=2128120000L)
54+
US_NUMBER3 = FrozenPhoneNumber(country_code=1, national_number=6174240000L)
55+
US_INVALID_NUMBER = FrozenPhoneNumber(country_code=1, national_number=123456789L)
56+
NUMBER_WITH_INVALID_COUNTRY_CODE = FrozenPhoneNumber(country_code=999, national_number=2423651234L)
57+
INTERNATIONAL_TOLL_FREE = FrozenPhoneNumber(country_code=800, national_number=12345678L)
58+
59+
# NANPA time zones.
60+
_CHICAGO_TZ = "America/Chicago"
61+
_LOS_ANGELES_TZ = "America/Los_Angeles"
62+
_NEW_YORK_TZ = "America/New_York"
63+
_WINNIPEG_TZ = "America/Winnipeg"
64+
_NANPA_TZ_LIST = (_NEW_YORK_TZ, _CHICAGO_TZ, _WINNIPEG_TZ, _LOS_ANGELES_TZ)
65+
66+
# Non NANPA time zones.
67+
_SEOUL_TZ = "Asia/Seoul"
68+
_SYDNEY_TZ = "Australia/Sydney"
69+
70+
71+
class PhoneNumberToTimeZonesMapperTest(unittest.TestCase):
72+
"""Unit tests for timezone.py"""
73+
74+
def setUp(self):
75+
insert_test_tzdata()
76+
77+
def tearDown(self):
78+
reinstate_real_tzdata()
79+
80+
def testGetTimeZonesForNumber(self):
81+
# Test with invalid numbers even when their country code prefixes exist in the mapper.
82+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_number(US_INVALID_NUMBER))
83+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_number(KO_INVALID_NUMBER))
84+
# Test with valid prefixes.
85+
self.assertEqual((_SYDNEY_TZ,), time_zones_for_number(AU_NUMBER))
86+
self.assertEqual((_SEOUL_TZ,), time_zones_for_number(KO_NUMBER))
87+
self.assertEqual((_WINNIPEG_TZ,), time_zones_for_number(CA_NUMBER))
88+
self.assertEqual((_LOS_ANGELES_TZ,), time_zones_for_number(US_NUMBER1))
89+
self.assertEqual((_NEW_YORK_TZ,), time_zones_for_number(US_NUMBER2))
90+
# Test with an invalid country code.
91+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_number(NUMBER_WITH_INVALID_COUNTRY_CODE))
92+
# Test with a non geographical phone number.
93+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_number(INTERNATIONAL_TOLL_FREE))
94+
# Python version extra test: check a number that can't geocoded; falls back to per-country
95+
kr_mobile_number = FrozenPhoneNumber(country_code=82, national_number=801234567L)
96+
self.assertEqual((_SEOUL_TZ,), time_zones_for_number(kr_mobile_number))
97+
98+
def testGetTimeZonesForValidNumber(self):
99+
# Test with invalid numbers even when their country code prefixes exist in the mapper.
100+
self.assertEqual(_NANPA_TZ_LIST, time_zones_for_geographical_number(US_INVALID_NUMBER))
101+
self.assertEqual((_SEOUL_TZ,), time_zones_for_geographical_number(KO_INVALID_NUMBER))
102+
# Test with valid prefixes.
103+
self.assertEqual((_SYDNEY_TZ,), time_zones_for_geographical_number(AU_NUMBER))
104+
self.assertEqual((_SEOUL_TZ,), time_zones_for_geographical_number(KO_NUMBER))
105+
self.assertEqual((_WINNIPEG_TZ,), time_zones_for_geographical_number(CA_NUMBER))
106+
self.assertEqual((_LOS_ANGELES_TZ,), time_zones_for_geographical_number(US_NUMBER1))
107+
self.assertEqual((_NEW_YORK_TZ,), time_zones_for_geographical_number(US_NUMBER2))
108+
# Test with an invalid country code.
109+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_geographical_number(NUMBER_WITH_INVALID_COUNTRY_CODE))
110+
# Test with a non geographical phone number.
111+
self.assertEqual(_UNKNOWN_TIME_ZONE_LIST, time_zones_for_geographical_number(INTERNATIONAL_TOLL_FREE))
112+
113+
def testGetTimeZonesForValidNumberSearchingAtCountryCodeLevel(self):
114+
# Test that the country level time zones are returned when the number passed in is valid but
115+
# not covered by any non-country level prefixes in the mapper.
116+
self.assertEqual(time_zones_for_number(US_NUMBER3), _NANPA_TZ_LIST)

python/testwrapper.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from phonenumbers import unicode_util
88
from phonenumbers import geocoder
99
from phonenumbers import carrier
10+
from phonenumbers import timezone
1011
from tests import *
1112

1213
if __name__ == '__main__':
@@ -16,4 +17,5 @@
1617
doctest.testmod(unicode_util)
1718
doctest.testmod(geocoder)
1819
doctest.testmod(carrier)
20+
doctest.testmod(timezone)
1921
unittest.main()

0 commit comments

Comments
 (0)