[Python-checkins] Closes bpo-31800: Support for colon when parsing time offsets (#4015)

Alexander Belopolsky webhook-mailer at python.org
Wed Oct 25 20:35:44 EDT 2017


https://github.com/python/cpython/commit/32318930da70ff03320ec50813b843e7db6fbc2e
commit: 32318930da70ff03320ec50813b843e7db6fbc2e
branch: master
author: Mario Corchero <mariocj89 at gmail.com>
committer: Alexander Belopolsky <abalkin at users.noreply.github.com>
date: 2017-10-25T20:35:41-04:00
summary:

Closes bpo-31800: Support for colon when parsing time offsets (#4015)

Add support to strptime to parse time offsets with a colon between the hour and the minutes.

files:
A Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst
M Doc/library/datetime.rst
M Lib/_strptime.py
M Lib/test/datetimetester.py
M Lib/test/test_strptime.py

diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index 55be8694a06..d032d283eb9 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -2174,6 +2174,13 @@ Notes:
    .. versionchanged:: 3.7
       The UTC offset is not restricted to a whole number of minutes.
 
+   .. versionchanged:: 3.7
+      When the ``%z`` directive is provided to the  :meth:`strptime` method,
+      the UTC offsets can have a colon as a separator between hours, minutes
+      and seconds.
+      For example, ``'+01:00:00'`` will be parsed as an offset of one hour.
+      In addition, providing ``'Z'`` is identical to ``'+00:00'``.
+
    ``%Z``
       If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
       string.  Otherwise ``%Z`` is replaced by the returned value, which must
diff --git a/Lib/_strptime.py b/Lib/_strptime.py
index 284175d0cfe..f5195af90c8 100644
--- a/Lib/_strptime.py
+++ b/Lib/_strptime.py
@@ -210,7 +210,7 @@ def __init__(self, locale_time=None):
             #XXX: Does 'Y' need to worry about having less or more than
             #     4 digits?
             'Y': r"(?P<Y>\d\d\d\d)",
-            'z': r"(?P<z>[+-]\d\d[0-5]\d)",
+            'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|Z)",
             'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
             'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
             'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'),
@@ -365,7 +365,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
     month = day = 1
     hour = minute = second = fraction = 0
     tz = -1
-    tzoffset = None
+    gmtoff = None
+    gmtoff_fraction = 0
     # Default to -1 to signify that values not known; not critical to have,
     # though
     iso_week = week_of_year = None
@@ -455,9 +456,24 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
             iso_week = int(found_dict['V'])
         elif group_key == 'z':
             z = found_dict['z']
-            tzoffset = int(z[1:3]) * 60 + int(z[3:5])
-            if z.startswith("-"):
-                tzoffset = -tzoffset
+            if z == 'Z':
+                gmtoff = 0
+            else:
+                if z[3] == ':':
+                    z = z[:3] + z[4:]
+                    if len(z) > 5:
+                        if z[5] != ':':
+                            msg = f"Unconsistent use of : in {found_dict['z']}"
+                            raise ValueError(msg)
+                        z = z[:5] + z[6:]
+                hours = int(z[1:3])
+                minutes = int(z[3:5])
+                seconds = int(z[5:7] or 0)
+                gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds
+                gmtoff_fraction = int(z[8:] or 0)
+                if z.startswith("-"):
+                    gmtoff = -gmtoff
+                    gmtoff_fraction = -gmtoff_fraction
         elif group_key == 'Z':
             # Since -1 is default value only need to worry about setting tz if
             # it can be something other than -1.
@@ -535,10 +551,6 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
         weekday = datetime_date(year, month, day).weekday()
     # Add timezone info
     tzname = found_dict.get("Z")
-    if tzoffset is not None:
-        gmtoff = tzoffset * 60
-    else:
-        gmtoff = None
 
     if leap_year_fix:
         # the caller didn't supply a year but asked for Feb 29th. We couldn't
@@ -548,7 +560,7 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
 
     return (year, month, day,
             hour, minute, second,
-            weekday, julian, tz, tzname, gmtoff), fraction
+            weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction
 
 def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
     """Return a time struct based on the input string and the
@@ -559,11 +571,11 @@ def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
 def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
     """Return a class cls instance based on the input string and the
     format string."""
-    tt, fraction = _strptime(data_string, format)
+    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
     tzname, gmtoff = tt[-2:]
     args = tt[:6] + (fraction,)
     if gmtoff is not None:
-        tzdelta = datetime_timedelta(seconds=gmtoff)
+        tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
         if tzname:
             tz = datetime_timezone(tzdelta, tzname)
         else:
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 4edfb42d355..c5f91fbe184 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -2147,6 +2147,10 @@ def test_strptime(self):
         strptime = self.theclass.strptime
         self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
         self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE)
+        self.assertEqual(
+            strptime("-00:02:01.000003", "%z").utcoffset(),
+            -timedelta(minutes=2, seconds=1, microseconds=3)
+        )
         # Only local timezone and UTC are supported
         for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
                                  (-_time.timezone, _time.tzname[0])):
diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py
index 93431802575..1251886779d 100644
--- a/Lib/test/test_strptime.py
+++ b/Lib/test/test_strptime.py
@@ -305,7 +305,7 @@ def test_fraction(self):
         # Test microseconds
         import datetime
         d = datetime.datetime(2012, 12, 20, 12, 34, 56, 78987)
-        tup, frac = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f")
+        tup, frac, _ = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f")
         self.assertEqual(frac, d.microsecond)
 
     def test_weekday(self):
@@ -317,6 +317,51 @@ def test_julian(self):
         # Test julian directives
         self.helper('j', 7)
 
+    def test_offset(self):
+        one_hour = 60 * 60
+        half_hour = 30 * 60
+        half_minute = 30
+        (*_, offset), _, offset_fraction = _strptime._strptime("+0130", "%z")
+        self.assertEqual(offset, one_hour + half_hour)
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-0100", "%z")
+        self.assertEqual(offset, -one_hour)
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-013030", "%z")
+        self.assertEqual(offset, -(one_hour + half_hour + half_minute))
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z")
+        self.assertEqual(offset, -(one_hour + half_hour + half_minute))
+        self.assertEqual(offset_fraction, -1)
+        (*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z")
+        self.assertEqual(offset, one_hour)
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z")
+        self.assertEqual(offset, -(one_hour + half_hour))
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z")
+        self.assertEqual(offset, -(one_hour + half_hour + half_minute))
+        self.assertEqual(offset_fraction, 0)
+        (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z")
+        self.assertEqual(offset, -(one_hour + half_hour + half_minute))
+        self.assertEqual(offset_fraction, -1)
+        (*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z")
+        self.assertEqual(offset, 0)
+        self.assertEqual(offset_fraction, 0)
+
+    def test_bad_offset(self):
+        with self.assertRaises(ValueError):
+            _strptime._strptime("-01:30:30.", "%z")
+        with self.assertRaises(ValueError):
+            _strptime._strptime("-0130:30", "%z")
+        with self.assertRaises(ValueError):
+            _strptime._strptime("-01:30:30.1234567", "%z")
+        with self.assertRaises(ValueError):
+            _strptime._strptime("-01:30:30:123456", "%z")
+        with self.assertRaises(ValueError) as err:
+            _strptime._strptime("-01:3030", "%z")
+        self.assertEqual("Unconsistent use of : in -01:3030", str(err.exception))
+
     def test_timezone(self):
         # Test timezone directives.
         # When gmtime() is used with %Z, entire result of strftime() is empty.
diff --git a/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst b/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst
new file mode 100644
index 00000000000..1580440a595
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-10-17-20-08-19.bpo-31800.foOSCi.rst
@@ -0,0 +1,3 @@
+Extended support for parsing UTC offsets. strptime '%z' can now
+parse the output generated by datetime.isoformat, including seconds and
+microseconds.



More information about the Python-checkins mailing list