Comparing periods#
Equality, ordering, and the two questions about how one period nests inside another.
Equality, hashing and sorting#
Periods are immutable and hashable, so they work as dict keys and set members.
Two periods are equal when they split the timeline the same way and number it the same way - the way they were built is irrelevant:
print(Period.of_hours(24), Period.of_days(1))
print(Period.of("PT15M"), Period.of_minutes(15), Period.of_seconds(900))
P1D P1D
PT15M PT15M PT15M
Periods also sort, shortest first - including across the boundary between calendar and fixed-length periods:
periods = [Period.of_days(100), Period.of("PT1.5S"), Period.of_seconds(1), Period.of_years(1), Period.of_months(1)]
print(sorted(periods))
[PT1S, PT1.5S, P1M, P100D, P1Y]
Note
A calendar month has no fixed length, so ordering takes it as its mean Gregorian length of 30.436875 days (365.2425 / 12). That approximation is used only to order periods.
Note
Sorting periods with mismatched tzinfo values raises TypeError, exactly as sorting
naive and aware datetimes does. Equality is safe in all cases - a period with a timezone is never equal to one
without. See Time zones.
Counting one period inside another#
count() returns how many intervals of one period fit into each interval of another. A
count exists when the smaller period’s intervals fill the larger one exactly - one starts where the larger
interval starts, another ends where it ends, and no part-intervals fall in between.
pt15m = Period.of_minutes(15)
pt1h = Period.of_hours(1)
p1d = Period.of_days(1)
p1m = Period.of_months(1)
p1y = Period.of_years(1)
print(pt15m.count(pt1h))
print(pt1h.count(p1d))
print(p1m.count(p1y))
4
24
12
Two situations have no such number. Both return None rather than raising, because asking is legitimate and
the answer “there isn’t one” is a real answer:
Case |
Example |
|---|---|
aligned, but the count varies |
days in a month - clean boundaries, 28 to 31 of them |
not aligned, or this period is the larger |
15 minutes offset by 5 against a plain hour |
pt1h = Period.of_hours(1)
p1d = Period.of_days(1)
print(p1d.count(Period.of_months(1))) # aligned, but months hold 28-31 days
print(p1d.count(pt1h)) # the larger never fits inside the smaller
print(Period.of_minutes(25).count(pt1h)) # 60 is not a multiple of 25
None
None
None
To tell the two None cases apart, ask is_subperiod_of(), below - it is True for the
first and False for the second.
Offsets are taken into account. What matters is not that the offsets are equal, but that they differ by a whole number of the smaller period - that is what makes every boundary of the larger land on a boundary of the smaller:
pt15m_t5m = Period.of("PT15M+T5M")
print(pt15m_t5m.count(Period.of_hours(1))) # :05 :20 :35 :50 straddle the hour
print(pt15m_t5m.count(Period.of("PT1H+T5M"))) # both offset alike
print(pt15m_t5m.count(Period.of("PT1H+T20M"))) # offsets differ by one 15-minute step
None
4
4
Testing containment#
is_subperiod_of() asks the question: does every interval of this period fall wholly
inside a single interval of the other, never straddling a boundary?
pt15m = Period.of_minutes(15)
pt1h = Period.of_hours(1)
p1d = Period.of_days(1)
pt1h.is_subperiod_of(p1d) # True - an hour never straddles midnight
p1d.is_subperiod_of(Period.of_months(1)) # True - nor a day a month boundary
pt15m.is_subperiod_of(pt1h) # True
p1d.is_subperiod_of(pt1h) # False - the larger is never contained by the smaller
Period.of("PT15M+T5M").is_subperiod_of(pt1h) # False
p1d.is_subperiod_of(p1d) # True - a period always contains itself
Which to use: is_subperiod_of() when you need to know that aggregating is safe;
count() when you also need to know how many values to expect - for example to report
completeness.
Epoch agnosticism#
is_epoch_agnostic() says whether a period splits the timeline the same way regardless of
where counting begins. A period is epoch agnostic when its length divides evenly into the fixed unit above it - a
day for clock periods, a year for calendar ones:
Period.of_minutes(15).is_epoch_agnostic() # True - 15 minutes divides into a day
Period.of_hours(6).is_epoch_agnostic() # True
Period.of_days(1).is_epoch_agnostic() # True
Period.of_months(3).is_epoch_agnostic() # True - 3 months divides into a year
Period.of_days(7).is_epoch_agnostic() # False - which 7 days? depends where you start
Period.of_minutes(7).is_epoch_agnostic() # False
Period.of_months(5).is_epoch_agnostic() # False
Why it matters: an epoch-agnostic period is safe to publish on its own - anybody reading "PT15M" will
place the boundaries where you did. A period that is not needs an explicit origin to be unambiguous, so exchange
it as "2024-01-01/P7D" rather than "P7D". See Offsets and origins.
Note
An origin does not change the answer. is_epoch_agnostic() reports on the period’s
length alone, and 2024-01-01/P7D splits the timeline exactly as P7D+1D does - the origin only fixes
which interval is numbered zero:
p7d = Period.of("2024-01-01/P7D")
p7d.is_epoch_agnostic() # False - the same answer as a plain P7D
# The origin fixes the numbering, not the split: strip it and the plain offset period is left.
print(p7d.without_ordinal_shift())
P7D+1D