]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-122272: Guarantee specifiers %F and %C for datetime.strftime to be 0-padded (GH...
authorblhsing <blhsing@gmail.com>
Fri, 23 Aug 2024 15:45:03 +0000 (23:45 +0800)
committerGitHub <noreply@github.com>
Fri, 23 Aug 2024 15:45:03 +0000 (18:45 +0300)
Lib/_pydatetime.py
Lib/test/datetimetester.py
Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst [new file with mode: 0644]
Modules/_datetimemodule.c
configure
configure.ac
pyconfig.h.in

index 27cacb8e01ff3b676976050032c40d71bcaa8c50..78432d46506be8646b0dcc93f524153a4ae41904 100644 (file)
@@ -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)
index 38de1101072e52cd4f517acf623b83e1fa565111..02656434f4ac07836229846790b0426485328d33 100644 (file)
@@ -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 (file)
index 0000000..943010b
--- /dev/null
@@ -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
index 67b49aa6ac2301e5321074dc3a738da6485442df..79314e06c82f6965646915b4be55628e0dbd5210 100644 (file)
@@ -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
index c28c3335502ac4b4ab9271ba8bc5c0c853478921..2c58af3eace84ac3b652d8c3e3cc2c31f328155e 100755 (executable)
--- 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
 
index 9daace717938af9cd23b727014869cab9a0389aa..3c1dc1cc5018d4db07ae8ab5f1fc2a43fcd65cce 100644 (file)
@@ -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]
index 39978d11e8c1cb9da530140cddd1716a87bad130..a5946f3547b35cf805188e0ec1a7bae9b740a9bd 100644 (file)
 /* 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