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
|
s += '.%06d' % ss.microseconds
|
||||||
return s
|
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.
|
# Correctly substitute for %z and %Z escapes in strftime formats.
|
||||||
def _wrap_strftime(object, format, timetuple):
|
def _wrap_strftime(object, format, timetuple):
|
||||||
# Don't call utcoffset() or tzname() unless actually needed.
|
# 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 %
|
# strftime is going to have at this: escape %
|
||||||
Zreplace = s.replace('%', '%%')
|
Zreplace = s.replace('%', '%%')
|
||||||
newformat.append(Zreplace)
|
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:
|
else:
|
||||||
push('%')
|
push('%')
|
||||||
push(ch)
|
push(ch)
|
||||||
|
|
|
@ -1716,18 +1716,26 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
|
||||||
self.assertTrue(self.theclass.max)
|
self.assertTrue(self.theclass.max)
|
||||||
|
|
||||||
def test_strftime_y2k(self):
|
def test_strftime_y2k(self):
|
||||||
for y in (1, 49, 70, 99, 100, 999, 1000, 1970):
|
# Test that years less than 1000 are 0-padded; note that the beginning
|
||||||
d = self.theclass(y, 1, 1)
|
# of an ISO 8601 year may fall in an ISO week of the year before, and
|
||||||
# Issue 13305: For years < 1000, the value is not always
|
# therefore needs an offset of -1 when formatting with '%G'.
|
||||||
# padded to 4 digits across platforms. The C standard
|
dataset = (
|
||||||
# assumes year >= 1900, so it does not specify the number
|
(1, 0),
|
||||||
# of digits.
|
(49, -1),
|
||||||
if d.strftime("%Y") != '%04d' % y:
|
(70, 0),
|
||||||
# Year 42 returns '42', not padded
|
(99, 0),
|
||||||
self.assertEqual(d.strftime("%Y"), '%d' % y)
|
(100, -1),
|
||||||
# '0042' is obtained anyway
|
(999, 0),
|
||||||
if support.has_strftime_extensions:
|
(1000, 0),
|
||||||
self.assertEqual(d.strftime("%4Y"), '%04d' % y)
|
(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):
|
def test_replace(self):
|
||||||
cls = self.theclass
|
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 */
|
const char *ptoappend; /* ptr to string to append to output buffer */
|
||||||
Py_ssize_t ntoappend; /* # of bytes 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(object && format && timetuple);
|
||||||
assert(PyUnicode_Check(format));
|
assert(PyUnicode_Check(format));
|
||||||
/* Convert the input format to a C string and size */
|
/* Convert the input format to a C string and size */
|
||||||
|
@ -1855,6 +1860,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
||||||
if (!pin)
|
if (!pin)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
|
||||||
|
if (strftime == NULL) {
|
||||||
|
goto Done;
|
||||||
|
}
|
||||||
|
|
||||||
/* Scan the input format, looking for %z/%Z/%f escapes, building
|
/* Scan the input format, looking for %z/%Z/%f escapes, building
|
||||||
* a new format. Since computing the replacements for those codes
|
* a new format. Since computing the replacements for those codes
|
||||||
* is expensive, don't unless they're actually used.
|
* 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);
|
ptoappend = PyBytes_AS_STRING(freplacement);
|
||||||
ntoappend = PyBytes_GET_SIZE(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 {
|
else {
|
||||||
/* percent followed by something else */
|
/* percent followed by something else */
|
||||||
|
#ifdef Py_NORMALIZE_CENTURY
|
||||||
|
PassThrough:
|
||||||
|
#endif
|
||||||
ptoappend = pin - 2;
|
ptoappend = pin - 2;
|
||||||
ntoappend = 2;
|
ntoappend = 2;
|
||||||
}
|
}
|
||||||
|
@ -1969,17 +2018,13 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
||||||
goto Done;
|
goto Done;
|
||||||
{
|
{
|
||||||
PyObject *format;
|
PyObject *format;
|
||||||
PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime");
|
|
||||||
|
|
||||||
if (strftime == NULL)
|
|
||||||
goto Done;
|
|
||||||
format = PyUnicode_FromString(PyBytes_AS_STRING(newfmt));
|
format = PyUnicode_FromString(PyBytes_AS_STRING(newfmt));
|
||||||
if (format != NULL) {
|
if (format != NULL) {
|
||||||
result = PyObject_CallFunctionObjArgs(strftime,
|
result = PyObject_CallFunctionObjArgs(strftime,
|
||||||
format, timetuple, NULL);
|
format, timetuple, NULL);
|
||||||
Py_DECREF(format);
|
Py_DECREF(format);
|
||||||
}
|
}
|
||||||
Py_DECREF(strftime);
|
|
||||||
}
|
}
|
||||||
Done:
|
Done:
|
||||||
Py_XDECREF(freplacement);
|
Py_XDECREF(freplacement);
|
||||||
|
@ -1987,6 +2032,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
|
||||||
Py_XDECREF(colonzreplacement);
|
Py_XDECREF(colonzreplacement);
|
||||||
Py_XDECREF(Zreplacement);
|
Py_XDECREF(Zreplacement);
|
||||||
Py_XDECREF(newfmt);
|
Py_XDECREF(newfmt);
|
||||||
|
Py_XDECREF(strftime);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25866,6 +25866,58 @@ printf "%s\n" "#define HAVE_STAT_TV_NSEC2 1" >>confdefs.h
|
||||||
|
|
||||||
fi
|
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_curses=no
|
||||||
have_panel=no
|
have_panel=no
|
||||||
|
|
||||||
|
|
28
configure.ac
28
configure.ac
|
@ -6567,6 +6567,34 @@ then
|
||||||
[Define if you have struct stat.st_mtimensec])
|
[Define if you have struct stat.st_mtimensec])
|
||||||
fi
|
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 check for ncurses/ncursesw and panel/panelw
|
||||||
dnl NOTE: old curses is not detected.
|
dnl NOTE: old curses is not detected.
|
||||||
dnl have_curses=[no, ncursesw, ncurses]
|
dnl have_curses=[no, ncursesw, ncurses]
|
||||||
|
|
|
@ -1659,6 +1659,9 @@
|
||||||
SipHash13: 3, externally defined: 0 */
|
SipHash13: 3, externally defined: 0 */
|
||||||
#undef Py_HASH_ALGORITHM
|
#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 */
|
/* Define if rl_startup_hook takes arguments */
|
||||||
#undef Py_RL_STARTUP_HOOK_TAKES_ARGS
|
#undef Py_RL_STARTUP_HOOK_TAKES_ARGS
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue