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

Unexpected behavior of disambiguate during dst change foreward #150

Closed
spacemanspiff2007 opened this issue Jul 9, 2024 · 4 comments
Closed
Labels
documentation Improvements or additions to documentation in development Has been addressed but not released yet

Comments

@spacemanspiff2007
Copy link

When the dst change means that the clock moves forward disambiguate behaves rather unexpected.
I would have expected that earlier returns the time before the switch forward and later returns the time when the switch is complete.
However whenever is guessing that I might want to add/subtract an hour and does that for me resulting in the following behavior (Which is in brief described here)

# Clock moves from 2 -> 3, so this does not exist
dt = ZonedDateTime(2001, 3, 25, 2, 30, tz='Europe/Berlin', disambiguate='earlier')
print(dt)
dt = ZonedDateTime(2001, 3, 25, 2, 30, tz='Europe/Berlin', disambiguate='later')
print(dt)
2001-03-25T01:30:00+01:00[Europe/Berlin]
2001-03-25T03:30:00+02:00[Europe/Berlin]

What I would have expected

2001-03-25T01:59:59.999...+01:00[Europe/Berlin]
2001-03-25T03:00:00+02:00[Europe/Berlin]

Could you explain the reasoning behind this behavior?

@ariebovenberg
Copy link
Owner

ariebovenberg commented Jul 9, 2024

Happy to explain. This probably warrants a note in the docs.

The reasons for this behavior:

  • It corresponds to the likely reason for the missing date time: i.e. we forgot to adjust the clock, or adjusted it too early
  • It corresponds to how most other datetime libraries do it (JS (the new Temporal API), C# (Nodatime), java.time, Python itself to name a few)
  • It corresponds with the iCalendar (RFC5545) standard of handling gaps
  • Extrapolation keeps the most salient information about the time (i.e. 2:15am becoming 3:15am keeps the fact that it’s a ‘quarter past’ the hour. Truncating all 2:xx times to 1:59am loses more information IMHO)

The figure in the Python docs here also shows how this ‘extrapolation’ makes sense graphically.

edit:
PS: When first developing the library, I was surprised by this behavior as well. However, I soon discovered the reasoning behind it. There's an interesting thread here where the Temporal designers discuss the matter.

@ariebovenberg ariebovenberg added question Further information is requested documentation Improvements or additions to documentation and removed question Further information is requested labels Jul 9, 2024
@spacemanspiff2007
Copy link
Author

Thank you for the insight. What do you think is the best way to find the time after the DST switch?
I've come up with something like this:

def find_time_after_dst_switch(dt: SystemDateTime, time: Time) -> Instant:
    # DST changes typically occur on the full minute
    time = time.replace(second=0, nanosecond=0)
    hour = time.hour
    minute = time.minute

    while True:
        minute += 1
        if minute >= 60:
            minute = 0
            hour += 1
            if hour >= 24:
                hour = 0
        time = time.replace(hour=hour, minute=minute)
        try:
            return dt.replace_time(time.replace(hour=hour, minute=minute), disambiguate='raise').instant()
        except SkippedTime:
            continue

@ariebovenberg
Copy link
Owner

ariebovenberg commented Jul 9, 2024

@spacemanspiff2007 there's no clean way to do this, I'm afraid. This information would need to be exposed by ZoneInfo classes. I'll probably submit a feature request for this in the stdlib.

Some potential improvements to your code:

  • use bisect to perform a more efficient search.
  • instead of having those ugly minute >= 60 checks, use LocalDateTime to do arithmetic in local time:

Here is a naive algorithm which can be improved by bisect, and probably has some bugs in it still...

skipped_time = SystemDateTime(...)

guess = skipped_time.local().subtract(hours=48, ignore_dst=True)  # assume there are no two transitions so closeby
while True:
    try:
        guess.add(minutes=1, ignore_dst=True).assume_system_tz(disambiguate="raise")
    except SkippedTime:
        break

@ariebovenberg ariebovenberg added the in development Has been addressed but not released yet label Jul 13, 2024
@ariebovenberg
Copy link
Owner

@spacemanspiff2007 I've added an explicit note to the docs in the section about ambiguity. Let me know if you have any other ideas/suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation in development Has been addressed but not released yet
Projects
None yet
Development

No branches or pull requests

2 participants