mirror of https://github.com/python/cpython
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.
This commit is contained in:
parent
0f261583ba
commit
32318930da
|
@ -2174,6 +2174,13 @@ Notes:
|
||||||
.. versionchanged:: 3.7
|
.. versionchanged:: 3.7
|
||||||
The UTC offset is not restricted to a whole number of minutes.
|
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``
|
``%Z``
|
||||||
If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
|
If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
|
||||||
string. Otherwise ``%Z`` is replaced by the returned value, which must
|
string. Otherwise ``%Z`` is replaced by the returned value, which must
|
||||||
|
|
|
@ -210,7 +210,7 @@ class TimeRE(dict):
|
||||||
#XXX: Does 'Y' need to worry about having less or more than
|
#XXX: Does 'Y' need to worry about having less or more than
|
||||||
# 4 digits?
|
# 4 digits?
|
||||||
'Y': r"(?P<Y>\d\d\d\d)",
|
'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.f_weekday, 'A'),
|
||||||
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
|
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
|
||||||
'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'),
|
'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
|
month = day = 1
|
||||||
hour = minute = second = fraction = 0
|
hour = minute = second = fraction = 0
|
||||||
tz = -1
|
tz = -1
|
||||||
tzoffset = None
|
gmtoff = None
|
||||||
|
gmtoff_fraction = 0
|
||||||
# Default to -1 to signify that values not known; not critical to have,
|
# Default to -1 to signify that values not known; not critical to have,
|
||||||
# though
|
# though
|
||||||
iso_week = week_of_year = None
|
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'])
|
iso_week = int(found_dict['V'])
|
||||||
elif group_key == 'z':
|
elif group_key == 'z':
|
||||||
z = found_dict['z']
|
z = found_dict['z']
|
||||||
tzoffset = int(z[1:3]) * 60 + int(z[3:5])
|
if z == 'Z':
|
||||||
if z.startswith("-"):
|
gmtoff = 0
|
||||||
tzoffset = -tzoffset
|
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':
|
elif group_key == 'Z':
|
||||||
# Since -1 is default value only need to worry about setting tz if
|
# Since -1 is default value only need to worry about setting tz if
|
||||||
# it can be something other than -1.
|
# 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()
|
weekday = datetime_date(year, month, day).weekday()
|
||||||
# Add timezone info
|
# Add timezone info
|
||||||
tzname = found_dict.get("Z")
|
tzname = found_dict.get("Z")
|
||||||
if tzoffset is not None:
|
|
||||||
gmtoff = tzoffset * 60
|
|
||||||
else:
|
|
||||||
gmtoff = None
|
|
||||||
|
|
||||||
if leap_year_fix:
|
if leap_year_fix:
|
||||||
# the caller didn't supply a year but asked for Feb 29th. We couldn't
|
# 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,
|
return (year, month, day,
|
||||||
hour, minute, second,
|
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"):
|
def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
|
||||||
"""Return a time struct based on the input string and the
|
"""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"):
|
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
|
"""Return a class cls instance based on the input string and the
|
||||||
format string."""
|
format string."""
|
||||||
tt, fraction = _strptime(data_string, format)
|
tt, fraction, gmtoff_fraction = _strptime(data_string, format)
|
||||||
tzname, gmtoff = tt[-2:]
|
tzname, gmtoff = tt[-2:]
|
||||||
args = tt[:6] + (fraction,)
|
args = tt[:6] + (fraction,)
|
||||||
if gmtoff is not None:
|
if gmtoff is not None:
|
||||||
tzdelta = datetime_timedelta(seconds=gmtoff)
|
tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
|
||||||
if tzname:
|
if tzname:
|
||||||
tz = datetime_timezone(tzdelta, tzname)
|
tz = datetime_timezone(tzdelta, tzname)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -2147,6 +2147,10 @@ class TestDateTime(TestDate):
|
||||||
strptime = self.theclass.strptime
|
strptime = self.theclass.strptime
|
||||||
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
|
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
|
||||||
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
|
# Only local timezone and UTC are supported
|
||||||
for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
|
for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
|
||||||
(-_time.timezone, _time.tzname[0])):
|
(-_time.timezone, _time.tzname[0])):
|
||||||
|
|
|
@ -305,7 +305,7 @@ class StrptimeTests(unittest.TestCase):
|
||||||
# Test microseconds
|
# Test microseconds
|
||||||
import datetime
|
import datetime
|
||||||
d = datetime.datetime(2012, 12, 20, 12, 34, 56, 78987)
|
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)
|
self.assertEqual(frac, d.microsecond)
|
||||||
|
|
||||||
def test_weekday(self):
|
def test_weekday(self):
|
||||||
|
@ -317,6 +317,51 @@ class StrptimeTests(unittest.TestCase):
|
||||||
# Test julian directives
|
# Test julian directives
|
||||||
self.helper('j', 7)
|
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):
|
def test_timezone(self):
|
||||||
# Test timezone directives.
|
# Test timezone directives.
|
||||||
# When gmtime() is used with %Z, entire result of strftime() is empty.
|
# When gmtime() is used with %Z, entire result of strftime() is empty.
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue