Source code for finance_calcs._periods
"""Period and calendar-bucket helpers for expression metrics."""
from __future__ import annotations
from typing import TypeAlias
import polars as pl
from finance_enums import Frequency, to_frequency
try:
from finance_dates import period_grid as dates_period_grid
except ImportError: # pragma: no cover - compatibility with older finance-dates
def dates_period_grid(date: pl.Expr, period: Frequency | str | pl.Expr) -> pl.Expr:
if isinstance(period, pl.Expr):
return period
if isinstance(period, Frequency):
return date.dt.truncate(period.polars_truncate)
value = period.strip()
if not value:
raise ValueError("period must not be empty")
try:
return date.dt.truncate(to_frequency(value).polars_truncate)
except ValueError:
return date.dt.truncate(value)
PeriodLike: TypeAlias = Frequency | str | pl.Expr
[docs]
def period_bucket(date: pl.Expr, period: PeriodLike) -> pl.Expr:
"""Return a period bucket expression for ``date``.
``period`` accepts a :class:`finance_enums.Frequency`, any alias
understood by ``finance_enums.to_frequency()``, any Polars duration
string accepted by ``dt.truncate()``, or a precomputed bucket
expression.
"""
return dates_period_grid(date, period)
def _check_window_period(window: int | None, period: PeriodLike | None) -> None:
if window is not None and period is not None:
raise ValueError("window and period are mutually exclusive")
def _bucket_or_none(date: pl.Expr | None, period: PeriodLike | None) -> pl.Expr | None:
if period is None:
return None
if isinstance(period, pl.Expr):
return period
if date is None:
raise ValueError("date is required when period is a Frequency or duration string")
return period_bucket(date, period)