Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Oct 26, 2017
6 changes: 6 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2174,6 +2174,12 @@ Notes:
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.

.. versionchanged:: 3.7
When ``%z`` directive is provided to the :meth:`strptime` method, any valid
RFC-822/ISO 8601 standard utftime-offset can be parsed. For example, strings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utftime-offset? This should be UTC offset, right?

like ``'+01:00'`` will be parsed as an offset of one hour and or ``'Z'`` will
be transofmred to a 0 UTC offset.

``%Z``
If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
string. Otherwise ``%Z`` is replaced by the returned value, which must
Expand Down
36 changes: 24 additions & 12 deletions Lib/_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])):
Expand Down
47 changes: 46 additions & 1 deletion Lib/test/test_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for parsing RFC-822/ISO 8601 utf offsets