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