premise 0.1.5

the core pattern library of Premise: sequences, tables, records, money, days and text, every function carrying the reason it exists
Documentation
use crate::arith::{away, div, mul, raise_by, reduce_by, rem, Floor};
use crate::clamp::clamp_upper;
use crate::select::{either, is_above, is_same};
use crate::seq::{Facet, Valued};
use patterns_macros::because;

pub fn tally<F, T: Facet<F>>(items: &[T]) -> Vec<(T::Value, usize)>
where
    T::Value: Clone,
{
    let mut out: Vec<(T::Value, usize)> = Vec::new();
    for item in items {
        let value = item.facet();
        let mut seen = false;
        for row in out.iter_mut() {
            if row.0 == value {
                row.1 += 1;
                seen = true;
            }
        }
        if !seen {
            out.push((value, 1));
        }
    }
    out
}
because!(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");

pub fn running_total<M, T: Valued<M>>(items: &[T], start: T::Value) -> Vec<T::Value>
where
    T::Value: Floor,
{
    let mut out = Vec::new();
    let mut sum = start;
    for item in items {
        sum = raise_by(sum, item.value());
        out.push(sum);
    }
    out
}
because!(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");

pub fn share_of<T: Floor + Copy>(amount: T, part: T, whole: T) -> Option<T> {
    match (div(amount, whole), rem(amount, whole)) {
        (Some(wholes), Some(left)) => {
            div(mul(left, part), whole).map(|scrap| raise_by(mul(wholes, part), scrap))
        }
        _ => None,
    }
}
because!(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");

pub fn div_up<T: Floor + PartialEq + Copy>(value: T, by: T) -> Option<T> {
    match (div(value, by), rem(value, by)) {
        (Some(whole), Some(left)) => Some(either(
            is_same(left, T::ZERO),
            whole,
            raise_by(whole, T::ONE),
        )),
        _ => None,
    }
}
because!(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");

pub fn backoff<T: Floor + PartialOrd>(delay: T, factor: T, cap: T) -> T {
    clamp_upper(mul(delay, factor), cap)
}
because!(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");

pub fn between<T: Floor + Copy + PartialOrd>(from: T, to: T, part: T, whole: T) -> Option<T> {
    let moved = share_of(away(from, to), part, whole)?;
    Some(either(is_above(to, from), raise_by(from, moved), reduce_by(from, moved)))
}
because!(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");