diff --git a/Include/internal/pycore_time.h b/Include/internal/pycore_time.h index b671225ca6ea44d..12940cdce8abbde 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 716c662ad453f4a..bd7e0e8f5eea18b 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. @@ -3088,6 +3181,51 @@ def test_strptime(self): with self.assertRaises(ValueError): strptime("-000", "%z") with self.assertRaises(ValueError): strptime("z", "%z") + 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): diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 985984b55123ce6..719d60f96cb4868 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -371,6 +371,45 @@ def test_strptime(self): self.fail("conversion specifier %r failed with '%s' input." % (format, strf_output)) + 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/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 000000000000000..9d3eeb245023a9d --- /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 bd76b3bd81cce40..3d229a0a8c66d9f 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 @@ -3514,6 +3500,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)); @@ -4786,6 +4777,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)); @@ -5866,6 +5871,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_subclass_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)); @@ -7687,19 +7708,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 0005974b52499ce..8c484c8767e897e 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -989,6 +989,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/Python/pytime.c b/Python/pytime.c index 53c82736137a16b..65656c5b81ed160 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -1416,3 +1416,235 @@ _PyDeadline_Get(PyTime_t deadline) (void)PyTime_MonotonicRaw(&now); return deadline - now; } + + +/* 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_space(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || + c == '\f' || c == '\v'; +} + +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 use the Python parser. */ + 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) +{ + /* 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]; + if (strptime_space(c)) { + if (pos == length || !strptime_space(data[pos])) { + return 0; + } + while (i + 1 < fmt_length && strptime_space(fmt[i + 1])) { + i++; + } + do { + pos++; + } while (pos < length && strptime_space(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. */ + const char *directives = "YymdHMSfz"; + const char *directive = strchr(directives, c); + if (directive == NULL || c == '\0') { + return 0; + } + unsigned int bit = 1U << (directive - directives); + 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': + while (count++ < 6) { + 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 & 3) == 3 || + ((seen & (1U << 3)) && !(seen & 3)) || + fields->year < 1 || + fields->day > month_days) { + return 0; + } + fields->yday = days_before_month[fields->month] + fields->day + + (fields->month > 2 && leap); + return 1; +}