summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/_pydatetime.py34
-rw-r--r--Lib/test/datetimetester.py11
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS.d/next/Library/2023-06-16-14-52-00.gh-issue-102450.MfeR6A.rst2
-rw-r--r--Modules/_datetimemodule.c36
5 files changed, 80 insertions, 4 deletions
diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index f8e121e..154e6eb 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -463,6 +463,17 @@ def _parse_isoformat_time(tstr):
time_comps = _parse_hh_mm_ss_ff(timestr)
+ hour, minute, second, microsecond = time_comps
+ became_next_day = False
+ error_from_components = False
+ if (hour == 24):
+ if all(time_comp == 0 for time_comp in time_comps[1:]):
+ hour = 0
+ time_comps[0] = hour
+ became_next_day = True
+ else:
+ error_from_components = True
+
tzi = None
if tz_pos == len_str and tstr[-1] == 'Z':
tzi = timezone.utc
@@ -495,7 +506,7 @@ def _parse_isoformat_time(tstr):
time_comps.append(tzi)
- return time_comps
+ return time_comps, became_next_day, error_from_components
# tuple[int, int, int] -> tuple[int, int, int] version of date.fromisocalendar
def _isoweek_to_gregorian(year, week, day):
@@ -1588,7 +1599,7 @@ class time:
time_string = time_string.removeprefix('T')
try:
- return cls(*_parse_isoformat_time(time_string))
+ return cls(*_parse_isoformat_time(time_string)[0])
except Exception:
raise ValueError(f'Invalid isoformat string: {time_string!r}')
@@ -1902,10 +1913,27 @@ class datetime(date):
if tstr:
try:
- time_components = _parse_isoformat_time(tstr)
+ time_components, became_next_day, error_from_components = _parse_isoformat_time(tstr)
except ValueError:
raise ValueError(
f'Invalid isoformat string: {date_string!r}') from None
+ else:
+ if error_from_components:
+ raise ValueError("minute, second, and microsecond must be 0 when hour is 24")
+
+ if became_next_day:
+ year, month, day = date_components
+ # Only wrap day/month when it was previously valid
+ if month <= 12 and day <= (days_in_month := _days_in_month(year, month)):
+ # Calculate midnight of the next day
+ day += 1
+ if day > days_in_month:
+ day = 1
+ month += 1
+ if month > 12:
+ month = 1
+ year += 1
+ date_components = [year, month, day]
else:
time_components = [0, 0, 0, 0, None]
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index aef24e1..16aff18 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -3342,6 +3342,9 @@ class TestDateTime(TestDate):
('2025-01-02T03:04:05,678+00:00:10',
self.theclass(2025, 1, 2, 3, 4, 5, 678000,
tzinfo=timezone(timedelta(seconds=10)))),
+ ('2025-01-02T24:00:00', self.theclass(2025, 1, 3, 0, 0, 0)),
+ ('2025-01-31T24:00:00', self.theclass(2025, 2, 1, 0, 0, 0)),
+ ('2025-12-31T24:00:00', self.theclass(2026, 1, 1, 0, 0, 0))
]
for input_str, expected in examples:
@@ -3378,6 +3381,12 @@ class TestDateTime(TestDate):
'2009-04-19T12:30:45.123456-05:00a', # Extra text
'2009-04-19T12:30:45.123-05:00a', # Extra text
'2009-04-19T12:30:45-05:00a', # Extra text
+ '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00
+ '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00
+ '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00
+ '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00
+ '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00
+ '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00
]
for bad_str in bad_strs:
@@ -4312,7 +4321,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
with self.subTest(tstr=tstr):
t_rt = self.theclass.fromisoformat(tstr)
- assert t == t_rt, t_rt
+ assert t == t_rt
def test_fromisoformat_timespecs(self):
time_bases = [
diff --git a/Misc/ACKS b/Misc/ACKS
index ef0f403..b252960 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1553,6 +1553,7 @@ Carl Robben
Ben Roberts
Mark Roberts
Andy Robinson
+Izan "TizzySaurus" Robinson
Jim Robinson
Yolanda Robla
Daniel Rocco
diff --git a/Misc/NEWS.d/next/Library/2023-06-16-14-52-00.gh-issue-102450.MfeR6A.rst b/Misc/NEWS.d/next/Library/2023-06-16-14-52-00.gh-issue-102450.MfeR6A.rst
new file mode 100644
index 0000000..abfad5f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-06-16-14-52-00.gh-issue-102450.MfeR6A.rst
@@ -0,0 +1,2 @@
+Add missing ISO-8601 24:00 alternative to midnight of next day to :meth:`datetime.datetime.fromisoformat` and :meth:`datetime.time.fromisoformat`.
+Patch by Izan "TizzySaurus" Robinson (tizzysaurus@gmail.com)
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 8562e0c..58b3653 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -4997,6 +4997,14 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
goto invalid_string_error;
}
+ if (hour == 24) {
+ if (minute == 0 && second == 0 && microsecond == 0) {
+ hour = 0;
+ } else {
+ goto invalid_iso_midnight;
+ }
+ }
+
PyObject *tzinfo = tzinfo_from_isoformat_results(rv, tzoffset,
tzimicrosecond);
@@ -5015,6 +5023,10 @@ time_fromisoformat(PyObject *cls, PyObject *tstr) {
Py_DECREF(tzinfo);
return t;
+invalid_iso_midnight:
+ PyErr_SetString(PyExc_ValueError, "minute, second, and microsecond must be 0 when hour is 24");
+ return NULL;
+
invalid_string_error:
PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", tstr);
return NULL;
@@ -5861,6 +5873,26 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
goto error;
}
+ if ((hour == 24) && (month <= 12)) {
+ int d_in_month = days_in_month(year, month);
+ if (day <= d_in_month) {
+ if (minute == 0 && second == 0 && microsecond == 0) {
+ // Calculate midnight of the next day
+ hour = 0;
+ day += 1;
+ if (day > d_in_month) {
+ day = 1;
+ month += 1;
+ if (month > 12) {
+ month = 1;
+ year += 1;
+ }
+ }
+ } else {
+ goto invalid_iso_midnight;
+ }
+ }
+ }
PyObject *dt = new_datetime_subclass_ex(year, month, day, hour, minute,
second, microsecond, tzinfo, cls);
@@ -5868,6 +5900,10 @@ datetime_fromisoformat(PyObject *cls, PyObject *dtstr)
Py_DECREF(dtstr_clean);
return dt;
+invalid_iso_midnight:
+ PyErr_SetString(PyExc_ValueError, "minute, second, and microsecond must be 0 when hour is 24");
+ return NULL;
+
invalid_string_error:
PyErr_Format(PyExc_ValueError, "Invalid isoformat string: %R", dtstr);