Delta types

Tip

For a quick introduction to adding and subtracting time, see Arithmetic. This page goes into more detail on working with durations as standalone objects.

As we’ve seen earlier, you can add and subtract time units from datetimes:

dt.add(hours=5, minutes=30)

However, sometimes you want to operate on these durations directly. 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 key concept is that there are three different delta types, each suited for different use cases:

  • Use TimeDelta if you’re working with Instant or exact time units (hours, minutes, seconds). Similar to datetime.timedelta.

  • Use ItemizedDateDelta if you’re working Date or only with calendar units (years, months, days).

  • Use ItemizedDelta if you need to work with both with calendar units (years, months, days) and exact time units (hours, minutes, seconds).

Note

ItemizedDelta and ItemizedDateDelta were introduced in version 0.10, and replace the (now deprecated) DateTimeDelta and DateDelta classes.

Here is a summary of the three delta types provided, and their key differences. Click on the features to learn more about them.

Overview

Exact and calendar units

A key distinction when working with durations is between exact time units and calendar units. See the fundamentals for an in-depth explanation.

In short:

  • Exact units (hours, minutes, seconds) have a fixed duration.

  • Calendar units (years, months, weeks, days) have a variable duration depending on context (e.g. leap years, DST).

Depending on the units you need to work with, you should choose the appropriate delta type:

Normalized or “itemized”

These delta classes also differ in how their components are stored. “Itemized” deltas keep track of their individual components (years, months, days, hours, minutes, seconds) separately, without normalizing them into each other.

For example, an ItemizedDelta of “1 hour and 90 minutes” will keep its components as “1 hour” and “90 minutes”, without converting the 90 minutes into 1 hour and 30 minutes. This is essential when working with calendar units, and sometimes useful when working with exact time units.

>>> d = ItemizedDelta(hours=1, minutes=90)
ItemizedDelta("PT1h90m")

You can imagine this working like a dict or Counter of components, where each unit is a key and its value is the corresponding amount:

>>> dict(d)
{'hours': 1, 'minutes': 90}

TimeDelta, on the other hand, normalizes all its components into each other. So “1 hour and 90 minutes” becomes “2 hours and 30 minutes”. This enables easier arithmetic and comparisons, as their duration is always the same.

>>> d = TimeDelta(hours=1, minutes=90)
TimeDelta("PT2h30m")

You can imagine this working like a big int of nanoseconds internally, which is then converted back into the appropriate units when needed:

>>> d.total("minutes")
150.0
>>> d.total("nanoseconds")
9000000000000

Equality

The difference between “itemized” and “normalized” is reflected in equality checks. Itemized deltas are considered equal only if all their individual components are the same:

>>> ItemizedDelta(hours=1, minutes=90) == ItemizedDelta(hours=2, minutes=30)
False  # items are not the same

Normalized deltas are considered equal if their total duration is the same, regardless of how their components are represented:

>>> TimeDelta(hours=1, minutes=90) == TimeDelta(hours=2, minutes=30)
True  # normalized durations are the same

Sign

All delta types carry a single sign that applies to every component uniformly—there are no mixed-sign deltas.

>>> ItemizedDelta(months=-3, days=-10, hours=-5)
ItemizedDelta("-P3m10dT5h")
>>> -ItemizedDateDelta(years=1, months=6)
ItemizedDateDelta("-P1y6m")

Negating a delta flips the sign of all components at once:

>>> d = ItemizedDelta(hours=2, minutes=30)
>>> -d
ItemizedDelta("-PT2h30m")

TimeDelta also has a single sign, but may be constructed with mixed-sign components, as they will be normalized into a single sign automatically:

>>> d = TimeDelta(hours=1, minutes=-15)
>>> d
TimeDelta("PT45m")

Convert into specific units

All delta types can be converted into specific units using their in_units() method. This is sometimes called “balancing”—redistributing the value across the requested units:

>>> delta = TimeDelta(hours=3, minutes=2, seconds=5)
>>> delta.in_units(["minutes", "seconds"])
ItemizedDelta("PT182m5s")
>>> # deltas can also be unpacked directly:
>>> hours, minutes = delta.in_units(["hours", "minutes"]).values()
(3, 2)

For example, 150 minutes balanced into hours and minutes:

>>> TimeDelta(minutes=150).in_units(["hours", "minutes"]).values()
(2, 30)

Tip

If you need the difference between two datetimes in specific units, use since() / until() instead of computing a delta and converting it. See Arithmetic.

If you’d like to convert into a single unit instead, see the next section.

Summing into a single unit

All delta types can also be summed into a single unit using their total() method, which returns a float.

>>> d = TimeDelta(hours=2, minutes=30, seconds=6)
>>> d.total("minutes")
150.1

When the total duration is requested in "nanoseconds" (the smallest supported unit), total() returns an int instead of a float to avoid precision issues.

Note

For ItemizedDelta and ItemizedDateDelta, both in_units() and total() require a relative_to parameter to resolve calendar units. This is because calendar units have variable lengths—1 month is 28, 29, 30, or 31 days depending on the starting date—so the conversion can only be performed with a concrete reference point. See the individual class reference pages for details.

Comparison

Only TimeDelta supports comparison operators (such as >, <, >=, and <=), as these operations only make sense when exclusively working with exact time units:

>>> TimeDelta(minutes=90) > TimeDelta(hours=1)
True

ItemizedDateDelta and ItemizedDelta do not support comparison operators, as they may contain calendar units, which have variable durations depending on context. For example, it’s not possible to say whether “1 month” is greater than “30 days” in general.

>>> a = ItemizedDateDelta(months=1)
>>> b = ItemizedDateDelta(days=30)
>>> a > b # TypeError

One way to compare itemized deltas is to convert them into one specific unit first, using their total() method and a relative date or datetime context:

>>> date = Date(2023, 1, 1)
>>> a.total("days", relative_to=date) > b.total("days", relative_to=date)
True

Addition and subtraction

All three delta types support addition and subtraction using the add() and subtract() methods. These methods return a new delta representing the sum or difference of the two deltas:

>>> TimeDelta(hours=2, minutes=30).add(hours=1)
TimeDelta("PT3h30m")

“Itemized” deltas do require a relative date or datetime context to resolve calendar units when adding or subtracting. For example, adding “1 month” to “30 days” requires knowing the starting date to determine the resulting duration:

>>> one_month = ItemizedDateDelta(months=1)
>>> one_month.add(days=30, relative_to=Date(2023, 1, 1))
ItemizedDateDelta("P2m2d")
>>> one_month.add(days=30, relative_to=Date(2023, 2, 28))
ItemizedDateDelta("P1m30d")

Operators

Mathematical operators such as +, -, *, and / are only supported for TimeDelta, as these operations only make sense for exact time units.

>>> delta = TimeDelta(hours=2, minutes=30)
>>> delta * 2
TimeDelta("PT5h")
>>> delta / 2
TimeDelta("PT1h15m")

Operators are not supported for itemized deltas, as they may contain calendar units, which have variable durations depending on context.

Rounding

Only TimeDelta has a round() method for rounding to a specific unit:

>>> delta = TimeDelta(hours=2, minutes=30, seconds=3)
>>> delta.round("hour")
TimeDelta("PT3h")

Rounding an itemized delta can only be done by also normalizing it, using the in_units() method:

>>> delta = ItemizedDelta(days=7, hours=2, minutes=84)
>>> delta.in_units(
...     ["days", "hours"],
...     relative_to=ZonedDateTime(2020, 1, 1, tz="UTC"),
...     round_mode="ceil",
...     round_increment=4
... )
ItemizedDelta("P7dT4h")

See Rounding for more information on rounding modes and increments.

ISO 8601 format

The ISO 8601 standard defines formats for specifying durations, the most common being:

±P nY nM nD T nH nM nS     (spaces added for clarity)

Where:

  • P is the period designator, and T separates date and time components.

  • nY is the number of years, nM is the number of months, etc.

  • Only seconds may have a fractional part.

  • At least one component must be present (it may be zero).

For example:

  • P3Y4DT12H30M is 3 years, 4 days, 12 hours, and 30 minutes.

  • -P2M5D is -2 months, and -5 days.

  • P0D is zero.

  • +PT5M4.25S is 5 minutes and 4.25 seconds.

All deltas can be converted to and from this format using the methods:

>>> TimeDelta(hours=3).format_iso()
'PT3H'
>>> ItemizedDelta(years=-1, months=-3, seconds=-15).format_iso()
'-P1Y3MT15S'
>>> ItemizedDateDelta.parse_iso('-P2M')
ItemizedDateDelta("-P2m")
>>> ItemizedDelta.parse_iso('P3YT90M')
ItemizedDelta("P3yT90m")

Why not support the full ISO 8601 standard?

Full conformance to the ISO 8601 standard is not provided, because:

  • It allows for a lot of unnecessary flexibility (e.g. fractional components other than seconds)

  • There are different revisions with different rules

  • The full specification is not freely available

Supporting a commonly used subset is more practical. This is also what all established libraries do.

Equivalents in other languages

The three delta types in whenever are similar to those in other languages:

Library

TimeDelta

ItemizedDateDelta

ItemizedDelta

NodaTime (C#)

Duration

[2]

Period

java.time (Java)

Duration

Period

PeriodDuration [3]

Jiff (Rust)

SignedDuration

Span

Temporal (JS)

Duration