1- from datetime import tzinfo , timedelta , datetime
1+ from datetime import tzinfo , timedelta , datetime , timezone
22
33ZERO = timedelta (0 )
44HOUR = timedelta (hours = 1 )
5-
6- # A UTC class.
7-
8- class UTC (tzinfo ):
9- """UTC"""
10-
11- def utcoffset (self , dt ):
12- return ZERO
13-
14- def tzname (self , dt ):
15- return "UTC"
16-
17- def dst (self , dt ):
18- return ZERO
19-
20- utc = UTC ()
21-
22- # A class building tzinfo objects for fixed-offset time zones.
23- # Note that FixedOffset(0, "UTC") is a different way to build a
24- # UTC tzinfo object.
25-
26- class FixedOffset (tzinfo ):
27- """Fixed offset in minutes east from UTC."""
28-
29- def __init__ (self , offset , name ):
30- self .__offset = timedelta (minutes = offset )
31- self .__name = name
32-
33- def utcoffset (self , dt ):
34- return self .__offset
35-
36- def tzname (self , dt ):
37- return self .__name
38-
39- def dst (self , dt ):
40- return ZERO
5+ SECOND = timedelta (seconds = 1 )
416
427# A class capturing the platform's idea of local time.
43-
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.)
4411import time as _time
4512
4613STDOFFSET = timedelta (seconds = - _time .timezone )
@@ -53,6 +20,16 @@ def dst(self, dt):
5320
5421class LocalTimezone (tzinfo ):
5522
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+
5633 def utcoffset (self , dt ):
5734 if self ._isdst (dt ):
5835 return DSTOFFSET
@@ -99,20 +76,37 @@ def first_sunday_on_or_after(dt):
9976# In the US, since 2007, DST starts at 2am (standard time) on the second
10077# Sunday in March, which is the first Sunday on or after Mar 8.
10178DSTSTART_2007 = datetime (1 , 3 , 8 , 2 )
102- # and ends at 2am (DST time; 1am standard time ) on the first Sunday of Nov.
103- DSTEND_2007 = datetime (1 , 11 , 1 , 1 )
79+ # and ends at 2am (DST time) on the first Sunday of Nov.
80+ DSTEND_2007 = datetime (1 , 11 , 1 , 2 )
10481# From 1987 to 2006, DST used to start at 2am (standard time) on the first
105- # Sunday in April and to end at 2am (DST time; 1am standard time ) on the last
82+ # Sunday in April and to end at 2am (DST time) on the last
10683# Sunday of October, which is the first Sunday on or after Oct 25.
10784DSTSTART_1987_2006 = datetime (1 , 4 , 1 , 2 )
108- DSTEND_1987_2006 = datetime (1 , 10 , 25 , 1 )
85+ DSTEND_1987_2006 = datetime (1 , 10 , 25 , 2 )
10986# From 1967 to 1986, DST used to start at 2am (standard time) on the last
110- # Sunday in April (the one on or after April 24) and to end at 2am (DST time;
111- # 1am standard time) on the last Sunday of October, which is the first Sunday
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
11289# on or after Oct 25.
11390DSTSTART_1967_1986 = datetime (1 , 4 , 24 , 2 )
11491DSTEND_1967_1986 = DSTEND_1987_2006
11592
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+
116110class USTimeZone (tzinfo ):
117111
118112 def __init__ (self , hours , reprname , stdname , dstname ):
@@ -141,27 +135,39 @@ def dst(self, dt):
141135 # implementation) passes a datetime with dt.tzinfo is self.
142136 return ZERO
143137 assert dt .tzinfo is self
144-
145- # Find start and end times for US DST. For years before 1967, return
146- # ZERO for no DST.
147- if 2006 < dt .year :
148- dststart , dstend = DSTSTART_2007 , DSTEND_2007
149- elif 1986 < dt .year < 2007 :
150- dststart , dstend = DSTSTART_1987_2006 , DSTEND_1987_2006
151- elif 1966 < dt .year < 1987 :
152- dststart , dstend = DSTSTART_1967_1986 , DSTEND_1967_1986
153- else :
154- return ZERO
155-
156- start = first_sunday_on_or_after (dststart .replace (year = dt .year ))
157- end = first_sunday_on_or_after (dstend .replace (year = dt .year ))
158-
138+ start , end = us_dst_range (dt .year )
159139 # Can't compare naive to aware objects, so strip the timezone from
160140 # dt first.
161- if start <= dt .replace (tzinfo = None ) < end :
141+ dt = dt .replace (tzinfo = None )
142+ if start + HOUR <= dt < end - HOUR :
143+ # DST is in effect.
162144 return HOUR
163- else :
164- return ZERO
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+
165171
166172Eastern = USTimeZone (- 5 , "Eastern" , "EST" , "EDT" )
167173Central = USTimeZone (- 6 , "Central" , "CST" , "CDT" )
0 commit comments