Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2785,6 +2785,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
18 changes: 18 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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not eat the dash for unsupported codes, e.g what will %-# do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an explicit check to ensure that only valid format specifiers (as documented in Doc/library/datetime.rst) are passed on Windows and Android.
For other platforms I didn’t add this validation because they currently handle unsupported specifiers silently—just echoing them back. For example for "%-#":

  • raises an error on Windows,
  • returns "#" on macOS/FreeBSD,
  • and returns "%-#" on Linux.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, removing one inconsistency you introduce another? I would expect them all to be the same as on linux, why are you raising an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the ValueError and unified the behavior for unsupported specifiers like on Linux.
Now, for example, when entering "%-#" on all systems, "%-#" will be displayed.


# 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,19 @@ 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 next_ch not in 'dmHIMSjUWVy':
push('%%-' + next_ch)
else:
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
24 changes: 24 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import textwrap
import unittest
import warnings
import platform

from array import array

Expand Down Expand Up @@ -1589,6 +1590,21 @@ 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.")

# unsupported %-format specifiers are passed through unchanged.
self.assertEqual(t.strftime("%-1"), "%-1")
self.assertEqual(t.strftime("%--"), "%--")
self.assertEqual(t.strftime("%-#"), "%-#")

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 @@ -4019,6 +4035,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.
75 changes: 75 additions & 0 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,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 @@ -1874,6 +1922,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 @@ -2003,6 +2052,31 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
}
continue;
}
/* non-0-pad Windows and Android support */
else if (ch == '-' && i < flen) {
Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i);
i++;

if (strchr("dmHIMSjUWVy", (int)next_ch) == NULL) {
replacement = PyUnicode_FromFormat("%%%%-%c", (char)next_ch);
if (replacement == NULL) {
goto Error;
}
}
else {
#if defined(MS_WINDOWS) || defined(__ANDROID__)
Py_XDECREF(dash_replacement);

dash_replacement = make_dash_replacement(object, next_ch, timetuple);
if (dash_replacement == NULL) {
goto Error;
}
replacement = dash_replacement;
#else
continue;
#endif
}
}
else {
/* percent followed by something else */
continue;
Expand Down Expand Up @@ -2041,6 +2115,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