-
Notifications
You must be signed in to change notification settings - Fork 6
/
__init__.py
385 lines (325 loc) · 12.9 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import datetime
import operator
from functools import reduce
import phonenumbers
import pytz
from dateutil.parser import parse as parse_date
from tztrout.data import ZIP_DATA, TroutData
from tztrout.data_exceptions import data_exceptions
td = TroutData()
def _dedupe_preserve_ord(lst):
"""Dedupe a list and preserve the order of its items."""
seen = set()
return [x for x in lst if x not in seen and not seen.add(x)]
def tz_ids_for_tz_name(tz_name):
"""Get the TZ identifiers that are currently in a specific time zone, e.g.
>>> tztrout.tz_ids_for_tz_name('PDT') # ran during DST
[
u'America/Dawson',
u'America/Los_Angeles',
u'America/Santa_Isabel',
u'America/Tijuana',
u'America/Vancouver',
u'America/Whitehorse',
u'Canada/Pacific',
u'US/Pacific'
]
>>> tztrout.tz_ids_for_tz_name('PDT') # ran outside of the DST period
[]
"""
ids = td.tz_name_to_tz_ids.get(tz_name)
# if the tz_name is just an alias, don't perform the fine-grained filtering
if tz_name in td.aliases:
return ids
valid_ids = []
if ids:
# only get the tz ids that match the tz name currently
for id in ids:
tz = pytz.timezone(id)
try:
if tz.tzname(datetime.datetime.utcnow()) == tz_name:
valid_ids.append(id)
except (pytz.NonExistentTimeError, pytz.AmbiguousTimeError):
pass
return valid_ids
def tz_ids_for_phone(phone, country='US'):
"""Get the TZ identifiers that a phone number might be related to, e.g.
>>> tztrout.tz_ids_for_phone('+16503334444')
[u'America/Los_Angeles']
>>> tztrout.tz_ids_for_phone('+49 (0)711 400 40990')
[u'Europe/Berlin', u'Europe/Busingen']
"""
from phonenumbers.geocoder import description_for_number
try:
phone = phonenumbers.parse(phone, country)
except Exception:
pass
else:
country_iso = phonenumbers.region_code_for_number(phone)
if not country_iso:
country_iso = phonenumbers.region_code_for_country_code(
phone.country_code
)
if country_iso == 'US':
# check if we have a specific exception for a given area code first
exception_key = 'areacode:%s' % str(phone.national_number)[:3]
if exception_key in data_exceptions:
return data_exceptions[exception_key]['include']
state = city = None
area = description_for_number(phone, 'en').split(',')
if len(area) == 2:
city = area[0].strip()
state = area[1].strip()
elif len(area) == 1 and area[0]:
state = area[0].lower().strip()
state = td.normalized_states['US'].get(state, None)
return tz_ids_for_address(country_iso, state=state, city=city)
elif country_iso == 'CA':
area_code = str(phone.national_number)[:3]
state = td.ca_area_code_to_state.get(area_code)
return td.ca_state_to_tz_ids.get(state)
elif country_iso == 'AU':
area_code = str(phone.national_number)[:2]
state = td.au_area_code_to_state.get(area_code)
# Some Australian number prefixes (e.g. 04) are country-wide - fall
# back to all the AU tz ids
if state:
return td.au_state_to_tz_ids.get(state)
return pytz.country_timezones.get(country_iso)
elif country_iso:
return pytz.country_timezones.get(country_iso)
return []
def tz_ids_for_address(country, state=None, city=None, zipcode=None, **kwargs):
"""Get the TZ identifiers for a given address, e.g.:
>>> tztrout.tz_ids_for_address('US', state='CA', city='Palo Alto')
[u'America/Los_Angeles']
>>> tztrout.tz_ids_for_address('PL')
[u'Europe/Warsaw']
>>> tztrout.tz_ids_for_address('CN')
[
u'Asia/Shanghai',
u'Asia/Harbin',
u'Asia/Chongqing',
u'Asia/Urumqi',
u'Asia/Kashgar'
]
"""
if country == 'US':
if zipcode:
if isinstance(zipcode, int):
zipcode = str(zipcode)
# If an extended zipcode in a form of XXXXX-XXXX is provided,
# only use the first part
zipcode = zipcode.split('-')[0]
return list(td.us_zip_to_tz_ids.get(zipcode, []))
elif state or city:
if city and 'city:%s' % city.lower() in data_exceptions:
return data_exceptions['city:%s' % city.lower()]['include']
if state and len(state) != 2:
state = td.normalized_states['US'].get(state.lower(), state)
code = ZIP_DATA.find_zip(city=city, state=state)
if code:
return list(td.us_zip_to_tz_ids.get(code, []))
elif city and state:
# Couldn't find by city+state, try ignoring city
code = ZIP_DATA.find_zip(state=state)
if code:
return list(td.us_zip_to_tz_ids.get(code, []))
elif country == 'CA' and state:
if len(state) != 2:
state = td.normalized_states['CA'].get(state.lower(), state)
return td.ca_state_to_tz_ids.get(state)
elif country == 'AU' and state:
if len(state) != 2:
state = td.normalized_states['AU'].get(state.lower(), state)
return td.au_state_to_tz_ids.get(state)
return pytz.country_timezones.get(country)
def tz_ids_for_offset(offset_in_minutes):
"""Get the TZ identifiers for a given UTC offset (in minutes), e.g.
>>> tztrout.tz_ids_for_offset(-7 * 60) # during DST
[
u'America/Creston',
u'America/Dawson',
u'America/Dawson_Creek',
u'America/Hermosillo',
u'America/Los_Angeles',
u'America/Phoenix',
u'America/Santa_Isabel',
u'America/Tijuana',
u'America/Vancouver',
u'America/Whitehorse',
u'Canada/Pacific',
u'US/Arizona',
u'US/Pacific'
]
"""
ids = td.offset_to_tz_ids.get(offset_in_minutes)
valid_ids = []
if ids:
# only get the tz ids that match the tz offset currently
for id in ids:
tz = pytz.timezone(id)
try:
off = tz.utcoffset(datetime.datetime.utcnow()).total_seconds()
if off / 60 == offset_in_minutes:
valid_ids.append(id)
except (pytz.NonExistentTimeError, pytz.AmbiguousTimeError):
pass
return valid_ids
def local_time_for_phone(phone, country='US'):
"""Get the local time for a given phone number, e.g.
>>> datetime.datetime.utcnow()
datetime.datetime(2013, 9, 17, 19, 44, 0, 966696)
>>> tztrout.local_time_for_phone('+16503334444')
datetime.datetime(2013, 9, 17, 12, 44, 0, 966696, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
"""
ids = tz_ids_for_phone(phone, country)
if ids:
return pytz.timezone(ids[0]).fromutc(datetime.datetime.utcnow())
def local_time_for_address(
country, state=None, city=None, zipcode=None, **kwargs
):
"""Get the local time for a given address, e.g.
>>> datetime.datetime.utcnow()
datetime.datetime(2013, 9, 17, 19, 44, 0, 966696)
>>> tztrout.local_time_for_address('US', state='California')
datetime.datetime(2013, 9, 17, 12, 44, 0, 966696, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
"""
ids = tz_ids_for_address(country, state, city, zipcode, **kwargs)
if ids:
return pytz.timezone(ids[0]).fromutc(datetime.datetime.utcnow())
def offset_ranges_for_local_time(local_start, local_end):
"""Return a list of UTC offset ranges matching a given local time range.
The returned ranges are expressed in minutes.
`local_start` and `local_end` can be instances of `datetime.time`,
integers, (minutes from midnight) or strings (e.g. '10:00', '20:00',
'8pm', etc.)
For example, if we want to find offset ranges for the timezones where the
local time is currently between 9am and 5pm, and it's 8pm UTC at the
moment, then the expected return value is:
* From -660 to -180. That's because:
* 20 - 660 / 60 == 9
* 20 - 180 / 60 == 17
* From 780 to 840. That's because:
* 20 + 780 / 60 % 24 == 9
* 20 + 840 / 60 % 24 == 10 (it's not 5pm, but 840/60 is 14 and UTC+14
is the furthest possible timezone offset).
>>> tztrout.offset_ranges_for_local_time(datetime.time(9), datetime.time(17)) # ran at 8pm UTC
[[-660, -180], [780, 840]]
"""
if not isinstance(local_start, (datetime.time, int, str)):
raise ValueError(
'local_start is not an instance of datetime.time or int or str'
)
if not isinstance(local_end, (datetime.time, int, str)):
raise ValueError(
'local_end is not an instance of datetime.time or int or str'
)
# Convert to `datetime.time` if `local_start` or `local_end` are strings.
local_start = (
parse_date(local_start).time()
if isinstance(local_start, str)
else local_start
)
local_end = (
parse_date(local_end).time()
if isinstance(local_end, str)
else local_end
)
# Convert to ints (minutes).
to_minutes = lambda t: t.hour * 60 + t.minute
local_start = (
local_start
if isinstance(local_start, int)
else to_minutes(local_start)
)
local_end = (
local_end if isinstance(local_end, int) else to_minutes(local_end)
)
# Get current UTC time.
current_time = to_minutes(datetime.datetime.utcnow().time())
# Tweak for ranges that pass midnight (e.g. (5pm, 9am)).
if local_end < local_start:
local_end += 24 * 60
# Calculate UTC offsets.
offset_ranges = [
[local_start - current_time, local_end - current_time],
[
24 * 60 - current_time + local_start,
24 * 60 - current_time + local_end,
],
[
-24 * 60 - current_time + local_start,
-24 * 60 - current_time + local_end,
],
]
# Cap the offsets at UTC-14:00 and UTC+14:00 (lowest/highest possible
# offset).
capped = (
lambda t: 14 * 60 if t > 14 * 60 else -14 * 60 if t < -14 * 60 else t
)
for i, range in enumerate(offset_ranges):
offset_ranges[i][0] = capped(range[0])
offset_ranges[i][1] = capped(range[1])
# Discard the irrelevant ranges (i.e. where start == end).
offset_ranges = [range for range in offset_ranges if range[0] != range[1]]
return offset_ranges
def tz_ids_for_offset_range(offset_start, offset_end):
"""Return all the time zone identifiers which offsets are within the
(offset_start, offset_end) range. The arguments should be integers
(UTC offsets in minutes).
>>> tztrout.tz_ids_for_offset_range(-7*60, -6 *60)
[
u'America/Belize', u'America/Boise', u'America/Cambridge_Bay',
u'America/Chihuahua', u'America/Costa_Rica', u'America/Denver',
u'America/Edmonton', u'America/El_Salvador', u'America/Guatemala',
u'America/Inuvik', u'America/Managua', u'America/Mazatlan',
u'America/Ojinaga', u'America/Regina', u'America/Shiprock',
u'America/Swift_Current', u'America/Tegucigalpa',
u'America/Yellowknife', u'Canada/Mountain', u'Pacific/Galapagos',
u'US/Mountain', u'America/Creston', u'America/Dawson',
u'America/Dawson_Creek', u'America/Hermosillo', u'America/Los_Angeles',
u'America/Phoenix', u'America/Santa_Isabel', u'America/Tijuana',
u'America/Vancouver', u'America/Whitehorse', u'Canada/Pacific',
u'US/Arizona', u'US/Pacific'
]
"""
offsets = [
int(o)
for o in td.offset_to_tz_ids.keys()
if (int(o) >= offset_start and int(o) <= offset_end)
]
ids = [tz_ids_for_offset(o) for o in offsets]
if ids:
ids = reduce(operator.add, ids) # flatten the list of lists
return ids
def non_dst_offsets_for_phone(phone):
"""Return the non-DST offsets (in minutes) for a given phone, e.g.
>>> tztrout.non_dst_offsets_for_phone('+1 650 248 6188')
[-480]
"""
ids = tz_ids_for_phone(phone)
if ids:
offsets = [
td._get_latest_non_dst_offset(pytz.timezone(id)) for id in ids
]
return _dedupe_preserve_ord(
[int(o.total_seconds() / 60) for o in offsets if o]
)
def non_dst_offsets_for_address(
country, state=None, city=None, zipcode=None, **kwargs
):
"""Return the non-DST offsets (in minutes) for a given address, e.g.
>>> tztrout.non_dst_offsets_for_address('US', state='CA')
[-480]
"""
ids = tz_ids_for_address(
country, state=state, city=city, zipcode=zipcode, **kwargs
)
if ids:
offsets = [
td._get_latest_non_dst_offset(pytz.timezone(id)) for id in ids
]
return _dedupe_preserve_ord(
[int(o.total_seconds() / 60) for o in offsets if o]
)