mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-158252: Add a C implementation for strptime (#158253)
Co-authored-by: Stan Ulbrych <stan@python.org>
This commit is contained in:
1 parent
ae5c45758b
commit
6854e79163
13 files changed
+639
-22
No files matched your search
@@ -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
|
||||
--
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user