loopable / platform /core /periods.py
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
c14ceee verified
Raw
History Blame Contribute Delete
4.97 kB
"""Period helpers — floral wholesale is highly seasonal, so YoY / same-period
comparisons are first-class. Everything returns ISO date strings (yyyy-mm-dd).
`today` is injectable for testing/reproducibility.
"""
import datetime as dt
def _d(x):
return x.isoformat()
def today():
return dt.date.today()
def ytd(t=None):
"""(start, end) for year-to-date through `t`."""
t = t or today()
return _d(dt.date(t.year, 1, 1)), _d(t)
def ytd_last_year(t=None):
"""Same calendar window one year earlier (Jan 1 LY → same month/day LY).
Clamps Feb 29 → Feb 28 in non-leap years."""
t = t or today()
try:
end = dt.date(t.year - 1, t.month, t.day)
except ValueError:
end = dt.date(t.year - 1, t.month, t.day - 1)
return _d(dt.date(t.year - 1, 1, 1)), end if isinstance(end, str) else _d(end)
def ltm(t=None):
"""Trailing 12 months ending `t`."""
t = t or today()
return _d(t - dt.timedelta(days=365)), _d(t)
def prior_ltm(t=None):
t = t or today()
return _d(t - dt.timedelta(days=730)), _d(t - dt.timedelta(days=365))
def mtd(t=None):
t = t or today()
return _d(dt.date(t.year, t.month, 1)), _d(t)
def wtd(t=None):
"""(Monday of this ISO week, t) — week-to-date."""
t = t or today()
return _d(t - dt.timedelta(days=t.weekday())), _d(t)
def qtd(t=None):
"""(first day of this calendar quarter, t) — quarter-to-date."""
t = t or today()
q_start_month = ((t.month - 1) // 3) * 3 + 1
return _d(dt.date(t.year, q_start_month, 1)), _d(t)
def quarter_range(t=None):
"""(first day, last day) of the calendar quarter containing t."""
t = t or today()
qs = ((t.month - 1) // 3) * 3 + 1
first = dt.date(t.year, qs, 1)
nxt = dt.date(t.year + (1 if qs + 3 > 12 else 0), ((qs + 2) % 12) + 1, 1)
return _d(first), _d(nxt - dt.timedelta(days=1))
def shift_year(date_from, date_to, weeks=False):
"""Same window one year earlier. For week-aligned comparisons (weeks=True) shift by exactly 52
weeks (364 days) so the weekday lines up; otherwise shift by calendar year (clamping Feb 29)."""
f = dt.date.fromisoformat(date_from)
t = dt.date.fromisoformat(date_to)
if weeks:
return _d(f - dt.timedelta(days=364)), _d(t - dt.timedelta(days=364))
def back(d):
try:
return dt.date(d.year - 1, d.month, d.day)
except ValueError:
return dt.date(d.year - 1, d.month, d.day - 1)
return _d(back(f)), _d(back(t))
def last_n_days(n, t=None):
t = t or today()
return _d(t - dt.timedelta(days=n)), _d(t)
def year_range(year):
return f'{year}-01-01', f'{year}-12-31'
def month_range(year, month):
"""(first day, last day) of a given calendar month, ISO strings."""
first = dt.date(year, month, 1)
nxt = dt.date(year + (1 if month == 12 else 0), 1 if month == 12 else month + 1, 1)
return _d(first), _d(nxt - dt.timedelta(days=1))
def period_options(t=None, n_months=15):
"""Ordered {label: (date_from, date_to)} for the close/reconciliation period picker."""
t = t or today()
opts = {}
opts['This month (to date)'] = (_d(dt.date(t.year, t.month, 1)), _d(t))
lm_year, lm_month = (t.year, t.month - 1) if t.month > 1 else (t.year - 1, 12)
lf, lt = month_range(lm_year, lm_month)
opts[f'Last month ({lf[:7]})'] = (lf, lt)
opts['Quarter to date'] = (_d(dt.date(t.year, ((t.month - 1) // 3) * 3 + 1, 1)), _d(t))
opts['Year to date'] = (_d(dt.date(t.year, 1, 1)), _d(t))
for label, mf, mt in reversed(month_starts(n_months, t)):
if label == f'{t.year}-{t.month:02d}' or label == lf[:7]:
continue
opts[label] = (mf, mt)
return opts
def month_starts(n_back=13, t=None):
"""List of (yyyy-mm, start, end) for the last n_back months including current."""
t = t or today()
out = []
y, m = t.year, t.month
for _ in range(n_back):
start = dt.date(y, m, 1)
end = (dt.date(y + (m // 12), (m % 12) + 1, 1) - dt.timedelta(days=1))
out.append((f'{y}-{m:02d}', _d(start), _d(end)))
m -= 1
if m == 0:
m = 12
y -= 1
return list(reversed(out))
def week_starts(n_back=12, t=None):
"""List of (label, start, end) for the last n_back ISO weeks (Mon–Sun) including the current
(partial) week. label = the Monday as 'MM-DD' (compact, chart-friendly); end is clamped to t
for the current week so weekly totals never include future days."""
t = t or today()
this_mon = t - dt.timedelta(days=t.weekday())
out = []
for i in range(n_back):
mon = this_mon - dt.timedelta(weeks=i)
sun = mon + dt.timedelta(days=6)
end = min(sun, t)
out.append((f'{mon.month:02d}-{mon.day:02d}', _d(mon), _d(end)))
return list(reversed(out))
def yoy_pct(this_v, last_v):
if last_v:
return (this_v - last_v) / last_v * 100.0
return None