Skip to main content

patterns/
series.rs

1use crate::seq::Facet;
2use patterns_macros::because;
3use std::collections::BTreeMap;
4
5#[derive(patterns_macros::Record)]
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7pub struct Running {
8    pub amount: i128,
9    pub count: i64,
10}
11because!(Running, "a total that events move, signed because an unlock takes back what a lock added and a count of open positions can go under zero when its opening was never seen");
12
13#[derive(patterns_macros::Record)]
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
15pub struct DayTotal {
16    pub amount: i128,
17    pub count: i64,
18    pub ending_amount: i128,
19    pub ending_count: i64,
20}
21because!(DayTotal, "what one day added to a running total and where the total stood at the day's end, the two readings a chart needs: the bar and the line");
22
23pub type Days<S> = BTreeMap<(S, u64), DayTotal>;
24
25pub type Totals<S> = BTreeMap<S, Running>;
26
27pub fn booked<S: Ord + Copy>(days: Days<S>, totals: Totals<S>, series: S, day: u64, amount: i128, count: i64) -> (Days<S>, Totals<S>) {
28    let mut totals = totals;
29    let mut days = days;
30    let running = totals.entry(series).or_default();
31    running.amount = running.amount.saturating_add(amount);
32    running.count = running.count.saturating_add(count);
33    let ending = *running;
34    let row = days.entry((series, day)).or_default();
35    row.amount = row.amount.saturating_add(amount);
36    row.count = row.count.saturating_add(count);
37    row.ending_amount = ending.amount;
38    row.ending_count = ending.count;
39    (days, totals)
40}
41because!(booked, "one movement written to a series: the running total moves, and the day's row takes the movement and the total the day now ends at, so the bar and the line can never disagree");
42
43pub fn booked_all<S: Ord + Copy>(days: Days<S>, totals: Totals<S>, day: u64, bookings: &[(S, i128, i64)]) -> (Days<S>, Totals<S>) {
44    let mut out = (days, totals);
45    for (series, amount, count) in bookings {
46        out = booked(out.0, out.1, *series, day, *amount, *count);
47    }
48    out
49}
50because!(booked_all, "several movements of one event written in order, so an event that moves two series is one call in a reducer");
51
52pub fn moved_total<S: Ord + Copy>(totals: Totals<S>, series: S, amount: i128) -> Totals<S> {
53    let mut out = totals;
54    let running = out.entry(series).or_default();
55    running.amount = running.amount.saturating_add(amount);
56    out
57}
58because!(moved_total, "a running total moved with no day row written, for supply that arrives from outside the program's own events and belongs to the total but to no day of the chart");
59
60pub fn amount_of<S: Ord>(totals: &Totals<S>, series: &S) -> i128 {
61    totals.get(series).map(|r| r.amount).unwrap_or(0)
62}
63because!(amount_of, "where a running total stands, or zero for a series nothing has moved yet");
64
65pub fn count_of<S: Ord>(totals: &Totals<S>, series: &S) -> i64 {
66    totals.get(series).map(|r| r.count).unwrap_or(0)
67}
68because!(count_of, "how many a running total counts, the other half of amount_of, zero for a series nothing has moved yet");
69
70pub fn series_days<S: Ord + Copy>(days: &Days<S>, series: S) -> Vec<(u64, DayTotal)> {
71    days.iter()
72        .filter(|((s, _), _)| *s == series)
73        .map(|((_, day), row)| (*day, *row))
74        .collect()
75}
76because!(series_days, "the day rows of one series in day order, out of a table that keys every series together");
77
78pub fn tallied<F, T: Facet<F>>(totals: BTreeMap<T::Value, u64>, rows: &[T]) -> BTreeMap<T::Value, u64>
79where
80    T::Value: Ord + Copy,
81{
82    let mut out = totals;
83    for row in rows {
84        *out.entry(row.facet()).or_default() += 1;
85    }
86    out
87}
88because!(tallied, "a count per kind raised by one for each row of that kind, the totals a feed shows in its filter menu without counting the feed");
89
90pub fn last_facet_or<F, T: Facet<F>>(items: &[T], fallback: T::Value) -> T::Value {
91    items.last().map(|item| item.facet()).unwrap_or(fallback)
92}
93because!(last_facet_or, "the projection of a list's last element, or a fallback for an empty list, which is where a walk's cursor moves to once its rows are priced");