1#[derive(Debug, Clone, Copy, PartialEq)]
43pub struct Discontinuity {
44 pub time: f64,
47 pub start: f64,
50 pub last: Option<f64>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum IncrementKind {
59 Normal,
61 Declared,
63 Wrap,
65 Unexplained,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct CounterIncrement {
72 pub start: f64,
74 pub end: f64,
76 pub increase: Option<f64>,
78 pub kind: IncrementKind,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct CounterTotal {
85 pub start: f64,
87 pub end: f64,
89 pub increase: f64,
93 pub pairs: usize,
95 pub unexplained: usize,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
101pub enum CounterError {
102 LengthMismatch {
104 times: usize,
106 values: usize,
108 },
109 TooFewReadings {
111 got: usize,
113 },
114 NotFinite {
117 parameter: &'static str,
119 index: Option<usize>,
121 },
122 Unordered {
124 parameter: &'static str,
126 index: usize,
128 },
129 NotPositive {
131 parameter: &'static str,
133 got: f64,
135 },
136 OutsideModulus {
139 parameter: &'static str,
141 index: usize,
143 got: f64,
145 modulus: f64,
147 },
148 OutsideReadings {
151 index: usize,
153 got: f64,
155 first: f64,
157 last: f64,
159 },
160 TwoInOnePair {
163 index: usize,
165 },
166}
167
168impl std::fmt::Display for CounterError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match *self {
171 Self::LengthMismatch { times, values } => {
172 write!(f, "{times} times but {values} values; each reading needs both")
173 }
174 Self::TooFewReadings { got } => {
175 write!(f, "at least 2 readings are needed, got {got}")
176 }
177 Self::NotFinite {
178 parameter,
179 index: Some(i),
180 } => write!(f, "{parameter}[{i}] is not a finite number"),
181 Self::NotFinite {
182 parameter,
183 index: None,
184 } => write!(f, "{parameter} is not a finite number"),
185 Self::Unordered { parameter, index } => write!(
186 f,
187 "{parameter}[{index}] is not after {parameter}[{}]; times must increase",
188 index - 1
189 ),
190 Self::NotPositive { parameter, got } => {
191 write!(f, "{parameter} must be > 0, got {got}")
192 }
193 Self::OutsideModulus {
194 parameter,
195 index,
196 got,
197 modulus,
198 } => write!(
199 f,
200 "{parameter}[{index}] is {got}; a register that wraps at {modulus} shows values in [0, {modulus})"
201 ),
202 Self::OutsideReadings {
203 index,
204 got,
205 first,
206 last,
207 } => write!(
208 f,
209 "discontinuities[{index}] is at {got}, outside every pair of readings: \
210 it must be after {first} and at or before {last}"
211 ),
212 Self::TwoInOnePair { index } => write!(
213 f,
214 "discontinuities[{index}] falls in the same pair of readings as the one \
215 before it; what the counter showed between them was not read"
216 ),
217 }
218 }
219}
220
221impl std::error::Error for CounterError {}
222
223#[derive(Debug, Clone, Default, PartialEq)]
225pub struct CounterOptions {
226 pub modulus: Option<f64>,
230 pub max_rate: Option<f64>,
235 pub discontinuities: Vec<Discontinuity>,
237}
238
239pub fn counter_increments(
271 times: &[f64],
272 values: &[f64],
273 options: &CounterOptions,
274) -> Result<Vec<CounterIncrement>, CounterError> {
275 if times.len() != values.len() {
276 return Err(CounterError::LengthMismatch {
277 times: times.len(),
278 values: values.len(),
279 });
280 }
281 if times.len() < 2 {
282 return Err(CounterError::TooFewReadings { got: times.len() });
283 }
284 check_times("times", times)?;
285 for (i, v) in values.iter().enumerate() {
286 if !v.is_finite() {
287 return Err(CounterError::NotFinite {
288 parameter: "values",
289 index: Some(i),
290 });
291 }
292 }
293 for (parameter, setting) in [("modulus", options.modulus), ("max_rate", options.max_rate)] {
294 if let Some(v) = setting {
295 if !v.is_finite() {
296 return Err(CounterError::NotFinite {
297 parameter,
298 index: None,
299 });
300 }
301 if v <= 0.0 {
302 return Err(CounterError::NotPositive { parameter, got: v });
303 }
304 }
305 }
306 let within = |parameter: &'static str, index: usize, v: f64| match options.modulus {
307 Some(m) if !(0.0..m).contains(&v) => Err(CounterError::OutsideModulus {
308 parameter,
309 index,
310 got: v,
311 modulus: m,
312 }),
313 _ => Ok(()),
314 };
315 for (i, &v) in values.iter().enumerate() {
316 within("values", i, v)?;
317 }
318
319 let mut declared: Vec<Option<&Discontinuity>> = vec![None; times.len() - 1];
322 for (i, d) in options.discontinuities.iter().enumerate() {
323 for (parameter, v) in [
324 ("discontinuities.time", Some(d.time)),
325 ("discontinuities.start", Some(d.start)),
326 ("discontinuities.last", d.last),
327 ] {
328 if v.is_some_and(|v| !v.is_finite()) {
329 return Err(CounterError::NotFinite {
330 parameter,
331 index: Some(i),
332 });
333 }
334 }
335 within("discontinuities.start", i, d.start)?;
336 if let Some(last) = d.last {
337 within("discontinuities.last", i, last)?;
338 }
339 let pair = times.partition_point(|&t| t < d.time);
340 if pair == 0 || pair == times.len() {
341 return Err(CounterError::OutsideReadings {
342 index: i,
343 got: d.time,
344 first: times[0],
345 last: times[times.len() - 1],
346 });
347 }
348 let slot = &mut declared[pair - 1];
349 if slot.is_some() {
350 return Err(CounterError::TwoInOnePair { index: i });
351 }
352 *slot = Some(d);
353 }
354
355 Ok(times
356 .windows(2)
357 .zip(values.windows(2))
358 .zip(declared)
359 .map(|((t, v), declared)| {
360 let (start, end, before, after) = (t[0], t[1], v[0], v[1]);
361 let (increase, kind) = match declared {
362 Some(d) => (
363 Some(d.last.map_or(0.0, |last| last - before) + (after - d.start)),
364 IncrementKind::Declared,
365 ),
366 None if after >= before => (Some(after - before), IncrementKind::Normal),
367 None => match options.modulus {
368 Some(m) => {
369 let wrapped = m - before + after;
370 let possible = options
371 .max_rate
372 .is_none_or(|rate| wrapped <= rate * (end - start));
373 if possible {
374 (Some(wrapped), IncrementKind::Wrap)
375 } else {
376 (None, IncrementKind::Unexplained)
377 }
378 }
379 None => (None, IncrementKind::Unexplained),
380 },
381 };
382 CounterIncrement {
383 start,
384 end,
385 increase,
386 kind,
387 }
388 })
389 .collect())
390}
391
392pub fn counter_totals(
418 increments: &[CounterIncrement],
419 edges: &[f64],
420) -> Result<Vec<CounterTotal>, CounterError> {
421 if edges.len() < 2 {
422 return Err(CounterError::TooFewReadings { got: edges.len() });
423 }
424 check_times("edges", edges)?;
425 let mut totals: Vec<CounterTotal> = edges
426 .windows(2)
427 .map(|e| CounterTotal {
428 start: e[0],
429 end: e[1],
430 increase: 0.0,
431 pairs: 0,
432 unexplained: 0,
433 })
434 .collect();
435 for inc in increments {
436 let j = edges.partition_point(|&e| e <= inc.end);
437 if j == 0 || j == edges.len() {
438 continue;
439 }
440 let total = &mut totals[j - 1];
441 total.pairs += 1;
442 match inc.increase {
443 Some(x) => total.increase += x,
444 None => total.unexplained += 1,
445 }
446 }
447 Ok(totals)
448}
449
450fn check_times(parameter: &'static str, times: &[f64]) -> Result<(), CounterError> {
451 for (i, t) in times.iter().enumerate() {
452 if !t.is_finite() {
453 return Err(CounterError::NotFinite {
454 parameter,
455 index: Some(i),
456 });
457 }
458 if i > 0 && *t <= times[i - 1] {
459 return Err(CounterError::Unordered {
460 parameter,
461 index: i,
462 });
463 }
464 }
465 Ok(())
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 fn opts(modulus: Option<f64>) -> CounterOptions {
473 CounterOptions {
474 modulus,
475 ..Default::default()
476 }
477 }
478
479 #[test]
480 fn a_rising_counter_has_normal_increments() {
481 let s = counter_increments(&[0.0, 1.0, 2.0], &[5.0, 5.0, 9.5], &opts(None)).unwrap();
482 assert_eq!(s.len(), 2);
483 assert_eq!(
484 (s[0].increase, s[0].kind),
485 (Some(0.0), IncrementKind::Normal)
486 );
487 assert_eq!(
488 (s[1].increase, s[1].kind),
489 (Some(4.5), IncrementKind::Normal)
490 );
491 assert_eq!((s[1].start, s[1].end), (1.0, 2.0));
492 }
493
494 #[test]
497 fn a_wrap_is_counted_through_the_modulus() {
498 let s =
499 counter_increments(&[0.0, 1.0], &[999_995.0, 5.0], &opts(Some(1_000_000.0))).unwrap();
500 assert_eq!(s[0].kind, IncrementKind::Wrap);
501 assert_eq!(s[0].increase, Some(10.0));
502 }
503
504 #[test]
505 fn a_fall_without_a_modulus_is_not_guessed() {
506 let s = counter_increments(&[0.0, 1.0, 2.0], &[50.0, 10.0, 12.0], &opts(None)).unwrap();
507 assert_eq!(
508 (s[0].increase, s[0].kind),
509 (None, IncrementKind::Unexplained)
510 );
511 assert_eq!(
512 (s[1].increase, s[1].kind),
513 (Some(2.0), IncrementKind::Normal)
514 );
515 }
516
517 #[test]
520 fn a_wrap_faster_than_the_counter_can_go_is_unexplained() {
521 let o = CounterOptions {
522 modulus: Some(1000.0),
523 max_rate: Some(10.0),
524 ..Default::default()
525 };
526 let s = counter_increments(&[0.0, 1.0], &[990.0, 5.0], &o).unwrap();
528 assert_eq!(s[0].kind, IncrementKind::Unexplained);
529 let s = counter_increments(&[0.0, 1.0], &[998.0, 5.0], &o).unwrap();
531 assert_eq!((s[0].kind, s[0].increase), (IncrementKind::Wrap, Some(7.0)));
532 }
533
534 #[test]
535 fn a_declared_replacement_counts_from_the_new_start() {
536 let mut o = opts(None);
537 o.discontinuities = vec![Discontinuity {
538 time: 1.5,
539 start: 0.0,
540 last: None,
541 }];
542 let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
543 assert_eq!(
544 (s[1].kind, s[1].increase),
545 (IncrementKind::Declared, Some(7.0))
546 );
547 o.discontinuities[0].last = Some(126.0);
549 let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
550 assert_eq!(s[1].increase, Some(13.0));
551 o.discontinuities[0].time = 2.0;
553 let s = counter_increments(&[0.0, 1.0, 2.0], &[100.0, 120.0, 7.0], &o).unwrap();
554 assert_eq!(s[1].kind, IncrementKind::Declared);
555 }
556
557 #[test]
558 fn a_declared_change_takes_precedence_over_a_wrap() {
559 let mut o = opts(Some(1000.0));
560 o.discontinuities = vec![Discontinuity {
561 time: 1.0,
562 start: 0.0,
563 last: None,
564 }];
565 let s = counter_increments(&[0.0, 1.0], &[500.0, 3.0], &o).unwrap();
566 assert_eq!(
567 (s[0].kind, s[0].increase),
568 (IncrementKind::Declared, Some(3.0))
569 );
570 }
571
572 #[test]
573 fn refusals_name_what_is_wrong() {
574 let e = |t: &[f64], v: &[f64], o: &CounterOptions| counter_increments(t, v, o).unwrap_err();
575 assert_eq!(
576 e(&[0.0, 1.0], &[1.0], &opts(None)),
577 CounterError::LengthMismatch {
578 times: 2,
579 values: 1
580 }
581 );
582 assert_eq!(
583 e(&[0.0], &[1.0], &opts(None)),
584 CounterError::TooFewReadings { got: 1 }
585 );
586 assert_eq!(
587 e(&[0.0, 0.0], &[1.0, 2.0], &opts(None)),
588 CounterError::Unordered {
589 parameter: "times",
590 index: 1
591 }
592 );
593 assert_eq!(
594 e(&[0.0, 1.0], &[1.0, f64::NAN], &opts(None)),
595 CounterError::NotFinite {
596 parameter: "values",
597 index: Some(1)
598 }
599 );
600 assert_eq!(
601 e(&[0.0, 1.0], &[1.0, 2.0], &opts(Some(0.0))),
602 CounterError::NotPositive {
603 parameter: "modulus",
604 got: 0.0
605 }
606 );
607 assert_eq!(
608 e(&[0.0, 1.0], &[1.0, 1000.0], &opts(Some(1000.0))),
609 CounterError::OutsideModulus {
610 parameter: "values",
611 index: 1,
612 got: 1000.0,
613 modulus: 1000.0
614 }
615 );
616 let mut o = opts(None);
617 o.discontinuities = vec![Discontinuity {
618 time: 0.0,
619 start: 0.0,
620 last: None,
621 }];
622 assert_eq!(
623 e(&[0.0, 1.0], &[1.0, 2.0], &o),
624 CounterError::OutsideReadings {
625 index: 0,
626 got: 0.0,
627 first: 0.0,
628 last: 1.0
629 }
630 );
631 o.discontinuities = vec![
632 Discontinuity {
633 time: 0.5,
634 start: 0.0,
635 last: None,
636 },
637 Discontinuity {
638 time: 0.7,
639 start: 0.0,
640 last: None,
641 },
642 ];
643 assert_eq!(
644 e(&[0.0, 1.0], &[1.0, 2.0], &o),
645 CounterError::TwoInOnePair { index: 1 }
646 );
647 }
648
649 #[test]
650 fn totals_attribute_each_pair_to_the_interval_its_end_falls_in() {
651 let s = counter_increments(
652 &[0.0, 10.0, 20.0, 30.0, 40.0],
653 &[0.0, 5.0, 2.0, 4.0, 9.0],
654 &opts(None),
655 )
656 .unwrap();
657 let t = counter_totals(&s, &[0.0, 20.0, 40.0, 60.0]).unwrap();
658 assert_eq!((t[0].increase, t[0].pairs, t[0].unexplained), (5.0, 1, 0));
661 assert_eq!((t[1].increase, t[1].pairs, t[1].unexplained), (2.0, 2, 1));
662 assert_eq!((t[2].increase, t[2].pairs, t[2].unexplained), (5.0, 1, 0));
663 assert!(counter_totals(&s, &[0.0]).is_err());
664 assert!(counter_totals(&s, &[10.0, 5.0]).is_err());
665 }
666}
667
668#[cfg(test)]
677mod generated_meter {
678 use super::*;
679
680 const TIMES: [f64; 71] = [
681 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,
682 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,
683 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,
684 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,
685 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0,
686 ];
687 const VALUES: [f64; 71] = [
688 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,
689 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,
690 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,
691 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,
692 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,
693 234.0, 247.0, 286.0, 300.0, 303.0,
694 ];
695 const CONSUMED: [f64; 70] = [
697 2.0,
698 9.0,
699 32.0,
700 25.0,
701 17.0,
702 12.0,
703 31.0,
704 34.0,
705 7.0,
706 25.0,
707 9.0,
708 34.0,
709 33.0,
710 32.0,
711 38.0,
712 28.0,
713 15.0,
714 17.0,
715 29.0,
716 36.0,
717 11.0,
718 12.0,
719 32.0,
720 27.0,
721 17.0,
722 15.0,
723 19.0,
724 32.0,
725 20.0,
726 37.0,
727 15.0,
728 13.0,
729 5.0,
730 27.0,
731 16.0,
732 31.0,
733 5.0,
734 0.0,
735 34.0,
736 71.0,
737 20.0,
738 28.0,
739 12.0,
740 19.0,
741 12.0,
742 10.0,
743 33.0,
744 40.0,
745 22.0,
746 39.0,
747 f64::NAN,
748 6.0,
749 29.0,
750 6.0,
751 12.0,
752 6.0,
753 15.0,
754 9.0,
755 2.0,
756 3.0,
757 39.0,
758 8.0,
759 8.0,
760 34.0,
761 29.0,
762 23.0,
763 13.0,
764 39.0,
765 14.0,
766 3.0,
767 ];
768 const KINDS: [IncrementKind; 70] = [
769 IncrementKind::Normal,
770 IncrementKind::Normal,
771 IncrementKind::Wrap,
772 IncrementKind::Normal,
773 IncrementKind::Normal,
774 IncrementKind::Normal,
775 IncrementKind::Normal,
776 IncrementKind::Normal,
777 IncrementKind::Normal,
778 IncrementKind::Normal,
779 IncrementKind::Normal,
780 IncrementKind::Normal,
781 IncrementKind::Normal,
782 IncrementKind::Normal,
783 IncrementKind::Normal,
784 IncrementKind::Normal,
785 IncrementKind::Normal,
786 IncrementKind::Normal,
787 IncrementKind::Wrap,
788 IncrementKind::Normal,
789 IncrementKind::Normal,
790 IncrementKind::Normal,
791 IncrementKind::Normal,
792 IncrementKind::Normal,
793 IncrementKind::Normal,
794 IncrementKind::Normal,
795 IncrementKind::Normal,
796 IncrementKind::Normal,
797 IncrementKind::Normal,
798 IncrementKind::Normal,
799 IncrementKind::Declared,
800 IncrementKind::Normal,
801 IncrementKind::Normal,
802 IncrementKind::Normal,
803 IncrementKind::Normal,
804 IncrementKind::Normal,
805 IncrementKind::Normal,
806 IncrementKind::Normal,
807 IncrementKind::Normal,
808 IncrementKind::Normal,
809 IncrementKind::Normal,
810 IncrementKind::Normal,
811 IncrementKind::Normal,
812 IncrementKind::Normal,
813 IncrementKind::Normal,
814 IncrementKind::Normal,
815 IncrementKind::Normal,
816 IncrementKind::Normal,
817 IncrementKind::Wrap,
818 IncrementKind::Normal,
819 IncrementKind::Unexplained,
820 IncrementKind::Normal,
821 IncrementKind::Normal,
822 IncrementKind::Normal,
823 IncrementKind::Normal,
824 IncrementKind::Normal,
825 IncrementKind::Normal,
826 IncrementKind::Normal,
827 IncrementKind::Normal,
828 IncrementKind::Normal,
829 IncrementKind::Normal,
830 IncrementKind::Normal,
831 IncrementKind::Normal,
832 IncrementKind::Normal,
833 IncrementKind::Normal,
834 IncrementKind::Normal,
835 IncrementKind::Normal,
836 IncrementKind::Normal,
837 IncrementKind::Normal,
838 IncrementKind::Normal,
839 ];
840
841 fn options() -> CounterOptions {
842 CounterOptions {
843 modulus: Some(400.0),
844 max_rate: Some(45.0),
845 discontinuities: vec![Discontinuity {
846 time: 30.5,
847 start: 7.0,
848 last: Some(272.0),
849 }],
850 }
851 }
852
853 #[test]
854 fn every_pair_is_classified_and_counted_as_the_process_made_it() {
855 let steps = counter_increments(&TIMES, &VALUES, &options()).expect("valid readings");
856 assert_eq!(steps.len(), CONSUMED.len());
857 for (i, step) in steps.iter().enumerate() {
858 assert_eq!(step.kind, KINDS[i], "pair {i}");
859 if CONSUMED[i].is_nan() {
860 assert_eq!(step.increase, None, "pair {i}");
861 } else {
862 assert_eq!(step.increase, Some(CONSUMED[i]), "pair {i}");
863 }
864 }
865 }
866
867 #[test]
868 fn daily_totals_sum_the_pairs_ending_in_each_day() {
869 let steps = counter_increments(&TIMES, &VALUES, &options()).expect("valid readings");
870 let totals = counter_totals(&steps, &[0.0, 24.0, 48.0, 72.0]).expect("valid edges");
873 let want: [(f64, f64, f64, usize, usize); 3] = [
874 (0.0, 24.0, 520.0, 23, 0),
875 (24.0, 48.0, 475.0, 22, 0),
876 (48.0, 72.0, 439.0, 24, 1),
877 ];
878 assert_eq!(totals.len(), want.len());
879 for (t, (start, end, increase, pairs, unexplained)) in totals.iter().zip(want) {
880 assert_eq!(
881 (t.start, t.end, t.increase, t.pairs, t.unexplained),
882 (start, end, increase, pairs, unexplained)
883 );
884 }
885 }
886}