Skip to content

Commit ec3bb6e

Browse files
committed
gh-158252: Share the numeric strptime parser
Move numeric field parsing into pytime so the time and datetime modules can use it without importing each other. Keep result construction in the caller and expose the day of year for struct_time construction.
1 parent efe2c4b commit ec3bb6e

3 files changed

Lines changed: 238 additions & 214 deletions

File tree

‎Include/internal/pycore_time.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,22 @@ extern PyTime_t _PyTimeFraction_Mul(
317317
extern double _PyTimeFraction_Resolution(
318318
const _PyTimeFraction *frac);
319319

320+
// Locale-independent numeric strptime parsing.
321+
// A complete match returns 1; otherwise use Lib/_strptime.py. This function
322+
// neither allocates nor sets exceptions. The calendar date is validated, but
323+
// seconds may be 60 or 61: callers must apply their own time validation.
324+
typedef struct {
325+
int year, month, day;
326+
int hour, minute, second, fraction;
327+
int gmtoff; // INT_MIN means no offset was supplied.
328+
int yday; // January 1 is day 1.
329+
} _PyTime_StrptimeFields;
330+
331+
// Export for the '_datetime' shared extension.
332+
PyAPI_FUNC(int) _PyTime_Strptime(
333+
PyObject *string, PyObject *format, _PyTime_StrptimeFields *fields);
334+
335+
320336
extern PyStatus _PyTime_Init(struct _Py_time_runtime_state *state);
321337

322338
#ifdef __cplusplus

‎Modules/_datetimemodule.c‎

Lines changed: 2 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -5846,218 +5846,6 @@ datetime_datetime_utcfromtimestamp_impl(PyTypeObject *type,
58465846
return datetime_from_timestamp(type, _PyTime_gmtime, timestamp, Py_None);
58475847
}
58485848

5849-
/* Locale-independent strptime parsing for _datetime.
5850-
*
5851-
* Return 1 for a complete numeric match, or 0 to use Lib/_strptime.py.
5852-
* Parsing doesn't allocate or set exceptions. In particular, mismatches may
5853-
* require regex backtracking, so their diagnostics belong to the fallback.
5854-
*/
5855-
5856-
typedef struct {
5857-
int year, month, day;
5858-
int hour, minute, second, fraction;
5859-
int gmtoff; /* INT_MIN means no offset was supplied. */
5860-
} StrptimeFields;
5861-
5862-
static int
5863-
strptime_digits(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
5864-
int minimum, int maximum, int *value)
5865-
{
5866-
int count = 0;
5867-
*value = 0;
5868-
while (count < maximum && pos + count < length) {
5869-
unsigned char c = data[pos + count];
5870-
if (c < '0' || c > '9') {
5871-
break;
5872-
}
5873-
*value = *value * 10 + c - '0';
5874-
count++;
5875-
}
5876-
return count >= minimum ? count : 0;
5877-
}
5878-
5879-
static int
5880-
strptime_space(unsigned char c)
5881-
{
5882-
return c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
5883-
c == '\f' || c == '\v';
5884-
}
5885-
5886-
static int
5887-
strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
5888-
StrptimeFields *fields)
5889-
{
5890-
Py_ssize_t remaining = length - pos;
5891-
if (remaining == 0) {
5892-
return 1;
5893-
}
5894-
if (remaining == 1 && data[pos] == 'Z') {
5895-
fields->gmtoff = 0;
5896-
return 1;
5897-
}
5898-
/* Offsets containing seconds or fractions use the Python parser. */
5899-
if ((remaining != 5 && remaining != 6) ||
5900-
(data[pos] != '+' && data[pos] != '-')) {
5901-
return 0;
5902-
}
5903-
int hours, minutes;
5904-
int colon = remaining == 6;
5905-
if ((colon && data[pos + 3] != ':') ||
5906-
strptime_digits(data, length, pos + 1, 2, 2, &hours) != 2 ||
5907-
strptime_digits(data, length, pos + 3 + colon, 2, 2, &minutes) != 2 ||
5908-
hours > 23 || minutes > 59) {
5909-
return 0;
5910-
}
5911-
fields->gmtoff = (hours * 3600 + minutes * 60) *
5912-
(data[pos] == '-' ? -1 : 1);
5913-
return 1;
5914-
}
5915-
5916-
static int
5917-
strptime_fields(PyObject *string, PyObject *format, StrptimeFields *fields)
5918-
{
5919-
/* String subclasses and non-ASCII input retain Python's behavior. */
5920-
if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
5921-
!PyUnicode_IS_ASCII(string) || !PyUnicode_IS_ASCII(format)) {
5922-
return 0;
5923-
}
5924-
const unsigned char *data = PyUnicode_1BYTE_DATA(string);
5925-
const unsigned char *fmt = PyUnicode_1BYTE_DATA(format);
5926-
Py_ssize_t length = PyUnicode_GET_LENGTH(string);
5927-
Py_ssize_t fmt_length = PyUnicode_GET_LENGTH(format);
5928-
*fields = (StrptimeFields){1900, 1, 1, 0, 0, 0, 0, INT_MIN};
5929-
unsigned int seen = 0;
5930-
Py_ssize_t pos = 0;
5931-
5932-
for (Py_ssize_t i = 0; i < fmt_length; i++) {
5933-
unsigned char c = fmt[i];
5934-
if (strptime_space(c)) {
5935-
if (pos == length || !strptime_space(data[pos])) {
5936-
return 0;
5937-
}
5938-
while (i + 1 < fmt_length && strptime_space(fmt[i + 1])) {
5939-
i++;
5940-
}
5941-
do {
5942-
pos++;
5943-
} while (pos < length && strptime_space(data[pos]));
5944-
continue;
5945-
}
5946-
if (c != '%') {
5947-
if (pos == length || data[pos++] != c) {
5948-
return 0;
5949-
}
5950-
continue;
5951-
}
5952-
if (++i == fmt_length) {
5953-
return 0;
5954-
}
5955-
c = fmt[i];
5956-
if (c == '%') {
5957-
if (pos == length || data[pos++] != '%') {
5958-
return 0;
5959-
}
5960-
continue;
5961-
}
5962-
5963-
/* Reject duplicate groups, aliases, and locale-dependent directives. */
5964-
const char *directives = "YymdHMSfz";
5965-
const char *directive = strchr(directives, c);
5966-
if (directive == NULL || c == '\0') {
5967-
return 0;
5968-
}
5969-
unsigned int bit = 1U << (directive - directives);
5970-
if (seen & bit) {
5971-
return 0;
5972-
}
5973-
seen |= bit;
5974-
5975-
if (c == 'z') {
5976-
if (i != fmt_length - 1 ||
5977-
!strptime_offset(data, length, pos, fields)) {
5978-
return 0;
5979-
}
5980-
pos = length;
5981-
continue;
5982-
}
5983-
5984-
int minimum = 1;
5985-
int maximum = 2;
5986-
if (c == 'Y') {
5987-
minimum = maximum = 4;
5988-
}
5989-
else if (c == 'y') {
5990-
minimum = maximum = 2;
5991-
}
5992-
else if (c == 'f') {
5993-
maximum = 6;
5994-
}
5995-
else if ((c == 'd' || c == 'H') && pos < length && data[pos] == ' ') {
5996-
pos++;
5997-
maximum = 1;
5998-
}
5999-
int value;
6000-
int count = strptime_digits(data, length, pos, minimum, maximum, &value);
6001-
if (count == 0) {
6002-
return 0;
6003-
}
6004-
pos += count;
6005-
switch (c) {
6006-
case 'Y':
6007-
fields->year = value;
6008-
break;
6009-
case 'y':
6010-
fields->year = value + (value <= 68 ? 2000 : 1900);
6011-
break;
6012-
case 'm':
6013-
if (value < 1 || value > 12) {
6014-
return 0;
6015-
}
6016-
fields->month = value;
6017-
break;
6018-
case 'd':
6019-
if (value < 1 || value > 31) {
6020-
return 0;
6021-
}
6022-
fields->day = value;
6023-
break;
6024-
case 'H':
6025-
if (value > 23) {
6026-
return 0;
6027-
}
6028-
fields->hour = value;
6029-
break;
6030-
case 'M':
6031-
if (value > 59) {
6032-
return 0;
6033-
}
6034-
fields->minute = value;
6035-
break;
6036-
case 'S':
6037-
if (value > 61) {
6038-
return 0;
6039-
}
6040-
fields->second = value;
6041-
break;
6042-
case 'f':
6043-
while (count++ < 6) {
6044-
value *= 10;
6045-
}
6046-
fields->fraction = value;
6047-
break;
6048-
}
6049-
}
6050-
/* Mixed %Y/%y and day-without-year diagnostics belong to Python. */
6051-
if (pos != length || (seen & 3) == 3 ||
6052-
((seen & (1U << 3)) && !(seen & 3)) ||
6053-
fields->year < 1 ||
6054-
fields->day > days_in_month(fields->year, fields->month)) {
6055-
return 0;
6056-
}
6057-
return 1;
6058-
}
6059-
6060-
60615849
/*[clinic input]
60625850
@permit_long_summary
60635851
@classmethod
@@ -6079,9 +5867,9 @@ datetime_datetime_strptime_impl(PyTypeObject *type, PyObject *string,
60795867
/*[clinic end generated code: output=af2c2d024f3203f5 input=ef7807589f1d50e7]*/
60805868
{
60815869
/* Subclasses retain the Python parser's constructor arguments. */
6082-
StrptimeFields fields;
5870+
_PyTime_StrptimeFields fields;
60835871
if (type == DATETIME_TYPE(NO_STATE) &&
6084-
strptime_fields(string, format, &fields)) {
5872+
_PyTime_Strptime(string, format, &fields)) {
60855873
PyObject *tzinfo = tzinfo_from_isoformat_results(
60865874
fields.gmtoff != INT_MIN, fields.gmtoff, 0);
60875875
if (tzinfo == NULL) {

0 commit comments

Comments
 (0)