Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Include/internal/pycore_time.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
138 changes: 138 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
65 changes: 43 additions & 22 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading