summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/datetime.rst16
-rw-r--r--Lib/datetime.py45
-rw-r--r--Lib/test/datetimetester.py21
-rw-r--r--Misc/NEWS.d/next/Library/2022-08-14-18-59-54.gh-issue-69142.6is5Pq.rst1
-rw-r--r--Modules/_datetimemodule.c64
5 files changed, 92 insertions, 55 deletions
diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index 0f04237..c3a66a4 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -2443,6 +2443,11 @@ convenience. These parameters all correspond to ISO 8601 date values.
| | Week 01 is the week containing | | |
| | Jan 4. | | |
+-----------+--------------------------------+------------------------+-------+
+| ``%:z`` | UTC offset in the form | (empty), +00:00, | \(6) |
+| | ``±HH:MM[:SS[.ffffff]]`` | -04:00, +10:30, | |
+| | (empty string if the object is | +06:34:15, | |
+| | naive). | -03:07:12.345216 | |
++-----------+--------------------------------+------------------------+-------+
These may not be available on all platforms when used with the :meth:`strftime`
method. The ISO 8601 year and ISO 8601 week directives are not interchangeable
@@ -2458,6 +2463,9 @@ differences between platforms in handling of unsupported format specifiers.
.. versionadded:: 3.6
``%G``, ``%u`` and ``%V`` were added.
+.. versionadded:: 3.12
+ ``%:z`` was added.
+
Technical Detail
^^^^^^^^^^^^^^^^
@@ -2530,8 +2538,8 @@ Notes:
available).
(6)
- For a naive object, the ``%z`` and ``%Z`` format codes are replaced by empty
- strings.
+ For a naive object, the ``%z``, ``%:z`` and ``%Z`` format codes are replaced
+ by empty strings.
For an aware object:
@@ -2557,6 +2565,10 @@ Notes:
For example, ``'+01:00:00'`` will be parsed as an offset of one hour.
In addition, providing ``'Z'`` is identical to ``'+00:00'``.
+ ``%:z``
+ Behaves exactly as ``%z``, but has a colon separator added between
+ hours, minutes and seconds.
+
``%Z``
In :meth:`strftime`, ``%Z`` is replaced by an empty string if
:meth:`tzname` returns ``None``; otherwise ``%Z`` is replaced by the
diff --git a/Lib/datetime.py b/Lib/datetime.py
index 00ded32..007114a 100644
--- a/Lib/datetime.py
+++ b/Lib/datetime.py
@@ -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:
@@ -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
@@ -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)
@@ -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 = ""
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 7e7f4f3..bba9669 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -1463,8 +1463,8 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
# 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")
@@ -1528,7 +1528,7 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
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))
@@ -2134,7 +2134,7 @@ class TestDateTime(TestDate):
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))
@@ -2777,6 +2777,7 @@ class TestDateTime(TestDate):
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:
@@ -3515,8 +3516,8 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
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:
@@ -3934,10 +3935,10 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
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)
diff --git a/Misc/NEWS.d/next/Library/2022-08-14-18-59-54.gh-issue-69142.6is5Pq.rst b/Misc/NEWS.d/next/Library/2022-08-14-18-59-54.gh-issue-69142.6is5Pq.rst
new file mode 100644
index 0000000..0db8b37
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-08-14-18-59-54.gh-issue-69142.6is5Pq.rst
@@ -0,0 +1 @@
+Add ``%:z`` strftime format code (generates tzoffset with colons as separator), see :ref:`strftime-strptime-behavior`.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index eca7c6b..d86418a 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -1507,6 +1507,27 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
}
static PyObject *
+make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg)
+{
+ char buf[100];
+ PyObject *tzinfo = get_tzinfo_member(object);
+
+ if (tzinfo == Py_None || tzinfo == NULL) {
+ return PyBytes_FromStringAndSize(NULL, 0);
+ }
+
+ assert(tzinfoarg != NULL);
+ if (format_utcoffset(buf,
+ sizeof(buf),
+ sep,
+ tzinfo,
+ tzinfoarg) < 0)
+ return NULL;
+
+ return PyBytes_FromStringAndSize(buf, strlen(buf));
+}
+
+static PyObject *
make_Zreplacement(PyObject *object, PyObject *tzinfoarg)
{
PyObject *temp;
@@ -1566,7 +1587,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.
@@ -1578,6 +1599,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 */
@@ -1632,32 +1654,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));
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));
+ ptoappend = PyBytes_AS_STRING(colonzreplacement);
+ ntoappend = PyBytes_GET_SIZE(colonzreplacement);
+ }
else if (ch == 'Z') {
/* format tzname */
if (Zreplacement == NULL) {
@@ -1686,7 +1705,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;
}
@@ -1733,6 +1752,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;