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