mirror of https://github.com/python/cpython
gh-122272: Guarantee specifiers %F and %C for datetime.strftime to be 0-padded (GH-122436)
This commit is contained in:
parent
7cd3aa42f0
commit
126910edba
|
@ -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.
|
||||
|
@ -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.
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -1710,13 +1710,22 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
|
|||
(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
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
|
|
@ -26196,6 +26196,58 @@ printf "%s\n" "#define Py_NORMALIZE_CENTURY 1" >>confdefs.h
|
|||
|
||||
fi
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether C99-specific strftime specifiers are supported" >&5
|
||||
printf %s "checking whether C99-specific strftime specifiers are supported... " >&6; }
|
||||
if test ${ac_cv_strftime_c99_support+y}
|
||||
then :
|
||||
printf %s "(cached) " >&6
|
||||
else $as_nop
|
||||
|
||||
if test "$cross_compiling" = yes
|
||||
then :
|
||||
ac_cv_strftime_c99_support=no
|
||||
else $as_nop
|
||||
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
|
||||
/* end confdefs.h. */
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
_ACEOF
|
||||
if ac_fn_c_try_run "$LINENO"
|
||||
then :
|
||||
ac_cv_strftime_c99_support=yes
|
||||
else $as_nop
|
||||
ac_cv_strftime_c99_support=no
|
||||
fi
|
||||
rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
|
||||
conftest.$ac_objext conftest.beam conftest.$ac_ext
|
||||
fi
|
||||
|
||||
fi
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_strftime_c99_support" >&5
|
||||
printf "%s\n" "$ac_cv_strftime_c99_support" >&6; }
|
||||
if test "$ac_cv_strftime_c99_support" = yes
|
||||
then
|
||||
|
||||
printf "%s\n" "#define Py_STRFTIME_C99_SUPPORT 1" >>confdefs.h
|
||||
|
||||
fi
|
||||
|
||||
have_curses=no
|
||||
have_panel=no
|
||||
|
||||
|
|
28
configure.ac
28
configure.ac
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue