Skip to content

Commit 207ae59

Browse files
committed
gh-158252: Share calendar helpers and clarify strptime fallback
Reuse the datetime calendar arithmetic for struct_time construction. Document the numeric parser scope and why even known-invalid inputs use the Python parser for error messages and error precedence.
1 parent bf4f1dd commit 207ae59

4 files changed

Lines changed: 50 additions & 33 deletions

File tree

‎Include/internal/pycore_time.h‎

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

320+
/* year -> number of days before January 1st of year. Remember that we
321+
* start with year 1, so days_before_year(1) == 0.
322+
*/
323+
static inline int
324+
_PyTime_DaysBeforeYear(int year)
325+
{
326+
int y = year - 1;
327+
/* This is incorrect if year <= 0; we really want the floor
328+
* here. But so long as MINYEAR is 1, the smallest year this
329+
* can see is 1.
330+
*/
331+
assert(year >= 1);
332+
return y*365 + y/4 - y/100 + y/400;
333+
}
334+
335+
// Weekday from a valid Gregorian year and 1-based day of year; Monday is 0.
336+
static inline int
337+
_PyTime_Weekday(int year, int yday)
338+
{
339+
return (_PyTime_DaysBeforeYear(year) + yday + 6) % 7;
340+
}
341+
342+
320343
// Locale-independent numeric strptime parsing.
321344
// A complete match returns 1; otherwise use Lib/_strptime.py. This function
322345
// neither allocates nor sets exceptions. The calendar date is validated, but

‎Modules/_datetimemodule.c‎

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -463,27 +463,13 @@ days_before_month(int year, int month)
463463
return days;
464464
}
465465

466-
/* year -> number of days before January 1st of year. Remember that we
467-
* start with year 1, so days_before_year(1) == 0.
468-
*/
469-
static int
470-
days_before_year(int year)
471-
{
472-
int y = year - 1;
473-
/* This is incorrect if year <= 0; we really want the floor
474-
* here. But so long as MINYEAR is 1, the smallest year this
475-
* can see is 1.
476-
*/
477-
assert (year >= 1);
478-
return y*365 + y/4 - y/100 + y/400;
479-
}
480466

481467
/* Number of days in 4, 100, and 400 year cycles. That these have
482468
* the correct values is asserted in the module init function.
483469
*/
484-
#define DI4Y 1461 /* days_before_year(5); days in 4 years */
485-
#define DI100Y 36524 /* days_before_year(101); days in 100 years */
486-
#define DI400Y 146097 /* days_before_year(401); days in 400 years */
470+
#define DI4Y 1461 /* _PyTime_DaysBeforeYear(5); days in 4 years */
471+
#define DI100Y 36524 /* _PyTime_DaysBeforeYear(101); days in 100 years */
472+
#define DI400Y 146097 /* _PyTime_DaysBeforeYear(401); days in 400 years */
487473

488474
/* ordinal -> year, month, day, considering 01-Jan-0001 as day 1. */
489475
static void
@@ -573,14 +559,14 @@ ord_to_ymd(int ordinal, int *year, int *month, int *day)
573559
static int
574560
ymd_to_ord(int year, int month, int day)
575561
{
576-
return days_before_year(year) + days_before_month(year, month) + day;
562+
return _PyTime_DaysBeforeYear(year) + days_before_month(year, month) + day;
577563
}
578564

579565
/* Day of week, where Monday==0, ..., Sunday==6. 1/1/1 was a Monday. */
580566
static int
581567
weekday(int year, int month, int day)
582568
{
583-
return (ymd_to_ord(year, month, day) + 6) % 7;
569+
return _PyTime_Weekday(year, days_before_month(year, month) + day);
584570
}
585571

586572
/* Ordinal of the Monday starting week 1 of the ISO year. Week 1 is the
@@ -7722,19 +7708,19 @@ _datetime_exec(PyObject *module)
77227708
* pasting together 4 single years.
77237709
*/
77247710
static_assert(DI4Y == 4 * 365 + 1, "DI4Y");
7725-
assert(DI4Y == days_before_year(4+1));
7711+
assert(DI4Y == _PyTime_DaysBeforeYear(4+1));
77267712

77277713
/* Similarly, a 400-year cycle has an extra leap day over what we'd
77287714
* get from pasting together 4 100-year cycles.
77297715
*/
77307716
static_assert(DI400Y == 4 * DI100Y + 1, "DI400Y");
7731-
assert(DI400Y == days_before_year(400+1));
7717+
assert(DI400Y == _PyTime_DaysBeforeYear(400+1));
77327718

77337719
/* OTOH, a 100-year cycle has one fewer leap day than we'd get from
77347720
* pasting together 25 4-year cycles.
77357721
*/
77367722
static_assert(DI100Y == 25 * DI4Y - 1, "DI100Y");
7737-
assert(DI100Y == days_before_year(100+1));
7723+
assert(DI100Y == _PyTime_DaysBeforeYear(100+1));
77387724

77397725
if (set_current_module(interp, module) < 0) {
77407726
goto error;

‎Modules/timemodule.c‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -998,14 +998,10 @@ time_strptime(PyObject *self, PyObject *args)
998998
if (result == NULL) {
999999
return NULL;
10001000
}
1001-
/* January 1 of year 1 was a Monday. */
1002-
int year = fields.year - 1;
1003-
int weekday = (365 * year + year / 4 - year / 100 + year / 400 +
1004-
fields.yday - 1) % 7;
10051001
const int values[] = {
10061002
fields.year, fields.month, fields.day,
10071003
fields.hour, fields.minute, fields.second,
1008-
weekday, fields.yday, -1
1004+
_PyTime_Weekday(fields.year, fields.yday), fields.yday, -1
10091005
};
10101006
for (size_t i = 0; i < Py_ARRAY_LENGTH(values); i++) {
10111007
PyObject *value = PyLong_FromLong(values[i]);

‎Python/pytime.c‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,15 +1418,18 @@ _PyDeadline_Get(PyTime_t deadline)
14181418
}
14191419

14201420

1421-
/* Locale-independent numeric strptime parsing.
1421+
/* Implement common parts of strptime in C to improve performance. Callers
1422+
* fall back to Lib/_strptime.py for a full parse when this doesn't match.
14221423
*
1423-
* Return 1 for a complete numeric match, or 0 to use Lib/_strptime.py.
1424-
* Parsing doesn't allocate or set exceptions. In particular, mismatches may
1425-
* require regex backtracking, so their diagnostics belong to the fallback.
1424+
* Supported inputs are exact str objects containing only ASCII, with:
1425+
* - numeric %Y, %y, %m, %d, %H, %M, %S, and %f fields;
1426+
* - a terminal %z: empty, Z, or +/-HHMM or +/-HH:MM with hours below 24;
1427+
* - literal characters, %%, and ASCII whitespace.
1428+
* Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
1429+
* fallback. So do other directives (including locale-dependent names and
1430+
* week/day-of-year calculations), and offsets with seconds or fractions.
14261431
*/
14271432

1428-
1429-
14301433
static int
14311434
strptime_digits(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14321435
int minimum, int maximum, int *value)
@@ -1481,6 +1484,15 @@ strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14811484
return 1;
14821485
}
14831486

1487+
/* Return 1 for a complete numeric match, or 0 if fallback is required.
1488+
*
1489+
* Parsing doesn't allocate or set exceptions. Some mismatches require regex
1490+
* backtracking, but even inputs known to be invalid use the Python parser:
1491+
* it owns the exception messages and error precedence. For example, month 13
1492+
* and empty input can't be repaired by backtracking, but diagnosing them here
1493+
* would duplicate the Python parser's error handling. A return value of 0
1494+
* therefore doesn't distinguish invalid input from an unsupported format.
1495+
*/
14841496
int
14851497
_PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fields)
14861498
{

0 commit comments

Comments
 (0)