Skip to content

gh-118948: add support for ISO 8601 basic format to datetime #120553

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
39 changes: 33 additions & 6 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -772,13 +772,25 @@ Instance methods:
.. versionchanged:: 3.9
Result changed from a tuple to a :term:`named tuple`.

.. method:: date.isoformat()

Return a string representing the date in ISO 8601 format, ``YYYY-MM-DD``::
.. method:: date.isoformat(basic=False)

Return a string representing the date in:

- ISO 8601 extended format ``YYYY-MM-DD`` (the default), or
- ISO 8601 basic format ``YYYYMMDD`` via the *basic* argument.

Examples:

>>> from datetime import date
>>> date(2002, 12, 4).isoformat()
'2002-12-04'
>>> date(2002, 12, 4).isoformat(basic=True)
'20021204'

.. versionchanged:: next
Added the *basic* parameter.


.. method:: date.__str__()

Expand Down Expand Up @@ -1536,9 +1548,9 @@ Instance methods:
and ``weekday``. The same as ``self.date().isocalendar()``.


.. method:: datetime.isoformat(sep='T', timespec='auto')
.. method:: datetime.isoformat(sep='T', timespec='auto', basic=False)

Return a string representing the date and time in ISO 8601 format:
Return a string representing the date and time in ISO 8601 extended format:

- ``YYYY-MM-DDTHH:MM:SS.ffffff``, if :attr:`microsecond` is not 0
- ``YYYY-MM-DDTHH:MM:SS``, if :attr:`microsecond` is 0
Expand All @@ -1550,13 +1562,20 @@ Instance methods:
is not 0
- ``YYYY-MM-DDTHH:MM:SS+HH:MM[:SS[.ffffff]]``, if :attr:`microsecond` is 0

If *basic* is true, this uses the ISO 8601 basic format for the date,
time and offset components.

Examples::

>>> from datetime import datetime, timezone
>>> datetime(2019, 5, 18, 15, 17, 8, 132263).isoformat()
'2019-05-18T15:17:08.132263'
>>> datetime(2019, 5, 18, 15, 17, 8, 132263).isoformat(basic=True)
'20190518T151708.132263'
>>> datetime(2019, 5, 18, 15, 17, tzinfo=timezone.utc).isoformat()
'2019-05-18T15:17:00+00:00'
>>> datetime(2019, 5, 18, 15, 17, tzinfo=timezone.utc).isoformat(basic=True)
'20190518T151700+0000'

The optional argument *sep* (default ``'T'``) is a one-character separator,
placed between the date and time portions of the result. For example::
Expand Down Expand Up @@ -1603,6 +1622,9 @@ Instance methods:
.. versionchanged:: 3.6
Added the *timespec* parameter.

.. versionadded:: next
Added the *basic* parameter.


.. method:: datetime.__str__()

Expand Down Expand Up @@ -1954,15 +1976,17 @@ Instance methods:
Added the *fold* parameter.


.. method:: time.isoformat(timespec='auto')
.. method:: time.isoformat(timespec='auto', basic=False)

Return a string representing the time in ISO 8601 format, one of:
Return a string representing the time in ISO 8601 (extended) format, one of:

- ``HH:MM:SS.ffffff``, if :attr:`microsecond` is not 0
- ``HH:MM:SS``, if :attr:`microsecond` is 0
- ``HH:MM:SS.ffffff+HH:MM[:SS[.ffffff]]``, if :meth:`utcoffset` does not return ``None``
- ``HH:MM:SS+HH:MM[:SS[.ffffff]]``, if :attr:`microsecond` is 0 and :meth:`utcoffset` does not return ``None``

If *basic* is true, this uses the ISO 8601 basic format which omits the colons.

The optional argument *timespec* specifies the number of additional
components of the time to include (the default is ``'auto'``).
It can be one of the following:
Expand Down Expand Up @@ -1997,6 +2021,9 @@ Instance methods:
.. versionchanged:: 3.6
Added the *timespec* parameter.

.. versionchanged:: next
Added the *basic* parameter.


.. method:: time.__str__()

Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,18 @@ operator
(Contributed by Raymond Hettinger and Nico Mexis in :gh:`115808`.)


datetime
--------

* Add support for the ISO 8601 basic format for the following methods:

- :meth:`date.isoformat <datetime.date.isoformat>`
- :meth:`datetime.isoformat <datetime.datetime.isoformat>`
- :meth:`time.isoformat <datetime.time.isoformat>`

(Contributed by Bénédikt Tran in :gh:`118948`.)


os
--

Expand Down
76 changes: 51 additions & 25 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,23 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag):
dnum = _days_before_month(y, m) + d
return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag))

def _format_time(hh, mm, ss, us, timespec='auto'):
specs = {
'hours': '{:02d}',
'minutes': '{:02d}:{:02d}',
'seconds': '{:02d}:{:02d}:{:02d}',
'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
}
def _format_time(hh, mm, ss, us, timespec='auto', basic=False):
if basic:
specs = {
'hours': '{:02d}',
'minutes': '{:02d}{:02d}',
'seconds': '{:02d}{:02d}{:02d}',
'milliseconds': '{:02d}{:02d}{:02d}.{:03d}',
'microseconds': '{:02d}{:02d}{:02d}.{:06d}'
}
else:
specs = {
'hours': '{:02d}',
'minutes': '{:02d}:{:02d}',
'seconds': '{:02d}:{:02d}:{:02d}',
'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
}

if timespec == 'auto':
# Skip trailing microseconds when us==0.
Expand Down Expand Up @@ -1117,16 +1126,18 @@ def __format__(self, fmt):
return self.strftime(fmt)
return str(self)

def isoformat(self):
"""Return the date formatted according to ISO.
def isoformat(self, basic=False):
"""Return the date formatted according to ISO 8601.

This is 'YYYY-MM-DD'.
This is 'YYYY-MM-DD' or 'YYYYMMDD' if *basic* is true.

References:
- http://www.w3.org/TR/NOTE-datetime
- http://www.cl.cam.ac.uk/~mgk25/iso-time.html
- https://www.w3.org/TR/NOTE-datetime
- https://www.cl.cam.ac.uk/~mgk25/iso-time.html
"""
return "%04d-%02d-%02d" % (self._year, self._month, self._day)
if basic:
return f"{self._year:04d}{self._month:02d}{self._day:02d}"
return f"{self._year:04d}-{self._month:02d}-{self._day:02d}"

__str__ = isoformat

Expand Down Expand Up @@ -1574,10 +1585,13 @@ def __hash__(self):

# Conversion to string

def _tzstr(self):
"""Return formatted timezone offset (+xx:xx) or an empty string."""
def _tzstr(self, basic):
"""Return formatted timezone offset (+xx:xx) or an empty string.
The colon separator is omitted if *basic* is true.
"""
off = self.utcoffset()
return _format_offset(off)
sep = '' if basic else ':'
return _format_offset(off, sep)

def __repr__(self):
"""Convert to formal string, for repr()."""
Expand All @@ -1598,19 +1612,21 @@ def __repr__(self):
s = s[:-1] + ", fold=1)"
return s

def isoformat(self, timespec='auto'):
def isoformat(self, timespec='auto', basic=False):
"""Return the time formatted according to ISO.

The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
part is omitted if self.microsecond == 0.

If *basic* is true, separators ':' are omitted.

The optional argument timespec specifies the number of additional
terms of the time to include. Valid options are 'auto', 'hours',
'minutes', 'seconds', 'milliseconds' and 'microseconds'.
"""
s = _format_time(self._hour, self._minute, self._second,
self._microsecond, timespec)
tz = self._tzstr()
self._microsecond, timespec, basic)
tz = self._tzstr(basic)
if tz:
s += tz
return s
Expand Down Expand Up @@ -2118,6 +2134,14 @@ def astimezone(self, tz=None):

# Ways to produce a string.

def _tzstr(self, basic):
"""Return formatted timezone offset (+xx:xx) or an empty string.
The colon separator is omitted if *basic* is true.
"""
off = self.utcoffset()
sep = '' if basic else ':'
return _format_offset(off, sep)

def ctime(self):
"Return ctime() style string."
weekday = self.toordinal() % 7 or 7
Expand All @@ -2128,12 +2152,14 @@ def ctime(self):
self._hour, self._minute, self._second,
self._year)

def isoformat(self, sep='T', timespec='auto'):
def isoformat(self, sep='T', timespec='auto', basic=False):
"""Return the time formatted according to ISO.

The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
By default, the fractional part is omitted if self.microsecond == 0.

If *basic* is true, separators ':' and '-' are omitted.

If self.tzinfo is not None, the UTC offset is also attached, giving
giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.

Expand All @@ -2144,12 +2170,12 @@ def isoformat(self, sep='T', timespec='auto'):
terms of the time to include. Valid options are 'auto', 'hours',
'minutes', 'seconds', 'milliseconds' and 'microseconds'.
"""
s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
fmt = "%04d%02d%02d%c" if basic else "%04d-%02d-%02d%c"
s = (fmt % (self._year, self._month, self._day, sep) +
_format_time(self._hour, self._minute, self._second,
self._microsecond, timespec))
self._microsecond, timespec, basic))

off = self.utcoffset()
tz = _format_offset(off)
tz = self._tzstr(basic)
if tz:
s += tz

Expand Down
Loading
Loading