#[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());
}
}
#[cfg(test)]
mod generated_meter {
use super::*;
const TIMES: [f64; 71] = [
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0,
32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0,
49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0,
64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0,
];
const VALUES: [f64; 71] = [
380.0, 382.0, 391.0, 23.0, 48.0, 65.0, 77.0, 108.0, 142.0, 149.0, 174.0, 183.0, 217.0,
250.0, 282.0, 320.0, 348.0, 363.0, 380.0, 9.0, 45.0, 56.0, 68.0, 100.0, 127.0, 144.0,
159.0, 178.0, 210.0, 230.0, 267.0, 17.0, 30.0, 35.0, 62.0, 78.0, 109.0, 114.0, 114.0,
148.0, 219.0, 239.0, 267.0, 279.0, 298.0, 310.0, 320.0, 353.0, 393.0, 15.0, 54.0, 5.0,
11.0, 40.0, 46.0, 58.0, 64.0, 79.0, 88.0, 90.0, 93.0, 132.0, 140.0, 148.0, 182.0, 211.0,
234.0, 247.0, 286.0, 300.0, 303.0,
];
const CONSUMED: [f64; 70] = [
2.0,
9.0,
32.0,
25.0,
17.0,
12.0,
31.0,
34.0,
7.0,
25.0,
9.0,
34.0,
33.0,
32.0,
38.0,
28.0,
15.0,
17.0,
29.0,
36.0,
11.0,
12.0,
32.0,
27.0,
17.0,
15.0,
19.0,
32.0,
20.0,
37.0,
15.0,
13.0,
5.0,
27.0,
16.0,
31.0,
5.0,
0.0,
34.0,
71.0,
20.0,
28.0,
12.0,
19.0,
12.0,
10.0,
33.0,
40.0,
22.0,
39.0,
f64::NAN,
6.0,
29.0,
6.0,
12.0,
6.0,
15.0,
9.0,
2.0,
3.0,
39.0,
8.0,
8.0,
34.0,
29.0,
23.0,
13.0,
39.0,
14.0,
3.0,
];
const KINDS: [IncrementKind; 70] = [
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Wrap,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Wrap,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Declared,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Wrap,
IncrementKind::Normal,
IncrementKind::Unexplained,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
IncrementKind::Normal,
];
fn options() -> CounterOptions {
CounterOptions {
modulus: Some(400.0),
max_rate: Some(45.0),
discontinuities: vec![Discontinuity {
time: 30.5,
start: 7.0,
last: Some(272.0),
}],
}
}
#[test]
fn every_pair_is_classified_and_counted_as_the_process_made_it() {
let steps = counter_increments(&TIMES, &VALUES, &options()).expect("valid readings");
assert_eq!(steps.len(), CONSUMED.len());
for (i, step) in steps.iter().enumerate() {
assert_eq!(step.kind, KINDS[i], "pair {i}");
if CONSUMED[i].is_nan() {
assert_eq!(step.increase, None, "pair {i}");
} else {
assert_eq!(step.increase, Some(CONSUMED[i]), "pair {i}");
}
}
}
#[test]
fn daily_totals_sum_the_pairs_ending_in_each_day() {
let steps = counter_increments(&TIMES, &VALUES, &options()).expect("valid readings");
let totals = counter_totals(&steps, &[0.0, 24.0, 48.0, 72.0]).expect("valid edges");
let want: [(f64, f64, f64, usize, usize); 3] = [
(0.0, 24.0, 520.0, 23, 0),
(24.0, 48.0, 475.0, 22, 0),
(48.0, 72.0, 439.0, 24, 1),
];
assert_eq!(totals.len(), want.len());
for (t, (start, end, increase, pairs, unexplained)) in totals.iter().zip(want) {
assert_eq!(
(t.start, t.end, t.increase, t.pairs, t.unexplained),
(start, end, increase, pairs, unexplained)
);
}
}
}