use crate::arithmetic::Fx;
#[derive(Clone, Copy, Debug)]
pub struct EpochSample {
pub omega: Fx,
pub delta_phi: Fx,
}
#[inline]
pub fn epoch_yield(sample: EpochSample) -> Fx {
let omega = sample.omega.max(Fx::ZERO);
let growth = sample.delta_phi.max(Fx::ZERO);
omega * growth
}
pub fn accrue(samples: &[EpochSample]) -> Fx {
samples
.iter()
.copied()
.fold(Fx::ZERO, |acc, s| acc + epoch_yield(s))
}
pub fn accrue_running(samples: &[EpochSample]) -> Vec<Fx> {
let mut total = Fx::ZERO;
samples
.iter()
.copied()
.map(|s| {
total = total + epoch_yield(s);
total
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(omega_n: i64, omega_d: i64, delta_n: i64, delta_d: i64) -> EpochSample {
EpochSample {
omega: Fx::from_ratio(omega_n, omega_d),
delta_phi: Fx::from_ratio(delta_n, delta_d),
}
}
fn close(a: Fx, want: f64) {
let got = a.to_f64();
assert!((got - want).abs() < 1e-6, "got {got}, want {want}");
}
#[test]
fn empty_history_accrues_nothing() {
assert_eq!(accrue(&[]), Fx::ZERO);
assert!(accrue_running(&[]).is_empty());
}
#[test]
fn single_epoch_is_the_integrand() {
let s = sample(1, 2, 1, 4); close(accrue(&[s]), 0.125);
}
#[test]
fn accrual_sums_across_epochs() {
let samples = [
sample(1, 1, 1, 10),
sample(1, 1, 2, 10),
sample(1, 1, 3, 10),
];
close(accrue(&samples), 0.6); }
#[test]
fn running_total_never_drops() {
let samples = [
sample(1, 1, 1, 10),
sample(1, 1, 2, 10),
sample(1, 1, 0, 10),
sample(1, 1, -5, 10),
];
let running = accrue_running(&samples);
assert_eq!(running.len(), 4);
for w in running.windows(2) {
assert!(
w[0] <= w[1],
"running total dropped: {:?} -> {:?}",
w[0],
w[1]
);
}
assert_eq!(
running[1], running[3],
"zero and negative growth must both be flat"
);
assert_eq!(epoch_yield(sample(1, 1, -5, 10)), Fx::ZERO);
close(running[3], accrue(&samples).to_f64());
}
#[test]
fn falsified_link_stops_drawing_but_keeps_past_accrual() {
let samples = [
sample(1, 1, 3, 10),
sample(1, 1, 3, 10),
sample(0, 1, 100, 1), ];
let running = accrue_running(&samples);
close(running[2], running[1].to_f64());
assert!(running[1] > Fx::ZERO);
}
#[test]
fn order_of_epochs_is_not_commutative_with_running_totals() {
let a = sample(1, 1, 1, 10);
let b = sample(1, 1, 2, 10);
close(accrue(&[a, b]), accrue(&[b, a]).to_f64());
assert_ne!(accrue_running(&[a, b])[0], accrue_running(&[b, a])[0]);
}
}