Skip to content

gh-69142: add %:z strftime format code #95983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,12 @@ requires, and these work on all platforms with a standard C implementation.
| | string if the object is | +063415, | |
| | naive). | -030712.345216 | |
+-----------+--------------------------------+------------------------+-------+
| ``%:z`` | UTC offset in the form | (empty), +00:00, | |
| | ``±HH:MM[:SS[.ffffff]]`` | -04:00, +10:30, | |
| | (empty string if the object is | +06:34:15, | |
| | naive). | -03:07:12.345216 | |
| | .. versionadded:: 3.12 | | |
+-----------+--------------------------------+------------------------+-------+
| ``%Z`` | Time zone name (empty string | (empty), UTC, GMT | \(6) |
| | if the object is naive). | | |
+-----------+--------------------------------+------------------------+-------+
Expand Down
21 changes: 11 additions & 10 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,8 +1463,8 @@ def test_strftime(self):
# test that unicode input is allowed (issue 2782)
self.assertEqual(t.strftime("%m"), "03")

# A naive object replaces %z and %Z w/ empty strings.
self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")
# A naive object replaces %z, %:z and %Z w/ empty strings.
self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''")

#make sure that invalid format specifiers are handled correctly
#self.assertRaises(ValueError, t.strftime, "%e")
Expand Down Expand Up @@ -1528,7 +1528,7 @@ def strftime(self, format_spec):

for fmt in ["m:%m d:%d y:%y",
"m:%m d:%d y:%y H:%H M:%M S:%S",
"%z %Z",
"%z %:z %Z",
]:
self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
Expand Down Expand Up @@ -2134,7 +2134,7 @@ def strftime(self, format_spec):

for fmt in ["m:%m d:%d y:%y",
"m:%m d:%d y:%y H:%H M:%M S:%S",
"%z %Z",
"%z %:z %Z",
]:
self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
Expand Down Expand Up @@ -2777,6 +2777,7 @@ def test_more_strftime(self):
tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us))
t = t.replace(tzinfo=tz)
self.assertEqual(t.strftime("%z"), "-0200" + z)
self.assertEqual(t.strftime("%:z"), "-02:00:" + z)

# bpo-34482: Check that surrogates don't cause a crash.
try:
Expand Down Expand Up @@ -3515,8 +3516,8 @@ def test_1653736(self):
def test_strftime(self):
t = self.theclass(1, 2, 3, 4)
self.assertEqual(t.strftime('%H %M %S %f'), "01 02 03 000004")
# A naive object replaces %z and %Z with empty strings.
self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")
# A naive object replaces %z, %:z and %Z with empty strings.
self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''")

# bpo-34482: Check that surrogates don't cause a crash.
try:
Expand Down Expand Up @@ -3934,10 +3935,10 @@ def test_zones(self):
self.assertEqual(repr(t4), d + "(0, 0, 0, 40)")
self.assertEqual(repr(t5), d + "(0, 0, 0, 40, tzinfo=utc)")

self.assertEqual(t1.strftime("%H:%M:%S %%Z=%Z %%z=%z"),
"07:47:00 %Z=EST %z=-0500")
self.assertEqual(t2.strftime("%H:%M:%S %Z %z"), "12:47:00 UTC +0000")
self.assertEqual(t3.strftime("%H:%M:%S %Z %z"), "13:47:00 MET +0100")
self.assertEqual(t1.strftime("%H:%M:%S %%Z=%Z %%z=%z %%:z=%:z"),
"07:47:00 %Z=EST %z=-0500 %:z=-05:00")
self.assertEqual(t2.strftime("%H:%M:%S %Z %z %:z"), "12:47:00 UTC +0000 +00:00")
self.assertEqual(t3.strftime("%H:%M:%S %Z %z %:z"), "13:47:00 MET +0100 +01:00")

yuck = FixedOffset(-1439, "%z %Z %%z%%Z")
t1 = time(23, 59, tzinfo=yuck)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add %:z strftime format code (generates tzoffset with colons as separator)
38 changes: 25 additions & 13 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,7 @@ make_freplacement(PyObject *object)

/* 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 and %f format codes via a
* giving special meanings to the %z, %:z, %Z and %f format codes via a
* preprocessing step on the format string.
* tzinfoarg is the argument to pass to the object's tzinfo method, if
* needed.
Expand All @@ -1578,6 +1578,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
PyObject *result = NULL; /* guilty until proved innocent */

PyObject *zreplacement = NULL; /* py string, replacement for %z */
PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
PyObject *freplacement = NULL; /* py string, replacement for %f */

Expand Down Expand Up @@ -1631,32 +1632,42 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
ntoappend = 1;
}
/* A % has been seen and ch is the character after it. */
else if (ch == 'z') {
if (zreplacement == NULL) {
else if (ch == 'z' || (ch == ':' && *pin == 'z' && pin++)) {
/* %z -> +HHMM, %:z -> +HH:MM */
PyObject **replacement_p;
char *sep;
if (ch == ':') {
sep = ":";
replacement_p = &colonzreplacement;
} else {
sep = "";
replacement_p = &zreplacement;
}
if (*replacement_p == NULL) {
/* format utcoffset */
char buf[100];
PyObject *tzinfo = get_tzinfo_member(object);
zreplacement = PyBytes_FromStringAndSize("", 0);
if (zreplacement == NULL) goto Done;
*replacement_p = PyBytes_FromStringAndSize("", 0);
if (*replacement_p == NULL) goto Done;
if (tzinfo != Py_None && tzinfo != NULL) {
assert(tzinfoarg != NULL);
if (format_utcoffset(buf,
sizeof(buf),
"",
sep,
tzinfo,
tzinfoarg) < 0)
goto Done;
Py_DECREF(zreplacement);
zreplacement =
Py_DECREF(*replacement_p);
*replacement_p =
PyBytes_FromStringAndSize(buf,
strlen(buf));
if (zreplacement == NULL)
if (*replacement_p == NULL)
goto Done;
}
}
assert(zreplacement != NULL);
ptoappend = PyBytes_AS_STRING(zreplacement);
ntoappend = PyBytes_GET_SIZE(zreplacement);
assert(*replacement_p != NULL);
ptoappend = PyBytes_AS_STRING(*replacement_p);
ntoappend = PyBytes_GET_SIZE(*replacement_p);
}
else if (ch == 'Z') {
/* format tzname */
Expand Down Expand Up @@ -1686,7 +1697,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
ntoappend = PyBytes_GET_SIZE(freplacement);
}
else {
/* percent followed by neither z nor Z */
/* percent followed by something else */
ptoappend = pin - 2;
ntoappend = 2;
}
Expand Down Expand Up @@ -1733,6 +1744,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
Done:
Py_XDECREF(freplacement);
Py_XDECREF(zreplacement);
Py_XDECREF(colonzreplacement);
Py_XDECREF(Zreplacement);
Py_XDECREF(newfmt);
return result;
Expand Down