summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblhsing <blhsing@gmail.com>2024-08-23 15:45:03 (GMT)
committerGitHub <noreply@github.com>2024-08-23 15:45:03 (GMT)
commit126910edba812a01794f307b0cfa2a7f02bda190 (patch)
treee86e74c3e38f2b8cfcbe9484dc696a0c52767a3f
parent7cd3aa42f0cf72bf9a214e2630850879fe078377 (diff)
downloadcpython-126910edba812a01794f307b0cfa2a7f02bda190.zip
cpython-126910edba812a01794f307b0cfa2a7f02bda190.tar.gz
cpython-126910edba812a01794f307b0cfa2a7f02bda190.tar.bz2
gh-122272: Guarantee specifiers %F and %C for datetime.strftime to be 0-padded (GH-122436)
-rw-r--r--Lib/_pydatetime.py25
-rw-r--r--Lib/test/datetimetester.py17
-rw-r--r--Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst2
-rw-r--r--Modules/_datetimemodule.c32
-rwxr-xr-xconfigure52
-rw-r--r--configure.ac28
-rw-r--r--pyconfig.h.in3
7 files changed, 145 insertions, 14 deletions
diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index 27cacb8..78432d4 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -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.
+ # 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)
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 38de110..0265643 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -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
diff --git a/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst
new file mode 100644
index 0000000..943010b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst
@@ -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
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 67b49aa..79314e0 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -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
diff --git a/configure b/configure
index c28c333..2c58af3 100755
--- a/configure
+++ b/configure
@@ -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
diff --git a/configure.ac b/configure.ac
index 9daace7..3c1dc1c 100644
--- a/configure.ac
+++ b/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]
diff --git a/pyconfig.h.in b/pyconfig.h.in
index 39978d1..a5946f3 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -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