Skip to main content

patterns/
ledger.rs

1use crate::arith::{away, div, mul, raise_by, reduce_by, rem, Floor};
2use crate::clamp::clamp_upper;
3use crate::select::{either, is_above, is_same};
4use crate::seq::{Facet, Valued};
5use patterns_macros::because;
6
7pub fn tally<F, T: Facet<F>>(items: &[T]) -> Vec<(T::Value, usize)>
8where
9    T::Value: Clone,
10{
11    let mut out: Vec<(T::Value, usize)> = Vec::new();
12    for item in items {
13        let value = item.facet();
14        let mut seen = false;
15        for row in out.iter_mut() {
16            if row.0 == value {
17                row.1 += 1;
18                seen = true;
19            }
20        }
21        if !seen {
22            out.push((value, 1));
23        }
24    }
25    out
26}
27because!(tally, "a count per facet value in one pass, so a report by category needs no list of categories beforehand and each value appears once, in the order first seen");
28
29pub fn running_total<M, T: Valued<M>>(items: &[T], start: T::Value) -> Vec<T::Value>
30where
31    T::Value: Floor,
32{
33    let mut out = Vec::new();
34    let mut sum = start;
35    for item in items {
36        sum = raise_by(sum, item.value());
37        out.push(sum);
38    }
39    out
40}
41because!(running_total, "the balance after every step from a seed the caller gives, reading the same marker value that total sums so the last line of a statement equals its total, and the pattern never conjures a zero");
42
43pub fn share_of<T: Floor + Copy>(amount: T, part: T, whole: T) -> Option<T> {
44    match (div(amount, whole), rem(amount, whole)) {
45        (Some(wholes), Some(left)) => {
46            div(mul(left, part), whole).map(|scrap| raise_by(mul(wholes, part), scrap))
47        }
48        _ => None,
49    }
50}
51because!(share_of, "a proportional share of an amount, split into a whole quotient and a remainder before scaling: the remainder term cannot overflow because a remainder is under the whole, while the quotient term saturates through mul; truncated because a rounding rule is a fact the spec states");
52
53pub fn div_up<T: Floor + PartialEq + Copy>(value: T, by: T) -> Option<T> {
54    match (div(value, by), rem(value, by)) {
55        (Some(whole), Some(left)) => Some(either(
56            is_same(left, T::ZERO),
57            whole,
58            raise_by(whole, T::ONE),
59        )),
60        _ => None,
61    }
62}
63because!(div_up, "division rounded toward the ceiling, separate from div because a started period is charged in full and rounding down is the common error in a tariff");
64
65pub fn backoff<T: Floor + PartialOrd>(delay: T, factor: T, cap: T) -> T {
66    clamp_upper(mul(delay, factor), cap)
67}
68because!(backoff, "the next wait in a retry schedule, scaled then capped in one call so a schedule cannot grow past its ceiling and the two steps are never written in the wrong order");
69
70pub fn between<T: Floor + Copy + PartialOrd>(from: T, to: T, part: T, whole: T) -> Option<T> {
71    let moved = share_of(away(from, to), part, whole)?;
72    Some(either(is_above(to, from), raise_by(from, moved), reduce_by(from, moved)))
73}
74because!(between, "a point some part of the way from one value to another, which a colour that drifts, a meter that fills and an arc that counts down are all the same question wearing different clothes; the direction is read from the two ends rather than asked for, so a run backwards needs no second function");