Skip to content
Open
7 changes: 7 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2766,6 +2766,13 @@ Notes:
for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``,
``%W``, and ``%V``. Format ``%y`` does require a leading zero.

When used with the :meth:`~.datetime.strftime` method, leading zeroes
are included by default for formats ``%d``, ``%m``, ``%H``, ``%I``,
``%M``, ``%S``, ``%j``, ``%U``, ``%W``, ``%V`` and ``%y``.
The ``%-`` flag (for example, ``%-d``) will produce non-zero-padded
output, except for ``%-y`` on Apple platforms and FreeBSD,
which is still zero-padded.

(10)
When parsing a month and day using :meth:`~.datetime.strptime`, always
include a year in the format. If the value you need to parse lacks a year,
Expand Down
15 changes: 15 additions & 0 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 in ['win32', 'android']:
push(_make_dash_replacement(next_ch, timetuple))
else:
push('%-' + next_ch)
else:
push('%-')
else:
push('%')
push(ch)
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import textwrap
import unittest
import warnings
import platform

from array import array

Expand Down Expand Up @@ -1588,6 +1589,16 @@ def test_strftime(self):
self.assertEqual(t.strftime(""), "") # SF bug #761337
self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784

# See gh-137165
if platform.system() in ('Darwin', 'iOS', 'FreeBSD'):
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.assertEqual(t.strftime("%-j. %-U. %-W. %-V."), "61. 9. 9. 9.")

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
Expand Down Expand Up @@ -3890,6 +3901,14 @@ def test_strftime(self):
# A naive object replaces %z, %:z and %Z with empty strings.
self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''")

# See gh-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")

t_zero = self.theclass(0, 0, 0, 4)
self.assertEqual(t_zero.strftime('%-H %-M %-S %f'), "0 0 0 000004")

# bpo-34482: Check that surrogates don't cause a crash.
try:
t.strftime('%H\ud800%M')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Standardized non-zero-padded numeric formatting for dates and times in
:func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across
all platforms.
64 changes: 64 additions & 0 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,54 @@ 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;
}

if (PyUnicode_GET_LENGTH(stripped) == 0) {
Py_DECREF(stripped);
stripped = PyUnicode_FromString("0");
}

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
Expand All @@ -1873,6 +1921,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
PyObject *freplacement = NULL; /* py string, replacement for %f */
PyObject *dash_replacement = NULL; /* py string, replacement for %- */

assert(object && format && timetuple);
assert(PyUnicode_Check(format));
Expand Down Expand Up @@ -2002,6 +2051,20 @@ 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++;

Py_XDECREF(dash_replacement);
dash_replacement = make_dash_replacement(object, next_ch, timetuple);
if (dash_replacement == NULL) {
goto Error;
}
replacement = dash_replacement;
}
#endif
else {
/* percent followed by something else */
continue;
Expand Down Expand Up @@ -2040,6 +2103,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
Py_XDECREF(zreplacement);
Py_XDECREF(colonzreplacement);
Py_XDECREF(Zreplacement);
Py_XDECREF(dash_replacement);
Py_XDECREF(strftime);
return result;

Expand Down
Loading