gh-41431: Add datetime.time.strptime() and datetime.date.strptime() (#120752)

* Python implementation

* C implementation

* Test `date.strptime`

* Test `time.strptime`

* 📜🤖 Added by blurb_it.

* Update whatsnew

* Update documentation

* Add leap year note

* Update 2024-06-19-19-53-42.gh-issue-41431.gnkUc5.rst

* Apply suggestions from code review

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>

* Remove parentheses

* Use helper function

* Remove bad return

* Link to github issue

* Fix directive

* Apply suggestions from code review

Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com>

* Fix test cases

---------

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com>
This commit is contained in:
Nice Zombies 2024-09-25 23:43:58 +02:00 committed by GitHub
parent b0c6cf5f17
commit 9968caa0cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 350 additions and 36 deletions

View file

@ -1106,6 +1106,85 @@ class TestDateOnly(unittest.TestCase):
dt2 = dt - delta
self.assertEqual(dt2, dt - days)
def test_strptime(self):
inputs = [
# Basic valid cases
(date(1998, 2, 3), '1998-02-03', '%Y-%m-%d'),
(date(2004, 12, 2), '2004-12-02', '%Y-%m-%d'),
# Edge cases: Leap year
(date(2020, 2, 29), '2020-02-29', '%Y-%m-%d'), # Valid leap year date
# bpo-34482: Handle surrogate pairs
(date(2004, 12, 2), '2004-12\ud80002', '%Y-%m\ud800%d'),
(date(2004, 12, 2), '2004\ud80012-02', '%Y\ud800%m-%d'),
# Month/day variations
(date(2004, 2, 1), '2004-02', '%Y-%m'), # No day provided
(date(2004, 2, 1), '02-2004', '%m-%Y'), # Month and year swapped
# Different day-month-year formats
(date(2004, 12, 2), '02/12/2004', '%d/%m/%Y'), # Day/Month/Year
(date(2004, 12, 2), '12/02/2004', '%m/%d/%Y'), # Month/Day/Year
# Different separators
(date(2023, 9, 24), '24.09.2023', '%d.%m.%Y'), # Dots as separators
(date(2023, 9, 24), '24-09-2023', '%d-%m-%Y'), # Dashes
(date(2023, 9, 24), '2023/09/24', '%Y/%m/%d'), # Slashes
# Handling years with fewer digits
(date(127, 2, 3), '0127-02-03', '%Y-%m-%d'),
(date(99, 2, 3), '0099-02-03', '%Y-%m-%d'),
(date(5, 2, 3), '0005-02-03', '%Y-%m-%d'),
# Variations on ISO 8601 format
(date(2023, 9, 25), '2023-W39-1', '%G-W%V-%u'), # ISO week date (Week 39, Monday)
(date(2023, 9, 25), '2023-268', '%Y-%j'), # Year and day of the year (Julian)
]
for expected, string, format in inputs:
with self.subTest(string=string, format=format):
got = date.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(got), date)
def test_strptime_single_digit(self):
# bpo-34903: Check that single digit dates are allowed.
strptime = date.strptime
with self.assertRaises(ValueError):
# %y does require two digits.
newdate = strptime('01/02/3', '%d/%m/%y')
d1 = date(2003, 2, 1)
d2 = date(2003, 1, 2)
d3 = date(2003, 1, 25)
inputs = [
('%d', '1/02/03', '%d/%m/%y', d1),
('%m', '01/2/03', '%d/%m/%y', d1),
('%j', '2/03', '%j/%y', d2),
('%w', '6/04/03', '%w/%U/%y', d1),
# %u requires a single digit.
('%W', '6/4/2003', '%u/%W/%Y', d1),
('%V', '6/4/2003', '%u/%V/%G', d3),
]
for reason, string, format, target in inputs:
reason = 'test single digit ' + reason
with self.subTest(reason=reason,
string=string,
format=format,
target=target):
newdate = strptime(string, format)
self.assertEqual(newdate, target, msg=reason)
@warnings_helper.ignore_warnings(category=DeprecationWarning)
def test_strptime_leap_year(self):
# GH-70647: warns if parsing a format with a day and no year.
with self.assertRaises(ValueError):
# The existing behavior that GH-70647 seeks to change.
date.strptime('02-29', '%m-%d')
with self._assertNotWarns(DeprecationWarning):
date.strptime('20-03-14', '%y-%m-%d')
date.strptime('02-29,2024', '%m-%d,%Y')
class SubclassDate(date):
sub_var = 1
@ -2732,7 +2811,8 @@ class TestDateTime(TestDate):
def test_strptime(self):
string = '2004-12-01 13:02:47.197'
format = '%Y-%m-%d %H:%M:%S.%f'
expected = _strptime._strptime_datetime(self.theclass, string, format)
expected = _strptime._strptime_datetime_datetime(self.theclass, string,
format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(expected), self.theclass)
@ -2746,8 +2826,8 @@ class TestDateTime(TestDate):
]
for string, format in inputs:
with self.subTest(string=string, format=format):
expected = _strptime._strptime_datetime(self.theclass, string,
format)
expected = _strptime._strptime_datetime_datetime(self.theclass,
string, format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
@ -3749,6 +3829,78 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
derived = loads(data, encoding='latin1')
self.assertEqual(derived, expected)
def test_strptime(self):
# bpo-34482: Check that surrogates are handled properly.
inputs = [
(self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%f'),
(self.theclass(13, 2, 47, 197000), '13:02\ud80047.197', '%H:%M\ud800%S.%f'),
(self.theclass(13, 2, 47, 197000), '13\ud80002:47.197', '%H\ud800%M:%S.%f'),
]
for expected, string, format in inputs:
with self.subTest(string=string, format=format):
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(got), self.theclass)
def test_strptime_tz(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])):
if tzseconds < 0:
sign = '-'
seconds = -tzseconds
else:
sign ='+'
seconds = tzseconds
hours, minutes = divmod(seconds//60, 60)
tstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname)
with self.subTest(tstr=tstr):
t = strptime(tstr, "%z %Z")
self.assertEqual(t.utcoffset(), timedelta(seconds=tzseconds))
self.assertEqual(t.tzname(), tzname)
self.assertIs(type(t), self.theclass)
# Can produce inconsistent time
tstr, fmt = "+1234 UTC", "%z %Z"
t = strptime(tstr, fmt)
self.assertEqual(t.utcoffset(), 12 * HOUR + 34 * MINUTE)
self.assertEqual(t.tzname(), 'UTC')
# yet will roundtrip
self.assertEqual(t.strftime(fmt), tstr)
# Produce naive time if no %z is provided
self.assertEqual(strptime("UTC", "%Z").tzinfo, None)
def test_strptime_errors(self):
for tzstr in ("-2400", "-000", "z"):
with self.assertRaises(ValueError):
self.theclass.strptime(tzstr, "%z")
def test_strptime_single_digit(self):
# bpo-34903: Check that single digit times are allowed.
t = self.theclass(4, 5, 6)
inputs = [
('%H', '4:05:06', '%H:%M:%S', t),
('%M', '04:5:06', '%H:%M:%S', t),
('%S', '04:05:6', '%H:%M:%S', t),
('%I', '4am:05:06', '%I%p:%M:%S', t),
]
for reason, string, format, target in inputs:
reason = 'test single digit ' + reason
with self.subTest(reason=reason,
string=string,
format=format,
target=target):
newdate = self.theclass.strptime(string, format)
self.assertEqual(newdate, target, msg=reason)
def test_bool(self):
# time is always True.
cls = self.theclass