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

with_year_offset()

Period.of_years(10).with_year_offset(1)

with_month_offset()

Period.of_years(1).with_month_offset(9)

with_day_offset()

Period.of("P7D").with_day_offset(3)

with_hour_offset()

Period.of_days(1).with_hour_offset(9)

with_minute_offset()

Period.of_minutes(15).with_minute_offset(5)

with_second_offset()

Period.of_minutes(1).with_second_offset(30)

with_microsecond_offset()

Period.of_seconds(1).with_microsecond_offset(500_000)

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

without_offset()

the date/time offset, keeping the ordinal numbering

without_ordinal_shift()

the origin’s numbering, keeping the boundaries

base_period()

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