use super::ValueGenerator;
pub struct Sawtooth {
min: f64,
max: f64,
period_ticks: f64,
}
impl Sawtooth {
pub fn new(min: f64, max: f64, period_secs: f64, rate: f64) -> Self {
let period_ticks = period_secs * rate;
Self {
min,
max,
period_ticks,
}
}
}
impl ValueGenerator for Sawtooth {
fn value(&self, tick: u64) -> f64 {
let position = (tick as f64) % self.period_ticks;
let fraction = position / self.period_ticks;
self.min + fraction * (self.max - self.min)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-9;
fn saw_rate1(min: f64, max: f64, period_secs: f64) -> Sawtooth {
Sawtooth::new(min, max, period_secs, 1.0)
}
#[test]
fn sawtooth_at_tick_zero_returns_min() {
let gen = saw_rate1(2.0, 10.0, 8.0);
assert_eq!(gen.value(0), 2.0, "value at tick 0 must equal min");
}
#[test]
fn sawtooth_at_period_boundary_resets_to_min() {
let period_secs = 10.0;
let gen = saw_rate1(2.0, 10.0, period_secs);
let period_tick = period_secs as u64; assert!(
(gen.value(period_tick) - 2.0).abs() < EPSILON,
"value at period boundary must reset to min"
);
}
#[test]
fn sawtooth_approaches_max_near_period_end() {
let min = 0.0;
let max = 100.0;
let period_secs = 100.0;
let gen = saw_rate1(min, max, period_secs);
let last_tick = period_secs as u64 - 1; let v = gen.value(last_tick);
assert!(
v >= 98.0 && v < 100.0,
"value near period end should approach max, got {v}"
);
}
#[test]
fn sawtooth_linear_ramp_between_ticks() {
let gen = saw_rate1(0.0, 10.0, 10.0);
let mut prev = gen.value(0);
for tick in 1..10u64 {
let curr = gen.value(tick);
assert!(
curr > prev,
"ramp must be strictly increasing within a period (tick {tick}): prev={prev}, curr={curr}"
);
prev = curr;
}
}
#[test]
fn sawtooth_resets_at_second_period() {
let period_secs = 10.0;
let min = 5.0;
let gen = saw_rate1(min, 20.0, period_secs);
let two_periods = 2 * period_secs as u64; assert!(
(gen.value(two_periods) - min).abs() < EPSILON,
"value at second period boundary must reset to min"
);
}
#[test]
fn sawtooth_period_ticks_pre_computed_from_rate() {
let min = 0.0;
let max = 50.0;
let rate = 10.0;
let gen = Sawtooth::new(min, max, 5.0, rate);
assert!(
(gen.value(50) - min).abs() < EPSILON,
"period_ticks pre-computed from rate must reset at tick 50"
);
}
}