root/django/trunk/contrib/dateutil/tz.py

Revision 84, 31.6 kB (checked in by steadicat, 15 months ago)

Added loads of useful contrib.

  • Property svn:keywords set to Id
Line 
1"""
2Copyright (c) 2003-2005  Gustavo Niemeyer <gustavo@niemeyer.net>
3
4This module offers extensions to the standard python 2.3+
5datetime module.
6"""
7__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
8__license__ = "PSF License"
9
10import datetime
11import struct
12import time
13import sys
14import os
15
16relativedelta = None
17parser = None
18rrule = None
19
20__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
21           "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"]
22
23try:
24    from dateutil.tzwin import tzwin, tzwinlocal
25except (ImportError, OSError):
26    tzwin, tzwinlocal = None, None
27
28ZERO = datetime.timedelta(0)
29EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
30
31class tzutc(datetime.tzinfo):
32
33    def utcoffset(self, dt):
34        return ZERO
35     
36    def dst(self, dt):
37        return ZERO
38
39    def tzname(self, dt):
40        return "UTC"
41
42    def __eq__(self, other):
43        return (isinstance(other, tzutc) or
44                (isinstance(other, tzoffset) and other._offset == ZERO))
45
46    def __ne__(self, other):
47        return not self.__eq__(other)
48
49    def __repr__(self):
50        return "%s()" % self.__class__.__name__
51
52    __reduce__ = object.__reduce__
53
54class tzoffset(datetime.tzinfo):
55
56    def __init__(self, name, offset):
57        self._name = name
58        self._offset = datetime.timedelta(seconds=offset)
59
60    def utcoffset(self, dt):
61        return self._offset
62
63    def dst(self, dt):
64        return ZERO
65
66    def tzname(self, dt):
67        return self._name
68
69    def __eq__(self, other):
70        return (isinstance(other, tzoffset) and
71                self._offset == other._offset)
72
73    def __ne__(self, other):
74        return not self.__eq__(other)
75
76    def __repr__(self):
77        return "%s(%s, %s)" % (self.__class__.__name__,
78                               `self._name`,
79                               self._offset.days*86400+self._offset.seconds)
80
81    __reduce__ = object.__reduce__
82
83class tzlocal(datetime.tzinfo):
84
85    _std_offset = datetime.timedelta(seconds=-time.timezone)
86    if time.daylight:
87        _dst_offset = datetime.timedelta(seconds=-time.altzone)
88    else:
89        _dst_offset = _std_offset
90
91    def utcoffset(self, dt):
92        if self._isdst(dt):
93            return self._dst_offset
94        else:
95            return self._std_offset
96
97    def dst(self, dt):
98        if self._isdst(dt):
99            return self._dst_offset-self._std_offset
100        else:
101            return ZERO
102
103    def tzname(self, dt):
104        return time.tzname[self._isdst(dt)]
105
106    def _isdst(self, dt):
107        # We can't use mktime here. It is unstable when deciding if
108        # the hour near to a change is DST or not.
109        #
110        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
111        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
112        # return time.localtime(timestamp).tm_isdst
113        #
114        # The code above yields the following result:
115        #
116        #>>> import tz, datetime
117        #>>> t = tz.tzlocal()
118        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
119        #'BRDT'
120        #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
121        #'BRST'
122        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
123        #'BRST'
124        #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
125        #'BRDT'
126        #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
127        #'BRDT'
128        #
129        # Here is a more stable implementation:
130        #
131        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
132                     + dt.hour * 3600
133                     + dt.minute * 60
134                     + dt.second)
135        return time.localtime(timestamp+time.timezone).tm_isdst
136
137    def __eq__(self, other):
138        if not isinstance(other, tzlocal):
139            return False
140        return (self._std_offset == other._std_offset and
141                self._dst_offset == other._dst_offset)
142        return True
143
144    def __ne__(self, other):
145        return not self.__eq__(other)
146
147    def __repr__(self):
148        return "%s()" % self.__class__.__name__
149
150    __reduce__ = object.__reduce__
151
152class _ttinfo(object):
153    __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"]
154
155    def __init__(self):
156        for attr in self.__slots__:
157            setattr(self, attr, None)
158
159    def __repr__(self):
160        l = []
161        for attr in self.__slots__:
162            value = getattr(self, attr)
163            if value is not None:
164                l.append("%s=%s" % (attr, `value`))
165        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
166
167    def __eq__(self, other):
168        if not isinstance(other, _ttinfo):
169            return False
170        return (self.offset == other.offset and
171                self.delta == other.delta and
172                self.isdst == other.isdst and
173                self.abbr == other.abbr and
174                self.isstd == other.isstd and
175                self.isgmt == other.isgmt)
176
177    def __ne__(self, other):
178        return not self.__eq__(other)
179
180    def __getstate__(self):
181        state = {}
182        for name in self.__slots__:
183            state[name] = getattr(self, name, None)
184        return state
185
186    def __setstate__(self, state):
187        for name in self.__slots__:
188            if name in state:
189                setattr(self, name, state[name])
190
191class tzfile(datetime.tzinfo):
192
193    # http://www.twinsun.com/tz/tz-link.htm
194    # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz
195   
196    def __init__(self, fileobj):
197        if isinstance(fileobj, basestring):
198            self._filename = fileobj
199            fileobj = open(fileobj)
200        elif hasattr(fileobj, "name"):
201            self._filename = fileobj.name
202        else:
203            self._filename = `fileobj`
204
205        # From tzfile(5):
206        #
207        # The time zone information files used by tzset(3)
208        # begin with the magic characters "TZif" to identify
209        # them as time zone information files, followed by
210        # sixteen bytes reserved for future use, followed by
211        # six four-byte values of type long, written in a
212        # ``standard'' byte order (the high-order  byte
213        # of the value is written first).
214
215        if fileobj.read(4) != "TZif":
216            raise ValueError, "magic not found"
217
218        fileobj.read(16)
219
220        (
221         # The number of UTC/local indicators stored in the file.
222         ttisgmtcnt,
223
224         # The number of standard/wall indicators stored in the file.
225         ttisstdcnt,
226         
227         # The number of leap seconds for which data is
228         # stored in the file.
229         leapcnt,
230
231         # The number of "transition times" for which data
232         # is stored in the file.
233         timecnt,
234
235         # The number of "local time types" for which data
236         # is stored in the file (must not be zero).
237         typecnt,
238
239         # The  number  of  characters  of "time zone
240         # abbreviation strings" stored in the file.
241         charcnt,
242
243        ) = struct.unpack(">6l", fileobj.read(24))
244
245        # The above header is followed by tzh_timecnt four-byte
246        # values  of  type long,  sorted  in ascending order.
247        # These values are written in ``standard'' byte order.
248        # Each is used as a transition time (as  returned  by
249        # time(2)) at which the rules for computing local time
250        # change.
251
252        if timecnt:
253            self._trans_list = struct.unpack(">%dl" % timecnt,
254                                             fileobj.read(timecnt*4))
255        else:
256            self._trans_list = []
257
258        # Next come tzh_timecnt one-byte values of type unsigned
259        # char; each one tells which of the different types of
260        # ``local time'' types described in the file is associated
261        # with the same-indexed transition time. These values
262        # serve as indices into an array of ttinfo structures that
263        # appears next in the file.
264       
265        if timecnt:
266            self._trans_idx = struct.unpack(">%dB" % timecnt,
267                                            fileobj.read(timecnt))
268        else:
269            self._trans_idx = []
270       
271        # Each ttinfo structure is written as a four-byte value
272        # for tt_gmtoff  of  type long,  in  a  standard  byte
273        # order, followed  by a one-byte value for tt_isdst
274        # and a one-byte  value  for  tt_abbrind.   In  each
275        # structure, tt_gmtoff  gives  the  number  of
276        # seconds to be added to UTC, tt_isdst tells whether
277        # tm_isdst should be set by  localtime(3),  and
278        # tt_abbrind serves  as an index into the array of
279        # time zone abbreviation characters that follow the
280        # ttinfo structure(s) in the file.
281
282        ttinfo = []
283
284        for i in range(typecnt):
285            ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
286
287        abbr = fileobj.read(charcnt)
288
289        # Then there are tzh_leapcnt pairs of four-byte
290        # values, written in  standard byte  order;  the
291        # first  value  of  each pair gives the time (as
292        # returned by time(2)) at which a leap second
293        # occurs;  the  second  gives the  total  number of
294        # leap seconds to be applied after the given time.
295        # The pairs of values are sorted in ascending order
296        # by time.
297
298        # Not used, for now
299        if leapcnt:
300            leap = struct.unpack(">%dl" % (leapcnt*2),
301                                 fileobj.read(leapcnt*8))
302
303        # Then there are tzh_ttisstdcnt standard/wall
304        # indicators, each stored as a one-byte value;
305        # they tell whether the transition times associated
306        # with local time types were specified as standard
307        # time or wall clock time, and are used when
308        # a time zone file is used in handling POSIX-style
309        # time zone environment variables.
310
311        if ttisstdcnt:
312            isstd = struct.unpack(">%db" % ttisstdcnt,
313                                  fileobj.read(ttisstdcnt))
314
315        # Finally, there are tzh_ttisgmtcnt UTC/local
316        # indicators, each stored as a one-byte value;
317        # they tell whether the transition times associated
318        # with local time types were specified as UTC or
319        # local time, and are used when a time zone file
320        # is used in handling POSIX-style time zone envi-
321        # ronment variables.
322
323        if ttisgmtcnt:
324            isgmt = struct.unpack(">%db" % ttisgmtcnt,
325                                  fileobj.read(ttisgmtcnt))
326
327        # ** Everything has been read **
328
329        # Build ttinfo list
330        self._ttinfo_list = []
331        for i in range(typecnt):
332            gmtoff, isdst, abbrind =  ttinfo[i]
333            # Round to full-minutes if that's not the case. Python's
334            # datetime doesn't accept sub-minute timezones. Check
335            # http://python.org/sf/1447945 for some information.
336            gmtoff = (gmtoff+30)//60*60
337            tti = _ttinfo()
338            tti.offset = gmtoff
339            tti.delta = datetime.timedelta(seconds=gmtoff)
340            tti.isdst = isdst
341            tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
342            tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
343            tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
344            self._ttinfo_list.append(tti)
345
346        # Replace ttinfo indexes for ttinfo objects.
347        trans_idx = []
348        for idx in self._trans_idx:
349            trans_idx.append(self._ttinfo_list[idx])
350        self._trans_idx = tuple(trans_idx)
351
352        # Set standard, dst, and before ttinfos. before will be
353        # used when a given time is before any transitions,
354        # and will be set to the first non-dst ttinfo, or to
355        # the first dst, if all of them are dst.
356        self._ttinfo_std = None
357        self._ttinfo_dst = None
358        self._ttinfo_before = None
359        if self._ttinfo_list:
360            if not self._trans_list:
361                self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
362            else:
363                for i in range(timecnt-1,-1,-1):
364                    tti = self._trans_idx[i]
365                    if not self._ttinfo_std and not tti.isdst:
366                        self._ttinfo_std = tti
367                    elif not self._ttinfo_dst and tti.isdst:
368                        self._ttinfo_dst = tti
369                    if self._ttinfo_std and self._ttinfo_dst:
370                        break
371                else:
372                    if self._ttinfo_dst and not self._ttinfo_std:
373                        self._ttinfo_std = self._ttinfo_dst
374
375                for tti in self._ttinfo_list:
376                    if not tti.isdst:
377                        self._ttinfo_before = tti
378                        break
379                else:
380                    self._ttinfo_before = self._ttinfo_list[0]
381
382        # Now fix transition times to become relative to wall time.
383        #
384        # I'm not sure about this. In my tests, the tz source file
385        # is setup to wall time, and in the binary file isstd and
386        # isgmt are off, so it should be in wall time. OTOH, it's
387        # always in gmt time. Let me know if you have comments
388        # about this.
389        laststdoffset = 0
390        self._trans_list = list(self._trans_list)
391        for i in range(len(self._trans_list)):
392            tti = self._trans_idx[i]
393            if not tti.isdst:
394                # This is std time.
395                self._trans_list[i] += tti.offset
396                laststdoffset = tti.offset
397            else:
398                # This is dst time. Convert to std.
399                self._trans_list[i] += laststdoffset
400        self._trans_list = tuple(self._trans_list)
401
402    def _find_ttinfo(self, dt, laststd=0):
403        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
404                     + dt.hour * 3600
405                     + dt.minute * 60
406                     + dt.second)
407        idx = 0
408        for trans in self._trans_list:
409            if timestamp < trans:
410                break
411            idx += 1
412        else:
413            return self._ttinfo_std
414        if idx == 0:
415            return self._ttinfo_before
416        if laststd:
417            while idx > 0:
418                tti = self._trans_idx[idx-1]
419                if not tti.isdst:
420                    return tti
421                idx -= 1
422            else:
423                return self._ttinfo_std
424        else:
425            return self._trans_idx[idx-1]
426
427    def utcoffset(self, dt):
428        if not self._ttinfo_std:
429            return ZERO
430        return self._find_ttinfo(dt).delta
431
432    def dst(self, dt):
433        if not self._ttinfo_dst:
434            return ZERO
435        tti = self._find_ttinfo(dt)
436        if not tti.isdst:
437            return ZERO
438
439        # The documentation says that utcoffset()-dst() must
440        # be constant for every dt.
441        return self._find_ttinfo(dt, laststd=1).delta-tti.delta
442
443        # An alternative for that would be:
444        #
445        # return self._ttinfo_dst.offset-self._ttinfo_std.offset
446        #
447        # However, this class stores historical changes in the
448        # dst offset, so I belive that this wouldn't be the right
449        # way to implement this.
450       
451    def tzname(self, dt):
452        if not self._ttinfo_std:
453            return None
454        return self._find_ttinfo(dt).abbr
455
456    def __eq__(self, other):
457        if not isinstance(other, tzfile):
458            return False
459        return (self._trans_list == other._trans_list and
460                self._trans_idx == other._trans_idx and
461                self._ttinfo_list == other._ttinfo_list)
462
463    def __ne__(self, other):
464        return not self.__eq__(other)
465
466
467    def __repr__(self):
468        return "%s(%s)" % (self.__class__.__name__, `self._filename`)
469
470    def __reduce__(self):
471        if not os.path.isfile(self._filename):
472            raise ValueError, "Unpickable %s class" % self.__class__.__name__
473        return (self.__class__, (self._filename,))
474
475class tzrange(datetime.tzinfo):
476
477    def __init__(self, stdabbr, stdoffset=None,
478                 dstabbr=None, dstoffset=None,
479                 start=None, end=None):
480        global relativedelta
481        if not relativedelta:
482            from dateutil import relativedelta
483        self._std_abbr = stdabbr
484        self._dst_abbr = dstabbr
485        if stdoffset is not None:
486            self._std_offset = datetime.timedelta(seconds=stdoffset)
487        else:
488            self._std_offset = ZERO
489        if dstoffset is not None:
490            self._dst_offset = datetime.timedelta(seconds=dstoffset)
491        elif dstabbr and stdoffset is not None:
492            self._dst_offset = self._std_offset+datetime.timedelta(hours=+1)
493        else:
494            self._dst_offset = ZERO
495        if start is None:
496            self._start_delta = relativedelta.relativedelta(
497                    hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
498        else:
499            self._start_delta = start
500        if end is None:
501            self._end_delta = relativedelta.relativedelta(
502                    hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
503        else:
504            self._end_delta = end
505
506    def utcoffset(self, dt):
507        if self._isdst(dt):
508            return self._dst_offset
509        else:
510            return self._std_offset
511
512    def dst(self, dt):
513        if self._isdst(dt):
514            return self._dst_offset-self._std_offset
515        else:
516            return ZERO
517
518    def tzname(self, dt):
519        if self._isdst(dt):
520            return self._dst_abbr
521        else:
522            return self._std_abbr
523
524    def _isdst(self, dt):
525        if not self._start_delta:
526            return False
527        year = datetime.date(dt.year,1,1)
528        start = year+self._start_delta
529        end = year+self._end_delta
530        dt = dt.replace(tzinfo=None)
531        if start < end:
532            return dt >= start and dt < end
533        else:
534            return dt >= start or dt < end
535
536    def __eq__(self, other):
537        if not isinstance(other, tzrange):
538            return False
539        return (self._std_abbr == other._std_abbr and
540                self._dst_abbr == other._dst_abbr and
541                self._std_offset == other._std_offset and
542                self._dst_offset == other._dst_offset and
543                self._start_delta == other._start_delta and
544                self._end_delta == other._end_delta)
545
546    def __ne__(self, other):
547        return not self.__eq__(other)
548
549    def __repr__(self):
550        return "%s(...)" % self.__class__.__name__
551
552    __reduce__ = object.__reduce__
553
554class tzstr(tzrange):
555   
556    def __init__(self, s):
557        global parser
558        if not parser:
559            from dateutil import parser
560        self._s = s
561
562        res = parser._parsetz(s)
563        if res is None:
564            raise ValueError, "unknown string format"
565
566        # We must initialize it first, since _delta() needs
567        # _std_offset and _dst_offset set. Use False in start/end
568        # to avoid building it two times.
569        tzrange.__init__(self, res.stdabbr, res.stdoffset,
570                         res.dstabbr, res.dstoffset,
571                         start=False, end=False)
572
573        self._start_delta = self._delta(res.start)
574        if self._start_delta:
575            self._end_delta = self._delta(res.end, isend=1)
576
577    def _delta(self, x, isend=0):
578        kwargs = {}
579        if x.month is not None:
580            kwargs["month"] = x.month
581            if x.weekday is not None:
582                kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
583                if x.week > 0:
584                    kwargs["day"] = 1
585                else:
586                    kwargs["day"] = 31
587            elif x.day:
588                kwargs["day"] = x.day
589        elif x.yday is not None:
590            kwargs["yearday"] = x.yday
591        elif x.jyday is not None:
592            kwargs["nlyearday"] = x.jyday
593        if not kwargs:
594            # Default is to start on first sunday of april, and end
595            # on last sunday of october.
596            if not isend:
597                kwargs["month"] = 4
598                kwargs["day"] = 1
599                kwargs["weekday"] = relativedelta.SU(+1)
600            else:
601                kwargs["month"] = 10
602                kwargs["day"] = 31
603                kwargs["weekday"] = relativedelta.SU(-1)
604        if x.time is not None:
605            kwargs["seconds"] = x.time
606        else:
607            # Default is 2AM.
608            kwargs["seconds"] = 7200
609        if isend:
610            # Convert to standard time, to follow the documented way
611            # of working with the extra hour. See the documentation
612            # of the tzinfo class.
613            delta = self._dst_offset-self._std_offset
614            kwargs["seconds"] -= delta.seconds+delta.days*86400
615        return relativedelta.relativedelta(**kwargs)
616
617    def __repr__(self):
618        return "%s(%s)" % (self.__class__.__name__, `self._s`)
619
620class _tzicalvtzcomp:
621    def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
622                       tzname=None, rrule=None):
623        self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
624        self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
625        self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom
626        self.isdst = isdst
627        self.tzname = tzname
628        self.rrule = rrule
629
630class _tzicalvtz(datetime.tzinfo):
631    def __init__(self, tzid, comps=[]):
632        self._tzid = tzid
633        self._comps = comps
634        self._cachedate = []
635        self._cachecomp = []
636
637    def _find_comp(self, dt):
638        if len(self._comps) == 1:
639            return self._comps[0]
640        dt = dt.replace(tzinfo=None)
641        try:
642            return self._cachecomp[self._cachedate.index(dt)]
643        except ValueError:
644            pass
645        lastcomp = None
646        lastcompdt = None
647        for comp in self._comps:
648            if not comp.isdst:
649                # Handle the extra hour in DST -> STD
650                compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True)
651            else:
652                compdt = comp.rrule.before(dt, inc=True)
653            if compdt and (not lastcompdt or lastcompdt < compdt):
654                lastcompdt = compdt
655                lastcomp = comp
656        if not lastcomp:
657            # RFC says nothing about what to do when a given
658            # time is before the first onset date. We'll look for the
659            # first standard component, or the first component, if
660            # none is found.
661            for comp in self._comps:
662                if not comp.isdst:
663                    lastcomp = comp
664                    break
665            else:
666                lastcomp = comp[0]
667        self._cachedate.insert(0, dt)
668        self._cachecomp.insert(0, lastcomp)
669        if len(self._cachedate) > 10:
670            self._cachedate.pop()
671            self._cachecomp.pop()
672        return lastcomp
673
674    def utcoffset(self, dt):
675        return self._find_comp(dt).tzoffsetto
676
677    def dst(self, dt):
678        comp = self._find_comp(dt)
679        if comp.isdst:
680            return comp.tzoffsetdiff
681        else:
682            return ZERO
683
684    def tzname(self, dt):
685        return self._find_comp(dt).tzname
686
687    def __repr__(self):
688        return "<tzicalvtz %s>" % `self._tzid`
689
690    __reduce__ = object.__reduce__
691
692class tzical:
693    def __init__(self, fileobj):
694        global rrule
695        if not rrule:
696            from dateutil import rrule
697
698        if isinstance(fileobj, basestring):
699            self._s = fileobj
700            fileobj = open(fileobj)
701        elif hasattr(fileobj, "name"):
702            self._s = fileobj.name
703        else:
704            self._s = `fileobj`
705
706        self._vtz = {}
707
708        self._parse_rfc(fileobj.read())
709
710    def keys(self):
711        return self._vtz.keys()
712
713    def get(self, tzid=None):
714        if tzid is None:
715            keys = self._vtz.keys()
716            if len(keys) == 0: