Skip to content

Commit 4031133

Browse files
committed
gh-158252: Extend C strptime timezone parsing
Allow nonterminal offsets, offset seconds and fractions, and %:z. Preserve fractional offsets for datetime and time objects, and preserve terminal UTC/GMT names and DST metadata for time.strptime. Local timezone names and nonterminal %Z still use the Python parser.
1 parent 178a25e commit 4031133

5 files changed

Lines changed: 124 additions & 30 deletions

File tree

‎Include/internal/pycore_time.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ typedef struct {
348348
int year, month, day;
349349
int hour, minute, second, fraction;
350350
int gmtoff; // INT_MIN means no offset was supplied.
351+
int gmtoff_fraction;
352+
Py_ssize_t zone_start, zone_length; // Original spelling of a %Z name.
351353
int yday; // January 1 is day 1.
352354
int weekday; // Monday is 0; a supplied weekday need not agree with the date.
353355
} _PyTime_StrptimeFields;

‎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_offsets_and_zone_names(self):
20+
offsets = ('', 'Z', '+0530', '-03:30', '+01:02:03', '-000001.000001',
21+
'+01:02:03.456789', '+000000.5', '-00:00:00.5',
22+
'+2400', '+0060', '+01:0203', '+0102:03', '+01:02:99',
23+
'+01:02:03.', '+01:02:03.1234567', 'z', '+', '+000')
24+
for offset in offsets:
25+
for directive in ('%z', '%:z'):
26+
for suffix, fmt_suffix in (('', ''), (' UTC', ' %Z'),
27+
(' gMt', ' %Z'), (' 12:34', ' %H:%M')):
28+
self.assert_matches_python(offset + suffix,
29+
directive + fmt_suffix)
30+
for name in ('UTC', 'GMT', 'uTc', 'gmT', 'UTCx', 'LOCAL', ''):
31+
self.assert_matches_python(name, '%Z')
32+
self.assert_matches_python('+0100 +0200', '%z %:z')
33+
1934
@run_with_locales('LC_TIME', 'C', 'en_US', 'fr_FR', 'de_DE', 'ja_JP', '')
2035
def test_twelve_hour_clock(self):
2136
for hour in range(24):

‎Modules/_datetimemodule.c‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,27 @@ tzinfo_from_isoformat_results(int rv, int tzoffset, int tz_useconds)
16851685
* String format helpers.
16861686
*/
16871687

1688+
static PyObject *
1689+
tzinfo_from_strptime_results(const _PyTime_StrptimeFields *fields,
1690+
PyObject *string)
1691+
{
1692+
if (!fields->zone_length || fields->gmtoff == INT_MIN) {
1693+
return tzinfo_from_isoformat_results(fields->gmtoff != INT_MIN,
1694+
fields->gmtoff,
1695+
fields->gmtoff_fraction);
1696+
}
1697+
PyObject *name = PyUnicode_Substring(string, fields->zone_start,
1698+
fields->zone_start + fields->zone_length);
1699+
if (name == NULL) {
1700+
return NULL;
1701+
}
1702+
PyObject *delta = new_delta(0, fields->gmtoff, fields->gmtoff_fraction, 1);
1703+
PyObject *tzinfo = delta == NULL ? NULL : new_timezone(delta, name);
1704+
Py_XDECREF(delta);
1705+
Py_DECREF(name);
1706+
return tzinfo;
1707+
}
1708+
16881709
static PyObject *
16891710
format_ctime(PyObject *date, int hours, int minutes, int seconds)
16901711
{
@@ -4782,8 +4803,7 @@ datetime_time_strptime_impl(PyTypeObject *type, PyObject *string,
47824803
_PyTime_StrptimeFields fields;
47834804
if (type == TIME_TYPE(NO_STATE) &&
47844805
_PyTime_Strptime(string, format, &fields)) {
4785-
PyObject *tzinfo = tzinfo_from_isoformat_results(
4786-
fields.gmtoff != INT_MIN, fields.gmtoff, 0);
4806+
PyObject *tzinfo = tzinfo_from_strptime_results(&fields, string);
47874807
if (tzinfo == NULL) {
47884808
return NULL;
47894809
}
@@ -5877,8 +5897,7 @@ datetime_datetime_strptime_impl(PyTypeObject *type, PyObject *string,
58775897
_PyTime_StrptimeFields fields;
58785898
if (type == DATETIME_TYPE(NO_STATE) &&
58795899
_PyTime_Strptime(string, format, &fields)) {
5880-
PyObject *tzinfo = tzinfo_from_isoformat_results(
5881-
fields.gmtoff != INT_MIN, fields.gmtoff, 0);
5900+
PyObject *tzinfo = tzinfo_from_strptime_results(&fields, string);
58825901
if (tzinfo == NULL) {
58835902
return NULL;
58845903
}

‎Modules/timemodule.c‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ time_strptime(PyObject *self, PyObject *args)
10191019
const int values[] = {
10201020
fields.year, fields.month, fields.day,
10211021
fields.hour, fields.minute, fields.second,
1022-
fields.weekday, fields.yday, -1
1022+
fields.weekday, fields.yday, fields.zone_length ? 0 : -1
10231023
};
10241024
for (size_t i = 0; i < Py_ARRAY_LENGTH(values); i++) {
10251025
PyObject *value = PyLong_FromLong(values[i]);
@@ -1029,8 +1029,15 @@ time_strptime(PyObject *self, PyObject *args)
10291029
}
10301030
PyStructSequence_SET_ITEM(result, i, value);
10311031
}
1032-
/* Numeric offsets don't supply a timezone name or DST information. */
1033-
PyStructSequence_SET_ITEM(result, 9, Py_NewRef(Py_None));
1032+
PyObject *zone = fields.zone_length
1033+
? PyUnicode_Substring(PyTuple_GET_ITEM(args, 0), fields.zone_start,
1034+
fields.zone_start + fields.zone_length)
1035+
: Py_NewRef(Py_None);
1036+
if (zone == NULL) {
1037+
Py_DECREF(result);
1038+
return NULL;
1039+
}
1040+
PyStructSequence_SET_ITEM(result, 9, zone);
10341041
PyObject *offset = fields.gmtoff == INT_MIN
10351042
? Py_NewRef(Py_None)
10361043
: PyLong_FromLong(fields.gmtoff);

‎Python/_strptime.c‎

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
* - numeric %Y, %y, %m, %d, %H, %M, %S, and %f fields;
1212
* - ASCII locale month/weekday names and numeric %w/%u weekdays;
1313
* - %I with ASCII %p indicators, and the %k, %l, %P aliases;
14-
* - a terminal %z: empty, Z, or +/-HHMM or +/-HH:MM with hours below 24;
14+
* - %z and %:z offsets with hours below 24, and terminal UTC/GMT %Z names;
1515
* - literal characters, %%, and ASCII whitespace.
1616
* Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
1717
* fallback. So do other directives (including non-ASCII locale names and
18-
* week/day-of-year calculations), and offsets with seconds or fractions.
18+
* week/day-of-year calculations).
1919
*/
2020

2121
static int
@@ -104,32 +104,58 @@ strptime_name(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
104104

105105
static int
106106
strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
107-
_PyTime_StrptimeFields *fields)
107+
int require_colon, _PyTime_StrptimeFields *fields)
108108
{
109-
Py_ssize_t remaining = length - pos;
110-
if (remaining == 0) {
111-
return 1;
109+
Py_ssize_t start = pos;
110+
if (pos == length) {
111+
return 0;
112112
}
113-
if (remaining == 1 && data[pos] == 'Z') {
113+
if (data[pos] == 'Z') {
114114
fields->gmtoff = 0;
115115
return 1;
116116
}
117-
/* Offsets containing seconds or fractions fall back to _strptime.py. */
118-
if ((remaining != 5 && remaining != 6) ||
119-
(data[pos] != '+' && data[pos] != '-')) {
117+
if (data[pos] != '+' && data[pos] != '-') {
120118
return 0;
121119
}
122-
int hours, minutes;
123-
int colon = remaining == 6;
124-
if ((colon && data[pos + 3] != ':') ||
125-
strptime_digits(data, length, pos + 1, 2, 2, &hours) != 2 ||
126-
strptime_digits(data, length, pos + 3 + colon, 2, 2, &minutes) != 2 ||
127-
hours > 23 || minutes > 59) {
128-
return 0;
120+
int sign = data[pos++] == '-' ? -1 : 1;
121+
int hours, minutes, seconds = 0, fraction = 0;
122+
if (strptime_digits(data, length, pos, 2, 2, &hours) != 2 || hours > 23) {
123+
return -1;
129124
}
130-
fields->gmtoff = (hours * 3600 + minutes * 60) *
131-
(data[pos] == '-' ? -1 : 1);
132-
return 1;
125+
pos += 2;
126+
int colon = pos < length && data[pos] == ':';
127+
if (require_colon && !colon) {
128+
return -1;
129+
}
130+
pos += colon;
131+
if (strptime_digits(data, length, pos, 2, 2, &minutes) != 2 || minutes > 59) {
132+
return -1;
133+
}
134+
pos += 2;
135+
int seconds_colon = pos < length && data[pos] == ':';
136+
Py_ssize_t seconds_pos = pos + seconds_colon;
137+
if (strptime_digits(data, length, seconds_pos, 2, 2, &seconds) == 2) {
138+
if (seconds > 59 || seconds_colon != colon) {
139+
return -1;
140+
}
141+
pos = seconds_pos + 2;
142+
if (pos < length && data[pos] == '.') {
143+
int count = strptime_digits(data, length, pos + 1, 1, 6, &fraction);
144+
if (count == 0) {
145+
return -1;
146+
}
147+
pos += count + 1;
148+
for (; count < 6; count++) {
149+
fraction *= 10;
150+
}
151+
}
152+
}
153+
else {
154+
seconds = 0;
155+
}
156+
fields->gmtoff = sign * (hours * 3600 + minutes * 60 + seconds);
157+
fields->gmtoff_fraction = sign * fraction;
158+
return (int)(pos - start);
133159
}
134160

135161
/* Return 1 for a complete numeric match, or 0 if fallback is required.
@@ -156,6 +182,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
156182
OFFSET = 1U << 8,
157183
WEEKDAY = 1U << 9,
158184
AMPM = 1U << 10,
185+
ZONE = 1U << 11,
159186
ANY_YEAR = YEAR | SHORT_YEAR,
160187
};
161188
static const unsigned short directive_bits[128] = {
@@ -170,6 +197,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
170197
['z'] = OFFSET,
171198
['w'] = WEEKDAY,
172199
['p'] = AMPM,
200+
['Z'] = ZONE,
173201
};
174202
/* String subclasses and non-ASCII input retain Python's behavior. */
175203
if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
@@ -211,6 +239,11 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
211239
return 0;
212240
}
213241
c = fmt[i];
242+
int require_colon = 0;
243+
if (c == ':' && i + 1 < fmt_length && fmt[i + 1] == 'z') {
244+
c = fmt[++i];
245+
require_colon = 1;
246+
}
214247
if (c == 'k') {
215248
c = 'H';
216249
}
@@ -266,14 +299,32 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
266299
continue;
267300
}
268301

269-
if (c == 'z') {
270-
if (i != fmt_length - 1 ||
271-
!strptime_offset(data, length, pos, fields)) {
302+
if (c == 'Z') {
303+
/* Local timezone names and nonterminal names retain Python's
304+
* tzset state, DST rules and regex backtracking.
305+
*/
306+
if (i != fmt_length - 1 || length - pos != 3 ||
307+
!((Py_TOLOWER(data[pos]) == 'u' &&
308+
Py_TOLOWER(data[pos + 1]) == 't' &&
309+
Py_TOLOWER(data[pos + 2]) == 'c') ||
310+
(Py_TOLOWER(data[pos]) == 'g' &&
311+
Py_TOLOWER(data[pos + 1]) == 'm' &&
312+
Py_TOLOWER(data[pos + 2]) == 't'))) {
272313
return 0;
273314
}
315+
fields->zone_start = pos;
316+
fields->zone_length = 3;
274317
pos = length;
275318
continue;
276319
}
320+
if (c == 'z') {
321+
int count = strptime_offset(data, length, pos, require_colon, fields);
322+
if (count < 0) {
323+
return 0;
324+
}
325+
pos += count;
326+
continue;
327+
}
277328

278329
int minimum = 1;
279330
int maximum = 2;

0 commit comments

Comments
 (0)