1 from datetime import tzinfo, timedelta, datetime
2
3 ZERO = timedelta(0)
4 HOUR = timedelta(hours=1)
5 SECOND = timedelta(seconds=1)
6
7 # A class capturing the platform's idea of local time.
8 # (May result in wrong values on historical times in
9 # timezones where UTC offset and/or the DST rules had
10 # changed in the past.)
11 import time as _time
12
13 STDOFFSET = timedelta(seconds = -_time.timezone)
14 if _time.daylight:
15 DSTOFFSET = timedelta(seconds = -_time.altzone)
16 else:
17 DSTOFFSET = STDOFFSET
18
19 DSTDIFF = DSTOFFSET - STDOFFSET
20
21 class ESC[4;38;5;81mLocalTimezone(ESC[4;38;5;149mtzinfo):
22
23 def fromutc(self, dt):
24 assert dt.tzinfo is self
25 stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
26 args = _time.localtime(stamp)[:6]
27 dst_diff = DSTDIFF // SECOND
28 # Detect fold
29 fold = (args == _time.localtime(stamp - dst_diff))
30 return datetime(*args, microsecond=dt.microsecond,
31 tzinfo=self, fold=fold)
32
33 def utcoffset(self, dt):
34 if self._isdst(dt):
35 return DSTOFFSET
36 else:
37 return STDOFFSET
38
39 def dst(self, dt):
40 if self._isdst(dt):
41 return DSTDIFF
42 else:
43 return ZERO
44
45 def tzname(self, dt):
46 return _time.tzname[self._isdst(dt)]
47
48 def _isdst(self, dt):
49 tt = (dt.year, dt.month, dt.day,
50 dt.hour, dt.minute, dt.second,
51 dt.weekday(), 0, 0)
52 stamp = _time.mktime(tt)
53 tt = _time.localtime(stamp)
54 return tt.tm_isdst > 0
55
56 Local = LocalTimezone()
57
58
59 # A complete implementation of current DST rules for major US time zones.
60
61 def first_sunday_on_or_after(dt):
62 days_to_go = 6 - dt.weekday()
63 if days_to_go:
64 dt += timedelta(days_to_go)
65 return dt
66
67
68 # US DST Rules
69 #
70 # This is a simplified (i.e., wrong for a few cases) set of rules for US
71 # DST start and end times. For a complete and up-to-date set of DST rules
72 # and timezone definitions, visit the Olson Database (or try pytz):
73 # http://www.twinsun.com/tz/tz-link.htm
74 # https://sourceforge.net/projects/pytz/ (might not be up-to-date)
75 #
76 # In the US, since 2007, DST starts at 2am (standard time) on the second
77 # Sunday in March, which is the first Sunday on or after Mar 8.
78 DSTSTART_2007 = datetime(1, 3, 8, 2)
79 # and ends at 2am (DST time) on the first Sunday of Nov.
80 DSTEND_2007 = datetime(1, 11, 1, 2)
81 # From 1987 to 2006, DST used to start at 2am (standard time) on the first
82 # Sunday in April and to end at 2am (DST time) on the last
83 # Sunday of October, which is the first Sunday on or after Oct 25.
84 DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
85 DSTEND_1987_2006 = datetime(1, 10, 25, 2)
86 # From 1967 to 1986, DST used to start at 2am (standard time) on the last
87 # Sunday in April (the one on or after April 24) and to end at 2am (DST time)
88 # on the last Sunday of October, which is the first Sunday
89 # on or after Oct 25.
90 DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
91 DSTEND_1967_1986 = DSTEND_1987_2006
92
93 def us_dst_range(year):
94 # Find start and end times for US DST. For years before 1967, return
95 # start = end for no DST.
96 if 2006 < year:
97 dststart, dstend = DSTSTART_2007, DSTEND_2007
98 elif 1986 < year < 2007:
99 dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
100 elif 1966 < year < 1987:
101 dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
102 else:
103 return (datetime(year, 1, 1), ) * 2
104
105 start = first_sunday_on_or_after(dststart.replace(year=year))
106 end = first_sunday_on_or_after(dstend.replace(year=year))
107 return start, end
108
109
110 class ESC[4;38;5;81mUSTimeZone(ESC[4;38;5;149mtzinfo):
111
112 def __init__(self, hours, reprname, stdname, dstname):
113 self.stdoffset = timedelta(hours=hours)
114 self.reprname = reprname
115 self.stdname = stdname
116 self.dstname = dstname
117
118 def __repr__(self):
119 return self.reprname
120
121 def tzname(self, dt):
122 if self.dst(dt):
123 return self.dstname
124 else:
125 return self.stdname
126
127 def utcoffset(self, dt):
128 return self.stdoffset + self.dst(dt)
129
130 def dst(self, dt):
131 if dt is None or dt.tzinfo is None:
132 # An exception may be sensible here, in one or both cases.
133 # It depends on how you want to treat them. The default
134 # fromutc() implementation (called by the default astimezone()
135 # implementation) passes a datetime with dt.tzinfo is self.
136 return ZERO
137 assert dt.tzinfo is self
138 start, end = us_dst_range(dt.year)
139 # Can't compare naive to aware objects, so strip the timezone from
140 # dt first.
141 dt = dt.replace(tzinfo=None)
142 if start + HOUR <= dt < end - HOUR:
143 # DST is in effect.
144 return HOUR
145 if end - HOUR <= dt < end:
146 # Fold (an ambiguous hour): use dt.fold to disambiguate.
147 return ZERO if dt.fold else HOUR
148 if start <= dt < start + HOUR:
149 # Gap (a non-existent hour): reverse the fold rule.
150 return HOUR if dt.fold else ZERO
151 # DST is off.
152 return ZERO
153
154 def fromutc(self, dt):
155 assert dt.tzinfo is self
156 start, end = us_dst_range(dt.year)
157 start = start.replace(tzinfo=self)
158 end = end.replace(tzinfo=self)
159 std_time = dt + self.stdoffset
160 dst_time = std_time + HOUR
161 if end <= dst_time < end + HOUR:
162 # Repeated hour
163 return std_time.replace(fold=1)
164 if std_time < start or dst_time >= end:
165 # Standard time
166 return std_time
167 if start <= std_time < end - HOUR:
168 # Daylight saving time
169 return dst_time
170
171
172 Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
173 Central = USTimeZone(-6, "Central", "CST", "CDT")
174 Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
175 Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")