Skip to content

Commit

Permalink
pythongh-122272: Guarantee specifiers %F and %C for datetime.strftime…
Browse files Browse the repository at this point in the history
… to be 0-padded (pythonGH-122436)
  • Loading branch information
blhsing authored Aug 23, 2024
1 parent 7cd3aa4 commit 126910e
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 14 deletions.
25 changes: 21 additions & 4 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ def _need_normalize_century():
_normalize_century = True
return _normalize_century

_supports_c99 = None
def _can_support_c99():
global _supports_c99
if _supports_c99 is None:
try:
_supports_c99 = (
_time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01")
except ValueError:
_supports_c99 = False
return _supports_c99

# Correctly substitute for %z and %Z escapes in strftime formats.
def _wrap_strftime(object, format, timetuple):
# Don't call utcoffset() or tzname() unless actually needed.
Expand Down Expand Up @@ -272,14 +283,20 @@ 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 'YG' or ch in 'FC' and _can_support_c99()) 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
17 changes: 13 additions & 4 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1710,13 +1710,22 @@ def test_strftime_y2k(self):
(1000, 0),
(1970, 0),
)
for year, offset in dataset:
for specifier in 'YG':
specifiers = 'YG'
if _time.strftime('%F', (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == '1900-01-01':
specifiers += 'FC'
for year, g_offset in dataset:
for specifier in specifiers:
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
On some platforms such as Linux, year with century was not 0-padded when formatted by :meth:`~.datetime.strftime` with C99-specific specifiers ``'%C'`` or ``'%F'``. The 0-padding behavior is now guaranteed when the format specifiers ``'%C'`` and ``'%F'`` are supported by the C library.
Patch by Ben Hsing
32 changes: 26 additions & 6 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1853,7 +1853,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,

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

assert(object && format && timetuple);
Expand Down Expand Up @@ -1950,11 +1955,18 @@ 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'
#ifdef Py_STRFTIME_C99_SUPPORT
|| ch == 'F' || ch == 'C'
#endif
) {
/* 0-pad year with century as necessary */
PyObject *item = PyTuple_GET_ITEM(timetuple, 0);
PyObject *item = PySequence_GetItem(timetuple, 0);
if (item == NULL) {
goto Done;
}
long year_long = PyLong_AsLong(item);

Py_DECREF(item);
if (year_long == -1 && PyErr_Occurred()) {
goto Done;
}
Expand All @@ -1980,8 +1992,16 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
goto Done;
}
}

ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long);
ntoappend = PyOS_snprintf(buf, sizeof(buf),
#ifdef Py_STRFTIME_C99_SUPPORT
ch == 'F' ? "%04ld-%%m-%%d" :
#endif
"%04ld", year_long);
#ifdef Py_STRFTIME_C99_SUPPORT
if (ch == 'C') {
ntoappend -= 2;
}
#endif
ptoappend = buf;
}
#endif
Expand Down
52 changes: 52 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -6703,6 +6703,34 @@ then
[Define if year with century should be normalized for strftime.])
fi

AC_CACHE_CHECK([whether C99-specific strftime specifiers are supported], [ac_cv_strftime_c99_support], [
AC_RUN_IFELSE([AC_LANG_SOURCE([[
#include <time.h>
#include <string.h>
int main(void)
{
char full_date[11];
struct tm date = {
.tm_year = 0,
.tm_mon = 0,
.tm_mday = 1
};
if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) {
return 0;
}
return 1;
}
]])],
[ac_cv_strftime_c99_support=yes],
[ac_cv_strftime_c99_support=no],
[ac_cv_strftime_c99_support=no])])
if test "$ac_cv_strftime_c99_support" = yes
then
AC_DEFINE([Py_STRFTIME_C99_SUPPORT], [1],
[Define if C99-specific strftime specifiers are supported.])
fi

dnl check for ncursesw/ncurses and panelw/panel
dnl NOTE: old curses is not detected.
dnl have_curses=[no, yes]
Expand Down
3 changes: 3 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,9 @@
/* Define if you want to enable internal statistics gathering. */
#undef Py_STATS

/* Define if C99-specific strftime specifiers are supported. */
#undef Py_STRFTIME_C99_SUPPORT

/* The version of SunOS/Solaris as reported by `uname -r' without the dot. */
#undef Py_SUNOS_VERSION

Expand Down

0 comments on commit 126910e

Please sign in to comment.