1+ from datetime import datetime
2+ from datetime import timedelta
3+ from datetime import timezone
4+
5+
6+ def fromisoformat (date_string ):
7+ """Construct a datetime from the output of datetime.isoformat()."""
8+ if not isinstance (date_string , str ):
9+ raise TypeError ('fromisoformat: argument must be str' )
10+
11+ # Split this at the separator
12+ dstr = date_string [0 :10 ]
13+ tstr = date_string [11 :]
14+
15+ try :
16+ date_components = _parse_isoformat_date (dstr )
17+ except ValueError :
18+ raise ValueError (f'Invalid isoformat string: { date_string !r} ' )
19+
20+ if tstr :
21+ try :
22+ time_components = _parse_isoformat_time (tstr )
23+ except ValueError :
24+ raise ValueError (f'Invalid isoformat string: { date_string !r} ' )
25+ else :
26+ time_components = [0 , 0 , 0 , 0 , None ]
27+
28+ return datetime (* (date_components + time_components ))
29+
30+
31+ def _parse_isoformat_date (dtstr ):
32+ # It is assumed that this function will only be called with a
33+ # string of length exactly 10, and (though this is not used) ASCII-only
34+ year = int (dtstr [0 :4 ])
35+ if dtstr [4 ] != '-' :
36+ raise ValueError ('Invalid date separator: %s' % dtstr [4 ])
37+
38+ month = int (dtstr [5 :7 ])
39+
40+ if dtstr [7 ] != '-' :
41+ raise ValueError ('Invalid date separator' )
42+
43+ day = int (dtstr [8 :10 ])
44+
45+ return [year , month , day ]
46+
47+
48+ def _parse_hh_mm_ss_ff (tstr ):
49+ # Parses things of the form HH[:MM[:SS[.fff[fff]]]]
50+ len_str = len (tstr )
51+
52+ time_comps = [0 , 0 , 0 , 0 ]
53+ pos = 0
54+ for comp in range (0 , 3 ):
55+ if (len_str - pos ) < 2 :
56+ raise ValueError ('Incomplete time component' )
57+
58+ time_comps [comp ] = int (tstr [pos :pos + 2 ])
59+
60+ pos += 2
61+ next_char = tstr [pos :pos + 1 ]
62+
63+ if not next_char or comp >= 2 :
64+ break
65+
66+ if next_char != ':' :
67+ raise ValueError ('Invalid time separator: %c' % next_char )
68+
69+ pos += 1
70+
71+ if pos < len_str :
72+ if tstr [pos ] != '.' :
73+ raise ValueError ('Invalid microsecond component' )
74+ else :
75+ pos += 1
76+
77+ len_remainder = len_str - pos
78+ if len_remainder not in (3 , 6 ):
79+ raise ValueError ('Invalid microsecond component' )
80+
81+ time_comps [3 ] = int (tstr [pos :])
82+ if len_remainder == 3 :
83+ time_comps [3 ] *= 1000
84+
85+ return time_comps
86+
87+
88+ def _parse_isoformat_time (tstr ):
89+ # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
90+ len_str = len (tstr )
91+ if len_str < 2 :
92+ raise ValueError ('Isoformat time too short' )
93+
94+ # This is equivalent to re.search('[+-]', tstr), but faster
95+ tz_pos = (tstr .find ('-' ) + 1 or tstr .find ('+' ) + 1 )
96+ timestr = tstr [:tz_pos - 1 ] if tz_pos > 0 else tstr
97+
98+ time_comps = _parse_hh_mm_ss_ff (timestr )
99+
100+ tzi = None
101+ if tz_pos > 0 :
102+ tzstr = tstr [tz_pos :]
103+
104+ # Valid time zone strings are:
105+ # HH:MM len: 5
106+ # HH:MM:SS len: 8
107+ # HH:MM:SS.ffffff len: 15
108+
109+ if len (tzstr ) not in (5 , 8 , 15 ):
110+ raise ValueError ('Malformed time zone string' )
111+
112+ tz_comps = _parse_hh_mm_ss_ff (tzstr )
113+ if all (x == 0 for x in tz_comps ):
114+ tzi = timezone .utc
115+ else :
116+ tzsign = - 1 if tstr [tz_pos - 1 ] == '-' else 1
117+
118+ td = timedelta (hours = tz_comps [0 ], minutes = tz_comps [1 ],
119+ seconds = tz_comps [2 ], microseconds = tz_comps [3 ])
120+
121+ tzi = timezone (tzsign * td )
122+
123+ time_comps .append (tzi )
124+
125+ return time_comps
0 commit comments