Skip to content

Commit 903b89c

Browse files
committed
gh-158252: Parse numeric strptime aliases in C
Expand %F, %D, %T and %R through the same field parser without allocating a temporary format string. Include %e and the optional whitespace directives %n and %t.
1 parent d1ea3f1 commit 903b89c

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

‎Lib/test/test_strptime.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616

1717

1818
class PublicStrptimeTests(unittest.TestCase):
19+
def test_numeric_aliases(self):
20+
for text, fmt in (
21+
('2024-02-29', '%F'), ('02/29/24', '%D'),
22+
('2024-02-29 12:34:56', '%F %T'),
23+
('2024-02-29T12:34:56Z', '%FT%TZ'),
24+
('12:34', '%R'), ('12:34:56', '%R:%S'),
25+
('2024 9', '%Y %e'), ('2024\t02\n29', '%Y%t%m%n%d'),
26+
('20240229', '%Y%t%m%n%d'),
27+
('2024-02-29 2024', '%F %Y'),
28+
('12:34:56 56', '%T %S'),
29+
('2023-02-29', '%F'), ('12:60', '%R'),
30+
('2024-02-29 2024-02-29', '%F %F'),
31+
):
32+
self.assert_matches_python(text, fmt)
33+
1934
def test_offsets_and_zone_names(self):
2035
offsets = ('', 'Z', '+0530', '-03:30', '+01:02:03', '-000001.000001',
2136
'+01:02:03.456789', '+000000.5', '-00:00:00.5',

‎Python/_strptime.c‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* - ASCII locale month/weekday names and numeric %w/%u weekdays;
1313
* - %I with ASCII %p indicators, and the %k, %l, %P aliases;
1414
* - %z and %:z offsets with hours below 24, and terminal UTC/GMT %Z names;
15+
* - numeric aliases %F, %D, %T, %R, %e and whitespace %n/%t;
1516
* - literal characters, %%, and ASCII whitespace.
1617
* Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
1718
* fallback. So do other directives (including non-ASCII locale names and
@@ -206,6 +207,8 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
206207
}
207208
const unsigned char *data = PyUnicode_1BYTE_DATA(string);
208209
const unsigned char *fmt = PyUnicode_1BYTE_DATA(format);
210+
const unsigned char *outer_fmt = NULL;
211+
Py_ssize_t outer_length = 0, outer_pos = 0;
209212
Py_ssize_t length = PyUnicode_GET_LENGTH(string);
210213
Py_ssize_t fmt_length = PyUnicode_GET_LENGTH(format);
211214
*fields = (_PyTime_StrptimeFields){.year = 1900, .month = 1, .day = 1,
@@ -215,7 +218,19 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
215218
int ampm = -1;
216219
Py_ssize_t pos = 0;
217220

218-
for (Py_ssize_t i = 0; i < fmt_length; i++) {
221+
for (Py_ssize_t i = 0; ; i++) {
222+
if (i == fmt_length) {
223+
if (outer_fmt == NULL) {
224+
break;
225+
}
226+
fmt = outer_fmt;
227+
fmt_length = outer_length;
228+
i = outer_pos;
229+
outer_fmt = NULL;
230+
if (i == fmt_length) {
231+
break;
232+
}
233+
}
219234
unsigned char c = fmt[i];
220235
/* Match regex \s, including ASCII separators U+001C through U+001F. */
221236
if (Py_UNICODE_ISSPACE(c)) {
@@ -240,6 +255,34 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
240255
return 0;
241256
}
242257
c = fmt[i];
258+
const char *alias = NULL;
259+
switch (c) {
260+
case 'F': alias = "%Y-%m-%d"; break;
261+
case 'D': alias = "%m/%d/%y"; break;
262+
case 'T': alias = "%H:%M:%S"; break;
263+
case 'R': alias = "%H:%M"; break;
264+
case 'e': c = 'd'; break;
265+
}
266+
if (alias != NULL) {
267+
/* These fixed expansions contain no further aliases. Parsing
268+
* them in place keeps duplicate-field checks and avoids copying
269+
* an arbitrarily long user format into a temporary buffer.
270+
*/
271+
assert(outer_fmt == NULL);
272+
outer_fmt = fmt;
273+
outer_length = fmt_length;
274+
outer_pos = i + 1;
275+
fmt = (const unsigned char *)alias;
276+
fmt_length = strlen(alias);
277+
i = -1;
278+
continue;
279+
}
280+
if (c == 'n' || c == 't') {
281+
while (pos < length && Py_ISSPACE(data[pos])) {
282+
pos++;
283+
}
284+
continue;
285+
}
243286
int require_colon = 0;
244287
if (c == ':' && i + 1 < fmt_length && fmt[i + 1] == 'z') {
245288
c = fmt[++i];

0 commit comments

Comments
 (0)