Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
gh-122272: guarantee specifiers %F and %C for datetime.strftime to be…
… 0-padded
  • Loading branch information
blhsing committed Jul 30, 2024
commit 61c2c611ec7071c1ae19a8847877d0c0308b3fd2
13 changes: 9 additions & 4 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,19 @@ def _wrap_strftime(object, format, timetuple):
# strftime is going to have at this: escape %
Zreplace = s.replace('%', '%%')
newformat.append(Zreplace)
elif ch in 'YG' and object.year < 1000 and _need_normalize_century():
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
# year 1000 for %G can go on the fast path.
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
# year 1000 for %G can go on the fast path.
elif ch in 'YGFC' and object.year < 1000 and _need_normalize_century():
if ch == 'G':
year = int(_time.strftime("%G", timetuple))
else:
year = object.year
push('{:04}'.format(year))
if ch == 'C':
push('{:02}'.format(year // 100))
else:
push('{:04}'.format(year))
if ch == 'F':
push('-{:02}-{:02}'.format(*timetuple[1:3]))
else:
push('%')
push(ch)
Expand Down
14 changes: 10 additions & 4 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1710,13 +1710,19 @@ def test_strftime_y2k(self):
(1000, 0),
(1970, 0),
)
for year, offset in dataset:
for specifier in 'YG':
for year, g_offset in dataset:
for specifier in 'YGFC':
with self.subTest(year=year, specifier=specifier):
d = self.theclass(year, 1, 1)
if specifier == 'G':
year += offset
self.assertEqual(d.strftime(f"%{specifier}"), f"{year:04d}")
year += g_offset
if specifier == 'C':
expected = f"{year // 100:02d}"
else:
expected = f"{year:04d}"
if specifier == 'F':
expected += f"-01-01"
self.assertEqual(d.strftime(f"%{specifier}"), expected)

def test_replace(self):
cls = self.theclass
Expand Down
30 changes: 25 additions & 5 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1852,8 +1852,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
Py_ssize_t ntoappend; /* # of bytes to append to output buffer */

#ifdef Py_NORMALIZE_CENTURY
/* Buffer of maximum size of formatted year permitted by long. */
char buf[SIZEOF_LONG*5/2+2];
/* Buffer of maximum size of formatted year permitted by long.
* Adding 6 just to accomodate %F with dashes, 2-digit month and day.
*/
char buf[SIZEOF_LONG*5/2+2+6];
#endif

assert(object && format && timetuple);
Expand Down Expand Up @@ -1950,7 +1952,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
ntoappend = PyBytes_GET_SIZE(freplacement);
}
#ifdef Py_NORMALIZE_CENTURY
else if (ch == 'Y' || ch == 'G') {
else if (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C') {
/* 0-pad year with century as necessary */
PyObject *item = PyTuple_GET_ITEM(timetuple, 0);
long year_long = PyLong_AsLong(item);
Expand Down Expand Up @@ -1980,8 +1982,26 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
goto Done;
}
}

ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long);
if (ch == 'F') {
item = PyTuple_GET_ITEM(timetuple, 1);
long month = PyLong_AsLong(item);
if (month == -1 && PyErr_Occurred()) {
goto Done;
}
item = PyTuple_GET_ITEM(timetuple, 2);
long day = PyLong_AsLong(item);
if (day == -1 && PyErr_Occurred()) {
goto Done;
}
ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld-%02ld-%02ld",
year_long, month, day);
Comment thread
blhsing marked this conversation as resolved.
Outdated
}
else {
ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long);
if (ch == 'C') {
ntoappend -= 2;
}
}
ptoappend = buf;
}
#endif
Expand Down