~dongpo-deng/sahana-eden/test

« back to all changes in this revision

Viewing changes to modules/fixed_datetime.py

  • Committer: Deng Dongpo
  • Date: 2010-08-01 09:29:44 UTC
  • Revision ID: dongpo@dhcp-21193.iis.sinica.edu.tw-20100801092944-8t9obt4xtl7otesb
initial

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2008, Red Innovation Ltd., Finland
 
2
# All rights reserved.
 
3
#
 
4
# Redistribution and use in source and binary forms, with or without
 
5
# modification, are permitted provided that the following conditions are met:
 
6
#     * Redistributions of source code must retain the above copyright
 
7
#       notice, this list of conditions and the following disclaimer.
 
8
#     * Redistributions in binary form must reproduce the above copyright
 
9
#       notice, this list of conditions and the following disclaimer in the
 
10
#       documentation and/or other materials provided with the distribution.
 
11
#     * Neither the name of Red Innovation nor the names of its contributors 
 
12
#       may be used to endorse or promote products derived from this software 
 
13
#       without specific prior written permission.
 
14
#
 
15
# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
 
16
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 
17
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 
18
# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
 
19
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 
20
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 
21
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 
22
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 
23
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 
24
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
25
 
 
26
__doc__    = """
 
27
This module provides monkey-patched Python datetime class
 
28
that fully supports different time zones and conversions
 
29
between them.
 
30
 
 
31
See the source for licensing terms.
 
32
"""
 
33
 
 
34
__author__    = 'Antti Haapala <antti@redinnovation.com>'
 
35
__date__      = '24 Jun 2008'
 
36
__version__   = '$Revision$'
 
37
__copyright__ = '2008 Red Innovation Ltd.'
 
38
__license__   = '3-clause BSD'
 
39
 
 
40
from datetime import datetime as _datetime, tzinfo as _tzinfo
 
41
from pytz import timezone as _timezone
 
42
 
 
43
import time as _time
 
44
import pytz as _pytz
 
45
import math as _math
 
46
import re as _re
 
47
from pytz import utc, UTC, HOUR, ZERO
 
48
 
 
49
_utc = _pytz.utc
 
50
_default_tz = _utc
 
51
 
 
52
 
 
53
def set_default_timezone(new_tz):
 
54
    """Sets the default time zone used by the objects
 
55
       contained in this module. new_tz may be either
 
56
       a pytz-compatible tzinfo (requires normalize 
 
57
       and localize methods), or a time zone name known
 
58
       to pytz.
 
59
    """
 
60
 
 
61
    global _default_tz
 
62
    if type(new_tz) is str or type(new_tz) is unicode:
 
63
        new_tz = _pytz.timezone(new_tz)
 
64
 
 
65
    _default_tz = new_tz
 
66
 
 
67
class FixedOffset(_tzinfo):
 
68
    """Fixed offset in minutes east from UTC. Based on 
 
69
       the python tutorial and pytz test code."""
 
70
 
 
71
    def __init__(self, offset, name):
 
72
        """Constructor. Create a new tzinfo object
 
73
        with given offset in minutes and name."""
 
74
        self.__offset = timedelta(minutes = offset)
 
75
        self.__name = name
 
76
 
 
77
    def utcoffset(self, dt):
 
78
        return self.__offset
 
79
 
 
80
    def tzname(self, dt):
 
81
        return self.__name
 
82
 
 
83
    def dst(self, dt):
 
84
        return ZERO
 
85
 
 
86
    def localize(self, dt, is_dst=False):
 
87
        """Convert naive time to local time. Copied 
 
88
        from pytz tzinfo classes"""
 
89
 
 
90
        if dt.tzinfo is not None:
 
91
            raise ValueError, 'Not naive datetime (tzinfo is already set)'
 
92
 
 
93
        return dt.replace(tzinfo=self)
 
94
 
 
95
    def normalize(self, dt, is_dst=False):
 
96
        """Correct the timezone information on the 
 
97
        given datetime. Copied from pytz tzinfo classes."""
 
98
 
 
99
        if dt.tzinfo is None:
 
100
            raise ValueError, 'Naive time - no tzinfo set'
 
101
 
 
102
        return dt.replace(tzinfo=self)
 
103
 
 
104
    def __str__(self):
 
105
        return self.__name
 
106
 
 
107
    def __repr__(self):
 
108
        return '<%s>' % self.__name
 
109
 
 
110
 
 
111
_fixed_offset_tzs = { }
 
112
 
 
113
def _get_fixed_offset_tz(offsetmins):
 
114
    """For internal use only: Returns a tzinfo with 
 
115
    the given fixed offset. This creates only one instance
 
116
    for each offset; the zones are kept in a dictionary"""
 
117
 
 
118
    if offsetmins == 0:
 
119
        return _utc
 
120
 
 
121
    if not _fixed_offset_tzs.has_key(offsetmins):
 
122
       if offsetmins < 0:
 
123
           sign = '-'
 
124
           absoff = -offsetmins
 
125
       else:
 
126
           sign = '+'
 
127
           absoff = offsetmins
 
128
 
 
129
       name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
 
130
       inst = FixedOffset(offsetmins, name)
 
131
       _fixed_offset_tzs[offsetmins] = inst
 
132
 
 
133
    return _fixed_offset_tzs[offsetmins]
 
134
 
 
135
 
 
136
_iso8601_parser = _re.compile("""
 
137
    ^
 
138
    (?P<year> [0-9]{4})(?P<ymdsep>-?)
 
139
    (?P<month>[0-9]{2})(?P=ymdsep)
 
140
    (?P<day>  [0-9]{2})
 
141
 
 
142
    (?: # time part... optional... at least hour must be specified
 
143
        (?:T|\s+)
 
144
        (?P<hour>[0-9]{2})
 
145
        (?:
 
146
            # minutes, separated with :, or none, from hours
 
147
            (?P<hmssep>[:]?)
 
148
            (?P<minute>[0-9]{2})
 
149
            (?:
 
150
                # same for seconds, separated with :, or none, from hours
 
151
                (?P=hmssep)
 
152
                (?P<second>[0-9]{2})
 
153
            )?
 
154
        )?
 
155
        
 
156
        # fractions
 
157
        (?: [,.] (?P<frac>[0-9]{1,10}))?
 
158
 
 
159
        # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
 
160
        (
 
161
            (?P<tzempty>Z) 
 
162
        | 
 
163
            (?P<tzh>[+-][0-9]{2}) 
 
164
            (?: :? # optional separator 
 
165
                (?P<tzm>[0-9]{2})
 
166
            )?
 
167
        )?
 
168
    )?
 
169
    $
 
170
""", _re.X) # """
 
171
 
 
172
def _parse_iso(timestamp):
 
173
    """Internal function for parsing a timestamp in 
 
174
    ISO 8601 format"""
 
175
 
 
176
    timestamp = timestamp.strip()
 
177
    
 
178
    m = _iso8601_parser.match(timestamp)
 
179
    if not m:
 
180
        raise ValueError("Not a proper ISO 8601 timestamp!")
 
181
 
 
182
    year  = int(m.group('year'))
 
183
    month = int(m.group('month'))
 
184
    day   = int(m.group('day'))
 
185
    
 
186
    h, min, s, us = None, None, None, 0
 
187
    frac = 0
 
188
    if m.group('tzempty') == None and m.group('tzh') == None:
 
189
        raise ValueError("Not a proper ISO 8601 timestamp: " +
 
190
                "missing timezone (Z or +hh[:mm])!")
 
191
 
 
192
    if m.group('frac'):
 
193
        frac = m.group('frac')
 
194
        power = len(frac)
 
195
        frac  = long(frac) / 10.0 ** power
 
196
 
 
197
    if m.group('hour'):
 
198
        h = int(m.group('hour'))
 
199
 
 
200
    if m.group('minute'):
 
201
        min = int(m.group('minute'))
 
202
 
 
203
    if m.group('second'):
 
204
        s = int(m.group('second'))
 
205
 
 
206
    if frac != None:
 
207
        # ok, fractions of hour?
 
208
        if min == None:
 
209
           frac, min = _math.modf(frac * 60.0)
 
210
           min = int(min)
 
211
 
 
212
        # fractions of second?
 
213
        if s == None:
 
214
           frac, s = _math.modf(frac * 60.0)
 
215
           s = int(s)
 
216
 
 
217
        # and extract microseconds...
 
218
        us = int(frac * 1000000)
 
219
 
 
220
    if m.group('tzempty') == 'Z':
 
221
        offsetmins = 0
 
222
    else:
 
223
        # timezone: hour diff with sign
 
224
        offsetmins = int(m.group('tzh')) * 60
 
225
        tzm = m.group('tzm')
 
226
      
 
227
        # add optional minutes
 
228
        if tzm != None:
 
229
            tzm = long(tzm)
 
230
            offsetmins += tzm if offsetmins > 0 else -tzm
 
231
 
 
232
    tz = _get_fixed_offset_tz(offsetmins)
 
233
    return datetime(year, month, day, h, min, s, us, tz)
 
234
 
 
235
 
 
236
class datetime(_datetime):
 
237
    """Time zone aware subclass of Python datetime.datetime"""
 
238
 
 
239
    __name__ = 'fixed_datetime.datetime'
 
240
 
 
241
    def __new__(cls, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, is_dst=False):
 
242
        """Creates a localized timestamp with the given parameters.
 
243
        If tzinfo is omitted, the default time zone will be used."""
 
244
 
 
245
        if tzinfo == None:
 
246
            tzinfo = _default_tz
 
247
 
 
248
        dt = _datetime(year, month, day, hour, minute, second, microsecond)
 
249
        dt = tzinfo.localize(dt, is_dst=is_dst)
 
250
        return _datetime.__new__(
 
251
            cls, dt.year, dt.month, dt.day, 
 
252
            dt.hour, dt.minute, dt.second, 
 
253
            dt.microsecond, dt.tzinfo)
 
254
 
 
255
    def __radd__(self, addend):
 
256
        """Autonormalized addition of datetimes and timedeltas."""
 
257
 
 
258
        added = _datetime.__radd__(self, addend)
 
259
        added = self.tzinfo.normalize(added)
 
260
        return datetime.__from_datetime_with_tz(added)
 
261
 
 
262
    def __add__(self, addend):
 
263
        """Autonormalized addition of datetimes and timedeltas."""
 
264
 
 
265
        added = _datetime.__add__(self, addend)
 
266
        added = self.tzinfo.normalize(added)
 
267
        return datetime.__from_datetime_with_tz(added)
 
268
 
 
269
    def utctimetuple(self):
 
270
        """Return UTC time tuple, compatible with time.gmtime().
 
271
 
 
272
        Notice: the original datetime documentation is misleading:
 
273
        Calling utctimetuple() on a timezone-aware datetime will 
 
274
        return the tuple in UTC, not in local time."""
 
275
        return _datetime.utctimetuple(self)
 
276
 
 
277
    def astimezone(self, tzinfo):
 
278
        """Convert to local time in new timezone tz.
 
279
 
 
280
        The result is normalized across DST boundaries."""
 
281
 
 
282
        dt = _datetime.astimezone(self, tzinfo)
 
283
        dt = tzinfo.normalize(dt)
 
284
        return datetime.__from_datetime_with_tz(dt)
 
285
 
 
286
    @staticmethod
 
287
    def __from_datetime_with_tz(dt):
 
288
        """Internal: create a datetime instance from
 
289
        a timezone-aware instance of the builtin datetime type."""
 
290
 
 
291
        if dt.tzinfo == None:
 
292
            raise ValueError("The given datetime.datetime is not timezone-aware!")
 
293
 
 
294
        return datetime(dt.year, dt.month, dt.day,
 
295
            dt.hour, dt.minute, dt.second, dt.microsecond,
 
296
            dt.tzinfo)
 
297
 
 
298
    @staticmethod
 
299
    def fromtimestamp(timestamp, tz=None):
 
300
        """Tz's local time from POSIX timestamp."""
 
301
 
 
302
        bd = _time.gmtime(long(timestamp))
 
303
 
 
304
        us = 0
 
305
        if isinstance(timestamp, float):
 
306
            us  = timestamp % 1.0
 
307
            us *= 1000000
 
308
 
 
309
        args  = list(bd[:6])
 
310
        args += [ int(us), _utc ]
 
311
 
 
312
        _tmp = datetime(*args)
 
313
 
 
314
        if tz == None:
 
315
            tz = _default_tz
 
316
 
 
317
        rv = _tmp.astimezone(tz)
 
318
        return datetime.__from_datetime_with_tz(rv)
 
319
 
 
320
    @staticmethod
 
321
    def today(tz=None):
 
322
        """New datetime with tz's local day and time
 
323
        If tz is not specified, use the default timezone"""
 
324
 
 
325
        return datetime.fromtimestamp(long(_time.time()), tz)
 
326
 
 
327
    @staticmethod
 
328
    def now(tz=None):
 
329
        """New datetime with tz's local day and time
 
330
        If tz is not specified, use the default timezone"""
 
331
 
 
332
        return datetime.fromtimestamp(_time.time(), tz)
 
333
 
 
334
    @staticmethod
 
335
    def utcnow():
 
336
        """Return a new datetime representing UTC day and time."""
 
337
 
 
338
        return datetime.now(tz=_utc)
 
339
 
 
340
    @staticmethod
 
341
    def utcfromtimestamp():
 
342
        """Return a new UTC datetime from a POSIX timestamp (like time.time())."""
 
343
 
 
344
        return datetime.utcfromtimestamp(tz=_utc)
 
345
 
 
346
    @staticmethod
 
347
    def parseisoformat(timestamp):
 
348
        """Parses the given ISO 8601 compatible timestamp string 
 
349
        and converts it to fixed_datetime.datetime. The timestamp
 
350
        must conform to following formats:
 
351
 
 
352
             - the format is DATE SEP TIME TIMEZONE without
 
353
               any intervening spaces.
 
354
 
 
355
             - the date must be in format YYYY-MM-DD
 
356
 
 
357
             - the time may be either
 
358
                 * HH:MM:SS,FFFF
 
359
                 * HH:MM,FFFF
 
360
                 * HH,FFFF
 
361
               FFFF is the fractional part. Decimal point can be
 
362
               used too.
 
363
 
 
364
             - the time zone must be either Z, -HH:MM or +HH:MM
 
365
 
 
366
             - the date and time must be separated either by
 
367
               whitespace or single T letter
 
368
 
 
369
             - the separators - and : may all be omitted, or
 
370
               must all be present.
 
371
 
 
372
             Examples (Unix Epoch):
 
373
 
 
374
                 1970-01-01T00:00:00Z 
 
375
                 1970-01-01T00Z 
 
376
                 1969-12-31 19,5-04:30
 
377
                 19700101T030000+0300
 
378
        """
 
379
 
 
380
        return _parse_iso(timestamp)
 
381
 
 
382
    def isoformat(self, sep='T', short=False):
 
383
        """Returns the date represented by this instance
 
384
        in ISO 8601 conforming format. The separator
 
385
        is used to separate the date and time, and defaults
 
386
        to 'T'. This method supports both long and short 
 
387
        formats. The long format is
 
388
 
 
389
            YYYY-MM-DD HH:MM:SS[.FFFFFF]=HH:MM
 
390
 
 
391
        and short is
 
392
     
 
393
            YYYYMMDDTHHMMSS[.FFFFFF]=HHMM
 
394
 
 
395
        The fractional part is separated with decimal point and
 
396
        is omitted if microseconds stored in this datetime are
 
397
        0.
 
398
        """
 
399
 
 
400
        if not short:
 
401
            return _datetime.isoformat(self, sep)
 
402
 
 
403
        args = [ self.year, self.month, self.day, self.hour, self.minute, self.second ]
 
404
 
 
405
        fmt  = "%04d%02d%02dT%02d%02d%02d"
 
406
        tz   = "%s%02d%02d"
 
407
 
 
408
        if self.microsecond != 0:
 
409
            fmt += ".%06d"
 
410
            args += [ self.microsecond ]
 
411
 
 
412
        offset = self.tzinfo.utcoffset(self)
 
413
        tzseconds = offset.seconds + offset.days * 24 * 60 * 60
 
414
        sign = '+' if tzseconds >= 0 else '-'
 
415
        tzseconds = abs(tzseconds)
 
416
        tzout = tz % (sign, int(tzseconds / 3600), int((tzseconds / 60) % 60))
 
417
 
 
418
        dtout = fmt % tuple(args)        
 
419
        return dtout + tzout
 
420
 
 
421
from datetime import date, timedelta, time, tzinfo, MAXYEAR, MINYEAR