Interoperability#
Describing a period as a string, and handing it to other tools.
Describing a period as a string#
Three representations, for three purposes:
Form |
|
Use for |
|---|---|---|
|
strict ISO 8601 - the duration alone, no offset |
|
|
|
round-tripping through |
|
|
logging and debugging - as |
water_day = Period.of("P1D+T9H")
print(water_day.iso_duration) # strict ISO 8601 - the duration alone
print(str(water_day)) # round-trips through Period.of()
print(repr(water_day)) # as str(), plus the timezone when there is one
P1D
P1D+T9H
P1D+T9H
A period with an origin renders in the <origin>/<duration> form, so it round-trips too:
p7d = Period.of("2024-01-01/P7D")
print(p7d)
print(Period.of(str(p7d)) == p7d)
2024-01-01/P7D
True
repr() differs from str() only by adding the timezone, which str() leaves out because
of() cannot read one back:
print(repr(Period.of("PT15M").with_tzinfo(UTC)))
print(repr(Period.of("PT15M")))
PT15M[Z]
PT15M
Use repr() - or f"{period!r}" - in log lines and error messages, where the timezone matters and nothing
is going to parse the result back.
Note
iso_duration deliberately drops the offset, so it is not enough to reconstruct an
offset period. Persist str() if you need the period back.
Converting to a timedelta#
A period whose step is seconds or microseconds has a fixed length, so it converts:
Months and years do not, and return None rather than an approximation:
print(Period.of_minutes(15).timedelta)
print(Period.of_days(1).timedelta)
print(Period.of("PT0.04S").timedelta == timedelta(microseconds=40_000))
print(Period.of_months(1).timedelta) # no fixed length, so None
print(Period.of_years(1).timedelta)
0:15:00
1 day, 0:00:00
True
None
None
Always check for None before doing timedelta arithmetic - or better, use
ordinal() and datetime(), which work for calendar periods too.
Using periods with Polars#
Two properties emit Polars duration strings, so a period can drive a Polars grouping or range directly:
print(Period.of_minutes(15).pl_interval)
print(Period.of_months(1).pl_interval)
print(Period.of("PT0.04S").pl_interval)
print(Period.of("P1D+T9H").pl_offset)
print(Period.of("P1Y+9M").pl_offset)
900s
1mo
40000us
0mo32400000000us
9mo0us
pl_interval gives the period’s length, for arguments such as every and
interval; pl_offset gives its offset, for offset and Expr.dt.offset_by:
import polars as pl
water_day = Period.of("P1D+T9H")
df = pl.DataFrame(
{
"timestamp": pl.datetime_range(datetime(2024, 3, 1), datetime(2024, 3, 4), interval="1h", eager=True),
"flow": range(73),
}
)
daily = (
df.sort("timestamp")
.group_by_dynamic("timestamp", every=water_day.pl_interval, offset=water_day.pl_offset)
.agg(pl.col("flow").mean().alias("mean_flow"), pl.len().alias("n"))
)
print(daily)
shape: (4, 3)
┌─────────────────────┬───────────┬─────┐
│ timestamp ┆ mean_flow ┆ n │
│ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ f64 ┆ u32 │
╞═════════════════════╪═══════════╪═════╡
│ 2024-02-29 09:00:00 ┆ 4.0 ┆ 9 │
│ 2024-03-01 09:00:00 ┆ 20.5 ┆ 24 │
│ 2024-03-02 09:00:00 ┆ 44.5 ┆ 24 │
│ 2024-03-03 09:00:00 ┆ 64.5 ┆ 16 │
└─────────────────────┴───────────┴─────┘
Every window starts at 09:00, so the hourly readings are grouped into hydrological days rather than calendar ones. The first and last are partial, because the data does not begin or end on a 09:00 boundary.