Skip to content

Commit 3231893

Browse files
mariocj89abalkin
authored andcommitted
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.
1 parent 0f26158 commit 3231893

File tree

5 files changed

+84
-13
lines changed

5 files changed

+84
-13
lines changed

Doc/library/datetime.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2174,6 +2174,13 @@ Notes:
21742174
.. versionchanged:: 3.7
21752175
The UTC offset is not restricted to a whole number of minutes.
21762176

2177+
.. versionchanged:: 3.7
2178+
When the ``%z`` directive is provided to the :meth:`strptime` method,
2179+
the UTC offsets can have a colon as a separator between hours, minutes
2180+
and seconds.
2181+
For example, ``'+01:00:00'`` will be parsed as an offset of one hour.
2182+
In addition, providing ``'Z'`` is identical to ``'+00:00'``.
2183+
21772184
``%Z``
21782185
If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
21792186
string. Otherwise ``%Z`` is replaced by the returned value, which must

Lib/_strptime.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def __init__(self, locale_time=None):
210210
#XXX: Does 'Y' need to worry about having less or more than
211211
# 4 digits?
212212
'Y': r"(?P<Y>\d\d\d\d)",
213-
'z': r"(?P<z>[+-]\d\d[0-5]\d)",
213+
'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|Z)",
214214
'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
215215
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
216216
'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"):
365365
month = day = 1
366366
hour = minute = second = fraction = 0
367367
tz = -1
368-
tzoffset = None
368+
gmtoff = None
369+
gmtoff_fraction = 0
369370
# Default to -1 to signify that values not known; not critical to have,
370371
# though
371372
iso_week = week_of_year = None
@@ -455,9 +456,24 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
455456
iso_week = int(found_dict['V'])
456457
elif group_key == 'z':
457458
z = found_dict['z']
458-
tzoffset = int(z[1:3]) * 60 + int(z[3:5])
459-
if z.startswith("-"):
460-
tzoffset = -tzoffset
459+
if z == 'Z':
460+
gmtoff = 0
461+
else:
462+
if z[3] == ':':
463+
z = z[:3] + z[4:]
464+
if len(z) > 5:
465+
if z[5] != ':':
466+
msg = f"Unconsistent use of : in {found_dict['z']}"
467+
raise ValueError(msg)
468+
z = z[:5] + z[6:]
469+
hours = int(z[1:3])
470+
minutes = int(z[3:5])
471+
seconds = int(z[5:7] or 0)
472+
gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds
473+
gmtoff_fraction = int(z[8:] or 0)
474+
if z.startswith("-"):
475+
gmtoff = -gmtoff
476+
gmtoff_fraction = -gmtoff_fraction
461477
elif group_key == 'Z':
462478
# Since -1 is default value only need to worry about setting tz if
463479
# it can be something other than -1.
@@ -535,10 +551,6 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
535551
weekday = datetime_date(year, month, day).weekday()
536552
# Add timezone info
537553
tzname = found_dict.get("Z")
538-
if tzoffset is not None:
539-
gmtoff = tzoffset * 60
540-
else:
541-
gmtoff = None
542554

543555
if leap_year_fix:
544556
# 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"):
548560

549561
return (year, month, day,
550562
hour, minute, second,
551-
weekday, julian, tz, tzname, gmtoff), fraction
563+
weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction
552564

553565
def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
554566
"""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"):
559571
def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
560572
"""Return a class cls instance based on the input string and the
561573
format string."""
562-
tt, fraction = _strptime(data_string, format)
574+
tt, fraction, gmtoff_fraction = _strptime(data_string, format)
563575
tzname, gmtoff = tt[-2:]
564576
args = tt[:6] + (fraction,)
565577
if gmtoff is not None:
566-
tzdelta = datetime_timedelta(seconds=gmtoff)
578+
tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
567579
if tzname:
568580
tz = datetime_timezone(tzdelta, tzname)
569581
else:

Lib/test/datetimetester.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2147,6 +2147,10 @@ def test_strptime(self):
21472147
strptime = self.theclass.strptime
21482148
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
21492149
self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE)
2150+
self.assertEqual(
2151+
strptime("-00:02:01.000003", "%z").utcoffset(),
2152+
-timedelta(minutes=2, seconds=1, microseconds=3)
2153+
)
21502154
# Only local timezone and UTC are supported
21512155
for tzseconds, tzname in ((0, 'UTC'), (0, 'GMT'),
21522156
(-_time.timezone, _time.tzname[0])):

Lib/test/test_strptime.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ def test_fraction(self):
305305
# Test microseconds
306306
import datetime
307307
d = datetime.datetime(2012, 12, 20, 12, 34, 56, 78987)
308-
tup, frac = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f")
308+
tup, frac, _ = _strptime._strptime(str(d), format="%Y-%m-%d %H:%M:%S.%f")
309309
self.assertEqual(frac, d.microsecond)
310310

311311
def test_weekday(self):
@@ -317,6 +317,51 @@ def test_julian(self):
317317
# Test julian directives
318318
self.helper('j', 7)
319319

320+
def test_offset(self):
321+
one_hour = 60 * 60
322+
half_hour = 30 * 60
323+
half_minute = 30
324+
(*_, offset), _, offset_fraction = _strptime._strptime("+0130", "%z")
325+
self.assertEqual(offset, one_hour + half_hour)
326+
self.assertEqual(offset_fraction, 0)
327+
(*_, offset), _, offset_fraction = _strptime._strptime("-0100", "%z")
328+
self.assertEqual(offset, -one_hour)
329+
self.assertEqual(offset_fraction, 0)
330+
(*_, offset), _, offset_fraction = _strptime._strptime("-013030", "%z")
331+
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
332+
self.assertEqual(offset_fraction, 0)
333+
(*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z")
334+
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
335+
self.assertEqual(offset_fraction, -1)
336+
(*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z")
337+
self.assertEqual(offset, one_hour)
338+
self.assertEqual(offset_fraction, 0)
339+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z")
340+
self.assertEqual(offset, -(one_hour + half_hour))
341+
self.assertEqual(offset_fraction, 0)
342+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z")
343+
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
344+
self.assertEqual(offset_fraction, 0)
345+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z")
346+
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
347+
self.assertEqual(offset_fraction, -1)
348+
(*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z")
349+
self.assertEqual(offset, 0)
350+
self.assertEqual(offset_fraction, 0)
351+
352+
def test_bad_offset(self):
353+
with self.assertRaises(ValueError):
354+
_strptime._strptime("-01:30:30.", "%z")
355+
with self.assertRaises(ValueError):
356+
_strptime._strptime("-0130:30", "%z")
357+
with self.assertRaises(ValueError):
358+
_strptime._strptime("-01:30:30.1234567", "%z")
359+
with self.assertRaises(ValueError):
360+
_strptime._strptime("-01:30:30:123456", "%z")
361+
with self.assertRaises(ValueError) as err:
362+
_strptime._strptime("-01:3030", "%z")
363+
self.assertEqual("Unconsistent use of : in -01:3030", str(err.exception))
364+
320365
def test_timezone(self):
321366
# Test timezone directives.
322367
# When gmtime() is used with %Z, entire result of strftime() is empty.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Extended support for parsing UTC offsets. strptime '%z' can now
2+
parse the output generated by datetime.isoformat, including seconds and
3+
microseconds.

0 commit comments

Comments
 (0)