ariebovenberg / whenever

⏰ Sensible and typesafe datetimes for Python
https://whenever.rtfd.io
MIT License
362 stars 7 forks source link

ZonedDateTime should expose its 'fold' and 'is_ambiguous' properties in some way #68

Closed bxparks closed 3 months ago

bxparks commented 4 months ago

As far as I can tell, ZonedDateTime, which inherits from DateTime AwareDateTime time, does not expose the underlying fold value: https://whenever.readthedocs.io/en/latest/api.html#whenever.ZonedDateTime https://whenever.readthedocs.io/en/latest/api.html#whenever.AwareDateTime

It may not be obvious that the fold parameter described by PEP 495 is both an input parameter and an output parameter. The output fold parameter is what allows datetime.datetime to support round trip conversions without loss of information. In other words, epoch_seconds can be converted into datetime.datetime, which can then be converted back to epoch_seconds, and the 2 epoch_seconds will be identical, even during an overlap (when, for example, northern hemisphere countries "fall back" in the fall).

But more practically, the fold parameter of a datetime.datetime object is useful to a client app, because it allows the app to know if a specific epoch_second was converted to a datetime which could be interpreted by humans as ambiguous. Access to the fold parameter is required if the application wants to warn the user of an ambiguous time, something like: "The meeting time is 01:30 on Nov 3, 2024 (Warning: this is the first occurrence of 01:30, not the second occurrence.)"

Currently, ZonedDateTime is a thin wrapper around datetime.datetime. But it does not expose the fold parameter to the calling client. I think it should, but in a way that is more friendly than fold. As I was writing this ticket, it occurred to me that there is a complication: the underlying datetime.datetime class, which is used by ZonedDateTime, does not actually contain enough information to solve this problem properly. We actually needZonedDateTime to expose two bits of information:

1) is_ambiguous: bool: Whether or not the epoch_seconds mapped to a ZonedDateTime that can be ambiguous, i.e. a "duplicate" or an "overlap". Alternative names for this property may be is_overlap or is_duplicate. 2) fold: int: 0 if the ZonedDateTime corresponds to the first instance, and 1 if the ZonedDateTime corresponds to the second.

The is_ambiguous parameter can be calculated by brute force within ZonedDateTime. But it would be more efficient to delegate that calculation to a TimeZone class (which does not currently exist in whenever, but I described its contours in #60), so that we get efficient access to the metadata information about the given timezone.

Anyway, I don't know how you want to prioritize this issue. I figure that if you are going make a clean break from datetime, and solve a bunch of problems, you might as well fix this one too. :-)

ariebovenberg commented 4 months ago

Interesting.

For detecting ambiguity, there already is a disambiguated() method that checks if a time can be considered ambiguous. In 0.4 it will be renamed to is_ambiguous()

Regarding exposing the fold value: in the current version, ZonedDateTime does expose a fold attribute, it just is mistakenly undocumented. However, I will be removing it in version 0.4 because no other language I know of stores the disambiguation: the standard seems to be storing the offset instead. As an alternative, perhaps something like disambiguation() can be added later, which returns an enum like NOT_AMBIGUOUS | EARLIER | LATER or something...

also: I also wish that ZoneInfo objects themselves had methods like is_ambiguous...

ariebovenberg commented 3 months ago

is_ambiguous() is now part of ZonedDateTime as of 0.4. Thefoldattribute is considered an implementation detail and can be retrieved from the underlying datetime withdt.py_datetime().fold`