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
TimeDeltaif you’re working withInstantor exact time units (hours, minutes, seconds). Similar todatetime.timedelta.Use
ItemizedDateDeltaif you’re workingDateor only with calendar units (years, months, days).Use
ItemizedDeltaif 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¶
Feature |
|||
|---|---|---|---|
exact units |
calendar units |
exact and calendar units |
|
yes |
no |
no |
|
n/a |
n/a |
||
n/a |
n/a |
||
with |
with |
||
Applies to… |
|||
Similar to… |
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:
TimeDeltafor exact time unitsItemizedDateDeltafor calendar unitsItemizedDeltafor a combination of the two
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:
Pis the period designator, andTseparates date and time components.nYis the number of years,nMis 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:
P3Y4DT12H30Mis 3 years, 4 days, 12 hours, and 30 minutes.-P2M5Dis -2 months, and -5 days.P0Dis zero.+PT5M4.25Sis 5 minutes and 4.25 seconds.
All deltas can be converted to and from this format using the methods:
Delta Type |
Format Method |
Parse Method |
|---|---|---|
>>> 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 |
|||
|---|---|---|---|
NodaTime (C#) |
|
|
|
java.time (Java) |
|
|
|
Jiff (Rust) |
|
|
|
Temporal (JS) |
|