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");