Skip to content

Commit 80040b6

Browse files
StanFromIrelandkarolinepaulsvstinner
authored
gh-157290: Add support for %Nf to datetime.str{f,p}time() (#157704)
Co-authored-by: Karoline Pauls <code@karolinepauls.com> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 39bc958 commit 80040b6

7 files changed

Lines changed: 72 additions & 1 deletion

File tree

‎Doc/library/datetime.rst‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2699,6 +2699,10 @@ convenience.
26992699
| | number, zero-padded to 6 | 999999 | |
27002700
| | digits. | | |
27012701
+-----------+--------------------------------+------------------------+-------+
2702+
| ``%Nf`` | Fraction of a second truncated | 000, 001, ..., 999 | \(5) |
2703+
| | to *N* digits, where *N* is | (``%3f``) | |
2704+
| | from 1 to 6. | | |
2705+
+-----------+--------------------------------+------------------------+-------+
27022706
| ``%:z`` | UTC offset in the form | (empty), +00:00, | \(6) |
27032707
| | ``±HH:MM[:SS[.ffffff]]`` | -04:00, +10:30, | |
27042708
| | (empty string if the object is | +06:34:15, | |
@@ -2721,6 +2725,9 @@ differences between platforms in handling of unsupported format specifiers.
27212725
``%D``, ``%F``, ``%n``, ``%t``, and ``%:z`` were added for
27222726
:meth:`~.datetime.strptime`.
27232727

2728+
.. versionadded:: next
2729+
``%Nf`` was added.
2730+
27242731

27252732
Technical detail
27262733
^^^^^^^^^^^^^^^^
@@ -2833,7 +2840,8 @@ Notes:
28332840

28342841
(5)
28352842
When used with the :meth:`~.datetime.strptime` method, the ``%f`` directive
2836-
accepts from one to six digits and zero pads on the right. ``%f`` is
2843+
accepts from one to six digits and zero pads on the right, while ``%Nf``
2844+
accepts exactly *N* digits. ``%f`` is
28372845
an extension to the set of format characters in the C standard (but
28382846
implemented separately in datetime objects, and therefore always
28392847
available).

‎Doc/whatsnew/3.16.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ curses
362362
(Contributed by Serhiy Storchaka in :gh:`133031`.)
363363

364364

365+
datetime
366+
--------
367+
368+
* Add the ``%Nf`` format code (where *N* is from 1 to 6) to
369+
:meth:`datetime.datetime.strftime`, :meth:`datetime.time.strftime`,
370+
:meth:`datetime.date.strftime`, :meth:`datetime.datetime.strptime` and
371+
:meth:`datetime.time.strptime`.
372+
(Contributed by Stan Ulbrych and Karoline Pauls in :gh:`157290`.)
373+
374+
365375
dbm.gnu
366376
-------
367377

‎Lib/_pydatetime.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ def _wrap_strftime(object, format, timetuple):
237237
freplace = '%06d' % getattr(object,
238238
'microsecond', 0)
239239
newformat.append(freplace)
240+
elif ch in '123456' and format[i:i+1] == 'f':
241+
i += 1
242+
if freplace is None:
243+
freplace = '%06d' % getattr(object,
244+
'microsecond', 0)
245+
precision = int(ch)
246+
newformat.append(freplace[:precision])
240247
elif ch == 'z':
241248
if zreplace is None:
242249
if hasattr(object, "utcoffset"):

‎Lib/_strptime.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ def __init__(self, locale_time=None):
386386
't': r'\s*',
387387
'%': '%',
388388
}
389+
for n in range(1, 7):
390+
mapping[f'{n}f'] = rf"(?P<f>[0-9]{{{n}}})"
389391
if self.locale_time.LC_alt_digits is None:
390392
for d in 'dmyCHIMS':
391393
mapping['O' + d] = r'(?P<%s>\d\d|\d| \d)' % d

‎Lib/test/datetimetester.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,7 @@ def test_strftime(self):
16301630

16311631
#check that this standard extension works
16321632
t.strftime("%f")
1633+
self.assertEqual(t.strftime("%3f"), "000")
16331634

16341635
# bpo-41260: The parameter was named "fmt" in the pure python impl.
16351636
t.strftime(format="%f")
@@ -3189,6 +3190,14 @@ def test_more_strftime(self):
31893190
t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
31903191
self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"),
31913192
"12 31 04 000047 33 22 06 366")
3193+
t = self.theclass(2004, 12, 31, 6, 22, 33, 123456)
3194+
self.assertEqual(t.strftime("%1f %2f %3f %4f %5f %6f"),
3195+
"1 12 123 1234 12345 123456")
3196+
self.assertEqual(t.strftime("%f %3f"), "123456 123")
3197+
self.assertEqual(t.strftime("%3f %f"), "123 123456")
3198+
self.assertEqual(t.strftime("%S.%3f%%3f"), "33.123%3f")
3199+
t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
3200+
self.assertEqual(t.strftime("%3f %5f"), "000 00004")
31923201
for (s, us), z in [((33, 123), "33.000123"), ((33, 0), "33"),]:
31933202
tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us))
31943203
t = t.replace(tzinfo=tz)
@@ -4102,6 +4111,7 @@ def test_1653736(self):
41024111
def test_strftime(self):
41034112
t = self.theclass(1, 2, 3, 4)
41044113
self.assertEqual(t.strftime('%H %M %S %f'), "01 02 03 000004")
4114+
self.assertEqual(t.strftime('%H %M %S %3f %6f'), "01 02 03 000 000004")
41054115
# A naive object replaces %z, %:z and %Z with empty strings.
41064116
self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''")
41074117

@@ -4258,12 +4268,19 @@ def test_strptime(self):
42584268
(self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%f'),
42594269
(self.theclass(13, 2, 47, 197000), '13:02\ud80047.197', '%H:%M\ud800%S.%f'),
42604270
(self.theclass(13, 2, 47, 197000), '13\ud80002:47.197', '%H\ud800%M:%S.%f'),
4271+
(self.theclass(13, 2, 47, 197000), '13:02:47.197', '%H:%M:%S.%3f'),
4272+
(self.theclass(13, 2, 47, 100000), '13:02:47.1', '%H:%M:%S.%1f'),
4273+
(self.theclass(13, 2, 47, 197531), '13:02:47.197531', '%H:%M:%S.%6f'),
42614274
]
42624275
for expected, string, format in inputs:
42634276
with self.subTest(string=string, format=format):
42644277
got = self.theclass.strptime(string, format)
42654278
self.assertEqual(expected, got)
42664279
self.assertIs(type(got), self.theclass)
4280+
with self.assertRaises(ValueError):
4281+
self.theclass.strptime('13:02:47.19', '%H:%M:%S.%3f')
4282+
with self.assertRaises(ValueError):
4283+
self.theclass.strptime('13:02:47.1975', '%H:%M:%S.%3f')
42674284

42684285
def test_strptime_tz(self):
42694286
strptime = self.theclass.strptime
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the ``%Nf`` format code (where *N* is from 1 to 6) to
2+
:meth:`datetime.datetime.strftime`, :meth:`datetime.time.strftime`,
3+
:meth:`datetime.date.strftime`, :meth:`datetime.datetime.strptime` and
4+
:meth:`datetime.time.strptime`.

‎Modules/_datetimemodule.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,29 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
19711971
}
19721972
replacement = freplacement;
19731973
}
1974+
else if (ch >= '1' && ch <= '6'
1975+
&& i < flen && PyUnicode_READ_CHAR(format, i) == 'f')
1976+
{
1977+
/* %Nf -> first N digits of microseconds */
1978+
i++;
1979+
if (freplacement == NULL) {
1980+
freplacement = make_freplacement(object);
1981+
if (freplacement == NULL) {
1982+
goto Error;
1983+
}
1984+
}
1985+
if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 0) {
1986+
goto Error;
1987+
}
1988+
start = i;
1989+
Py_ssize_t precision = ch - '0';
1990+
if (PyUnicodeWriter_WriteSubstring(writer, freplacement,
1991+
0, precision) < 0)
1992+
{
1993+
goto Error;
1994+
}
1995+
continue;
1996+
}
19741997
else if (normalize_century()
19751998
&& (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C'))
19761999
{

0 commit comments

Comments
 (0)