#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Discontinuity {
pub time: f64,
pub start: f64,
pub last: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IncrementKind {
Normal,
Declared,
Wrap,
Unexplained,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CounterIncrement {
pub start: f64,
pub end: f64,
pub increase: Option<f64>,
pub kind: IncrementKind,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CounterTotal {
pub start: f64,
pub end: f64,
pub increase: f64,
pub pairs: usize,
pub unexplained: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CounterError {
LengthMismatch {
times: usize,
values: usize,
},
TooFewReadings {
got: usize,
},
NotFinite {
parameter: &'static str,
index: Option<usize>,
},
Unordered {
parameter: &'static str,
index: usize,
},
NotPositive {
parameter: &'static str,
got: f64,
},
OutsideModulus {
parameter: &'static str,
index: usize,
got: f64,
modulus: f64,
},
OutsideReadings {
index: usize,
got: f64,
first: f64,
last: f64,
},
TwoInOnePair {
index: usize,
},
}
impl std::fmt::Display for CounterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::LengthMismatch { times, values } => {
write!(f, "{times} times but {values} values; each reading needs both")
}
Self::TooFewReadings { got } => {
write!(f, "at least 2 readings are needed, got {got}")
}
Self::NotFinite {
parameter,
index: Some(i),
} => write!(f, "{parameter}[{i}] is not a finite number"),
Self::NotFinite {
parameter,
index: None,
} => write!(f, "{parameter} is not a finite number"),
Self::Unordered { parameter, index } => write!(
f,
"{parameter}[{index}] is not after {parameter}[{}]; times must increase",
index - 1
),
Self::NotPositive { parameter, got } => {
write!(f, "{parameter} must be > 0, got {got}")
}
Self::OutsideModulus {
parameter,
index,
got,
modulus,
} => write!(
f,
"{parameter}[{index}] is {got}; a register that wraps at {modulus} shows values in [0, {modulus})"
),
Self::OutsideReadings {
index,
got,
first,
last,
} => write!(
f,
"discontinuities[{index}] is at {got}, outside every pair of readings: \
it must be after {first} and at or before {last}"
),
Self::TwoInOnePair { index } => write!(
f,
"discontinuities[{index}] falls in the same pair of readings as the one \
before it; what the counter showed between them was not read"
),
}
}
}
impl std::error::Error for CounterError {}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CounterOptions {
pub modulus: Option<f64>,
pub max_rate: Option<f64>,
pub discontinuities: Vec<Discontinuity>,
}
pub fn counter_increments(
times: &[f64],
values: &[f64],
options: &CounterOptions,
) -> Result<Vec<CounterIncrement>, CounterError> {
if times.len() != values.len() {
return Err(CounterError::LengthMismatch {
times: times.len(),
values: values.len(),
});
}
if times.len() < 2 {
return Err(CounterError::TooFewReadings { got: times.len() });
}
check_times("times", times)?;
for (i, v) in values.iter().enumerate() {
if !v.is_finite() {
return Err(CounterError::NotFinite {
parameter: "values",
index: Some(i),
});
}
}
for (parameter, setting) in [("modulus", options.modulus), ("max_rate", options.max_rate)] {
if let Some(v) = setting {
if !v.is_finite() {
return Err(CounterError::NotFinite {
parameter,
index: None,
});
}
if v <= 0.0 {
return Err(CounterError::NotPositive { parameter, got: v });
}
}
}
let within = |parameter: &'static str, index: usize, v: f64| match options.modulus {
Some(m) if !(0.0..m).contains(&v) => Err(CounterError::OutsideModulus {
parameter,
index,
got: v,
modulus: m,
}),
_ => Ok(()),
};
for (i, &v) in values.iter().enumerate() {
within("values", i, v)?;
}
let mut declared: Vec<Option<&Discontinuity>> = vec![None; times.len() - 1];
for (i, d) in options.discontinuities.iter().enumerate() {
for (parameter, v) in [
("discontinuities.time", Some(d.time)),
("discontinuities.start", Some(d.start)),
("discontinuities.last", d.last),
] {
if v.is_some_and(|v| !v.is_finite()) {
return Err(CounterError::NotFinite {
parameter,
index: Some(i),
});
}
}
within("discontinuities.start", i, d.start)?;
if let Some(last) = d.last {
within("discontinuities.last", i, last)?;
}
let pair = times.partition_point(|&t| t < d.time);
if pair == 0 || pair == times.len() {
return Err(CounterError::OutsideReadings {
index: i,
got: d.time,
first: times[0],
last: times[times.len() - 1],
});
}
let slot = &mut declared[pair - 1];
if slot.is_some() {
return Err(CounterError::TwoInOnePair { index: i });
}
*slot = Some(d);
}
Ok(times
.windows(2)
.zip(values.windows(2))
.zip(declared)
.map(|((t, v), declared)| {
let (start, end, before, after) = (t[0], t[1], v[0], v[1]);
let (increase, kind) = match declared {
Some(d) => (
Some(d.last.map_or(0.0, |last| last - before) + (after - d.start)),
IncrementKind::Declared,
),
None if after >= before => (Some(after - before), IncrementKind::Normal),
None => match options.modulus {
Some(m) => {
let wrapped = m - before + after;
let possible = options
.max_rate
.is_none_or(|rate| wrapped <= rate * (end - start));
if possible {
(Some(wrapped), IncrementKind::Wrap)
} else {
(None, IncrementKind::Unexplained)
}
}
None => (None, IncrementKind::Unexplained),
},
};
CounterIncrement {
start,
end,
increase,
kind,
}
})
.collect())
}
pub fn counter_totals(
increments: &[CounterIncrement],
edges: &[f64],
) -> Result<Vec<CounterTotal>, CounterError> {
if edges.len() < 2 {
return Err(CounterError::TooFewReadings { got: edges.len() });
}
check_times("edges", edges)?;
let mut totals: Vec<CounterTotal> = edges
.windows(2)
.map(|e| CounterTotal {
start: e[0],
end: e[1],
increase: 0.0,
pairs: 0,
unexplained: 0,
})
.collect();
for inc in increments {
let j = edges.partition_point(|&e| e <= inc.end);
if j == 0 || j == edges.len() {
continue;
}
let total = &mut totals[j - 1];
total.pairs += 1;
match inc.increase {
Some(x) => total.increase += x,
None => total.unexplained += 1,
}
}
Ok(totals)
}
fn check_times(parameter: &'static str, times: &[f64]) -> Result<(), CounterError> {
for (i, t) in times.iter().enumerate() {
if !t.is_finite() {
return Err(CounterError::NotFinite {
parameter,
index: Some(i),
});
}
if i > 0 && *t <= times[i - 1] {
return Err(CounterError::Unordered {
parameter,
index: i,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(modulus: Option<f64>) -> CounterOptions {
CounterOptions {
modulus,
..Default::default()
}
}
#[test]
fn a_rising_counter_has_normal_increments() {
let s = counter_increments(&[0.0, 1.0, 2.0], &[5.0, 5.0, 9.5], &opts(None)).unwrap();
assert_eq!(s.len(), 2);
assert_eq!(
(s[0].increase, s[0].kind),
(Some(0.0), IncrementKind::Normal)
);
assert_eq!(
(s[1].increase, s[1].kind),
(Some(4.5), IncrementKind::Normal)
);
assert_eq!((s[1].start, s[1].end), (1.0, 2.0));
}
#[test]
fn a_wrap_is_counted_through_the_modulus() {
let s =
counter_increments(&[0.0, 1.0], &[999_995.0, 5.0], &opts(Some(1_000_000.0))).unwrap();
assert_eq!(s[0].kind, IncrementKind::Wrap);
assert_eq!(s[0].increase, Some(10.0));
}
#[test]
fn a_fall_without_a_modulus_is_not_guessed() {
let s = counter_increments(&[0.0, 1.0, 2.0], &[50.0, 10.0, 12.0], &opts(None)).unwrap();
assert_eq!(
(s[0].increase, s[0].kind),
(None, IncrementKind::Unexplained)
);
assert_eq!(
(s[1].increase, s[1].kind),
(Some(2.0), IncrementKind::Normal)
);
}
#[test]
fn a_wrap_faster_than_the_counter_can_go_is_unexplained() {
let o = CounterOptions {
modulus: Some(1000.0),
max_rate: Some(10.0),
..Default::default()
};
let s = counter_increments(&[0.0, 1.0], &[990.0, 5.0], &o).unwrap();
assert_eq!(s[0].kind, IncrementKind::Unexplained);
let s = counter_increments(&[0.0, 1.0], &[998.0, 5.0], &o).unwrap();
assert_eq!((s[0].kind, s[0].increase), (IncrementKind::Wrap, Some(7.0)));
}
#[test]
fn a_declared_replacement_counts_from_the_new_start() {
let mut o = opts(None);
o.discontinuities = vec![Discontinuity {
time: 1.5,
start: 0.0,
last: None,
}];
let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
assert_eq!(
(s[1].kind, s[1].increase),
(IncrementKind::Declared, Some(7.0))
);
o.discontinuities[0].last = Some(126.0);
let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
assert_eq!(s[1].increase, Some(13.0));
o.discontinuities[0].time = 2.0;
let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
assert_eq!(s[1].kind, IncrementKind::Declared);
}
#[test]
fn a_declared_change_takes_precedence_over_a_wrap() {
let mut o = opts(Some(1000.0));
o.discontinuities = vec![Discontinuity {
time: 1.0,
start: 0.0,
last: None,
}];
let s = counter_increments(&[0.0, 1.0], &[500.0, 3.0], &o).unwrap();
assert_eq!(
(s[0].kind, s[0].increase),
(IncrementKind::Declared, Some(3.0))
);
}
#[test]
fn refusals_name_what_is_wrong() {
let e = |t: &[f64], v: &[f64], o: &CounterOptions| counter_increments(t, v, o).unwrap_err();
assert_eq!(
e(&[0.0, 1.0], &[1.0], &opts(None)),
CounterError::LengthMismatch {
times: 2,
values: 1
}
);
assert_eq!(
e(&[0.0], &[1.0], &opts(None)),
CounterError::TooFewReadings { got: 1 }
);
assert_eq!(
e(&[0.0, 0.0], &[1.0, 2.0], &opts(None)),
CounterError::Unordered {
parameter: "times",
index: 1
}
);
assert_eq!(
e(&[0.0, 1.0], &[1.0, f64::NAN], &opts(None)),
CounterError::NotFinite {
parameter: "values",
index: Some(1)
}
);
assert_eq!(
e(&[0.0, 1.0], &[1.0, 2.0], &opts(Some(0.0))),
CounterError::NotPositive {
parameter: "modulus",
got: 0.0
}
);
assert_eq!(
e(&[0.0, 1.0], &[1.0, 1000.0], &opts(Some(1000.0))),
CounterError::OutsideModulus {
parameter: "values",
index: 1,
got: 1000.0,
modulus: 1000.0
}
);
let mut o = opts(None);
o.discontinuities = vec![Discontinuity {
time: 0.0,
start: 0.0,
last: None,
}];
assert_eq!(
e(&[0.0, 1.0], &[1.0, 2.0], &o),
CounterError::OutsideReadings {
index: 0,
got: 0.0,
first: 0.0,
last: 1.0
}
);
o.discontinuities = vec![
Discontinuity {
time: 0.5,
start: 0.0,
last: None,
},
Discontinuity {
time: 0.7,
start: 0.0,
last: None,
},
];
assert_eq!(
e(&[0.0, 1.0], &[1.0, 2.0], &o),
CounterError::TwoInOnePair { index: 1 }
);
}
#[test]
fn totals_attribute_each_pair_to_the_interval_its_end_falls_in() {
let s = counter_increments(
&[0.0, 10.0, 20.0, 30.0, 40.0],
&[0.0, 5.0, 2.0, 4.0, 9.0],
&opts(None),
)
.unwrap();
let t = counter_totals(&s, &[0.0, 20.0, 40.0, 60.0]).unwrap();
assert_eq!((t[0].increase, t[0].pairs, t[0].unexplained), (5.0, 1, 0));
assert_eq!((t[1].increase, t[1].pairs, t[1].unexplained), (2.0, 2, 1));
assert_eq!((t[2].increase, t[2].pairs, t[2].unexplained), (5.0, 1, 0));
assert!(counter_totals(&s, &[0.0]).is_err());
assert!(counter_totals(&s, &[10.0, 5.0]).is_err());
}
}