bpo-15873: Implement [date][time].fromisoformat (#4699) · python/cpython@09dc2f5 (original) (raw)
`@@ -173,6 +173,24 @@ def _format_time(hh, mm, ss, us, timespec='auto'):
`
173
173
`else:
`
174
174
`return fmt.format(hh, mm, ss, us)
`
175
175
``
``
176
`+
def _format_offset(off):
`
``
177
`+
s = ''
`
``
178
`+
if off is not None:
`
``
179
`+
if off.days < 0:
`
``
180
`+
sign = "-"
`
``
181
`+
off = -off
`
``
182
`+
else:
`
``
183
`+
sign = "+"
`
``
184
`+
hh, mm = divmod(off, timedelta(hours=1))
`
``
185
`+
mm, ss = divmod(mm, timedelta(minutes=1))
`
``
186
`+
s += "%s%02d:%02d" % (sign, hh, mm)
`
``
187
`+
if ss or ss.microseconds:
`
``
188
`+
s += ":%02d" % ss.seconds
`
``
189
+
``
190
`+
if ss.microseconds:
`
``
191
`+
s += '.%06d' % ss.microseconds
`
``
192
`+
return s
`
``
193
+
176
194
`# Correctly substitute for %z and %Z escapes in strftime formats.
`
177
195
`def _wrap_strftime(object, format, timetuple):
`
178
196
`# Don't call utcoffset() or tzname() unless actually needed.
`
`@@ -237,6 +255,102 @@ def _wrap_strftime(object, format, timetuple):
`
237
255
`newformat = "".join(newformat)
`
238
256
`return _time.strftime(newformat, timetuple)
`
239
257
``
``
258
`+
Helpers for parsing the result of isoformat()
`
``
259
`+
def _parse_isoformat_date(dtstr):
`
``
260
`+
It is assumed that this function will only be called with a
`
``
261
`+
string of length exactly 10, and (though this is not used) ASCII-only
`
``
262
`+
year = int(dtstr[0:4])
`
``
263
`+
if dtstr[4] != '-':
`
``
264
`+
raise ValueError('Invalid date separator: %s' % dtstr[4])
`
``
265
+
``
266
`+
month = int(dtstr[5:7])
`
``
267
+
``
268
`+
if dtstr[7] != '-':
`
``
269
`+
raise ValueError('Invalid date separator')
`
``
270
+
``
271
`+
day = int(dtstr[8:10])
`
``
272
+
``
273
`+
return [year, month, day]
`
``
274
+
``
275
`+
def _parse_hh_mm_ss_ff(tstr):
`
``
276
`+
Parses things of the form HH[:MM[:SS[.fff[fff]]]]
`
``
277
`+
len_str = len(tstr)
`
``
278
+
``
279
`+
time_comps = [0, 0, 0, 0]
`
``
280
`+
pos = 0
`
``
281
`+
for comp in range(0, 3):
`
``
282
`+
if (len_str - pos) < 2:
`
``
283
`+
raise ValueError('Incomplete time component')
`
``
284
+
``
285
`+
time_comps[comp] = int(tstr[pos:pos+2])
`
``
286
+
``
287
`+
pos += 2
`
``
288
`+
next_char = tstr[pos:pos+1]
`
``
289
+
``
290
`+
if not next_char or comp >= 2:
`
``
291
`+
break
`
``
292
+
``
293
`+
if next_char != ':':
`
``
294
`+
raise ValueError('Invalid time separator: %c' % next_char)
`
``
295
+
``
296
`+
pos += 1
`
``
297
+
``
298
`+
if pos < len_str:
`
``
299
`+
if tstr[pos] != '.':
`
``
300
`+
raise ValueError('Invalid microsecond component')
`
``
301
`+
else:
`
``
302
`+
pos += 1
`
``
303
+
``
304
`+
len_remainder = len_str - pos
`
``
305
`+
if len_remainder not in (3, 6):
`
``
306
`+
raise ValueError('Invalid microsecond component')
`
``
307
+
``
308
`+
time_comps[3] = int(tstr[pos:])
`
``
309
`+
if len_remainder == 3:
`
``
310
`+
time_comps[3] *= 1000
`
``
311
+
``
312
`+
return time_comps
`
``
313
+
``
314
`+
def _parse_isoformat_time(tstr):
`
``
315
`+
Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
`
``
316
`+
len_str = len(tstr)
`
``
317
`+
if len_str < 2:
`
``
318
`+
raise ValueError('Isoformat time too short')
`
``
319
+
``
320
`+
This is equivalent to re.search('[+-]', tstr), but faster
`
``
321
`+
tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1)
`
``
322
`+
timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr
`
``
323
+
``
324
`+
time_comps = _parse_hh_mm_ss_ff(timestr)
`
``
325
+
``
326
`+
tzi = None
`
``
327
`+
if tz_pos > 0:
`
``
328
`+
tzstr = tstr[tz_pos:]
`
``
329
+
``
330
`+
Valid time zone strings are:
`
``
331
`+
HH:MM len: 5
`
``
332
`+
HH:MM:SS len: 8
`
``
333
`+
HH:MM:SS.ffffff len: 15
`
``
334
+
``
335
`+
if len(tzstr) not in (5, 8, 15):
`
``
336
`+
raise ValueError('Malformed time zone string')
`
``
337
+
``
338
`+
tz_comps = _parse_hh_mm_ss_ff(tzstr)
`
``
339
`+
if all(x == 0 for x in tz_comps):
`
``
340
`+
tzi = timezone.utc
`
``
341
`+
else:
`
``
342
`+
tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
`
``
343
+
``
344
`+
td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
`
``
345
`+
seconds=tz_comps[2], microseconds=tz_comps[3])
`
``
346
+
``
347
`+
tzi = timezone(tzsign * td)
`
``
348
+
``
349
`+
time_comps.append(tzi)
`
``
350
+
``
351
`+
return time_comps
`
``
352
+
``
353
+
240
354
`# Just raise TypeError if the arg isn't None or a string.
`
241
355
`def _check_tzname(name):
`
242
356
`if name is not None and not isinstance(name, str):
`
`@@ -732,6 +846,19 @@ def fromordinal(cls, n):
`
732
846
`y, m, d = _ord2ymd(n)
`
733
847
`return cls(y, m, d)
`
734
848
``
``
849
`+
@classmethod
`
``
850
`+
def fromisoformat(cls, date_string):
`
``
851
`+
"""Construct a date from the output of date.isoformat()."""
`
``
852
`+
if not isinstance(date_string, str):
`
``
853
`+
raise TypeError('fromisoformat: argument must be str')
`
``
854
+
``
855
`+
try:
`
``
856
`+
assert len(date_string) == 10
`
``
857
`+
return cls(*_parse_isoformat_date(date_string))
`
``
858
`+
except Exception:
`
``
859
`+
raise ValueError('Invalid isoformat string: %s' % date_string)
`
``
860
+
``
861
+
735
862
`# Conversions to string
`
736
863
``
737
864
`def repr(self):
`
`@@ -1190,22 +1317,10 @@ def hash(self):
`
1190
1317
``
1191
1318
`# Conversion to string
`
1192
1319
``
1193
``
`-
def _tzstr(self, sep=":"):
`
1194
``
`-
"""Return formatted timezone offset (+xx:xx) or None."""
`
``
1320
`+
def _tzstr(self):
`
``
1321
`+
"""Return formatted timezone offset (+xx:xx) or an empty string."""
`
1195
1322
`off = self.utcoffset()
`
1196
``
`-
if off is not None:
`
1197
``
`-
if off.days < 0:
`
1198
``
`-
sign = "-"
`
1199
``
`-
off = -off
`
1200
``
`-
else:
`
1201
``
`-
sign = "+"
`
1202
``
`-
hh, mm = divmod(off, timedelta(hours=1))
`
1203
``
`-
mm, ss = divmod(mm, timedelta(minutes=1))
`
1204
``
`-
assert 0 <= hh < 24
`
1205
``
`-
off = "%s%02d%s%02d" % (sign, hh, sep, mm)
`
1206
``
`-
if ss:
`
1207
``
`-
off += ':%02d' % ss.seconds
`
1208
``
`-
return off
`
``
1323
`+
return _format_offset(off)
`
1209
1324
``
1210
1325
`def repr(self):
`
1211
1326
`"""Convert to formal string, for repr()."""
`
`@@ -1244,6 +1359,18 @@ def isoformat(self, timespec='auto'):
`
1244
1359
``
1245
1360
`str = isoformat
`
1246
1361
``
``
1362
`+
@classmethod
`
``
1363
`+
def fromisoformat(cls, time_string):
`
``
1364
`+
"""Construct a time from the output of isoformat()."""
`
``
1365
`+
if not isinstance(time_string, str):
`
``
1366
`+
raise TypeError('fromisoformat: argument must be str')
`
``
1367
+
``
1368
`+
try:
`
``
1369
`+
return cls(*_parse_isoformat_time(time_string))
`
``
1370
`+
except Exception:
`
``
1371
`+
raise ValueError('Invalid isoformat string: %s' % time_string)
`
``
1372
+
``
1373
+
1247
1374
`def strftime(self, fmt):
`
1248
1375
`"""Format using strftime(). The date part of the timestamp passed
`
1249
1376
` to underlying strftime should not be used.
`
`@@ -1497,6 +1624,31 @@ def combine(cls, date, time, tzinfo=True):
`
1497
1624
`time.hour, time.minute, time.second, time.microsecond,
`
1498
1625
`tzinfo, fold=time.fold)
`
1499
1626
``
``
1627
`+
@classmethod
`
``
1628
`+
def fromisoformat(cls, date_string):
`
``
1629
`+
"""Construct a datetime from the output of datetime.isoformat()."""
`
``
1630
`+
if not isinstance(date_string, str):
`
``
1631
`+
raise TypeError('fromisoformat: argument must be str')
`
``
1632
+
``
1633
`+
Split this at the separator
`
``
1634
`+
dstr = date_string[0:10]
`
``
1635
`+
tstr = date_string[11:]
`
``
1636
+
``
1637
`+
try:
`
``
1638
`+
date_components = _parse_isoformat_date(dstr)
`
``
1639
`+
except ValueError:
`
``
1640
`+
raise ValueError('Invalid isoformat string: %s' % date_string)
`
``
1641
+
``
1642
`+
if tstr:
`
``
1643
`+
try:
`
``
1644
`+
time_components = _parse_isoformat_time(tstr)
`
``
1645
`+
except ValueError:
`
``
1646
`+
raise ValueError('Invalid isoformat string: %s' % date_string)
`
``
1647
`+
else:
`
``
1648
`+
time_components = [0, 0, 0, 0, None]
`
``
1649
+
``
1650
`+
return cls(*(date_components + time_components))
`
``
1651
+
1500
1652
`def timetuple(self):
`
1501
1653
`"Return local time tuple compatible with time.localtime()."
`
1502
1654
`dst = self.dst()
`
`@@ -1673,18 +1825,10 @@ def isoformat(self, sep='T', timespec='auto'):
`
1673
1825
`self._microsecond, timespec))
`
1674
1826
``
1675
1827
`off = self.utcoffset()
`
1676
``
`-
if off is not None:
`
1677
``
`-
if off.days < 0:
`
1678
``
`-
sign = "-"
`
1679
``
`-
off = -off
`
1680
``
`-
else:
`
1681
``
`-
sign = "+"
`
1682
``
`-
hh, mm = divmod(off, timedelta(hours=1))
`
1683
``
`-
mm, ss = divmod(mm, timedelta(minutes=1))
`
1684
``
`-
s += "%s%02d:%02d" % (sign, hh, mm)
`
1685
``
`-
if ss:
`
1686
``
`-
assert not ss.microseconds
`
1687
``
`-
s += ":%02d" % ss.seconds
`
``
1828
`+
tz = _format_offset(off)
`
``
1829
`+
if tz:
`
``
1830
`+
s += tz
`
``
1831
+
1688
1832
`return s
`
1689
1833
``
1690
1834
`def repr(self):
`
`@@ -2275,9 +2419,10 @@ def _name_from_offset(delta):
`
2275
2419
`_check_date_fields, _check_int_field, _check_time_fields,
`
2276
2420
`_check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror,
`
2277
2421
`_date_class, _days_before_month, _days_before_year, _days_in_month,
`
2278
``
`-
_format_time, _is_leap, _isoweek1monday, _math, _ord2ymd,
`
2279
``
`-
_time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,
`
2280
``
`-
_divide_and_round)
`
``
2422
`+
_format_time, _format_offset, _is_leap, _isoweek1monday, _math,
`
``
2423
`+
_ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,
`
``
2424
`+
_divide_and_round, _parse_isoformat_date, _parse_isoformat_time,
`
``
2425
`+
_parse_hh_mm_ss_ff)
`
2281
2426
`# XXX Since import * above excludes names that start with _,
`
2282
2427
`# docstring does not get overwritten. In the future, it may be
`
2283
2428
`# appropriate to maintain a single module level docstring and
`