2
'''$Id: tzinfo.py,v 1.6 2004/07/24 21:21:28 zenzen Exp $'''
4
__rcs_id__ = '$Id: tzinfo.py,v 1.6 2004/07/24 21:21:28 zenzen Exp $'
5
__version__ = '$Revision: 1.6 $'[11:-2]
7
from datetime import datetime, timedelta, tzinfo
8
from bisect import bisect_right
12
def memorized_timedelta(seconds):
13
'''Create only one instance of each distinct timedelta'''
15
return _timedelta_cache[seconds]
17
delta = timedelta(seconds=seconds)
18
_timedelta_cache[seconds] = delta
22
def memorized_datetime(*args):
23
'''Create only one instance of each distinct datetime'''
25
return _datetime_cache[args]
28
_datetime_cache[args] = dt
32
def memorized_ttinfo(*args):
33
'''Create only one instance of each distinct tuple'''
35
return _ttinfo_cache[args]
38
memorized_timedelta(args[0]),
39
memorized_timedelta(args[1]),
42
_ttinfo_cache[args] = ttinfo
45
_notime = memorized_timedelta(0)
47
class BaseTzInfo(tzinfo):
48
# Overridden in subclass
57
class StaticTzInfo(BaseTzInfo):
58
'''A timezone that has a constant offset from UTC
60
These timezones are rare, as most regions have changed their
61
offset from UTC at some point in their history
64
def fromutc(self, dt):
65
'''See datetime.tzinfo.fromutc'''
66
return (dt + self._utcoffset).replace(tzinfo=self)
68
def utcoffset(self,dt):
69
'''See datetime.tzinfo.utcoffset'''
70
return self._utcoffset
73
'''See datetime.tzinfo.dst'''
77
'''See datetime.tzinfo.tzname'''
80
def localize(self, dt, is_dst=False):
81
'''Convert naive time to local time'''
82
if dt.tzinfo is not None:
83
raise ValueError, 'Not naive datetime (tzinfo is already set)'
84
return dt.replace(tzinfo=self)
86
def normalize(self, dt, is_dst=False):
87
'''Correct the timezone information on the given datetime'''
89
raise ValueError, 'Naive time - no tzinfo set'
90
return dt.replace(tzinfo=self)
93
return '<StaticTzInfo %r>' % (self._zone,)
96
class DstTzInfo(BaseTzInfo):
97
'''A timezone that has a variable offset from UTC
99
The offset might change if daylight savings time comes into effect,
100
or at a point in history when the region decides to change their
104
# Overridden in subclass
105
_utc_transition_times = None # Sorted list of DST transition times in UTC
106
_transition_info = None # [(utcoffset, dstoffset, tzname)] corresponding
107
# to _utc_transition_times entries
112
_dst = None # DST offset
114
def __init__(self, _inf=None, _tzinfos=None):
116
self._tzinfos = _tzinfos
117
self._utcoffset, self._dst, self._tzname = _inf
120
self._tzinfos = _tzinfos
121
self._utcoffset, self._dst, self._tzname = self._transition_info[0]
122
_tzinfos[self._transition_info[0]] = self
123
for inf in self._transition_info[1:]:
124
if not _tzinfos.has_key(inf):
125
_tzinfos[inf] = self.__class__(inf, _tzinfos)
127
def fromutc(self, dt):
128
'''See datetime.tzinfo.fromutc'''
129
dt = dt.replace(tzinfo=None)
130
idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)
131
inf = self._transition_info[idx]
132
return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])
134
def normalize(self, dt):
135
'''Correct the timezone information on the given datetime
137
If date arithmetic crosses DST boundaries, the tzinfo
138
is not magically adjusted. This method normalizes the
139
tzinfo to the correct one.
141
To test, first we need to do some setup
143
>>> from pytz import timezone
144
>>> utc = timezone('UTC')
145
>>> eastern = timezone('US/Eastern')
146
>>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
148
We next create a datetime right on an end-of-DST transition point,
149
the instant when the wallclocks are wound back one hour.
151
>>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
152
>>> loc_dt = utc_dt.astimezone(eastern)
153
>>> loc_dt.strftime(fmt)
154
'2002-10-27 01:00:00 EST (-0500)'
156
Now, if we subtract a few minutes from it, note that the timezone
157
information has not changed.
159
>>> before = loc_dt - timedelta(minutes=10)
160
>>> before.strftime(fmt)
161
'2002-10-27 00:50:00 EST (-0500)'
163
But we can fix that by calling the normalize method
165
>>> before = eastern.normalize(before)
166
>>> before.strftime(fmt)
167
'2002-10-27 01:50:00 EDT (-0400)'
170
if dt.tzinfo is None:
171
raise ValueError, 'Naive time - no tzinfo set'
173
# Convert dt in localtime to UTC
174
offset = dt.tzinfo._utcoffset
175
dt = dt.replace(tzinfo=None)
177
# convert it back, and return it
178
return self.fromutc(dt)
180
def localize(self, dt, is_dst=False):
181
'''Convert naive time to local time.
183
This method should be used to construct localtimes, rather
184
than passing a tzinfo argument to a datetime constructor.
186
is_dst is used to determine the correct timezone in the ambigous
187
period at the end of daylight savings time.
189
>>> from pytz import timezone
190
>>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
191
>>> amdam = timezone('Europe/Amsterdam')
192
>>> dt = datetime(2004, 10, 31, 2, 0, 0)
193
>>> loc_dt1 = amdam.localize(dt, is_dst=True)
194
>>> loc_dt2 = amdam.localize(dt, is_dst=False)
195
>>> loc_dt1.strftime(fmt)
196
'2004-10-31 02:00:00 CEST (+0200)'
197
>>> loc_dt2.strftime(fmt)
198
'2004-10-31 02:00:00 CET (+0100)'
199
>>> str(loc_dt2 - loc_dt1)
202
Use is_dst=None to raise an AmbiguousTimeError for ambiguous
203
times at the end of daylight savings
206
... loc_dt1 = amdam.localize(dt, is_dst=None)
207
... except AmbiguousTimeError:
211
>>> loc_dt1 = amdam.localize(dt, is_dst=None)
212
Traceback (most recent call last):
214
AmbiguousTimeError: 2004-10-31 02:00:00
216
is_dst defaults to False
218
>>> amdam.localize(dt) == amdam.localize(dt, False)
222
if dt.tzinfo is not None:
223
raise ValueError, 'Not naive datetime (tzinfo is already set)'
225
# Find the possibly correct timezones. We probably just have one,
226
# but we might end up with two if we are in the end-of-DST
227
# transition period. Or possibly more in some particularly confused
229
possible_loc_dt = Set()
230
for tzinfo in self._tzinfos.values():
231
loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
232
if loc_dt.replace(tzinfo=None) == dt:
233
possible_loc_dt.add(loc_dt)
235
if len(possible_loc_dt) == 1:
236
return possible_loc_dt.pop()
238
# If told to be strict, raise an exception since we have an
241
raise AmbiguousTimeError(dt)
243
# Filter out the possiblilities that don't match the requested
245
filtered_possible_loc_dt = [
246
p for p in possible_loc_dt
247
if bool(p.tzinfo._dst) == is_dst
250
# Hopefully we only have one possibility left. Return it.
251
if len(filtered_possible_loc_dt) == 1:
252
return filtered_possible_loc_dt[0]
254
if len(filtered_possible_loc_dt) == 0:
255
filtered_possible_loc_dt = list(possible_loc_dt)
257
# If we get this far, we have in a wierd timezone transition
258
# where the clocks have been wound back but is_dst is the same
259
# in both (eg. Europe/Warsaw 1915 when they switched to CET).
260
# At this point, we just have to guess unless we allow more
261
# hints to be passed in (such as the UTC offset or abbreviation),
262
# but that is just getting silly.
264
# Choose the earliest (by UTC) applicable timezone.
267
a.replace(tzinfo=None) - a.tzinfo._utcoffset,
268
b.replace(tzinfo=None) - b.tzinfo._utcoffset,
270
filtered_possible_loc_dt.sort(mycmp)
271
return filtered_possible_loc_dt[0]
273
def utcoffset(self, dt):
274
'''See datetime.tzinfo.utcoffset'''
275
return self._utcoffset
278
'''See datetime.tzinfo.dst'''
281
def tzname(self, dt):
282
'''See datetime.tzinfo.tzname'''
290
if self._utcoffset > _notime:
291
return '<DstTzInfo %r %s+%s %s>' % (
292
self._zone, self._tzname, self._utcoffset, dst
295
return '<DstTzInfo %r %s%s %s>' % (
296
self._zone, self._tzname, self._utcoffset, dst
299
class AmbiguousTimeError(Exception):
300
'''Exception raised when attempting to create an ambiguous wallclock time.
302
At the end of a DST transition period, a particular wallclock time will
303
occur twice (once before the clocks are set back, once after). Both
304
possibilities may be correct, unless further information is supplied.
306
See DstTzInfo.normalize() for more info