Skip to content

Commit 7d95910

Browse files
add %:z strftime format code
datetime.isoformat generates the tzoffset with colons, but there was no format code to make strftime output the same format. for simplicity and consistency the %:z formatting behaves mostly as %z, with the exception of adding colons. this includes the dynamic behaviour of adding seconds and microseconds only when needed (when not 0). this fixes the still open "generate" part of this issue: #69142
1 parent 32ac98e commit 7d95910

File tree

4 files changed

+37
-18
lines changed

4 files changed

+37
-18
lines changed

Doc/library/datetime.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,12 @@ requires, and these work on all platforms with a standard C implementation.
23862386
| | string if the object is | +063415, | |
23872387
| | naive). | -030712.345216 | |
23882388
+-----------+--------------------------------+------------------------+-------+
2389+
| ``%:z`` | UTC offset in the form | (empty), +00:00, | |
2390+
| | ``±HH:MM[:SS[.ffffff]]`` | -04:00, +10:30, | |
2391+
| | (empty string if the object is | +06:34:15, | |
2392+
| | naive). | -03:07:12.345216 | |
2393+
| | .. versionadded:: 3.12 | | |
2394+
+-----------+--------------------------------+------------------------+-------+
23892395
| ``%Z`` | Time zone name (empty string | (empty), UTC, GMT | \(6) |
23902396
| | if the object is naive). | | |
23912397
+-----------+--------------------------------+------------------------+-------+

Lib/test/datetimetester.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,8 +1463,8 @@ def test_strftime(self):
14631463
# test that unicode input is allowed (issue 2782)
14641464
self.assertEqual(t.strftime("%m"), "03")
14651465

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

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

15291529
for fmt in ["m:%m d:%d y:%y",
15301530
"m:%m d:%d y:%y H:%H M:%M S:%S",
1531-
"%z %Z",
1531+
"%z %:z %Z",
15321532
]:
15331533
self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
15341534
self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
@@ -2134,7 +2134,7 @@ def strftime(self, format_spec):
21342134

21352135
for fmt in ["m:%m d:%d y:%y",
21362136
"m:%m d:%d y:%y H:%H M:%M S:%S",
2137-
"%z %Z",
2137+
"%z %:z %Z",
21382138
]:
21392139
self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
21402140
self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
@@ -2777,6 +2777,7 @@ def test_more_strftime(self):
27772777
tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us))
27782778
t = t.replace(tzinfo=tz)
27792779
self.assertEqual(t.strftime("%z"), "-0200" + z)
2780+
self.assertEqual(t.strftime("%:z"), "-02:00:" + z)
27802781

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

35213522
# bpo-34482: Check that surrogates don't cause a crash.
35223523
try:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add %:z strftime format code (generates tzoffset with colons as separator)

Modules/_datetimemodule.c

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
15781578
PyObject *result = NULL; /* guilty until proved innocent */
15791579

15801580
PyObject *zreplacement = NULL; /* py string, replacement for %z */
1581+
PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */
15811582
PyObject *Zreplacement = NULL; /* py string, replacement for %Z */
15821583
PyObject *freplacement = NULL; /* py string, replacement for %f */
15831584

@@ -1631,32 +1632,42 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
16311632
ntoappend = 1;
16321633
}
16331634
/* A % has been seen and ch is the character after it. */
1634-
else if (ch == 'z') {
1635-
if (zreplacement == NULL) {
1635+
else if (ch == 'z' || (ch == ':' && *pin == 'z' && pin++)) {
1636+
/* %z -> +HHMM, %:z -> +HH:MM */
1637+
PyObject **replacement_p;
1638+
char *sep;
1639+
if (ch == ':') {
1640+
sep = ":";
1641+
replacement_p = &colonzreplacement;
1642+
} else {
1643+
sep = "";
1644+
replacement_p = &zreplacement;
1645+
}
1646+
if (*replacement_p == NULL) {
16361647
/* format utcoffset */
16371648
char buf[100];
16381649
PyObject *tzinfo = get_tzinfo_member(object);
1639-
zreplacement = PyBytes_FromStringAndSize("", 0);
1640-
if (zreplacement == NULL) goto Done;
1650+
*replacement_p = PyBytes_FromStringAndSize("", 0);
1651+
if (*replacement_p == NULL) goto Done;
16411652
if (tzinfo != Py_None && tzinfo != NULL) {
16421653
assert(tzinfoarg != NULL);
16431654
if (format_utcoffset(buf,
16441655
sizeof(buf),
1645-
"",
1656+
sep,
16461657
tzinfo,
16471658
tzinfoarg) < 0)
16481659
goto Done;
1649-
Py_DECREF(zreplacement);
1650-
zreplacement =
1660+
Py_DECREF(*replacement_p);
1661+
*replacement_p =
16511662
PyBytes_FromStringAndSize(buf,
16521663
strlen(buf));
1653-
if (zreplacement == NULL)
1664+
if (*replacement_p == NULL)
16541665
goto Done;
16551666
}
16561667
}
1657-
assert(zreplacement != NULL);
1658-
ptoappend = PyBytes_AS_STRING(zreplacement);
1659-
ntoappend = PyBytes_GET_SIZE(zreplacement);
1668+
assert(*replacement_p != NULL);
1669+
ptoappend = PyBytes_AS_STRING(*replacement_p);
1670+
ntoappend = PyBytes_GET_SIZE(*replacement_p);
16601671
}
16611672
else if (ch == 'Z') {
16621673
/* format tzname */
@@ -1686,7 +1697,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
16861697
ntoappend = PyBytes_GET_SIZE(freplacement);
16871698
}
16881699
else {
1689-
/* percent followed by neither z nor Z */
1700+
/* percent followed by something else */
16901701
ptoappend = pin - 2;
16911702
ntoappend = 2;
16921703
}

0 commit comments

Comments
 (0)