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.
This commit is contained in:
parent
fc632e3912
commit
68713e41a5
|
@ -1909,6 +1909,34 @@ format codes.
|
|||
| ``%%`` | A literal ``'%'`` character. | % | |
|
||||
+-----------+--------------------------------+------------------------+-------+
|
||||
|
||||
Several additional directives not required by the C89 standard are included for
|
||||
convenience. These parameters all correspond to ISO 8601 date values. These
|
||||
may not be available on all platforms when used with the :meth:`strftime`
|
||||
method. The ISO 8601 year and ISO 8601 week directives are not interchangeable
|
||||
with the year and week number directives above. Calling :meth:`strptime` with
|
||||
incomplete or ambiguous ISO 8601 directives will raise a :exc:`ValueError`.
|
||||
|
||||
+-----------+--------------------------------+------------------------+-------+
|
||||
| Directive | Meaning | Example | Notes |
|
||||
+===========+================================+========================+=======+
|
||||
| ``%G`` | ISO 8601 year with century | 0001, 0002, ..., 2013, | \(8) |
|
||||
| | representing the year that | 2014, ..., 9998, 9999 | |
|
||||
| | contains the greater part of | | |
|
||||
| | the ISO week (``%V``). | | |
|
||||
+-----------+--------------------------------+------------------------+-------+
|
||||
| ``%u`` | ISO 8601 weekday as a decimal | 1, 2, ..., 7 | |
|
||||
| | number where 1 is Monday. | | |
|
||||
+-----------+--------------------------------+------------------------+-------+
|
||||
| ``%V`` | ISO 8601 week as a decimal | 01, 02, ..., 53 | \(8) |
|
||||
| | number with Monday as | | |
|
||||
| | the first day of the week. | | |
|
||||
| | Week 01 is the week containing | | |
|
||||
| | Jan 4. | | |
|
||||
+-----------+--------------------------------+------------------------+-------+
|
||||
|
||||
.. versionadded:: 3.6
|
||||
``%G``, ``%u`` and ``%V`` were added.
|
||||
|
||||
Notes:
|
||||
|
||||
(1)
|
||||
|
@ -1973,7 +2001,14 @@ Notes:
|
|||
|
||||
(7)
|
||||
When used with the :meth:`strptime` method, ``%U`` and ``%W`` are only used
|
||||
in calculations when the day of the week and the year are specified.
|
||||
in calculations when the day of the week and the calendar year (``%Y``)
|
||||
are specified.
|
||||
|
||||
(8)
|
||||
Similar to ``%U`` and ``%W``, ``%V`` is only used in calculations when the
|
||||
day of the week and the ISO year (``%G``) are specified in a
|
||||
:meth:`strptime` format string. Also note that ``%G`` and ``%Y`` are not
|
||||
interchangable.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
|
|
|
@ -110,6 +110,14 @@ Private and special attribute names now are omitted unless the prefix starts
|
|||
with underscores. A space or a colon can be added after completed keyword.
|
||||
(Contributed by Serhiy Storchaka in :issue:`25011` and :issue:`25209`.)
|
||||
|
||||
datetime
|
||||
--------
|
||||
|
||||
* :meth:`datetime.stftime <datetime.datetime.stftime>` and
|
||||
:meth:`date.stftime <datetime.date.stftime>` methods now support ISO 8601
|
||||
date directives ``%G``, ``%u`` and ``%V``.
|
||||
(Contributed by Ashley Anderson in :issue:`12006`.)
|
||||
|
||||
|
||||
Optimizations
|
||||
=============
|
||||
|
|
|
@ -195,12 +195,15 @@ class TimeRE(dict):
|
|||
'f': r"(?P<f>[0-9]{1,6})",
|
||||
'H': r"(?P<H>2[0-3]|[0-1]\d|\d)",
|
||||
'I': r"(?P<I>1[0-2]|0[1-9]|[1-9])",
|
||||
'G': r"(?P<G>\d\d\d\d)",
|
||||
'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])",
|
||||
'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])",
|
||||
'M': r"(?P<M>[0-5]\d|\d)",
|
||||
'S': r"(?P<S>6[0-1]|[0-5]\d|\d)",
|
||||
'U': r"(?P<U>5[0-3]|[0-4]\d|\d)",
|
||||
'w': r"(?P<w>[0-6])",
|
||||
'u': r"(?P<u>[1-7])",
|
||||
'V': r"(?P<V>5[0-3]|0[1-9]|[1-4]\d|\d)",
|
||||
# W is set below by using 'U'
|
||||
'y': r"(?P<y>\d\d)",
|
||||
#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):
|
|||
return 1 + days_to_week + day_of_week
|
||||
|
||||
|
||||
def _calc_julian_from_V(iso_year, iso_week, iso_weekday):
|
||||
"""Calculate the Julian day based on the ISO 8601 year, week, and weekday.
|
||||
ISO weeks start on Mondays, with week 01 being the week containing 4 Jan.
|
||||
ISO week days range from 1 (Monday) to 7 (Sunday).
|
||||
"""
|
||||
correction = datetime_date(iso_year, 1, 4).isoweekday() + 3
|
||||
ordinal = (iso_week * 7) + iso_weekday - correction
|
||||
# ordinal may be negative or 0 now, which means the date is in the previous
|
||||
# calendar year
|
||||
if ordinal < 1:
|
||||
ordinal += datetime_date(iso_year, 1, 1).toordinal()
|
||||
iso_year -= 1
|
||||
ordinal -= datetime_date(iso_year, 1, 1).toordinal()
|
||||
return iso_year, ordinal
|
||||
|
||||
|
||||
def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
|
||||
"""Return a 2-tuple consisting of a time struct and an int containing
|
||||
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"):
|
|||
raise ValueError("unconverted data remains: %s" %
|
||||
data_string[found.end():])
|
||||
|
||||
year = None
|
||||
iso_year = year = None
|
||||
month = day = 1
|
||||
hour = minute = second = fraction = 0
|
||||
tz = -1
|
||||
tzoffset = None
|
||||
# Default to -1 to signify that values not known; not critical to have,
|
||||
# though
|
||||
week_of_year = -1
|
||||
week_of_year_start = -1
|
||||
iso_week = week_of_year = None
|
||||
week_of_year_start = None
|
||||
# weekday and julian defaulted to None so as to signal need to calculate
|
||||
# values
|
||||
weekday = julian = None
|
||||
|
@ -369,6 +388,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
|
|||
year += 1900
|
||||
elif group_key == 'Y':
|
||||
year = int(found_dict['Y'])
|
||||
elif group_key == 'G':
|
||||
iso_year = int(found_dict['G'])
|
||||
elif group_key == 'm':
|
||||
month = int(found_dict['m'])
|
||||
elif group_key == 'B':
|
||||
|
@ -414,6 +435,9 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
|
|||
weekday = 6
|
||||
else:
|
||||
weekday -= 1
|
||||
elif group_key == 'u':
|
||||
weekday = int(found_dict['u'])
|
||||
weekday -= 1
|
||||
elif group_key == 'j':
|
||||
julian = int(found_dict['j'])
|
||||
elif group_key in ('U', 'W'):
|
||||
|
@ -424,6 +448,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
|
|||
else:
|
||||
# W starts week on Monday.
|
||||
week_of_year_start = 0
|
||||
elif group_key == 'V':
|
||||
iso_week = int(found_dict['V'])
|
||||
elif group_key == 'z':
|
||||
z = found_dict['z']
|
||||
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"):
|
|||
else:
|
||||
tz = value
|
||||
break
|
||||
# Deal with the cases where ambiguities arize
|
||||
# don't assume default values for ISO week/year
|
||||
if year is None and iso_year is not None:
|
||||
if iso_week is None or weekday is None:
|
||||
raise ValueError("ISO year directive '%G' must be used with "
|
||||
"the ISO week directive '%V' and a weekday "
|
||||
"directive ('%A', '%a', '%w', or '%u').")
|
||||
if julian is not None:
|
||||
raise ValueError("Day of the year directive '%j' is not "
|
||||
"compatible with ISO year directive '%G'. "
|
||||
"Use '%Y' instead.")
|
||||
elif week_of_year is None and iso_week is not None:
|
||||
if weekday is None:
|
||||
raise ValueError("ISO week directive '%V' must be used with "
|
||||
"the ISO year directive '%G' and a weekday "
|
||||
"directive ('%A', '%a', '%w', or '%u').")
|
||||
else:
|
||||
raise ValueError("ISO week directive '%V' is incompatible with "
|
||||
"the year directive '%Y'. Use the ISO year '%G' "
|
||||
"instead.")
|
||||
|
||||
leap_year_fix = False
|
||||
if year is None and month == 2 and day == 29:
|
||||
year = 1904 # 1904 is first leap year of 20th century
|
||||
leap_year_fix = True
|
||||
elif year is None:
|
||||
year = 1900
|
||||
|
||||
|
||||
# If we know the week of the year and what day of that week, we can figure
|
||||
# out the Julian day of the year.
|
||||
if julian is None and week_of_year != -1 and weekday is not None:
|
||||
week_starts_Mon = True if week_of_year_start == 0 else False
|
||||
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
|
||||
week_starts_Mon)
|
||||
# Cannot pre-calculate datetime_date() since can change in Julian
|
||||
# calculation and thus could have different value for the day of the week
|
||||
# calculation.
|
||||
if julian is None and weekday is not None:
|
||||
if week_of_year is not None:
|
||||
week_starts_Mon = True if week_of_year_start == 0 else False
|
||||
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
|
||||
week_starts_Mon)
|
||||
elif iso_year is not None and iso_week is not None:
|
||||
year, julian = _calc_julian_from_V(iso_year, iso_week, weekday + 1)
|
||||
|
||||
if julian is None:
|
||||
# Cannot pre-calculate datetime_date() since can change in Julian
|
||||
# calculation and thus could have different value for the day of
|
||||
# the week calculation.
|
||||
# Need to add 1 to result since first day of the year is 1, not 0.
|
||||
julian = datetime_date(year, month, day).toordinal() - \
|
||||
datetime_date(year, 1, 1).toordinal() + 1
|
||||
else: # Assume that if they bothered to include Julian day it will
|
||||
# be accurate.
|
||||
datetime_result = datetime_date.fromordinal((julian - 1) + datetime_date(year, 1, 1).toordinal())
|
||||
else: # Assume that if they bothered to include Julian day (or if it was
|
||||
# calculated above with year/week/weekday) it will be accurate.
|
||||
datetime_result = datetime_date.fromordinal(
|
||||
(julian - 1) +
|
||||
datetime_date(year, 1, 1).toordinal())
|
||||
year = datetime_result.year
|
||||
month = datetime_result.month
|
||||
day = datetime_result.day
|
||||
|
|
|
@ -152,8 +152,8 @@ class TimeRETests(unittest.TestCase):
|
|||
"'%s' using '%s'; group 'a' = '%s', group 'b' = %s'" %
|
||||
(found.string, found.re.pattern, found.group('a'),
|
||||
found.group('b')))
|
||||
for directive in ('a','A','b','B','c','d','H','I','j','m','M','p','S',
|
||||
'U','w','W','x','X','y','Y','Z','%'):
|
||||
for directive in ('a','A','b','B','c','d','G','H','I','j','m','M','p',
|
||||
'S','u','U','V','w','W','x','X','y','Y','Z','%'):
|
||||
compiled = self.time_re.compile("%" + directive)
|
||||
found = compiled.match(time.strftime("%" + directive))
|
||||
self.assertTrue(found, "Matching failed on '%s' using '%s' regex" %
|
||||
|
@ -218,6 +218,26 @@ class StrptimeTests(unittest.TestCase):
|
|||
else:
|
||||
self.fail("'%s' did not raise ValueError" % bad_format)
|
||||
|
||||
# Ambiguous or incomplete cases using ISO year/week/weekday directives
|
||||
# 1. ISO week (%V) is specified, but the year is specified with %Y
|
||||
# instead of %G
|
||||
with self.assertRaises(ValueError):
|
||||
_strptime._strptime("1999 50", "%Y %V")
|
||||
# 2. ISO year (%G) and ISO week (%V) are specified, but weekday is not
|
||||
with self.assertRaises(ValueError):
|
||||
_strptime._strptime("1999 51", "%G %V")
|
||||
# 3. ISO year (%G) and weekday are specified, but ISO week (%V) is not
|
||||
for w in ('A', 'a', 'w', 'u'):
|
||||
with self.assertRaises(ValueError):
|
||||
_strptime._strptime("1999 51","%G %{}".format(w))
|
||||
# 4. ISO year is specified alone (e.g. time.strptime('2015', '%G'))
|
||||
with self.assertRaises(ValueError):
|
||||
_strptime._strptime("2015", "%G")
|
||||
# 5. Julian/ordinal day (%j) is specified with %G, but not %Y
|
||||
with self.assertRaises(ValueError):
|
||||
_strptime._strptime("1999 256", "%G %j")
|
||||
|
||||
|
||||
def test_strptime_exception_context(self):
|
||||
# check that this doesn't chain exceptions needlessly (see #17572)
|
||||
with self.assertRaises(ValueError) as e:
|
||||
|
@ -289,7 +309,7 @@ class StrptimeTests(unittest.TestCase):
|
|||
|
||||
def test_weekday(self):
|
||||
# Test weekday directives
|
||||
for directive in ('A', 'a', 'w'):
|
||||
for directive in ('A', 'a', 'w', 'u'):
|
||||
self.helper(directive,6)
|
||||
|
||||
def test_julian(self):
|
||||
|
@ -458,16 +478,20 @@ class CalculationTests(unittest.TestCase):
|
|||
# Should be able to infer date if given year, week of year (%U or %W)
|
||||
# and day of the week
|
||||
def test_helper(ymd_tuple, test_reason):
|
||||
for directive in ('W', 'U'):
|
||||
format_string = "%%Y %%%s %%w" % directive
|
||||
dt_date = datetime_date(*ymd_tuple)
|
||||
strp_input = dt_date.strftime(format_string)
|
||||
strp_output = _strptime._strptime_time(strp_input, format_string)
|
||||
self.assertTrue(strp_output[:3] == ymd_tuple,
|
||||
"%s(%s) test failed w/ '%s': %s != %s (%s != %s)" %
|
||||
(test_reason, directive, strp_input,
|
||||
strp_output[:3], ymd_tuple,
|
||||
strp_output[7], dt_date.timetuple()[7]))
|
||||
for year_week_format in ('%Y %W', '%Y %U', '%G %V'):
|
||||
for weekday_format in ('%w', '%u', '%a', '%A'):
|
||||
format_string = year_week_format + ' ' + weekday_format
|
||||
with self.subTest(test_reason,
|
||||
date=ymd_tuple,
|
||||
format=format_string):
|
||||
dt_date = datetime_date(*ymd_tuple)
|
||||
strp_input = dt_date.strftime(format_string)
|
||||
strp_output = _strptime._strptime_time(strp_input,
|
||||
format_string)
|
||||
msg = "%r: %s != %s" % (strp_input,
|
||||
strp_output[7],
|
||||
dt_date.timetuple()[7])
|
||||
self.assertEqual(strp_output[:3], ymd_tuple, msg)
|
||||
test_helper((1901, 1, 3), "week 0")
|
||||
test_helper((1901, 1, 8), "common case")
|
||||
test_helper((1901, 1, 13), "day on Sunday")
|
||||
|
@ -499,18 +523,25 @@ class CalculationTests(unittest.TestCase):
|
|||
self.assertEqual(_strptime._strptime_time(value, format)[:-1], expected)
|
||||
check('2015 0 0', '%Y %U %w', 2014, 12, 28, 0, 0, 0, 6, -3)
|
||||
check('2015 0 0', '%Y %W %w', 2015, 1, 4, 0, 0, 0, 6, 4)
|
||||
check('2015 1 1', '%G %V %u', 2014, 12, 29, 0, 0, 0, 0, 363)
|
||||
check('2015 0 1', '%Y %U %w', 2014, 12, 29, 0, 0, 0, 0, -2)
|
||||
check('2015 0 1', '%Y %W %w', 2014, 12, 29, 0, 0, 0, 0, -2)
|
||||
check('2015 1 2', '%G %V %u', 2014, 12, 30, 0, 0, 0, 1, 364)
|
||||
check('2015 0 2', '%Y %U %w', 2014, 12, 30, 0, 0, 0, 1, -1)
|
||||
check('2015 0 2', '%Y %W %w', 2014, 12, 30, 0, 0, 0, 1, -1)
|
||||
check('2015 1 3', '%G %V %u', 2014, 12, 31, 0, 0, 0, 2, 365)
|
||||
check('2015 0 3', '%Y %U %w', 2014, 12, 31, 0, 0, 0, 2, 0)
|
||||
check('2015 0 3', '%Y %W %w', 2014, 12, 31, 0, 0, 0, 2, 0)
|
||||
check('2015 1 4', '%G %V %u', 2015, 1, 1, 0, 0, 0, 3, 1)
|
||||
check('2015 0 4', '%Y %U %w', 2015, 1, 1, 0, 0, 0, 3, 1)
|
||||
check('2015 0 4', '%Y %W %w', 2015, 1, 1, 0, 0, 0, 3, 1)
|
||||
check('2015 1 5', '%G %V %u', 2015, 1, 2, 0, 0, 0, 4, 2)
|
||||
check('2015 0 5', '%Y %U %w', 2015, 1, 2, 0, 0, 0, 4, 2)
|
||||
check('2015 0 5', '%Y %W %w', 2015, 1, 2, 0, 0, 0, 4, 2)
|
||||
check('2015 1 6', '%G %V %u', 2015, 1, 3, 0, 0, 0, 5, 3)
|
||||
check('2015 0 6', '%Y %U %w', 2015, 1, 3, 0, 0, 0, 5, 3)
|
||||
check('2015 0 6', '%Y %W %w', 2015, 1, 3, 0, 0, 0, 5, 3)
|
||||
check('2015 1 7', '%G %V %u', 2015, 1, 4, 0, 0, 0, 6, 4)
|
||||
|
||||
|
||||
class CacheTests(unittest.TestCase):
|
||||
|
|
Loading…
Reference in New Issue