Skip to content

Commit

Permalink
Add ignore_dst and disambiguate arg to relevant methods
Browse files Browse the repository at this point in the history
  • Loading branch information
ariebovenberg committed Jul 3, 2024
1 parent 1d4b722 commit a0a3cf9
Show file tree
Hide file tree
Showing 23 changed files with 2,128 additions and 1,660 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,16 @@ True

# A 'Naive' local time can't accidentally mix with other types.
# You need to explicitly convert it and handle ambiguity.
>>> hackathon_invite = LocalDateTime(2023, 10, 28, hour=12)
>>> hackathon_start = hackathon_invite.assume_tz("Europe/Amsterdam", disambiguate="earlier")
ZonedDateTime(2023-10-28 12:00:00+02:00[Europe/Amsterdam])
>>> party_invite = LocalDateTime(2023, 10, 28, hour=22)
>>> party_invite.add(hours=8)
Traceback (most recent call last):
ImplicitlyIgnoringDST: Adjusting a local datetime implicitly ignores DST [...]
>>> party_starts = party_invite.assume_tz("Europe/Amsterdam", disambiguate="earlier")
ZonedDateTime(2023-10-28 22:00:00+02:00[Europe/Amsterdam])

# DST-safe arithmetic
>>> hackathon_start.add(hours=24)
ZonedDateTime(2022-10-29 11:00:00+01:00[Europe/Amsterdam])
>>> party_starts.add(hours=8)
ZonedDateTime(2022-10-29 05:00:00+01:00[Europe/Amsterdam])

# Formatting & parsing common formats (ISO8601, RFC3339, RFC2822)
>>> livestream_start.format_rfc2822()
Expand Down
16 changes: 7 additions & 9 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Concrete classes
assume_tz,
assume_system_tz,
strptime,
:special-members: __add__, __sub__, __eq__
difference,
:special-members: __eq__
:member-order: bysource
:show-inheritance:

Expand All @@ -69,25 +70,17 @@ Concrete classes
parse_rfc3339,
parse_rfc2822,
strptime,
:special-members: __sub__
:member-order: bysource
:show-inheritance:

.. autoclass:: whenever.ZonedDateTime
:members:
tz,
is_ambiguous,
add,
subtract
:special-members: __add__, __sub__
:member-order: bysource
:show-inheritance:

.. autoclass:: whenever.SystemDateTime
:members:
add,
subtract
:special-members: __add__, __sub__
:member-order: bysource
:show-inheritance:

Expand Down Expand Up @@ -143,5 +136,10 @@ Miscellaneous
:member-order: bysource

.. autoexception:: whenever.RepeatedTime
:show-inheritance:

.. autoexception:: whenever.SkippedTime
:show-inheritance:

.. autoexception:: whenever.InvalidOffset
:show-inheritance:
44 changes: 23 additions & 21 deletions docs/deltas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ For example, you might want to reuse a particular duration,
or perform arithmetic on it.
For this, **whenever** provides an API
designed to help you avoid common pitfalls.
The type annotations and descriptive errors should guide you
to the correct usage.

Durations are created using the duration units provided.
Here is a quick demo:
Expand Down Expand Up @@ -69,27 +71,27 @@ There are three duration types in **whenever**:

This distinction determines which operations are supported:

+------------------------------+-------------------+-----------------------+-------------------------+
| Feature | ``TimeDelta`` | ``DateDelta`` | ``DateTimeDelta`` |
+==============================+===================+=======================+=========================+
| Add to ``DateTime`` | .. centered:: | .. centered:: ✅ | .. centered:: ✅ |
+------------------------------+-------------------+-----------------------+-------------------------+
| Add to ``Date`` | .. centered:: ❌ | .. centered:: ✅ | .. centered:: ❌ |
+------------------------------+-------------------+-----------------------+-------------------------+
| division (÷) | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+-------------------+-----------------------+-------------------------+
| multiplication (×) | .. centered:: ✅ | .. centered:: ⚠️ [1]_ | .. centered:: ⚠️ [1]_ |
+------------------------------+-------------------+-----------------------+-------------------------+
| comparison (``>, >=, <, <=``)| .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+-------------------+-----------------------+-------------------------+
| Commutative: | | | |
| ``dt + a + b == dt + b + a`` | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+-------------------+-----------------------+-------------------------+
| Reversible: | | | |
| ``(dt + a) - a == dt`` | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+-------------------+-----------------------+-------------------------+
| normalized | .. centered:: ✅ | .. centered:: ⚠️ [2]_ | .. centered:: ⚠️ [2]_ |
+------------------------------+-------------------+-----------------------+-------------------------+
+------------------------------+--------------------------+-----------------------+-------------------------+
| Feature | ``TimeDelta`` | ``DateDelta`` | ``DateTimeDelta`` |
+==============================+==========================+=======================+=========================+
| Add to datetimes | .. centered:: See :ref:`here <arithmetic-dst>` |
+------------------------------+--------------------------+-----------------------+-------------------------+
| Add to ``Date`` | .. centered:: ❌ | .. centered:: ✅ | .. centered:: ❌ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| division (÷) | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| multiplication (×) | .. centered:: ✅ | .. centered:: ⚠️ [1]_ | .. centered:: ⚠️ [1]_ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| comparison (``>, >=, <, <=``)| .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| Commutative: | | | |
| ``dt + a + b == dt + b + a`` | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| Reversible: | | | |
| ``(dt + a) - a == dt`` | .. centered:: ✅ | .. centered:: ❌ | .. centered:: ❌ |
+------------------------------+--------------------------+-----------------------+-------------------------+
| normalized | .. centered:: ✅ | .. centered:: ⚠️ [2]_ | .. centered:: ⚠️ [2]_ |
+------------------------------+--------------------------+-----------------------+-------------------------+

.. [1] Only by integers
.. [2] Years/months and weeks/days are normalized amongst each other,
Expand Down
64 changes: 11 additions & 53 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,59 +57,6 @@ Of course, feel free to work with :class:`~whenever.ZonedDateTime` if
you know the system's IANA timezone. You can use
the `tzlocal <https://pypi.org/project/tzlocal/>`_ library to help with this.

.. _faq-offset-arithmetic:

Why can't :class:`~whenever.OffsetDateTime` add or subtract durations?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``OffsetDateTime`` does not support addition or subtraction of time deltas.
This is a deliberate decision to prevent inadvertent DST-related bugs.

In practice, fixed-offset datetimes are commonly used to express a time at
which something occurs at a specific location.
But for many locations, the offset changes throughout the year
(due to DST or political decisions).
Allowing users to add/subtract from fixed-offset datetimes gives them the
impression that they are doing valid arithmetic,
while in actuality they are setting themselves up for DST-bugs
(which, again, are rampant).

An example:

>>> departure = OffsetDateTime(2024, 11, 3, hour=1, offset=-7)
>>> departure.add(hours=2) # a 2 hour delay
OffsetDateTime(2024-11-03 03:00:00-07:00)

While this is correct in theory, it may not be what the user intended.
Does the ``-7:00`` offset correspond to Denver, or Phoenix?
It would be correct in Phoenix (which doesn't observe DST), but
in Denver, the correct result would
actually be ``02:00:00-06:00`` — an hour earlier on the clock!

For whenever, preventing a damaging pitfall weighs heavier than supporting
a more theoretical usage pattern.
This is consisent with other libraries that emphasize correctness, such as NodaTime.
If you do need to perform arithmetic on a fixed-offset datetime,
you should make the location explicit by converting it to a
:class:`~whenever.ZonedDateTime` first:

>>> departure.to_tz("America/Denver").add(hours=2)
ZonedDateTime(2024-11-03 02:00:00-06:00[America/Denver])
>>> departure.to_tz("America/Phoenix").add(hours=2)
ZonedDateTime(2024-11-03 03:00:00-07:00[America/Phoenix])
>>> # not recommended, but possible:
>>> departure.instant().add(hours=2).to_fixed_offset(departure.offset)
OffsetDateTime(2024-11-03 03:00:00-07:00)

.. note::

``OffsetDateTime`` *does* support calculating the difference between two datetimes,
because this isn't affected by DST changes:

>>> a = OffsetDateTime(2024, 11, 3, hour=1, offset=-7)
>>> a - OffsetDateTime(2024, 11, 3, hour=3, offset=4)
TimeDelta(09:00:00)

.. _faq-leap-seconds:

Are leap seconds supported?
Expand All @@ -127,6 +74,17 @@ Nonetheless, these improvements are possible in the future:
- Allow parsing of leap seconds, e.g. ``23:59:60``.
- Allow representation of leap seconds (similar to rust Chrono)

Why not adopt Rust's Chrono API?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I did consider this initially, but decided against it for the following reasons:

- While I love Rust's functional approach to error handling,
it doesn't map well to idiomatic Python.
- At the time of writing, Chrono is only on version 0.4 and its API is still evolving.
- Chrono's timezone functionality can't handle disambiguation in gaps yet
(see `this issue <https://github.com/chronotope/chrono/issues/1448>`_)

.. _faq-why-not-dropin:

Why isn't it a drop-in replacement for the standard library?
Expand Down
Loading

0 comments on commit a0a3cf9

Please sign in to comment.