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

datetime: support leap seconds #67762

Open
vstinner opened this issue Mar 3, 2015 · 20 comments
Open

datetime: support leap seconds #67762

vstinner opened this issue Mar 3, 2015 · 20 comments
Labels
stdlib Python modules in the Lib dir

Comments

@vstinner
Copy link
Member

vstinner commented Mar 3, 2015

BPO 23574
Nosy @malemburg, @abalkin, @vstinner, @dhellmann, @4kir4, @pganssle, @maxnoe
Files
  • datetime_leapsecond.patch
  • support_leap_seconds.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2015-03-03.14:42:23.359>
    labels = ['library']
    title = 'datetime: support leap seconds'
    updated_at = <Date 2020-03-29.23:25:48.165>
    user = 'https://github.com/vstinner'

    bugs.python.org fields:

    activity = <Date 2020-03-29.23:25:48.165>
    actor = 'vstinner'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2015-03-03.14:42:23.359>
    creator = 'vstinner'
    dependencies = []
    files = ['38317', '38319']
    hgrepos = []
    issue_num = 23574
    keywords = ['patch']
    message_count = 15.0
    messages = ['237142', '237144', '237146', '237147', '237148', '237413', '238004', '244064', '244151', '247059', '247743', '247760', '247762', '365224', '365285']
    nosy_count = 8.0
    nosy_names = ['lemburg', 'belopolsky', 'vstinner', 'doughellmann', 'akira', 'dlroo', 'p-ganssle', 'maxnoe']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = None
    status = 'open'
    superseder = None
    type = None
    url = 'https://bugs.python.org/issue23574'
    versions = ['Python 2.7']

    @vstinner
    Copy link
    Member Author

    vstinner commented Mar 3, 2015

    A leap second will be added in June 2015:
    http://www.usatoday.com/story/tech/2015/01/08/computer-chaos-feares/21433363/

    The datetime module explicitly doesn't support leap seconds:
    https://docs.python.org/dev/library/datetime.html#datetime.date.fromtimestamp
    "Note that on non-POSIX systems that include leap seconds in their notion of a timestamp, leap seconds are ignored by fromtimestamp()."

    The following bug in oslo.utils was reported because datetime is indirectly used to unserialize a date, but it fails with ValueError("second must be in 0..59") if the second is 60:
    https://bugs.launchpad.net/oslo.utils/+bug/1427212

    Would it be possible to silently drop ignore leap seconds in datetime.datetime constructor, as already done in datetime.datetime.fromtimestamp?

    Attached patch modified datetime constructor to drop leap seconds: replace second=60 with second=59. I also changed the error message for second (valid range is now 0..60).

    @vstinner vstinner added the stdlib Python modules in the Lib dir label Mar 3, 2015
    @vstinner
    Copy link
    Member Author

    vstinner commented Mar 3, 2015

    Leap seconds are ignored, so a difference of <datetime before the leap second> and <datetime with the leap second> is zero:

    >>> import datetime
    >>> t1=datetime.datetime(2012, 6, 30, 23, 59, 59)
    >>> t2=datetime.datetime(2012, 6, 30, 23, 59, 59)
    >>> t2-t1
    datetime.timedelta(0)

    Supporting leap seconds might be possible, but it requires much more work.

    @vstinner
    Copy link
    Member Author

    vstinner commented Mar 3, 2015

    Ignoring leap seconds introduces unexpected result.

    datetime.timestamp -> datetime.fromtimestamp drops one second:

    $ ./python
    Python 3.5.0a1+ (default:760f222103c7+, Mar  3 2015, 15:36:36) 
    >>> t=datetime.datetime(2012, 6, 30, 23, 59, 60).timestamp()
    >>> datetime.datetime.fromtimestamp(t)
    datetime.datetime(2012, 6, 30, 23, 59, 59)

    time and datetime modules behave differently:

    $ ./python
    Python 3.5.0a1+ (default:760f222103c7+, Mar  3 2015, 15:36:36) 
    >>> import datetime, time
    >>> t1=datetime.datetime(2012, 6, 30, 23, 59, 59).timestamp()
    >>> t2=datetime.datetime(2012, 6, 30, 23, 59, 60).timestamp()
    >>> t2-t1
    0.0
    
    >>> t3=time.mktime((2012, 6, 30, 23, 59, 59, -1, -1, -1))
    >>> t4=time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1))
    >>> t4-t3
    1.0
    
    >>> t1 == t2 == t3
    True
    >>> t3, t4
    (1341093599.0, 1341093600.0)

    @vstinner
    Copy link
    Member Author

    vstinner commented Mar 3, 2015

    support_leap_seconds.patch: different approach, accept second=60. Problem: fromtimestamp() returns the wrong day.

    haypo@smithers$ ./python
    Python 3.5.0a1+ (default:760f222103c7+, Mar  3 2015, 15:36:36) 
    >>> import datetime
    >>> datetime.datetime(2012, 6, 30, 23, 59, 60)
    datetime.datetime(2012, 6, 30, 23, 59, 60)
    >>> dt1=datetime.datetime(2012, 6, 30, 23, 59, 60)
    >>> t1=datetime.datetime(2012, 6, 30, 23, 59, 60).timestamp()
    >>> dt2=datetime.datetime.fromtimestamp(t1)
    >>> dt2
    datetime.datetime(2012, 7, 1, 0, 0)
    >>> dt2 == dt1
    False
    >>> dt1
    datetime.datetime(2012, 6, 30, 23, 59, 60)
    >>> print(dt1)
    2012-06-30 23:59:60
    >>> print(dt2)
    2012-07-01 00:00:00
    
    >>> import time
    >>> time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1))
    1341093600.0
    >>> t1
    1341093600.0
    >>> t2=time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1))
    >>> t2 == t1
    True
    >>> time.localtime(time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1)))
    time.struct_time(tm_year=2012, tm_mon=7, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=6, tm_yday=183, tm_isdst=1)

    http://cr.yp.to/proto/utctai.html

    """
    For many years, the UNIX localtime() time-display routine didn't support leap seconds.
    (...)
    Why not fix it?
    (...)
    The main obstacle is POSIX. POSIX is a ``standard'' designed by a vendor consortium several years ago to eliminate progress and protect the installed base. The behavior of the broken localtime() libraries was documented and turned into a POSIX requirement.
    """

    @vstinner
    Copy link
    Member Author

    vstinner commented Mar 3, 2015

    Oh, mktime() returns the same timestamp with and without the leap second:

    >>> time.mktime((2012, 6, 30, 23, 59, 59, -1, -1, -1))
    1341093599.0
    >>> time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1))
    1341093600.0
    >>> time.mktime((2012, 7, 1, 0, 0, 0, -1, -1, -1))
    1341093600.0

    @abalkin
    Copy link
    Member

    abalkin commented Mar 7, 2015

    POSIX is a ``standard'' designed by a vendor consortium several years ago to eliminate progress and protect the installed base.

    No, POSIX is an attempt to bring some sanity to the installed base of human calendars. The established standard tell's us that a year is 365 days. Wait, every 4-th year is 366 days, except some other rule every 400 years.

    POSIX says: fine as long as we can enumerate all YYYY-MM-DD's, we can live with it. But the line is drawn where each day is divided into 86,400 seconds.

    The problem is that unlike ancient astronomers who were finding better and better approximations to the ratio of two Earth's rotation periods (around the Sun and around itself) every few hundred years, modern astronomers will tell us how many seconds there will be in any given year with only a six month notice.

    @4kir4
    Copy link
    Mannequin

    4kir4 mannequin commented Mar 13, 2015

    POSIX timestamp doesn't count (literally) past/future leap seconds.
    It allows to find out that the timestamp 2**31-1 corresponds to
    2038-01-19T03:14:07Z (UTC) regardless of how many leap seconds will
    occur before 2038:

      >>> from datetime import datetime, timedelta
      >>> str(datetime(1970,1,1) + timedelta(seconds=2**31-1))
      '2038-01-19 03:14:07'

    If you use "right" timezone then mktime() may count leap seconds:

      $ TZ=right/UTC ./python
      >>> import time
      >>> time.mktime((2012, 6, 30, 23, 59, 59, -1, -1, -1))
      1341100823.0
      >>> time.mktime((2012, 6, 30, 23, 59, 60, -1, -1, -1))
      1341100824.0
      >>> time.mktime((2012, 7, 1, 0, 0, 0, -1, -1, -1))
      1341100825.0

    It is a different time scale. There are no leap seconds in TAI:

      >>> str(datetime(1970,1,1, 0,0, 10) + timedelta(seconds=1341100825))
      '2012-07-01 00:00:35'

    i.e., 2012-07-01 00:00:35 TAI that corresponds to 2012-07-01 00:00:00
    UTC. Each positive leap second increases the difference TAI-UTC (on
    2015-07-01UTC it will be 36 [1]).

    TAI-UTC in the future (more than 6 months) is unknown but it is less
    than ~200 seconds until 2100 [2].

    It might be convenient to think about datetime as a broken-down
    timestamp and therefore

    (datetime(2012,6,30,23,59,60) - epoch) ==
    (datetime(2012,7, 1, 0, 0, 0) - epoch)

    The code [3] that silently truncates 60 to 59 when datetime
    constructor is called implicitly should retire.

    Use case: parse timestamps that might include a leap second [4]

    [1] https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat
    [2] http://www.ucolick.org/~sla/leapsecs/year2100.html
    [3] https://bugs.python.org/msg155689
    [4] http://stackoverflow.com/q/21027639

    @vstinner
    Copy link
    Member Author

    Sorry, I give up on this issue. I don't know how to fix it, nor if it's possible to fix it.

    @malemburg
    Copy link
    Member

    Here's what mxDateTime uses:

    >>> import mx.DateTime
    >>>
    >>> t1 = mx.DateTime.DateTime(2012,6,30,23,59,60)
    >>> t2 = mx.DateTime.DateTime(2012,7,1,0,0,0)
    >>>
    >>> t1
    <mx.DateTime.DateTime object for '2012-06-30 23:59:60.00' at 7fbb36008d68>
    >>> t2
    <mx.DateTime.DateTime object for '2012-07-01 00:00:00.00' at 7fbb36008d20>
    >>>
    >>> t2-t1
    <mx.DateTime.DateTimeDelta object for '00:00:00.00' at 7fbb35ff0540>
    >>> (t2-t1).seconds
    0.0
    >>>
    >>> t1 + mx.DateTime.oneSecond
    <mx.DateTime.DateTime object for '2012-07-01 00:00:01.00' at 7fbb360083d8>

    It preserves the broken down values, but uses POSIX days of 86400 seconds per day to calculate time deltas.

    It's a compromise, not a perfect solution, but it prevents applications from failing for that one second every now and then.

    I don't believe there is a perfect solution, since what your application or users expect may well be different. All I can say is that raising exceptions in these rare cases is not what your users typically want :-)

    @dlroo
    Copy link
    Mannequin

    dlroo mannequin commented Jul 21, 2015

    If you are using mx.DateTime make certain you do not use the .strftime method. If you use .strftime method and have a 60th second in your DateTime object it will crash python with no error message. This occurs because the .strftime method is fully inherited from Python's datetime.datetime.

    @malemburg
    Copy link
    Member

    On 21.07.2015 22:15, dlroo wrote:

    dlroo added the comment:

    If you are using mx.DateTime make certain you do not use the .strftime method. If you use .strftime method and have a 60th second in your DateTime object it will crash python with no error message. This occurs because the .strftime method is fully inherited from Python's datetime.datetime.

    Thanks for the report. We will fix this in the next mxDateTime release.

    @dlroo
    Copy link
    Mannequin

    dlroo mannequin commented Jul 31, 2015

    Is it possible to modify datetime so that the check_time_args function in the datetimemodule.c does not error when given a seconds value of greater than 59? I was thinking that if the seconds were greater than 59, the seconds are set to 59 and any extra seconds are kept in a book keeping "attribute" (not a real attribute because its C) that is accessible from the Python side? You would have to make the seconds argue passed by reference (thus returning a modified second). Also would want the book keeping value to be zero in nominal conditions.
    This would do nothing for any of the datetime arithmetic, but that can be handled externally.

    @abalkin
    Copy link
    Member

    abalkin commented Jul 31, 2015

    Please redirect this discussion to the recently opened datetime-sig mailing list.

    https://mail.python.org/pipermail/datetime-sig/

    @maxnoe
    Copy link
    Mannequin

    maxnoe mannequin commented Mar 28, 2020

    Could this be revisited?

    Especially now that datetime supports fromisoformat, as there are valid ISO8601 timestamps in UTC out there, that contain the leap seconds, e.g. files describing when those occured or will occur.

    E.g. the NTP Leap second file:
    https://kb.meinbergglobal.com/kb/time_sync/ntp/configuration/ntp_leap_second_file

    This get's synced on linux to /usr/share/zoneinfo/leapseconds and could even be used by python to lookup when leap seconds occured.

    The datetime also gained a fold argument, which if it is not wanted to support second values of 60 to at least be able to parse those.

    The 60th second of a minute is a reality with our current civil time keeping, so python should be able to handle it.

    @vstinner
    Copy link
    Member Author

    One option to explore is to add a "leap seconds" field to datetime.datetime which can be negative (just in case someone decides to add negative leap seconds in the future).

    It can use in operations which involve time zones, it can be serialized/deserialized, but datetime.datetime.timestamp() would ignore this field ("drop" leap seconds on purpose).

    @vstinner vstinner reopened this Mar 29, 2020
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @maxnoe
    Copy link
    Contributor

    maxnoe commented Jul 14, 2022

    @vstinner No, that is not the correct way to deal with this. In UTC, a positive leap second is really the 60th second. A negative leap second the non-existence of second 59.

    Leap seconds are introduced either at the end of June or December of each year, so at YYYY-12-31T23:59:60 UTC or YYYY-06-30T23:59:60 UTC.
    Leap seconds happen in UTC, which means they happen simultaneously in all time zones, which means that any naive datetime has to accept second 60 and only when making an non-naïve datetime during the leap second validity period, it can be checked if the given timestamp is an actually existing one (so up to one year in the future, e.g. IERS just announced that there will be no leap second at the end of 2022, so the earliest possible leap second is end of june next year).

    This is first disconnected from the internal representation.

    The question is then how this can be internally represented and how the absolute date is created from e.g. a time since epoch like the unix time. The problem is here that:

    a) Unix Time UTC does not count leap seconds
    b) How Unix TIme UTC behaves on a day with a leap second is not standardized and might differ on different systems (not just different OSes, different versions of leap second smearing, repeating the same timestamp twice, ...)

    But from a user perspective, I think that python's datetime should:

    a) accept all valid UTC timestamps, including the instance of a leap second
    b) correctly report the time difference between two instances in time (non-naïve datetimes)

    E.g. these should all work and e.g. correctly roundtrip via {to,from}isoformat:

    # last leap second in UTC
    datetime.fromisoformat("2016-12-31T23:59:60+00:00")
    # Last leap second in UTC+01
    datetime.fromisoformat("2017-01-01T00:59:60+01:00")
    # Last June Leap Second in UTC
    datetime(2015, 6, 30, 23, 59, 60, tzinfo=timezone.utc)
    # Last June Leap Second in Central European Summer Time
    datetime(2015, 7, 1, 1, 59, 60, tzinfo=ZoneInfo("Europe/Berlin"))
    

    And e.g. this should be a timedelta of two seconds, not one:

    >> t1 = datetime.fromisoformat("2017-01-01T00:00:00+00:00")
    >> t2 = datetime.fromisoformat("2016-12-31T23:59:59+00:00")
    >> t1 - t2
    datetime.timedelta(seconds=1)
    

    @pganssle
    Copy link
    Member

    I'm inclined to mostly agree with @maxnoe here, though the changes to arithmetic might be a problem from a performance, data and backwards compatibility point of view. It seems to me that the minimum viable change here would be to allow datetime to accept 60 in the second field. I don't think that adding 1 seconds to a datetime at second 59 should construct a datetime with second 60.

    Even though we know the dates of leap seconds, I think trying to restrict 60 to specific dates or minutes might be tricky. We could theoretically say you can do it within ±24 hours of 30 June or 31 December and that should mostly cover us. I am not sure how we'd want to deal with a weird case (luckily none active right now) where there's an offset of a non-integer number of minutes from UTC, when the leap second might be some random extra second in the middle of a minute (not sure how that would even work anyway).

    If we want something like "actual elapsed time", we need a data source for leap seconds, so maybe that part of it could / should be something that lives in zoneinfo?

    At the end of the day, the big problem I have with the idea of adding support for leap seconds here is that while I frequently get asked about how datetime supports leap seconds, almost no one has given me any practical use cases for leap seconds. The closest practical use case is one like the one reported by @vstinner in this issue — something out there is generating a leap second date time that can't be represented by datetime.datetime, and it's causing an error that's hard to deal with. No one seems to care about supporting leap seconds any more deeply than allowing them to be represented, so I think just allowing construction of datetime with a value of 60 for the seconds should be enough to handle those use cases.

    If we go forward with that, I think the biggest open question is how this affects arithmetic and equality. Presumably datetime(2022, 12, 31, 23, 59, 60) + timedelta(seconds=1) should result in datetime(2023, 1, 1), but I think datetime(2022, 12, 31, 23, 59, 59) + timedelta(seconds=1) should also result in the same thing. If we decide that datetime(2022, 12, 31, 23, 59, 60) is basically an alias for datetime(2022, 12, 31, 23, 59, 59), should those two compare equal or no? If no, that adds a new case where dt2 != dt1 + (dt2 - dt1) (prior art for this). If yes, they need to have the same hash, which may cause its own problems.

    @maxnoe
    Copy link
    Contributor

    maxnoe commented Jul 14, 2022

    No one seems to care about supporting leap seconds any more deeply than allowing them to be represented, so I think just allowing construction of datetime with a value of 60 for the seconds should be enough to handle those use cases.

    You need to know them for calculating actual elapsed times between two UTC instances.

    E.g. cpython currently gets this wrong by 27 seconds:

    In [1]: from datetime import datetime, timezone
    In [2]: t0 = datetime(1972, 1, 1, 0, 0, tzinfo=timezone.utc)
    In [3]: t1 = datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)
    In [4]: (t1 - t0).total_seconds()
    Out[4]: 1514764800.0
    In [5]: from astropy.time import Time
    In [6]: (Time('2020-01-01T00:00:00') - Time('1972-01-01T00:00:00')).to_value('s')
    Out[6]: 1514764827.0

    There are also multiple efforts underway to add support for more timescales to different systems / programming languages (TAI for example to get a mononic timescale) in Linux (https://github.com/torvalds/linux/blob/4a57a8400075bc5287c5c877702c68aeae2a033d/include/uapi/linux/time.h#L64) and C++ (https://en.cppreference.com/w/cpp/chrono/tai_clock).

    To convert between e.g. between TAI and UTC, you also need a leap second table.

    Actually, it would make a lot of sense to change the internal representation to be unix time in TAI, not UTC and then convert on input/output in UTC.

    The C++ 20 proposal for calendars and timezones is here, which also includes dealing with leap seconds:
    https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0355r7.html

    C++ implementation library is here:
    https://github.com/HowardHinnant/date

    almost no one has given me any practical use cases for leap seconds.

    At the end of the day, the use case is to give instances in time with a precision of better than a minute and to represent all possible instances of our civil time keeping standard.
    Leap seconds are a fact of life in UTC, we need to deal with it.

    Not being able to represent actual correct non-naïve datetimes is a bug.

    The time difference between two instances in time being off by the number of leap seconds introduced between the two timestamps is a bug.

    Side note: The story is even more complicated for dates before 1972 as there were fractional adjustments between UTC and TAI and the second was slowed down / sped up to keep the relationship between UTC and UT1. E.g. there are 8 seconds and 82 microseconds between 1970-01-01 00:00:00 UTC and 1970-01-01 00:00:00 TAI.

    @vstinner
    Copy link
    Member Author

    This issue is open for 8 years. Maybe it's time to give up and accept that this issue is not important to motivate someone to through technical challenges? I propose to just close this issue.

    @cwatenpool
    Copy link

    I'd propose to keep this issue open to allow for additional meaningful discussion since I agree with @maxnoe remarks. This issue need to be addressed when precision is valued over close enough approximations.

    At the end of the day, the use case is to give instances in time with a precision of better than a minute and to represent all possible instances of our civil time keeping standard.
    Leap seconds are a fact of life in UTC, we need to deal with it.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    stdlib Python modules in the Lib dir
    Projects
    Development

    No branches or pull requests

    6 participants