Offsets and origins#
Moving a period’s interval boundaries off their natural position.
By default a period’s intervals sit on the natural boundaries of their unit: a day runs midnight to midnight, a year from 1 January. Real measurement regimes often do not. Two mechanisms move the grid, and they answer different questions:
- Offset
Where are the boundaries? Shifts every boundary by a fixed amount, keeping the interval size.
- Origin
Which interval is number zero? Pins the numbering to a chosen datetime, and aligns the boundaries so that datetime is one of them.
Setting an offset#
Each with_*_offset method returns a new period - periods are immutable, so the original is untouched:
Method |
Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
Warning
A month offset only makes sense for a period whose step is months, because a month has no fixed length in
seconds. Period.of_days(1).with_month_offset(1) raises PeriodValidationError.
Worked example: the hydrological day#
UK hydrological convention measures a “day” from 09:00 to 09:00. As a period that is a one-day interval offset by nine hours:
water_day = Period.of_days(1).with_hour_offset(9)
# A reading at 07:30 belongs to the water day that began at 09:00 the previous day.
print(water_day.floor(datetime(2024, 3, 15, 7, 30)))
# A reading at 10:00 belongs to the one that began this morning.
print(water_day.floor(datetime(2024, 3, 15, 10, 0)))
2024-03-14 09:00:00
2024-03-15 09:00:00
Alignment follows the shifted boundaries:
water_day = Period.of("P1D+T9H")
water_day.is_aligned(datetime(2024, 3, 15, 9, 0)) # True - the water day starts here
water_day.is_aligned(datetime(2024, 3, 15, 0, 0)) # False - midnight starts nothing now
Worked example: the water year#
The UK water year runs from 09:00 on 1 October. That is a one-year period offset by nine months and nine hours:
water_year = Period.of("P1Y+9MT9H")
print(water_year.floor(datetime(2024, 3, 15)))
print(water_year.floor(datetime(2024, 11, 15)))
2023-10-01 09:00:00
2024-10-01 09:00:00
Offsets are accounted for when periods are compared, so a hydrological day nests cleanly inside a water year - while a calendar day, which straddles the 09:00 boundary every 1 October, does not:
water_year = Period.of("P1Y+9MT9H")
Period.of("P1D+T9H").is_subperiod_of(water_year) # True - a water day nests cleanly
Period.of_days(1).is_subperiod_of(water_year) # False - straddles every 1 October
Period.of_hours(1).count(Period.of("P1D+T9H")) # 24 - hours still fit a 09:00 day
Period.of_hours(1).is_subperiod_of(Period.of("P1D+T9H30M")) # False - the half hour breaks it
See Comparing periods for the full rules.
Inspecting an offset#
water_day = Period.of("P1D+T9H")
print(water_day.has_offset())
print(water_day.month_offset, water_day.microsecond_offset)
print(water_day.offset)
print(water_day.iso_duration) # the duration alone, without the offset
print(water_day) # duration and offset together
True
0 32400000000
+T32400S
P1D
P1D+T9H
Setting an origin#
with_origin() makes a chosen datetime ordinal 0 and an interval boundary. It is
what you need for a period with no meaningful natural boundary - e.g. a 7-day period, or an instrument started at an
arbitrary time:
p7d = Period.of_days(7).with_origin(datetime(2024, 1, 1))
print(p7d.ordinal(datetime(2024, 1, 1)))
print(p7d.is_aligned(datetime(2024, 1, 1)))
print(p7d.datetime(1))
print(p7d.datetime(-1))
0
True
2024-01-08 00:00:00
2023-12-25 00:00:00
The <start>/<duration> string format does the same thing:
from_string = Period.of("2024-01-01/P7D")
from_builder = Period.of_days(7).with_origin(datetime(2024, 1, 1))
print(from_string == from_builder)
print(from_string)
True
2024-01-01/P7D
An origin discards any offset already on the period and recomputes it from the origin datetime. If the origin carries a
tzinfo, the resulting period adopts it.
Removing offsets and origins#
Three methods remove the modifications, each returning a new period:
Method |
Removes |
|---|---|
the date/time offset, keeping the ordinal numbering |
|
the origin’s numbering, keeping the boundaries |
|
both - back to the plain period the factories produce |
p7d = Period.of("2024-01-01/P7D")
print(p7d.base_period())
print(Period.of("P1D+T9H").base_period())
# The boundaries are unchanged; only the numbering is.
p7d_no_shift = p7d.without_ordinal_shift()
print(p7d_no_shift.is_aligned(datetime(2024, 1, 1)))
print(p7d_no_shift.ordinal(datetime(2024, 1, 1)))
P7D
P1D
True
105555