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

Co-authored-by: Stan Ulbrych <stan@python.org>
This commit is contained in:
Paul GanssleandStan Ulbrych authored and GitHub committed 2026-10-23 15:23:02 +00:00
1 parent ae5c45758b
commit 6854e79163
13 files changed
+639 -22

No files matched your search

+10
View File
@@ -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
--
+39
View File
@@ -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
+193
View File
@@ -1104,6 +1104,99 @@ class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase):
#############################################################################
# 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 @@ class TestDateOnly(unittest.TestCase):
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 @@ class TestDateTime(TestDate):
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 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
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)
+58
View File
@@ -371,6 +371,64 @@ class TimeTestCase(unittest.TestCase):
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")
+1
View File
@@ -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 \
@@ -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.
+43 -22
View File
@@ -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;
+34
View File
@@ -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");
+1
View File
@@ -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" />
+3
View File
@@ -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>
+1
View File
@@ -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" />
+3
View File
@@ -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>
+249
View File
@@ -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;
}