diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..50066df792fc7d 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -213,6 +213,11 @@ def _need_normalize_century(): _normalize_century = True return _normalize_century +def _make_dash_replacement(ch, timetuple): + fmt = '%' + ch + val = _time.strftime(fmt, timetuple) + return val.lstrip('0') or '0' + # 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. @@ -284,6 +289,16 @@ def _wrap_strftime(object, format, timetuple): push('{:04}'.format(year)) if ch == 'F': push('-{:02}-{:02}'.format(*timetuple[1:3])) + elif ch == '-': + if i < n: + next_ch = format[i] + i += 1 + if sys.platform.startswith('win') or sys.platform.startswith('android'): + push(_make_dash_replacement(next_ch, timetuple)) + else: + push('%-' + next_ch) + else: + push('%-') else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2299d1fab2e73d..5031f2629e7e95 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -14,6 +14,7 @@ import textwrap import unittest import warnings +import platform from array import array @@ -1588,6 +1589,14 @@ def test_strftime(self): self.assertEqual(t.strftime(""), "") # SF bug #761337 self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + # SF bug #137165 + if platform.system() == 'Darwin': + self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") + else: + if platform.system() == 'Windows': + self.assertEqual(t.strftime("m:%#m d:%#d y:%#y"), "m:3 d:2 y:5") + self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") + self.assertRaises(TypeError, t.strftime) # needs an arg self.assertRaises(TypeError, t.strftime, "one", "two") # too many args self.assertRaises(TypeError, t.strftime, 42) # arg wrong type @@ -3890,6 +3899,11 @@ def test_strftime(self): # A naive object replaces %z, %:z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") + # SF bug #137165 + self.assertEqual(t.strftime('%-H %-M %-S %f'), "1 2 3 000004") + if platform.system() == 'Windows': + self.assertEqual(t.strftime('%#H %#M %#S %f'), "1 2 3 000004") + # bpo-34482: Check that surrogates don't cause a crash. try: t.strftime('%H\ud800%M') diff --git a/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst b/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst new file mode 100644 index 00000000000000..eb8c562f28dfc8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst @@ -0,0 +1,3 @@ +Standardized non-zero-padded date formatting in +:func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across +Windows and Unix. (e.g. ``"m:%-m d:%-d y:%-y"``). diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 12d316985fceb9..38321a7cb5114b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1856,6 +1856,49 @@ make_freplacement(PyObject *object) return PyUnicode_FromString(freplacement); } +#if defined(MS_WINDOWS) || defined(__ANDROID__) +static PyObject * +make_dash_replacement(PyObject *object, Py_UCS4 ch, PyObject *timetuple) +{ + PyObject *strftime = NULL; + PyObject *fmt_obj = NULL; + PyObject *res = NULL; + PyObject *stripped = NULL; + + strftime = PyImport_ImportModuleAttrString("time", "strftime"); + if (strftime == NULL) { + goto error; + } + + fmt_obj = PyUnicode_FromFormat("%%%c", (char)ch); + if (fmt_obj == NULL) { + goto error; + } + + res = PyObject_CallFunctionObjArgs(strftime, fmt_obj, timetuple, NULL); + if (res == NULL) { + goto error; + } + + stripped = PyObject_CallMethod(res, "lstrip", "s", "0"); + if (stripped == NULL) { + goto error; + } + + Py_DECREF(fmt_obj); + Py_DECREF(strftime); + Py_DECREF(res); + return stripped; + +error: + Py_XDECREF(fmt_obj); + Py_XDECREF(strftime); + Py_XDECREF(res); + Py_XDECREF(stripped); + return NULL; +} +#endif + /* I sure don't want to reproduce the strftime code from the time module, * so this imports the module and calls it. All the hair is due to * giving special meanings to the %z, %:z, %Z and %f format codes via a @@ -2002,6 +2045,17 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } continue; } + #if defined(MS_WINDOWS) || defined(__ANDROID__) + /* non-0-pad Windows and Android support */ + else if (ch == '-' && i < flen) { + Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); + i++; + replacement = make_dash_replacement(object, next_ch, timetuple); + if (replacement == NULL) { + goto Error; + } + } + #endif else { /* percent followed by something else */ continue;