https://github.com/python/cpython/commit/6854e79163a2c871fbd4f394b49c0548eaa3cab2
commit: 6854e79163a2c871fbd4f394b49c0548eaa3cab2
branch: main
author: Paul Ganssle <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-23T15:23:02Z
summary:

gh-158252: Add a C implementation for `strptime` (#158253)

Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-17-10-30-00.gh-issue-158252.strptime.rst
A Python/_strptime.c
M Doc/whatsnew/3.16.rst
M Include/internal/pycore_time.h
M Lib/test/datetimetester.py
M Lib/test/test_time.py
M Makefile.pre.in
M Modules/_datetimemodule.c
M Modules/timemodule.c
M PCbuild/_freeze_module.vcxproj
M PCbuild/_freeze_module.vcxproj.filters
M PCbuild/pythoncore.vcxproj
M PCbuild/pythoncore.vcxproj.filters

diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 78b2492badba5a..f8fb9bf4d47e7b 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -983,6 +983,16 @@ zlib
 Optimizations
 =============
 
+datetime and time
+-----------------
+
+* :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime`,
+  :meth:`datetime.time.strptime`, and :func:`time.strptime` are faster
+  for common numeric formats such as ``%Y-%m-%d`` and
+  ``%Y-%m-%d %H:%M:%S``.
+  (Contributed by Paul Ganssle in :gh:`158252`, based on work by
+  Stan Ulbrych.)
+
 re
 --
 
diff --git a/Include/internal/pycore_time.h b/Include/internal/pycore_time.h
index b671225ca6ea44..12940cdce8abbd 100644
--- a/Include/internal/pycore_time.h
+++ b/Include/internal/pycore_time.h
@@ -317,6 +317,45 @@ extern PyTime_t _PyTimeFraction_Mul(
 extern double _PyTimeFraction_Resolution(
     const _PyTimeFraction *frac);
 
+/* year -> number of days before January 1st of year.  Remember that we
+ * start with year 1, so days_before_year(1) == 0.
+ */
+static inline int
+_PyTime_DaysBeforeYear(int year)
+{
+    int y = year - 1;
+    /* This is incorrect if year <= 0; we really want the floor
+     * here.  But so long as MINYEAR is 1, the smallest year this
+     * can see is 1.
+     */
+    assert(year >= 1);
+    return y*365 + y/4 - y/100 + y/400;
+}
+
+// Weekday from a valid Gregorian year and 1-based day of year; Monday is 0.
+static inline int
+_PyTime_Weekday(int year, int yday)
+{
+    return (_PyTime_DaysBeforeYear(year) + yday + 6) % 7;
+}
+
+
+// Locale-independent numeric strptime parsing.
+// A complete match returns 1; otherwise use Lib/_strptime.py. This function
+// neither allocates nor sets exceptions. The calendar date is validated, but
+// seconds may be 60 or 61: callers must apply their own time validation.
+typedef struct {
+    int year, month, day;
+    int hour, minute, second, fraction;
+    int gmtoff;  // INT_MIN means no offset was supplied.
+    int yday;    // January 1 is day 1.
+} _PyTime_StrptimeFields;
+
+// Export for the '_datetime' shared extension.
+PyAPI_FUNC(int) _PyTime_Strptime(
+    PyObject *string, PyObject *format, _PyTime_StrptimeFields *fields);
+
+
 extern PyStatus _PyTime_Init(struct _Py_time_runtime_state *state);
 
 #ifdef __cplusplus
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index df7d540cb362f7..a251247f6122bb 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -1104,6 +1104,99 @@ def __divmod__(self, other):
 #############################################################################
 # date tests
 
+class TestStrptime(unittest.TestCase):
+    def test_numeric_fields(self):
+        cases = (
+            ('2024-02-29 12:34:56.123', '%Y-%m-%d %H:%M:%S.%f',
+             datetime(2024, 2, 29, 12, 34, 56, 123000)),
+            ('2024-02-29T12:34:56.123+0530', '%Y-%m-%dT%H:%M:%S.%f%z',
+             datetime(2024, 2, 29, 12, 34, 56, 123000,
+                      timezone(timedelta(hours=5, minutes=30)))),
+            ('12:34:56Z', '%H:%M:%S%z', datetime(1900, 1, 1, 12, 34, 56, 
tzinfo=UTC)),
+            ('12:34:56-03:30', '%H:%M:%S%z',
+             datetime(1900, 1, 1, 12, 34, 56,
+                      tzinfo=timezone(-timedelta(hours=3, minutes=30)))),
+            ('12:34:56', '%H:%M:%S%z', datetime(1900, 1, 1, 12, 34, 56)),
+            ('', '', datetime(1900, 1, 1)),
+            ('2024111', '%Y%m%d', datetime(2024, 11, 1)),
+            ('2024131', '%Y%m%d', datetime(2024, 1, 31)),
+            ('2024\0-02-29', '%Y\0-%m-%d', datetime(2024, 2, 29)),
+            ('٢٠٢٤-02-29', '%Y-%m-%d', datetime(2024, 2, 29)),
+            ('2024\u200302\u200329', '%Y %m %d', datetime(2024, 2, 29)),
+            ('2024t02t29', '%YT%mt%d', datetime(2024, 2, 29)),
+            ('24 2025-02-01', '%y %Y-%m-%d', datetime(2025, 2, 1)),
+        )
+        for text, fmt, expected in cases:
+            for cls, result in ((datetime, expected), (date, expected.date()),
+                                (time, expected.timetz())):
+                with self.subTest(text=text, fmt=fmt, cls=cls):
+                    actual = cls.strptime(text, fmt)
+                    self.assertEqual(actual, result)
+                    self.assertIs(type(actual), cls)
+            with self.subTest(text=text, fmt=fmt, cls=_time.struct_time):
+                actual = _time.strptime(text, fmt)
+                self.assertEqual(actual, expected.timetuple())
+                self.assertIsNone(actual.tm_zone)
+                offset = expected.utcoffset()
+                self.assertEqual(actual.tm_gmtoff,
+                                 None if offset is None else 
offset.total_seconds())
+
+    def test_invalid_fields(self):
+        cases = (
+            ('2024-02-30', '%Y-%m-%d'),
+            ('1900-02-29', '%Y-%m-%d'),
+            ('0000-01-01', '%Y-%m-%d'),
+            ('2024-01- 12', '%Y-%m-%d'),
+            ('2024\0ignored', '%Y'),
+            ('24:00:00', '%H:%M:%S'),
+            ('23:60:00', '%H:%M:%S'),
+            ('23:59:62', '%H:%M:%S'),
+        )
+        for text, fmt in cases:
+            for parse in (datetime.strptime, date.strptime, time.strptime,
+                          _time.strptime):
+                with self.subTest(text=text, fmt=fmt, parse=parse):
+                    with self.assertRaises(ValueError):
+                        parse(text, fmt)
+
+    def test_leap_seconds(self):
+        for second in (60, 61):
+            text = f'2024-02-29 23:59:{second}'
+            fmt = '%Y-%m-%d %H:%M:%S'
+            with self.subTest(second=second):
+                self.assertEqual(date.strptime(text, fmt), date(2024, 2, 29))
+                self.assertEqual(_time.strptime(text, fmt),
+                                 (2024, 2, 29, 23, 59, second, 3, 60, -1))
+                for cls in (datetime, time):
+                    with self.assertRaises(ValueError):
+                        cls.strptime(text, fmt)
+
+    def test_offset_outside_datetime_range(self):
+        # date discards the offset; struct_time doesn't construct a timezone.
+        self.assertEqual(date.strptime('+2400', '%z'), date(1900, 1, 1))
+        self.assertEqual(_time.strptime('+2400', '%z').tm_gmtoff, 86400)
+        for cls in (datetime, time):
+            with self.subTest(cls=cls):
+                with self.assertRaises(ValueError):
+                    cls.strptime('+2400', '%z')
+
+    def test_subclass_constructor(self):
+        for cls, args in ((date, (2024, 2, 29)),
+                          (time, (12, 34, 56, 123000)),
+                          (datetime, (2024, 2, 29, 12, 34, 56, 123000))):
+            class Capture(cls):
+                def __new__(cls, *args, **kwargs):
+                    return args, kwargs
+
+            text = '2024-02-29 12:34:56.123'
+            fmt = '%Y-%m-%d %H:%M:%S.%f'
+            with self.subTest(cls=cls):
+                self.assertEqual(Capture.strptime(text, fmt), (args, {}))
+                aware_args = args if cls is date else args + (UTC,)
+                self.assertEqual(Capture.strptime(text + 'Z', fmt + '%z'),
+                                 (aware_args, {}))
+
+
 class TestDateOnly(unittest.TestCase):
     # Tests here won't pass if also run on datetime objects, so don't
     # subclass this to test datetimes too.
@@ -1178,6 +1271,23 @@ def test_strptime(self):
                 self.assertEqual(expected, got)
                 self.assertIs(type(got), date)
 
+    def test_strptime_whitespace(self):
+        whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
+        for input_space in whitespace:
+            for format_space in whitespace:
+                with self.subTest(input_space=input_space,
+                                  format_space=format_space):
+                    text = input_space.join(('2024', '02', '29'))
+                    fmt = format_space.join(('%Y', '%m', '%d'))
+                    self.assertEqual(date.strptime(text, fmt), date(2024, 2, 
29))
+
+    def test_strptime_whitespace_before_adjacent_fields(self):
+        for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
+            with self.subTest(separator=separator):
+                text = f'2024{separator} 112'
+                fmt = f'%Y{separator}%d%m'
+                self.assertEqual(date.strptime(text, fmt), date(2024, 2, 11))
+
     def test_strptime_single_digit(self):
         # bpo-34903: Check that single digit dates are allowed.
         strptime = date.strptime
@@ -3089,6 +3199,70 @@ def test_strptime(self):
         with self.assertRaises(ValueError): strptime("-000", "%z")
         with self.assertRaises(ValueError): strptime("z", "%z")
 
+    def test_strptime_whitespace(self):
+        whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
+        for input_space in whitespace:
+            for format_space in whitespace:
+                with self.subTest(input_space=input_space,
+                                  format_space=format_space):
+                    text = input_space.join(('2024-02-29', '12:34:56'))
+                    fmt = format_space.join(('%Y-%m-%d', '%H:%M:%S'))
+                    self.assertEqual(self.theclass.strptime(text, fmt),
+                                     self.theclass(2024, 2, 29, 12, 34, 56))
+
+    def test_strptime_whitespace_before_adjacent_fields(self):
+        for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
+            with self.subTest(separator=separator):
+                text = f'2024{separator} 112 34{separator} 123'
+                fmt = f'%Y{separator}%d%m %M{separator}%H%S'
+                self.assertEqual(self.theclass.strptime(text, fmt),
+                                 self.theclass(2024, 2, 11, 12, 34, 3))
+
+    def test_strptime_numeric_fallback(self):
+        cases = (
+            ('2024\0-02-29', '%Y\0-%m-%d', (2024, 2, 29)),
+            ('٢٠٢٤-02-29', '%Y-%m-%d', (2024, 2, 29)),
+            ('2024\u200302\u200329', '%Y %m %d', (2024, 2, 29)),
+            ('2024t02t29', '%YT%mt%d', (2024, 2, 29)),
+            ('2024111', '%Y%m%d', (2024, 11, 1)),
+            ('2024131', '%Y%m%d', (2024, 1, 31)),
+            ('24 2025-02-01', '%y %Y-%m-%d', (2025, 2, 1)),
+            ('2025 24-02-01', '%Y %y-%m-%d', (2024, 2, 1)),
+        )
+        for text, fmt, expected in cases:
+            with self.subTest(text=text, fmt=fmt):
+                self.assertEqual(self.theclass.strptime(text, fmt),
+                                 self.theclass(*expected))
+
+    def test_strptime_numeric_invalid(self):
+        cases = (
+            ('2024\0ignored', '%Y'),
+            ('2024', '%Y\0ignored'),
+            ('2024', '%4Y'),
+            ('2024-02-30', '%Y-%m-%d'),
+            ('1900-02-29', '%Y-%m-%d'),
+            ('0000-01-01', '%Y-%m-%d'),
+            ('2024-01- 12', '%Y-%m-%d'),
+        )
+        for text, fmt in cases:
+            with self.subTest(text=text, fmt=fmt):
+                with self.assertRaises(ValueError):
+                    self.theclass.strptime(text, fmt)
+        with self.assertRaises(re.PatternError):
+            self.theclass.strptime('2024 2025', '%Y %Y')
+
+    def test_strptime_subclass_constructor(self):
+        class Capture(self.theclass):
+            def __new__(cls, *args, **kwargs):
+                return args, kwargs
+
+        args = (2024, 2, 29, 12, 34, 56, 123000)
+        fmt = '%Y-%m-%d %H:%M:%S.%f'
+        text = '2024-02-29 12:34:56.123'
+        self.assertEqual(Capture.strptime(text, fmt), (args, {}))
+        self.assertEqual(Capture.strptime(text + '+0530', fmt + '%z'),
+                         (args + (timezone(timedelta(hours=5, minutes=30)),), 
{}))
+
     def test_strptime_ampm(self):
         dt = datetime(1999, 3, 17, 0, 44, 55, 2)
         for hour in range(0, 24):
@@ -4286,6 +4460,25 @@ def test_strptime(self):
         with self.assertRaises(ValueError):
             self.theclass.strptime('13:02:47.1975', '%H:%M:%S.%3f')
 
+    def test_strptime_whitespace(self):
+        whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
+        for input_space in whitespace:
+            for format_space in whitespace:
+                with self.subTest(input_space=input_space,
+                                  format_space=format_space):
+                    text = input_space.join(('12', '34', '56'))
+                    fmt = format_space.join(('%H', '%M', '%S'))
+                    self.assertEqual(self.theclass.strptime(text, fmt),
+                                     self.theclass(12, 34, 56))
+
+    def test_strptime_whitespace_before_adjacent_fields(self):
+        for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
+            with self.subTest(separator=separator):
+                text = f'34{separator} 123'
+                fmt = f'%M{separator}%H%S'
+                self.assertEqual(self.theclass.strptime(text, fmt),
+                                 self.theclass(12, 34, 3))
+
     def test_strptime_tz(self):
         strptime = self.theclass.strptime
         self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py
index 985984b55123ce..dac4dd9060ccdd 100644
--- a/Lib/test/test_time.py
+++ b/Lib/test/test_time.py
@@ -371,6 +371,64 @@ def test_strptime(self):
                 self.fail("conversion specifier %r failed with '%s' input." %
                           (format, strf_output))
 
+    def test_strptime_whitespace(self):
+        whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
+        for input_space in whitespace:
+            for format_space in whitespace:
+                with self.subTest(input_space=input_space,
+                                  format_space=format_space):
+                    text = input_space.join(('2024-02-29', '12:34:56'))
+                    fmt = format_space.join(('%Y-%m-%d', '%H:%M:%S'))
+                    self.assertEqual(time.strptime(text, fmt),
+                                     (2024, 2, 29, 12, 34, 56, 3, 60, -1))
+
+    def test_strptime_whitespace_before_adjacent_fields(self):
+        for separator in ('\x1c', '\x1d', '\x1e', '\x1f'):
+            with self.subTest(separator=separator):
+                text = f'2024{separator} 112 34{separator} 123'
+                fmt = f'%Y{separator}%d%m %M{separator}%H%S'
+                self.assertEqual(time.strptime(text, fmt),
+                                 (2024, 2, 11, 12, 34, 3, 6, 42, -1))
+
+    def test_strptime_numeric_calendar(self):
+        cases = (
+            ('0001-01-01', 1, 1, 1, 0, 1),
+            ('1900-03-01', 1900, 3, 1, 3, 60),
+            ('2000-02-29', 2000, 2, 29, 1, 60),
+            ('2000-03-01', 2000, 3, 1, 2, 61),
+            ('2024-12-31', 2024, 12, 31, 1, 366),
+            ('2100-03-01', 2100, 3, 1, 0, 60),
+            ('9999-12-31', 9999, 12, 31, 4, 365),
+        )
+        for text, year, month, day, weekday, yday in cases:
+            with self.subTest(text=text):
+                result = time.strptime(text, '%Y-%m-%d')
+                self.assertIs(type(result), time.struct_time)
+                self.assertEqual(result,
+                                 (year, month, day, 0, 0, 0, weekday, yday, 
-1))
+                self.assertIsNone(result.tm_zone)
+                self.assertIsNone(result.tm_gmtoff)
+
+    def test_strptime_numeric_offset(self):
+        for text, offset in (('', None), ('Z', 0), ('+0000', 0),
+                             ('-00:00', 0), ('+0530', 19800),
+                             ('-03:30', -12600), ('+2359', 86340),
+                             ('+01:02:03.456', 3723)):
+            with self.subTest(text=text):
+                result = time.strptime('12:34:56.123' + text, '%H:%M:%S.%f%z')
+                self.assertEqual(result, (1900, 1, 1, 12, 34, 56, 0, 1, -1))
+                self.assertIsNone(result.tm_zone)
+                self.assertEqual(result.tm_gmtoff, offset)
+
+    @support.run_with_locale('LC_TIME', 'C')
+    def test_strptime_default_format(self):
+        self.assertEqual(time.strptime('Thu Feb 29 12:34:56 2024'),
+                         (2024, 2, 29, 12, 34, 56, 3, 60, -1))
+        for args in ((), ('2024', '%Y', 'extra'), (None, '%Y')):
+            with self.subTest(args=args):
+                with self.assertRaises(TypeError):
+                    time.strptime(*args)
+
     def test_strptime_bytes(self):
         # Make sure only strings are accepted as arguments to strptime.
         self.assertRaises(TypeError, time.strptime, b'2009', "%Y")
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 3d971ddc8cd132..842ba83afb3061 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -435,6 +435,7 @@ PARSER_HEADERS= \
 
 PYTHON_OBJS=   \
                Python/_contextvars.o \
+               Python/_strptime.o \
                Python/_warnings.o \
                Python/Python-ast.o \
                Python/Python-tokenize.o \
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-17-10-30-00.gh-issue-158252.strptime.rst 
b/Misc/NEWS.d/next/Library/2026-09-17-10-30-00.gh-issue-158252.strptime.rst
new file mode 100644
index 00000000000000..9d3eeb245023a9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-17-10-30-00.gh-issue-158252.strptime.rst
@@ -0,0 +1,4 @@
+Speed up :func:`time.strptime`, :meth:`datetime.date.strptime`,
+:meth:`datetime.time.strptime`, and :meth:`datetime.datetime.strptime` for
+common numeric formats by parsing fields and constructing the result in C.
+Other inputs continue to use the Python parser.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index cd2dd94c8164a5..71057ad704d012 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -463,27 +463,13 @@ days_before_month(int year, int month)
     return days;
 }
 
-/* year -> number of days before January 1st of year.  Remember that we
- * start with year 1, so days_before_year(1) == 0.
- */
-static int
-days_before_year(int year)
-{
-    int y = year - 1;
-    /* This is incorrect if year <= 0; we really want the floor
-     * here.  But so long as MINYEAR is 1, the smallest year this
-     * can see is 1.
-     */
-    assert (year >= 1);
-    return y*365 + y/4 - y/100 + y/400;
-}
 
 /* Number of days in 4, 100, and 400 year cycles.  That these have
  * the correct values is asserted in the module init function.
  */
-#define DI4Y    1461    /* days_before_year(5); days in 4 years */
-#define DI100Y  36524   /* days_before_year(101); days in 100 years */
-#define DI400Y  146097  /* days_before_year(401); days in 400 years  */
+#define DI4Y    1461    /* _PyTime_DaysBeforeYear(5); days in 4 years */
+#define DI100Y  36524   /* _PyTime_DaysBeforeYear(101); days in 100 years */
+#define DI400Y  146097  /* _PyTime_DaysBeforeYear(401); days in 400 years  */
 
 /* ordinal -> year, month, day, considering 01-Jan-0001 as day 1. */
 static void
@@ -573,14 +559,14 @@ ord_to_ymd(int ordinal, int *year, int *month, int *day)
 static int
 ymd_to_ord(int year, int month, int day)
 {
-    return days_before_year(year) + days_before_month(year, month) + day;
+    return _PyTime_DaysBeforeYear(year) + days_before_month(year, month) + day;
 }
 
 /* Day of week, where Monday==0, ..., Sunday==6.  1/1/1 was a Monday. */
 static int
 weekday(int year, int month, int day)
 {
-    return (ymd_to_ord(year, month, day) + 6) % 7;
+    return _PyTime_Weekday(year, days_before_month(year, month) + day);
 }
 
 /* Ordinal of the Monday starting week 1 of the ISO year.  Week 1 is the
@@ -3538,6 +3524,11 @@ datetime_date_strptime_impl(PyTypeObject *type, PyObject 
*string,
                             PyObject *format)
 /*[clinic end generated code: output=454d473bee2d5161 input=2db8f0b2b5242deb]*/
 {
+    _PyTime_StrptimeFields fields;
+    if (type == DATE_TYPE(NO_STATE) &&
+        _PyTime_Strptime(string, format, &fields)) {
+        return new_date_ex(fields.year, fields.month, fields.day, type);
+    }
     PyObject *result;
 
     PyObject *module = PyImport_Import(&_Py_ID(_strptime));
@@ -4810,6 +4801,20 @@ datetime_time_strptime_impl(PyTypeObject *type, PyObject 
*string,
                             PyObject *format)
 /*[clinic end generated code: output=ae05a9bc0241d3bf input=f01d0b9eb5383da5]*/
 {
+    _PyTime_StrptimeFields fields;
+    if (type == TIME_TYPE(NO_STATE) &&
+        _PyTime_Strptime(string, format, &fields)) {
+        PyObject *tzinfo = tzinfo_from_isoformat_results(
+            fields.gmtoff != INT_MIN, fields.gmtoff, 0);
+        if (tzinfo == NULL) {
+            return NULL;
+        }
+        PyObject *result = new_time_ex(
+            fields.hour, fields.minute, fields.second, fields.fraction,
+            tzinfo, type);
+        Py_DECREF(tzinfo);
+        return result;
+    }
     PyObject *result;
 
     PyObject *module = PyImport_Import(&_Py_ID(_strptime));
@@ -5890,6 +5895,22 @@ datetime_datetime_strptime_impl(PyTypeObject *type, 
PyObject *string,
                                 PyObject *format)
 /*[clinic end generated code: output=af2c2d024f3203f5 input=ef7807589f1d50e7]*/
 {
+    /* Subclasses retain the Python parser's constructor arguments. */
+    _PyTime_StrptimeFields fields;
+    if (type == DATETIME_TYPE(NO_STATE) &&
+        _PyTime_Strptime(string, format, &fields)) {
+        PyObject *tzinfo = tzinfo_from_isoformat_results(
+            fields.gmtoff != INT_MIN, fields.gmtoff, 0);
+        if (tzinfo == NULL) {
+            return NULL;
+        }
+        PyObject *result = new_datetime_ex(
+            fields.year, fields.month, fields.day,
+            fields.hour, fields.minute, fields.second, fields.fraction,
+            tzinfo, type);
+        Py_DECREF(tzinfo);
+        return result;
+    }
     PyObject *result;
 
     PyObject *module = PyImport_Import(&_Py_ID(_strptime));
@@ -7711,19 +7732,19 @@ _datetime_exec(PyObject *module)
      * pasting together 4 single years.
      */
     static_assert(DI4Y == 4 * 365 + 1, "DI4Y");
-    assert(DI4Y == days_before_year(4+1));
+    assert(DI4Y == _PyTime_DaysBeforeYear(4+1));
 
     /* Similarly, a 400-year cycle has an extra leap day over what we'd
      * get from pasting together 4 100-year cycles.
      */
     static_assert(DI400Y == 4 * DI100Y + 1, "DI400Y");
-    assert(DI400Y == days_before_year(400+1));
+    assert(DI400Y == _PyTime_DaysBeforeYear(400+1));
 
     /* OTOH, a 100-year cycle has one fewer leap day than we'd get from
      * pasting together 25 4-year cycles.
      */
     static_assert(DI100Y == 25 * DI4Y - 1, "DI100Y");
-    assert(DI100Y == days_before_year(100+1));
+    assert(DI100Y == _PyTime_DaysBeforeYear(100+1));
 
     if (set_current_module(interp, module) < 0) {
         goto error;
diff --git a/Modules/timemodule.c b/Modules/timemodule.c
index e5162fd3b67855..f3bfef333b383b 100644
--- a/Modules/timemodule.c
+++ b/Modules/timemodule.c
@@ -1007,6 +1007,40 @@ is not present, current time as returned by localtime() 
is used.\n\
 static PyObject *
 time_strptime(PyObject *self, PyObject *args)
 {
+    _PyTime_StrptimeFields fields;
+    if (PyTuple_GET_SIZE(args) == 2 &&
+        _PyTime_Strptime(PyTuple_GET_ITEM(args, 0),
+                         PyTuple_GET_ITEM(args, 1), &fields)) {
+        time_module_state *state = get_time_state(self);
+        PyObject *result = PyStructSequence_New(state->struct_time_type);
+        if (result == NULL) {
+            return NULL;
+        }
+        const int values[] = {
+            fields.year, fields.month, fields.day,
+            fields.hour, fields.minute, fields.second,
+            _PyTime_Weekday(fields.year, fields.yday), fields.yday, -1
+        };
+        for (size_t i = 0; i < Py_ARRAY_LENGTH(values); i++) {
+            PyObject *value = PyLong_FromLong(values[i]);
+            if (value == NULL) {
+                Py_DECREF(result);
+                return NULL;
+            }
+            PyStructSequence_SET_ITEM(result, i, value);
+        }
+        /* Numeric offsets don't supply a timezone name or DST information. */
+        PyStructSequence_SET_ITEM(result, 9, Py_NewRef(Py_None));
+        PyObject *offset = fields.gmtoff == INT_MIN
+                          ? Py_NewRef(Py_None)
+                          : PyLong_FromLong(fields.gmtoff);
+        if (offset == NULL) {
+            Py_DECREF(result);
+            return NULL;
+        }
+        PyStructSequence_SET_ITEM(result, 10, offset);
+        return result;
+    }
     PyObject *func, *result;
 
     func = PyImport_ImportModuleAttrString("_strptime", "_strptime_time");
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index 36a1d768486a3f..b00f2c1b486f21 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -194,6 +194,7 @@
     <ClCompile Include="..\PC\invalid_parameter_handler.c" />
     <ClCompile Include="..\PC\msvcrtmodule.c" />
     <ClCompile Include="..\PC\winreg.c" />
+    <ClCompile Include="..\Python\_strptime.c" />
     <ClCompile Include="..\Python\_warnings.c" />
     <ClCompile Include="..\Python\asdl.c" />
     <ClCompile Include="..\Python\assemble.c" />
diff --git a/PCbuild/_freeze_module.vcxproj.filters 
b/PCbuild/_freeze_module.vcxproj.filters
index b358e09950a443..e236cb907ae564 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -19,6 +19,9 @@
     <ClCompile Include="..\Modules\_tracemalloc.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\_strptime.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\_warnings.c">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 51d18739d7c93f..44c4b9a08ca977 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -615,6 +615,7 @@
     <ClCompile Include="..\PC\msvcrtmodule.c" />
     <ClCompile Include="..\Python\pyhash.c" />
     <ClCompile Include="..\Python\_contextvars.c" />
+    <ClCompile Include="..\Python\_strptime.c" />
     <ClCompile Include="..\Python\_warnings.c" />
     <ClCompile Include="..\Python\asdl.c" />
     <ClCompile Include="..\Python\assemble.c" />
diff --git a/PCbuild/pythoncore.vcxproj.filters 
b/PCbuild/pythoncore.vcxproj.filters
index 9abf09b6550ecf..224de01c089789 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -1397,6 +1397,9 @@
     <ClCompile Include="..\Python\_contextvars.c">
       <Filter>Python</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\_strptime.c">
+      <Filter>Python</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\_warnings.c">
       <Filter>Python</Filter>
     </ClCompile>
diff --git a/Python/_strptime.c b/Python/_strptime.c
new file mode 100644
index 00000000000000..42f546f9714f86
--- /dev/null
+++ b/Python/_strptime.c
@@ -0,0 +1,249 @@
+#include "Python.h"
+#include "pycore_time.h"
+
+
+/* Implement common parts of strptime in C to improve performance. Callers
+ * fall back to Lib/_strptime.py for a full parse when this doesn't match.
+ *
+ * Supported inputs are exact str objects containing only ASCII, with:
+ * - numeric %Y, %y, %m, %d, %H, %M, %S, and %f fields;
+ * - a terminal %z: empty, Z, or +/-HHMM or +/-HH:MM with hours below 24;
+ * - literal characters, %%, and ASCII whitespace.
+ * Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
+ * fallback. So do other directives (including locale-dependent names and
+ * week/day-of-year calculations), and offsets with seconds or fractions.
+ */
+
+static int
+strptime_digits(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
+                int minimum, int maximum, int *value)
+{
+    int count = 0;
+    *value = 0;
+    while (count < maximum && pos + count < length) {
+        unsigned char c = data[pos + count];
+        if (c < '0' || c > '9') {
+            break;
+        }
+        *value = *value * 10 + c - '0';
+        count++;
+    }
+    return count >= minimum ? count : 0;
+}
+
+static int
+strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
+                _PyTime_StrptimeFields *fields)
+{
+    Py_ssize_t remaining = length - pos;
+    if (remaining == 0) {
+        return 1;
+    }
+    if (remaining == 1 && data[pos] == 'Z') {
+        fields->gmtoff = 0;
+        return 1;
+    }
+    /* Offsets containing seconds or fractions fall back to _strptime.py. */
+    if ((remaining != 5 && remaining != 6) ||
+        (data[pos] != '+' && data[pos] != '-')) {
+        return 0;
+    }
+    int hours, minutes;
+    int colon = remaining == 6;
+    if ((colon && data[pos + 3] != ':') ||
+        strptime_digits(data, length, pos + 1, 2, 2, &hours) != 2 ||
+        strptime_digits(data, length, pos + 3 + colon, 2, 2, &minutes) != 2 ||
+        hours > 23 || minutes > 59) {
+        return 0;
+    }
+    fields->gmtoff = (hours * 3600 + minutes * 60) *
+                    (data[pos] == '-' ? -1 : 1);
+    return 1;
+}
+
+/* Return 1 for a complete numeric match, or 0 if fallback is required.
+ *
+ * Parsing doesn't allocate or set exceptions. Some mismatches require regex
+ * backtracking, but even inputs known to be invalid use the Python parser:
+ * it owns the exception messages and error precedence. For example, month 13
+ * and empty input can't be repaired by backtracking, but diagnosing them here
+ * would duplicate the Python parser's error handling. A return value of 0
+ * therefore doesn't distinguish invalid input from an unsupported format.
+ */
+int
+_PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields 
*fields)
+{
+    enum {
+        YEAR = 1U << 0,
+        SHORT_YEAR = 1U << 1,
+        MONTH = 1U << 2,
+        DAY = 1U << 3,
+        HOUR = 1U << 4,
+        MINUTE = 1U << 5,
+        SECOND = 1U << 6,
+        FRACTION = 1U << 7,
+        OFFSET = 1U << 8,
+        ANY_YEAR = YEAR | SHORT_YEAR,
+    };
+    static const unsigned short directive_bits[128] = {
+        ['Y'] = YEAR,
+        ['y'] = SHORT_YEAR,
+        ['m'] = MONTH,
+        ['d'] = DAY,
+        ['H'] = HOUR,
+        ['M'] = MINUTE,
+        ['S'] = SECOND,
+        ['f'] = FRACTION,
+        ['z'] = OFFSET,
+    };
+    /* String subclasses and non-ASCII input retain Python's behavior. */
+    if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
+        !PyUnicode_IS_ASCII(string) || !PyUnicode_IS_ASCII(format)) {
+        return 0;
+    }
+    const unsigned char *data = PyUnicode_1BYTE_DATA(string);
+    const unsigned char *fmt = PyUnicode_1BYTE_DATA(format);
+    Py_ssize_t length = PyUnicode_GET_LENGTH(string);
+    Py_ssize_t fmt_length = PyUnicode_GET_LENGTH(format);
+    *fields = (_PyTime_StrptimeFields){.year = 1900, .month = 1, .day = 1, 
.gmtoff = INT_MIN};
+    unsigned int seen = 0;
+    Py_ssize_t pos = 0;
+
+    for (Py_ssize_t i = 0; i < fmt_length; i++) {
+        unsigned char c = fmt[i];
+        /* Match regex \s, including ASCII separators U+001C through U+001F. */
+        if (Py_UNICODE_ISSPACE(c)) {
+            if (pos == length || !Py_UNICODE_ISSPACE(data[pos])) {
+                return 0;
+            }
+            while (i + 1 < fmt_length && Py_UNICODE_ISSPACE(fmt[i + 1])) {
+                i++;
+            }
+            do {
+                pos++;
+            } while (pos < length && Py_UNICODE_ISSPACE(data[pos]));
+            continue;
+        }
+        if (c != '%') {
+            if (pos == length || data[pos++] != c) {
+                return 0;
+            }
+            continue;
+        }
+        if (++i == fmt_length) {
+            return 0;
+        }
+        c = fmt[i];
+        if (c == '%') {
+            if (pos == length || data[pos++] != '%') {
+                return 0;
+            }
+            continue;
+        }
+
+        /* Reject duplicate groups, aliases, and locale-dependent directives. 
*/
+        unsigned int bit = directive_bits[c];
+        if (bit == 0) {
+            return 0;
+        }
+        if (seen & bit) {
+            return 0;
+        }
+        seen |= bit;
+
+        if (c == 'z') {
+            if (i != fmt_length - 1 ||
+                !strptime_offset(data, length, pos, fields)) {
+                return 0;
+            }
+            pos = length;
+            continue;
+        }
+
+        int minimum = 1;
+        int maximum = 2;
+        if (c == 'Y') {
+            minimum = maximum = 4;
+        }
+        else if (c == 'y') {
+            minimum = maximum = 2;
+        }
+        else if (c == 'f') {
+            maximum = 6;
+        }
+        else if ((c == 'd' || c == 'H') && pos < length && data[pos] == ' ') {
+            pos++;
+            maximum = 1;
+        }
+        int value;
+        int count = strptime_digits(data, length, pos, minimum, maximum, 
&value);
+        if (count == 0) {
+            return 0;
+        }
+        pos += count;
+        switch (c) {
+            case 'Y':
+                fields->year = value;
+                break;
+            case 'y':
+                fields->year = value + (value <= 68 ? 2000 : 1900);
+                break;
+            case 'm':
+                if (value < 1 || value > 12) {
+                    return 0;
+                }
+                fields->month = value;
+                break;
+            case 'd':
+                if (value < 1 || value > 31) {
+                    return 0;
+                }
+                fields->day = value;
+                break;
+            case 'H':
+                if (value > 23) {
+                    return 0;
+                }
+                fields->hour = value;
+                break;
+            case 'M':
+                if (value > 59) {
+                    return 0;
+                }
+                fields->minute = value;
+                break;
+            case 'S':
+                if (value > 61) {
+                    return 0;
+                }
+                fields->second = value;
+                break;
+            case 'f':
+                for (; count < 6; count++) {
+                    value *= 10;
+                }
+                fields->fraction = value;
+                break;
+        }
+    }
+    static const int days_before_month[] = {
+        0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
+    };
+    int leap = fields->year % 4 == 0 &&
+               (fields->year % 100 != 0 || fields->year % 400 == 0);
+    int month_days = days_before_month[fields->month + 1] -
+                     days_before_month[fields->month];
+    if (fields->month == 2) {
+        month_days += leap;
+    }
+    /* Mixed %Y/%y and day-without-year diagnostics belong to Python. */
+    if (pos != length || (seen & ANY_YEAR) == ANY_YEAR ||
+        ((seen & DAY) && !(seen & ANY_YEAR)) ||
+        fields->year < 1 ||
+        fields->day > month_days) {
+        return 0;
+    }
+    fields->yday = days_before_month[fields->month] + fields->day +
+                   (fields->month > 2 && leap);
+    return 1;
+}

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to