Skip to content
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

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

Merged
merged 17 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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 | | |
ThomasWaldmann marked this conversation as resolved.
Show resolved Hide resolved
+-----------+--------------------------------+------------------------+-------+
| ``%Z`` | Time zone name (empty string | (empty), UTC, GMT | \(6) |
| | if the object is naive). | | |
+-----------+--------------------------------+------------------------+-------+
Expand Down
45 changes: 24 additions & 21 deletions Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def _format_time(hh, mm, ss, us, timespec='auto'):
else:
return fmt.format(hh, mm, ss, us)

def _format_offset(off):
def _format_offset(off, sep=':'):
s = ''
if off is not None:
if off.days < 0:
Expand All @@ -189,9 +189,9 @@ def _format_offset(off):
sign = "+"
hh, mm = divmod(off, timedelta(hours=1))
mm, ss = divmod(mm, timedelta(minutes=1))
s += "%s%02d:%02d" % (sign, hh, mm)
s += "%s%02d%s%02d" % (sign, hh, sep, mm)
if ss or ss.microseconds:
s += ":%02d" % ss.seconds
s += "%s%02d" % (sep, ss.seconds)

if ss.microseconds:
s += '.%06d' % ss.microseconds
Expand All @@ -202,9 +202,10 @@ def _wrap_strftime(object, format, timetuple):
# Don't call utcoffset() or tzname() unless actually needed.
freplace = None # the string to use for %f
zreplace = None # the string to use for %z
colonzreplace = None # the string to use for %:z
Zreplace = None # the string to use for %Z

# Scan format for %z and %Z escapes, replacing as needed.
# Scan format for %z, %:z and %Z escapes, replacing as needed.
newformat = []
push = newformat.append
i, n = 0, len(format)
Expand All @@ -222,26 +223,28 @@ def _wrap_strftime(object, format, timetuple):
newformat.append(freplace)
elif ch == 'z':
if zreplace is None:
zreplace = ""
if hasattr(object, "utcoffset"):
offset = object.utcoffset()
if offset is not None:
sign = '+'
if offset.days < 0:
offset = -offset
sign = '-'
h, rest = divmod(offset, timedelta(hours=1))
m, rest = divmod(rest, timedelta(minutes=1))
s = rest.seconds
u = offset.microseconds
if u:
zreplace = '%c%02d%02d%02d.%06d' % (sign, h, m, s, u)
elif s:
zreplace = '%c%02d%02d%02d' % (sign, h, m, s)
else:
zreplace = '%c%02d%02d' % (sign, h, m)
zreplace = _format_offset(object.utcoffset(), sep="")
else:
zreplace = ""
assert '%' not in zreplace
newformat.append(zreplace)
elif ch == ':':
if i < n:
ch2 = format[i]
i += 1
if ch2 == 'z':
if colonzreplace is None:
if hasattr(object, "utcoffset"):
colonzreplace = _format_offset(object.utcoffset(), sep=":")
else:
colonzreplace = ""
assert '%' not in colonzreplace
newformat.append(colonzreplace)
else:
push('%')
push(ch)
push(ch2)
elif ch == 'Z':
if Zreplace is None:
Zreplace = ""
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)
merwok marked this conversation as resolved.
Show resolved Hide resolved
69 changes: 47 additions & 22 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,32 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
return 0;
}

static PyObject *
make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg)
{
char buf[100];
PyObject *tzinfo = get_tzinfo_member(object);
PyObject *replacement = PyBytes_FromStringAndSize(NULL, 0);

if (replacement == NULL)
return NULL;
if (tzinfo == Py_None || tzinfo == NULL)
return replacement;

Py_DECREF(replacement);

assert(tzinfoarg != NULL);
if (format_utcoffset(buf,
sizeof(buf),
sep,
tzinfo,
tzinfoarg) < 0)
return NULL;

replacement = PyBytes_FromStringAndSize(buf, strlen(buf));
return replacement;
ThomasWaldmann marked this conversation as resolved.
Show resolved Hide resolved
}

static PyObject *
make_Zreplacement(PyObject *object, PyObject *tzinfoarg)
{
Expand Down Expand Up @@ -1566,7 +1592,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 +1604,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 */
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
PyObject *freplacement = NULL; /* py string, replacement for %f */

Expand Down Expand Up @@ -1632,32 +1659,29 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
}
/* A % has been seen and ch is the character after it. */
else if (ch == 'z') {
/* %z -> +HHMM */
if (zreplacement == NULL) {
/* format utcoffset */
char buf[100];
PyObject *tzinfo = get_tzinfo_member(object);
zreplacement = PyBytes_FromStringAndSize("", 0);
if (zreplacement == NULL) goto Done;
if (tzinfo != Py_None && tzinfo != NULL) {
assert(tzinfoarg != NULL);
if (format_utcoffset(buf,
sizeof(buf),
"",
tzinfo,
tzinfoarg) < 0)
goto Done;
Py_DECREF(zreplacement);
zreplacement =
PyBytes_FromStringAndSize(buf,
strlen(buf));
if (zreplacement == NULL)
goto Done;
}
zreplacement = make_somezreplacement(object, "", tzinfoarg);
if (zreplacement == NULL)
goto Done;
}
assert(zreplacement != NULL);
assert(PyBytes_Check(zreplacement));
ThomasWaldmann marked this conversation as resolved.
Show resolved Hide resolved
ptoappend = PyBytes_AS_STRING(zreplacement);
ntoappend = PyBytes_GET_SIZE(zreplacement);
}
else if (ch == ':' && *pin == 'z' && pin++) {
/* %:z -> +HH:MM */
if (colonzreplacement == NULL) {
colonzreplacement = make_somezreplacement(object, ":", tzinfoarg);
if (colonzreplacement == NULL)
goto Done;
}
assert(colonzreplacement != NULL);
assert(PyBytes_Check(colonzreplacement));
ThomasWaldmann marked this conversation as resolved.
Show resolved Hide resolved
ptoappend = PyBytes_AS_STRING(colonzreplacement);
ntoappend = PyBytes_GET_SIZE(colonzreplacement);
}
else if (ch == 'Z') {
/* format tzname */
if (Zreplacement == NULL) {
Expand Down Expand Up @@ -1686,7 +1710,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 +1757,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