Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 68713e4

Browse files
committed
Closes issue #12006: Add ISO 8601 year, week, and day directives to strptime.
This commit adds %G, %V, and %u directives to strptime. Thanks Ashley Anderson for the implementation.
1 parent fc632e3 commit 68713e4

5 files changed

Lines changed: 159 additions & 27 deletions

File tree

Doc/library/datetime.rst

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,34 @@ format codes.
19091909
| ``%%`` | A literal ``'%'`` character. | % | |
19101910
+-----------+--------------------------------+------------------------+-------+
19111911

1912+
Several additional directives not required by the C89 standard are included for
1913+
convenience. These parameters all correspond to ISO 8601 date values. These
1914+
may not be available on all platforms when used with the :meth:`strftime`
1915+
method. The ISO 8601 year and ISO 8601 week directives are not interchangeable
1916+
with the year and week number directives above. Calling :meth:`strptime` with
1917+
incomplete or ambiguous ISO 8601 directives will raise a :exc:`ValueError`.
1918+
1919+
+-----------+--------------------------------+------------------------+-------+
1920+
| Directive | Meaning | Example | Notes |
1921+
+===========+================================+========================+=======+
1922+
| ``%G`` | ISO 8601 year with century | 0001, 0002, ..., 2013, | \(8) |
1923+
| | representing the year that | 2014, ..., 9998, 9999 | |
1924+
| | contains the greater part of | | |
1925+
| | the ISO week (``%V``). | | |
1926+
+-----------+--------------------------------+------------------------+-------+
1927+
| ``%u`` | ISO 8601 weekday as a decimal | 1, 2, ..., 7 | |
1928+
| | number where 1 is Monday. | | |
1929+
+-----------+--------------------------------+------------------------+-------+
1930+
| ``%V`` | ISO 8601 week as a decimal | 01, 02, ..., 53 | \(8) |
1931+
| | number with Monday as | | |
1932+
| | the first day of the week. | | |
1933+
| | Week 01 is the week containing | | |
1934+
| | Jan 4. | | |
1935+
+-----------+--------------------------------+------------------------+-------+
1936+
1937+
.. versionadded:: 3.6
1938+
``%G``, ``%u`` and ``%V`` were added.
1939+
19121940
Notes:
19131941

19141942
(1)
@@ -1973,7 +2001,14 @@ Notes:
19732001

19742002
(7)
19752003
When used with the :meth:`strptime` method, ``%U`` and ``%W`` are only used
1976-
in calculations when the day of the week and the year are specified.
2004+
in calculations when the day of the week and the calendar year (``%Y``)
2005+
are specified.
2006+
2007+
(8)
2008+
Similar to ``%U`` and ``%W``, ``%V`` is only used in calculations when the
2009+
day of the week and the ISO year (``%G``) are specified in a
2010+
:meth:`strptime` format string. Also note that ``%G`` and ``%Y`` are not
2011+
interchangable.
19772012

19782013
.. rubric:: Footnotes
19792014

Doc/whatsnew/3.6.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ Private and special attribute names now are omitted unless the prefix starts
110110
with underscores. A space or a colon can be added after completed keyword.
111111
(Contributed by Serhiy Storchaka in :issue:`25011` and :issue:`25209`.)
112112

113+
datetime
114+
--------
115+
116+
* :meth:`datetime.stftime <datetime.datetime.stftime>` and
117+
:meth:`date.stftime <datetime.date.stftime>` methods now support ISO 8601
118+
date directives ``%G``, ``%u`` and ``%V``.
119+
(Contributed by Ashley Anderson in :issue:`12006`.)
120+
113121

114122
Optimizations
115123
=============

Lib/_strptime.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,15 @@ def __init__(self, locale_time=None):
195195
'f': r"(?P<f>[0-9]{1,6})",
196196
'H': r"(?P<H>2[0-3]|[0-1]\d|\d)",
197197
'I': r"(?P<I>1[0-2]|0[1-9]|[1-9])",
198+
'G': r"(?P<G>\d\d\d\d)",
198199
'j': r"(?P<j>36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])",
199200
'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])",
200201
'M': r"(?P<M>[0-5]\d|\d)",
201202
'S': r"(?P<S>6[0-1]|[0-5]\d|\d)",
202203
'U': r"(?P<U>5[0-3]|[0-4]\d|\d)",
203204
'w': r"(?P<w>[0-6])",
205+
'u': r"(?P<u>[1-7])",
206+
'V': r"(?P<V>5[0-3]|0[1-9]|[1-4]\d|\d)",
204207
# W is set below by using 'U'
205208
'y': r"(?P<y>\d\d)",
206209
#XXX: Does 'Y' need to worry about having less or more than
@@ -295,6 +298,22 @@ def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon):
295298
return 1 + days_to_week + day_of_week
296299

297300

301+
def _calc_julian_from_V(iso_year, iso_week, iso_weekday):
302+
"""Calculate the Julian day based on the ISO 8601 year, week, and weekday.
303+
ISO weeks start on Mondays, with week 01 being the week containing 4 Jan.
304+
ISO week days range from 1 (Monday) to 7 (Sunday).
305+
"""
306+
correction = datetime_date(iso_year, 1, 4).isoweekday() + 3
307+
ordinal = (iso_week * 7) + iso_weekday - correction
308+
# ordinal may be negative or 0 now, which means the date is in the previous
309+
# calendar year
310+
if ordinal < 1:
311+
ordinal += datetime_date(iso_year, 1, 1).toordinal()
312+
iso_year -= 1
313+
ordinal -= datetime_date(iso_year, 1, 1).toordinal()
314+
return iso_year, ordinal
315+
316+
298317
def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
299318
"""Return a 2-tuple consisting of a time struct and an int containing
300319
the number of microseconds based on the input string and the
@@ -339,15 +358,15 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
339358
raise ValueError("unconverted data remains: %s" %
340359
data_string[found.end():])
341360

342-
year = None
361+
iso_year = year = None
343362
month = day = 1
344363
hour = minute = second = fraction = 0
345364
tz = -1
346365
tzoffset = None
347366
# Default to -1 to signify that values not known; not critical to have,
348367
# though
349-
week_of_year = -1
350-
week_of_year_start = -1
368+
iso_week = week_of_year = None
369+
week_of_year_start = None
351370
# weekday and julian defaulted to None so as to signal need to calculate
352371
# values
353372
weekday = julian = None
@@ -369,6 +388,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
369388
year += 1900
370389
elif group_key == 'Y':
371390
year = int(found_dict['Y'])
391+
elif group_key == 'G':
392+
iso_year = int(found_dict['G'])
372393
elif group_key == 'm':
373394
month = int(found_dict['m'])
374395
elif group_key == 'B':
@@ -414,6 +435,9 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
414435
weekday = 6
415436
else:
416437
weekday -= 1
438+
elif group_key == 'u':
439+
weekday = int(found_dict['u'])
440+
weekday -= 1
417441
elif group_key == 'j':
418442
julian = int(found_dict['j'])
419443
elif group_key in ('U', 'W'):
@@ -424,6 +448,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
424448
else:
425449
# W starts week on Monday.
426450
week_of_year_start = 0
451+
elif group_key == 'V':
452+
iso_week = int(found_dict['V'])
427453
elif group_key == 'z':
428454
z = found_dict['z']
429455
tzoffset = int(z[1:3]) * 60 + int(z[3:5])
@@ -444,28 +470,57 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
444470
else:
445471
tz = value
446472
break
473+
# Deal with the cases where ambiguities arize
474+
# don't assume default values for ISO week/year
475+
if year is None and iso_year is not None:
476+
if iso_week is None or weekday is None:
477+
raise ValueError("ISO year directive '%G' must be used with "
478+
"the ISO week directive '%V' and a weekday "
479+
"directive ('%A', '%a', '%w', or '%u').")
480+
if julian is not None:
481+
raise ValueError("Day of the year directive '%j' is not "
482+
"compatible with ISO year directive '%G'. "
483+
"Use '%Y' instead.")
484+
elif week_of_year is None and iso_week is not None:
485+
if weekday is None:
486+
raise ValueError("ISO week directive '%V' must be used with "
487+
"the ISO year directive '%G' and a weekday "
488+
"directive ('%A', '%a', '%w', or '%u').")
489+
else:
490+
raise ValueError("ISO week directive '%V' is incompatible with "
491+
"the year directive '%Y'. Use the ISO year '%G' "
492+
"instead.")
493+
447494
leap_year_fix = False
448495
if year is None and month == 2 and day == 29:
449496
year = 1904 # 1904 is first leap year of 20th century
450497
leap_year_fix = True
451498
elif year is None:
452499
year = 1900
500+
501+
453502
# If we know the week of the year and what day of that week, we can figure
454503
# out the Julian day of the year.
455-
if julian is None and week_of_year != -1 and weekday is not None:
456-
week_starts_Mon = True if week_of_year_start == 0 else False
457-
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
458-
week_starts_Mon)
459-
# Cannot pre-calculate datetime_date() since can change in Julian
460-
# calculation and thus could have different value for the day of the week
461-
# calculation.
504+
if julian is None and weekday is not None:
505+
if week_of_year is not None:
506+
week_starts_Mon = True if week_of_year_start == 0 else False
507+
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
508+
week_starts_Mon)
509+
elif iso_year is not None and iso_week is not None:
510+
year, julian = _calc_julian_from_V(iso_year, iso_week, weekday + 1)
511+
462512
if julian is None:
513+
# Cannot pre-calculate datetime_date() since can change in Julian
514+
# calculation and thus could have different value for the day of
515+
# the week calculation.
463516
# Need to add 1 to result since first day of the year is 1, not 0.
464517
julian = datetime_date(year, month, day).toordinal() - \
465518
datetime_date(year, 1, 1).toordinal() + 1
466-
else: # Assume that if they bothered to include Julian day it will
467-
# be accurate.
468-
datetime_result = datetime_date.fromordinal((julian - 1) + datetime_date(year, 1, 1).toordinal())
519+
else: # Assume that if they bothered to include Julian day (or if it was
520+
# calculated above with year/week/weekday) it will be accurate.
521+
datetime_result = datetime_date.fromordinal(
522+
(julian - 1) +
523+
datetime_date(year, 1, 1).toordinal())
469524
year = datetime_result.year
470525
month = datetime_result.month
471526
day = datetime_result.day

Lib/test/test_strptime.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ def test_compile(self):
152152
"'%s' using '%s'; group 'a' = '%s', group 'b' = %s'" %
153153
(found.string, found.re.pattern, found.group('a'),
154154
found.group('b')))
155-
for directive in ('a','A','b','B','c','d','H','I','j','m','M','p','S',
156-
'U','w','W','x','X','y','Y','Z','%'):
155+
for directive in ('a','A','b','B','c','d','G','H','I','j','m','M','p',
156+
'S','u','U','V','w','W','x','X','y','Y','Z','%'):
157157
compiled = self.time_re.compile("%" + directive)
158158
found = compiled.match(time.strftime("%" + directive))
159159
self.assertTrue(found, "Matching failed on '%s' using '%s' regex" %
@@ -218,6 +218,26 @@ def test_ValueError(self):
218218
else:
219219
self.fail("'%s' did not raise ValueError" % bad_format)
220220

221+
# Ambiguous or incomplete cases using ISO year/week/weekday directives
222+
# 1. ISO week (%V) is specified, but the year is specified with %Y
223+
# instead of %G
224+
with self.assertRaises(ValueError):
225+
_strptime._strptime("1999 50", "%Y %V")
226+
# 2. ISO year (%G) and ISO week (%V) are specified, but weekday is not
227+
with self.assertRaises(ValueError):
228+
_strptime._strptime("1999 51", "%G %V")
229+
# 3. ISO year (%G) and weekday are specified, but ISO week (%V) is not
230+
for w in ('A', 'a', 'w', 'u'):
231+
with self.assertRaises(ValueError):
232+
_strptime._strptime("1999 51","%G %{}".format(w))
233+
# 4. ISO year is specified alone (e.g. time.strptime('2015', '%G'))
234+
with self.assertRaises(ValueError):
235+
_strptime._strptime("2015", "%G")
236+
# 5. Julian/ordinal day (%j) is specified with %G, but not %Y
237+
with self.assertRaises(ValueError):
238+
_strptime._strptime("1999 256", "%G %j")
239+
240+
221241
def test_strptime_exception_context(self):
222242
# check that this doesn't chain exceptions needlessly (see #17572)
223243
with self.assertRaises(ValueError) as e:
@@ -289,7 +309,7 @@ def test_fraction(self):
289309

290310
def test_weekday(self):
291311
# Test weekday directives
292-
for directive in ('A', 'a', 'w'):
312+
for directive in ('A', 'a', 'w', 'u'):
293313
self.helper(directive,6)
294314

295315
def test_julian(self):
@@ -458,16 +478,20 @@ def test_week_of_year_and_day_of_week_calculation(self):
458478
# Should be able to infer date if given year, week of year (%U or %W)
459479
# and day of the week
460480
def test_helper(ymd_tuple, test_reason):
461-
for directive in ('W', 'U'):
462-
format_string = "%%Y %%%s %%w" % directive
463-
dt_date = datetime_date(*ymd_tuple)
464-
strp_input = dt_date.strftime(format_string)
465-
strp_output = _strptime._strptime_time(strp_input, format_string)
466-
self.assertTrue(strp_output[:3] == ymd_tuple,
467-
"%s(%s) test failed w/ '%s': %s != %s (%s != %s)" %
468-
(test_reason, directive, strp_input,
469-
strp_output[:3], ymd_tuple,
470-
strp_output[7], dt_date.timetuple()[7]))
481+
for year_week_format in ('%Y %W', '%Y %U', '%G %V'):
482+
for weekday_format in ('%w', '%u', '%a', '%A'):
483+
format_string = year_week_format + ' ' + weekday_format
484+
with self.subTest(test_reason,
485+
date=ymd_tuple,
486+
format=format_string):
487+
dt_date = datetime_date(*ymd_tuple)
488+
strp_input = dt_date.strftime(format_string)
489+
strp_output = _strptime._strptime_time(strp_input,
490+
format_string)
491+
msg = "%r: %s != %s" % (strp_input,
492+
strp_output[7],
493+
dt_date.timetuple()[7])
494+
self.assertEqual(strp_output[:3], ymd_tuple, msg)
471495
test_helper((1901, 1, 3), "week 0")
472496
test_helper((1901, 1, 8), "common case")
473497
test_helper((1901, 1, 13), "day on Sunday")
@@ -499,18 +523,25 @@ def check(value, format, *expected):
499523
self.assertEqual(_strptime._strptime_time(value, format)[:-1], expected)
500524
check('2015 0 0', '%Y %U %w', 2014, 12, 28, 0, 0, 0, 6, -3)
501525
check('2015 0 0', '%Y %W %w', 2015, 1, 4, 0, 0, 0, 6, 4)
526+
check('2015 1 1', '%G %V %u', 2014, 12, 29, 0, 0, 0, 0, 363)
502527
check('2015 0 1', '%Y %U %w', 2014, 12, 29, 0, 0, 0, 0, -2)
503528
check('2015 0 1', '%Y %W %w', 2014, 12, 29, 0, 0, 0, 0, -2)
529+
check('2015 1 2', '%G %V %u', 2014, 12, 30, 0, 0, 0, 1, 364)
504530
check('2015 0 2', '%Y %U %w', 2014, 12, 30, 0, 0, 0, 1, -1)
505531
check('2015 0 2', '%Y %W %w', 2014, 12, 30, 0, 0, 0, 1, -1)
532+
check('2015 1 3', '%G %V %u', 2014, 12, 31, 0, 0, 0, 2, 365)
506533
check('2015 0 3', '%Y %U %w', 2014, 12, 31, 0, 0, 0, 2, 0)
507534
check('2015 0 3', '%Y %W %w', 2014, 12, 31, 0, 0, 0, 2, 0)
535+
check('2015 1 4', '%G %V %u', 2015, 1, 1, 0, 0, 0, 3, 1)
508536
check('2015 0 4', '%Y %U %w', 2015, 1, 1, 0, 0, 0, 3, 1)
509537
check('2015 0 4', '%Y %W %w', 2015, 1, 1, 0, 0, 0, 3, 1)
538+
check('2015 1 5', '%G %V %u', 2015, 1, 2, 0, 0, 0, 4, 2)
510539
check('2015 0 5', '%Y %U %w', 2015, 1, 2, 0, 0, 0, 4, 2)
511540
check('2015 0 5', '%Y %W %w', 2015, 1, 2, 0, 0, 0, 4, 2)
541+
check('2015 1 6', '%G %V %u', 2015, 1, 3, 0, 0, 0, 5, 3)
512542
check('2015 0 6', '%Y %U %w', 2015, 1, 3, 0, 0, 0, 5, 3)
513543
check('2015 0 6', '%Y %W %w', 2015, 1, 3, 0, 0, 0, 5, 3)
544+
check('2015 1 7', '%G %V %u', 2015, 1, 4, 0, 0, 0, 6, 4)
514545

515546

516547
class CacheTests(unittest.TestCase):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ Library
383383
- Issue #23572: Fixed functools.singledispatch on classes with falsy
384384
metaclasses. Patch by Ethan Furman.
385385

386+
- Issue #12006: Add ISO 8601 year, week, and day directives (%G, %V, %u) to
387+
strptime.
388+
386389
Documentation
387390
-------------
388391

0 commit comments

Comments
 (0)