mirror of https://github.com/python/cpython
[3.13] gh-120713: Normalize year with century for datetime.strftime (GH-120820) (GH-121144)
(cherry picked from commit 6d34938dc8
)
Co-authored-by: blhsing <blhsing@gmail.com>
This commit is contained in:
parent
cbbd95328d
commit
009618f112
|
@ -204,6 +204,17 @@ def _format_offset(off, sep=':'):
|
|||
s += '.%06d' % ss.microseconds
|
||||
return s
|
||||
|
||||
_normalize_century = None
|
||||
def _need_normalize_century():
|
||||
global _normalize_century
|
||||
if _normalize_century is None:
|
||||
try:
|
||||
_normalize_century = (
|
||||
_time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) != "0099")
|
||||
except ValueError:
|
||||
_normalize_century = True
|
||||
return _normalize_century
|
||||
|
||||
# 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.
|
||||
|
@ -261,6 +272,14 @@ 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.
|
||||
if ch == 'G':
|
||||
year = int(_time.strftime("%G", timetuple))
|
||||
else:
|
||||
year = object.year
|
||||
push('{:04}'.format(year))
|
||||
else:
|
||||
push('%')
|
||||
push(ch)
|
||||
|
|
|
@ -1716,18 +1716,26 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
|
|||
self.assertTrue(self.theclass.max)
|
||||
|
||||
def test_strftime_y2k(self):
|
||||
for y in (1, 49, 70, 99, 100, 999, 1000, 1970):
|
||||
d = self.theclass(y, 1, 1)
|
||||
# Issue 13305: For years < 1000, the value is not always
|
||||
# padded to 4 digits across platforms. The C standard
|
||||
# assumes year >= 1900, so it does not specify the number
|
||||
# of digits.
|
||||
if d.strftime("%Y") != '%04d' % y:
|
||||
# Year 42 returns '42', not padded
|
||||
self.assertEqual(d.strftime("%Y"), '%d' % y)
|
||||
# '0042' is obtained anyway
|
||||
if support.has_strftime_extensions:
|
||||
self.assertEqual(d.strftime("%4Y"), '%04d' % y)
|
||||
# Test that years less than 1000 are 0-padded; note that the beginning
|
||||
# of an ISO 8601 year may fall in an ISO week of the year before, and
|
||||
# therefore needs an offset of -1 when formatting with '%G'.
|
||||
dataset = (
|
||||
(1, 0),
|
||||
(49, -1),
|
||||
(70, 0),
|
||||
(99, 0),
|
||||
(100, -1),
|
||||
(999, 0),
|
||||
(1000, 0),
|
||||
(1970, 0),
|
||||
)
|
||||
for year, offset in dataset:
|
||||
for specifier in 'YG':
|
||||
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}")
|
||||
|
||||
def test_replace(self):
|
||||
cls = self.theclass
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
:meth:`datetime.datetime.strftime` now 0-pads years with less than four digits for the format specifiers ``%Y`` and ``%G`` on Linux.
|
||||
Patch by Ben Hsing
|
|
@ -1848,6 +1848,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
|||
const char *ptoappend; /* ptr to string to append to output buffer */
|
||||
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];
|
||||
#endif
|
||||
|
||||
assert(object && format && timetuple);
|
||||
assert(PyUnicode_Check(format));
|
||||
/* Convert the input format to a C string and size */
|
||||
|
@ -1855,6 +1860,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
|||
if (!pin)
|
||||
return NULL;
|
||||
|
||||
PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
|
||||
if (strftime == NULL) {
|
||||
goto Done;
|
||||
}
|
||||
|
||||
/* Scan the input format, looking for %z/%Z/%f escapes, building
|
||||
* a new format. Since computing the replacements for those codes
|
||||
* is expensive, don't unless they're actually used.
|
||||
|
@ -1936,8 +1946,47 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
|||
ptoappend = PyBytes_AS_STRING(freplacement);
|
||||
ntoappend = PyBytes_GET_SIZE(freplacement);
|
||||
}
|
||||
#ifdef Py_NORMALIZE_CENTURY
|
||||
else if (ch == 'Y' || ch == 'G') {
|
||||
/* 0-pad year with century as necessary */
|
||||
PyObject *item = PyTuple_GET_ITEM(timetuple, 0);
|
||||
long year_long = PyLong_AsLong(item);
|
||||
|
||||
if (year_long == -1 && PyErr_Occurred()) {
|
||||
goto Done;
|
||||
}
|
||||
/* Note that datetime(1000, 1, 1).strftime('%G') == '1000' so year
|
||||
1000 for %G can go on the fast path. */
|
||||
if (year_long >= 1000) {
|
||||
goto PassThrough;
|
||||
}
|
||||
if (ch == 'G') {
|
||||
PyObject *year_str = PyObject_CallFunction(strftime, "sO",
|
||||
"%G", timetuple);
|
||||
if (year_str == NULL) {
|
||||
goto Done;
|
||||
}
|
||||
PyObject *year = PyNumber_Long(year_str);
|
||||
Py_DECREF(year_str);
|
||||
if (year == NULL) {
|
||||
goto Done;
|
||||
}
|
||||
year_long = PyLong_AsLong(year);
|
||||
Py_DECREF(year);
|
||||
if (year_long == -1 && PyErr_Occurred()) {
|
||||
goto Done;
|
||||
}
|
||||
}
|
||||
|
||||
ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long);
|
||||
ptoappend = buf;
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
/* percent followed by something else */
|
||||
#ifdef Py_NORMALIZE_CENTURY
|
||||
PassThrough:
|
||||
#endif
|
||||
ptoappend = pin - 2;
|
||||
ntoappend = 2;
|
||||
}
|
||||
|
@ -1969,17 +2018,13 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
|||
goto Done;
|
||||
{
|
||||
PyObject *format;
|
||||
PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
|
||||
|
||||
if (strftime == NULL)
|
||||
goto Done;
|
||||
format = PyUnicode_FromString(PyBytes_AS_STRING(newfmt));
|
||||
if (format != NULL) {
|
||||
result = PyObject_CallFunctionObjArgs(strftime,
|
||||
format, timetuple, NULL);
|
||||
Py_DECREF(format);
|
||||
}
|
||||
Py_DECREF(strftime);
|
||||
}
|
||||
Done:
|
||||
Py_XDECREF(freplacement);
|
||||
|
@ -1987,6 +2032,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
|||
Py_XDECREF(colonzreplacement);
|
||||
Py_XDECREF(Zreplacement);
|
||||
Py_XDECREF(newfmt);
|
||||
Py_XDECREF(strftime);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -25866,6 +25866,58 @@ printf "%s\n" "#define HAVE_STAT_TV_NSEC2 1" >>confdefs.h
|
|||
|
||||
fi
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether year with century should be normalized for strftime" >&5
|
||||
printf %s "checking whether year with century should be normalized for strftime... " >&6; }
|
||||
if test ${ac_cv_normalize_century+y}
|
||||
then :
|
||||
printf %s "(cached) " >&6
|
||||
else $as_nop
|
||||
|
||||
if test "$cross_compiling" = yes
|
||||
then :
|
||||
ac_cv_normalize_century=yes
|
||||
else $as_nop
|
||||
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
|
||||
/* end confdefs.h. */
|
||||
|
||||
#include <time.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char year[5];
|
||||
struct tm date = {
|
||||
.tm_year = -1801,
|
||||
.tm_mon = 0,
|
||||
.tm_mday = 1
|
||||
};
|
||||
if (strftime(year, sizeof(year), "%Y", &date) && !strcmp(year, "0099")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
_ACEOF
|
||||
if ac_fn_c_try_run "$LINENO"
|
||||
then :
|
||||
ac_cv_normalize_century=yes
|
||||
else $as_nop
|
||||
ac_cv_normalize_century=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_normalize_century" >&5
|
||||
printf "%s\n" "$ac_cv_normalize_century" >&6; }
|
||||
if test "$ac_cv_normalize_century" = yes
|
||||
then
|
||||
|
||||
printf "%s\n" "#define Py_NORMALIZE_CENTURY 1" >>confdefs.h
|
||||
|
||||
fi
|
||||
|
||||
have_curses=no
|
||||
have_panel=no
|
||||
|
||||
|
|
28
configure.ac
28
configure.ac
|
@ -6567,6 +6567,34 @@ then
|
|||
[Define if you have struct stat.st_mtimensec])
|
||||
fi
|
||||
|
||||
AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [
|
||||
AC_RUN_IFELSE([AC_LANG_SOURCE([[
|
||||
#include <time.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char year[5];
|
||||
struct tm date = {
|
||||
.tm_year = -1801,
|
||||
.tm_mon = 0,
|
||||
.tm_mday = 1
|
||||
};
|
||||
if (strftime(year, sizeof(year), "%Y", &date) && !strcmp(year, "0099")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
]])],
|
||||
[ac_cv_normalize_century=yes],
|
||||
[ac_cv_normalize_century=no],
|
||||
[ac_cv_normalize_century=yes])])
|
||||
if test "$ac_cv_normalize_century" = yes
|
||||
then
|
||||
AC_DEFINE([Py_NORMALIZE_CENTURY], [1],
|
||||
[Define if year with century should be normalized for strftime.])
|
||||
fi
|
||||
|
||||
dnl check for ncurses/ncursesw and panel/panelw
|
||||
dnl NOTE: old curses is not detected.
|
||||
dnl have_curses=[no, ncursesw, ncurses]
|
||||
|
|
|
@ -1659,6 +1659,9 @@
|
|||
SipHash13: 3, externally defined: 0 */
|
||||
#undef Py_HASH_ALGORITHM
|
||||
|
||||
/* Define if year with century should be normalized for strftime. */
|
||||
#undef Py_NORMALIZE_CENTURY
|
||||
|
||||
/* Define if rl_startup_hook takes arguments */
|
||||
#undef Py_RL_STARTUP_HOOK_TAKES_ARGS
|
||||
|
||||
|
|
Loading…
Reference in New Issue