Skip to content

Commit 178a25e

Browse files
committed
gh-158252: Parse twelve-hour clocks in C
Use the current locale for AM/PM indicators and preserve the default without an indicator. Include the equivalent %k, %l and %P directives. Locales with empty or non-ASCII indicators retain the Python parser.
1 parent baaf475 commit 178a25e

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

‎Lib/test/test_strptime.py‎

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

1717

1818
class PublicStrptimeTests(unittest.TestCase):
19+
@run_with_locales('LC_TIME', 'C', 'en_US', 'fr_FR', 'de_DE', 'ja_JP', '')
20+
def test_twelve_hour_clock(self):
21+
for hour in range(24):
22+
value = datetime.datetime(2024, 1, 1, hour, 24, 35)
23+
for fmt in ('%Y-%m-%d %I:%M:%S %p', '%p %I:%M:%S', '%I', '%p'):
24+
self.assert_matches_python(value.strftime(fmt), fmt)
25+
26+
@support.run_with_locale('LC_TIME', 'C')
27+
def test_hour_aliases_and_precedence(self):
28+
for text, fmt in (
29+
('12 AM', '%I %p'), ('12 PM', '%I %p'),
30+
(' 1 pm', '%l %P'), (' 0', '%k'),
31+
('PM 12', '%p %I'), ('23 AM', '%H %p'),
32+
('12 13 PM', '%I %H %p'), ('13 12 PM', '%H %I %p'),
33+
('00 AM', '%I %p'), ('13 PM', '%I %p'),
34+
('1 pM', '%I %p'), ('1 PX', '%I %p'),
35+
('AM PM', '%p %P'),
36+
):
37+
self.assert_matches_python(text, fmt)
38+
1939
@run_with_locales('LC_TIME', 'C', 'en_US', 'fr_FR', 'de_DE', 'ja_JP', '')
2040
def test_locale_names(self):
2141
for month in range(1, 13):

‎Python/_strptime.c‎

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Supported inputs are exact str objects containing only ASCII, with:
1111
* - numeric %Y, %y, %m, %d, %H, %M, %S, and %f fields;
1212
* - ASCII locale month/weekday names and numeric %w/%u weekdays;
13+
* - %I with ASCII %p indicators, and the %k, %l, %P aliases;
1314
* - a terminal %z: empty, Z, or +/-HHMM or +/-HH:MM with hours below 24;
1415
* - literal characters, %%, and ASCII whitespace.
1516
* Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
@@ -44,13 +45,14 @@ strptime_name(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
4445
* As in TimeRE, prefer the longest match, then the first equal name.
4546
*/
4647
int month = directive == 'b' || directive == 'B';
47-
int count = month ? 12 : 7;
48+
int count = month ? 12 : directive == 'p' ? 2 : 7;
4849
char format[] = {'%', directive, '\0'};
4950
int matched = 0;
5051
for (int i = 0; i < count; i++) {
5152
struct tm tm = {.tm_year = 101, .tm_mday = 1,
5253
.tm_mon = month ? i : 0,
53-
.tm_wday = month ? 0 : (i + 1) % 7};
54+
.tm_wday = month ? 0 : (i + 1) % 7,
55+
.tm_hour = i == 0 ? 1 : 22};
5456
char name[256];
5557
size_t size = strftime(name, sizeof(name), format, &tm);
5658
if (size == 0) {
@@ -61,12 +63,31 @@ strptime_name(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
6163
return 0;
6264
}
6365
}
64-
if (size <= (size_t)matched || size > (size_t)(length - pos)) {
66+
char *start = name;
67+
if (directive == 'p') {
68+
while (size && Py_ISSPACE(start[size - 1])) {
69+
size--;
70+
}
71+
while (size && Py_ISSPACE(*start)) {
72+
start++;
73+
size--;
74+
}
75+
/* Empty indicators have special defaulting rules in Python. */
76+
if (size == 0) {
77+
return 0;
78+
}
79+
}
80+
if (data == NULL) {
81+
continue;
82+
}
83+
if (size < (size_t)matched ||
84+
(size == (size_t)matched && directive != 'p') ||
85+
size > (size_t)(length - pos)) {
6586
continue;
6687
}
6788
size_t j;
6889
for (j = 0; j < size; j++) {
69-
if (Py_TOLOWER(data[pos + j]) != Py_TOLOWER(name[j])) {
90+
if (Py_TOLOWER(data[pos + j]) != Py_TOLOWER(start[j])) {
7091
break;
7192
}
7293
}
@@ -75,7 +96,7 @@ strptime_name(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
7596
*value = month ? i + 1 : i;
7697
}
7798
}
78-
return matched;
99+
return data == NULL ? 1 : matched;
79100
#else
80101
return 0;
81102
#endif
@@ -134,6 +155,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
134155
FRACTION = 1U << 7,
135156
OFFSET = 1U << 8,
136157
WEEKDAY = 1U << 9,
158+
AMPM = 1U << 10,
137159
ANY_YEAR = YEAR | SHORT_YEAR,
138160
};
139161
static const unsigned short directive_bits[128] = {
@@ -147,6 +169,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
147169
['f'] = FRACTION,
148170
['z'] = OFFSET,
149171
['w'] = WEEKDAY,
172+
['p'] = AMPM,
150173
};
151174
/* String subclasses and non-ASCII input retain Python's behavior. */
152175
if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
@@ -160,6 +183,8 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
160183
*fields = (_PyTime_StrptimeFields){.year = 1900, .month = 1, .day = 1,
161184
.gmtoff = INT_MIN, .weekday = -1};
162185
unsigned int seen = 0;
186+
int hour12 = 0;
187+
int ampm = -1;
163188
Py_ssize_t pos = 0;
164189

165190
for (Py_ssize_t i = 0; i < fmt_length; i++) {
@@ -186,6 +211,15 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
186211
return 0;
187212
}
188213
c = fmt[i];
214+
if (c == 'k') {
215+
c = 'H';
216+
}
217+
else if (c == 'l') {
218+
c = 'I';
219+
}
220+
else if (c == 'P') {
221+
c = 'p';
222+
}
189223
if (c == '%') {
190224
if (pos == length || data[pos++] != '%') {
191225
return 0;
@@ -201,6 +235,9 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
201235
else if (c == 'a' || c == 'A' || c == 'u') {
202236
field = 'w';
203237
}
238+
else if (c == 'I') {
239+
field = 'H';
240+
}
204241
unsigned int bit = directive_bits[field];
205242
if (bit == 0) {
206243
return 0;
@@ -210,7 +247,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
210247
}
211248
seen |= bit;
212249

213-
if (c == 'b' || c == 'B' || c == 'a' || c == 'A') {
250+
if (c == 'b' || c == 'B' || c == 'a' || c == 'A' || c == 'p') {
214251
int value;
215252
int count = strptime_name(data, length, pos, c, &value);
216253
if (count == 0) {
@@ -220,6 +257,9 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
220257
if (field == 'm') {
221258
fields->month = value;
222259
}
260+
else if (c == 'p') {
261+
ampm = value;
262+
}
223263
else {
224264
fields->weekday = value;
225265
}
@@ -249,7 +289,8 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
249289
else if (field == 'w') {
250290
maximum = 1;
251291
}
252-
else if ((c == 'd' || c == 'H') && pos < length && data[pos] == ' ') {
292+
else if ((c == 'd' || c == 'H' || c == 'I') &&
293+
pos < length && data[pos] == ' ') {
253294
pos++;
254295
maximum = 1;
255296
}
@@ -260,6 +301,13 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
260301
}
261302
pos += count;
262303
switch (c) {
304+
case 'I':
305+
if (value < 1 || value > 12) {
306+
return 0;
307+
}
308+
hour12 = 1;
309+
fields->hour = value;
310+
break;
263311
case 'w':
264312
if (value > 6) {
265313
return 0;
@@ -316,6 +364,12 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
316364
break;
317365
}
318366
}
367+
if (hour12) {
368+
if (ampm < 0 && !strptime_name(NULL, 0, 0, 'p', &ampm)) {
369+
return 0;
370+
}
371+
fields->hour = fields->hour % 12 + (ampm == 1 ? 12 : 0);
372+
}
319373
static const int days_before_month[] = {
320374
0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
321375
};

0 commit comments

Comments
 (0)