commit 86a8fb2267380313ab4d3d7ebf2f0a7730c47eff Author: deronnax Date: Thu Feb 18 14:11:21 2016 +1030 wip1 diff --git a/Lib/datetime.py b/Lib/datetime.py index 3206923..14edea6 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -6,6 +6,8 @@ time zone and DST data sources. import time as _time import math as _math +import re + def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -639,6 +641,8 @@ timedelta.max = timedelta(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999) timedelta.resolution = timedelta(microseconds=1) +_DATE_RE = re.compile(r'(?P\d{4})-(?P\d{2})-(?P\d{2})\$', re.ASCII) + class date: """Concrete date type. @@ -715,6 +719,19 @@ class date: y, m, d = _ord2ymd(n) return cls(y, m, d) + @classmethod + def fromisoformat(cls, date_string): + """Constructs a date from an RFC 3339 string, a strict subset of ISO 8601 + + Raises ValueError in case of ill-formatted or invalid string. + """ + m = _DATE_RE.match(date_string) + if not m: + raise ValueError('invalid RFC 3339 date string: %r' % date_string) + kw = m.groupdict() + kw = {k: int(v) for k, v in kw.items()} + return cls(**kw) + # Conversions to string def __repr__(self): @@ -1003,6 +1020,10 @@ class tzinfo: _tzinfo_class = tzinfo +_TIME_RE = re.compile(r'(?P\d{2}):(?P\d{2}):(?P\d{2})' + r'(?P\.\d*)?(?PZ|([+-]\d{2}:\d{2}))?\$', + re.ASCII|re.IGNORECASE) + class time: """Time with time zone. @@ -1055,6 +1076,40 @@ class time: self._hashcode = -1 return self + @staticmethod + def _parse_isotime(reg, isostring, class_name): + match = reg.match(isostring) + if not match: + raise ValueError('invalid RFC 3339 %s string: %r' % (class_name, isostring)) + kw = match.groupdict() + tzinfo = kw.pop('tzinfo', None) + if tzinfo == 'Z' or tzinfo == 'z': + tzinfo = timezone.utc + elif tzinfo is not None: + offset_hours, _, offset_mins = tzinfo[1:].partition(':') + offset = timedelta(hours=int(offset_hours), minutes=int(offset_mins)) + if tzinfo[0] == '-': + offset = -offset + tzinfo = timezone(offset) + us = kw.pop('microsecond', None) + kw = {k: int(v) for k, v in kw.items() if v is not None} + if us: + us = round(float(us), 6) + kw['microsecond'] = int(us * 1e6) + kw['tzinfo'] = tzinfo + return kw + + @classmethod + def fromisoformat(cls, time_string): + """Constructs a time from an RFC 3339 string, a strict subset of ISO 8601 + + Microseconds are rounded to 6 digits. + Raises ValueError in case of ill-formatted or invalid string. + """ + kw = cls._parse_isotime(_TIME_RE, time_string, cls.__name__) + return cls(**kw) + + # Read-only field accessors @property def hour(self): @@ -1309,6 +1364,9 @@ time.min = time(0, 0, 0) time.max = time(23, 59, 59, 999999) time.resolution = timedelta(microseconds=1) +_DATETIME_RE = re.compile(_DATE_RE.pattern[:-1] + r'[T ]' + _TIME_RE.pattern, + re.ASCII|re.IGNORECASE) + class datetime(date): """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) @@ -1428,6 +1486,16 @@ class datetime(date): time.hour, time.minute, time.second, time.microsecond, time.tzinfo) + @classmethod + def fromisoformat(cls, datetime_string): + """Constructs a datetime from an RFC 3339 string, a strict subset of ISO 8601 + + Microseconds are rounded to 6 digits. + Raises ValueError if string is ill formatted or invalid + """ + kw = time._parse_isotime(_DATETIME_RE, datetime_string, cls.__name__) + return cls(**kw) + def timetuple(self): "Return local time tuple compatible with time.localtime()." dst = self.dst() diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 68c18bd..77f217f 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1093,6 +1093,17 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): self.assertEqual(d.month, month) self.assertEqual(d.day, day) + def test_fromisoformat(self): + self.assertEqual(self.theclass.fromisoformat('2014-12-31'), + self.theclass(2014, 12, 31)) + self.assertEqual(self.theclass.fromisoformat('4095-07-31'), + self.theclass(4095, 7, 31)) + + with self.assertRaises(ValueError): + self.theclass.fromisoformat('2014-12-011') + self.theclass.fromisoformat('20141211') + self.theclass.fromisoformat('043-12-01') + def test_insane_fromtimestamp(self): # It's possible that some platform maps time_t to double, # and that this test will fail there. This test should @@ -1863,6 +1874,18 @@ class TestDateTime(TestDate): got = self.theclass.utcfromtimestamp(ts) self.verify_field_equality(expected, got) + def test_fromisoformat(self): + self.assertEqual(self.theclass.fromisoformat('2015-12-31T14:27:00'), + self.theclass(2015, 12, 31, 14, 27, 0)) + self.assertEqual(self.theclass.fromisoformat('2015-12-31 14:27:00'), + self.theclass(2015, 12, 31, 14, 27, 0)) + # lowercase 'T' date-time separator. Uncommon but tolerated (rfc 3339) + self.assertEqual(self.theclass.fromisoformat('2015-12-31t14:27:00'), + self.theclass(2015, 12, 31, 14, 27, 0)) + + with self.assertRaises(ValueError): + self.theclass.fromisoformat('2015-01-07X00:00:00') + # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0). @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') @@ -2322,6 +2345,38 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): self.assertEqual(t.isoformat(), "00:00:00.100000") self.assertEqual(t.isoformat(), str(t)) + def test_fromisoformat(self): + # basic + self.assertEqual(self.theclass.fromisoformat('04:05:01.000123'), + self.theclass(4, 5, 1, 123)) + self.assertEqual(self.theclass.fromisoformat('00:00:00'), + self.theclass(0, 0, 0)) + # usec, rounding high + self.assertEqual(self.theclass.fromisoformat('10:20:30.40000059'), + self.theclass(10, 20, 30, 400001)) + # usec, rounding low + long digits we don't care about + self.assertEqual(self.theclass.fromisoformat('10:20:30.400003434'), + self.theclass(10, 20, 30, 400003)) + with self.assertRaises(ValueError): + self.theclass.fromisoformat('12:00AM') + self.theclass.fromisoformat('120000') + self.theclass.fromisoformat('1:00') + + def tz(h, m): + return timezone(timedelta(hours=h, minutes=m)) + + self.assertEqual(self.theclass.fromisoformat('00:00:00Z'), + self.theclass(0, 0, 0, tzinfo=timezone.utc)) + # lowercase UTC timezone. Uncommon but tolerated (rfc 3339) + self.assertEqual(self.theclass.fromisoformat('00:00:00z'), + self.theclass(0, 0, 0, tzinfo=timezone.utc)) + self.assertEqual(self.theclass.fromisoformat('00:00:00-00:00'), + self.theclass(0, 0, 0, tzinfo=tz(0, 0))) + self.assertEqual(self.theclass.fromisoformat('08:30:00.004255+02:30'), + self.theclass(8, 30, 0, 4255, tz(2, 30))) + self.assertEqual(self.theclass.fromisoformat('08:30:00.004255-02:30'), + self.theclass(8, 30, 0, 4255, tz(-2, -30))) + def test_1653736(self): # verify it doesn't accept extra keyword arguments t = self.theclass(second=1)