~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/python-dateutil-0.4/dateutil/tz.py

  • Committer: Robert Collins
  • Date: 2005-09-13 15:11:39 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913151139-9ac920fc9d7bda31
TODOification

Show diffs side-by-side

added added

removed removed

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