File size: 4,968 Bytes
c14ceee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | """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
|