1from datetime import tzinfo, timedelta, datetime
2
3ZERO = timedelta(0)
4HOUR = timedelta(hours=1)
5SECOND = 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.)
11import time as _time
12
13STDOFFSET = timedelta(seconds = -_time.timezone)
14if _time.daylight:
15    DSTOFFSET = timedelta(seconds = -_time.altzone)
16else:
17    DSTOFFSET = STDOFFSET
18
19DSTDIFF = DSTOFFSET - STDOFFSET
20
21class LocalTimezone(tzinfo):
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
56Local = LocalTimezone()
57
58
59# A complete implementation of current DST rules for major US time zones.
60
61def 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# http://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.
78DSTSTART_2007 = datetime(1, 3, 8, 2)
79# and ends at 2am (DST time) on the first Sunday of Nov.
80DSTEND_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.
84DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
85DSTEND_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.
90DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
91DSTEND_1967_1986 = DSTEND_1987_2006
92
93def 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
110class USTimeZone(tzinfo):
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
172Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
173Central  = USTimeZone(-6, "Central",  "CST", "CDT")
174Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
175Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")
176