1use std::error::Error;
25use std::fmt;
26
27use serde::de::{self, Visitor};
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30use crate::units::{
31 Cents, Correlation, Decibels, Degrees, Hz, MidiPitch, Normalized, Octaves, Percent, Polarity,
32 RangeError, Ratio, Seconds, Semitones,
33};
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Level {
40 Low,
42 Medium,
44 High,
46}
47
48macro_rules! reason_codes {
51 ($($(#[$doc:meta])* $variant:ident,)+) => {
52 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
57 #[serde(rename_all = "snake_case")]
58 pub enum ReasonCode {
59 $($(#[$doc])* $variant,)+
60 }
61
62 impl ReasonCode {
63 #[cfg(test)]
65 pub(crate) const ALL: &[Self] = &[$(Self::$variant,)+];
66 }
67 };
68}
69
70reason_codes! {
71 ActiveFrames,
73 TooFewNotes,
75 OctaveUncertain,
77 SilentStem,
79 OneChannel,
81 NoNoteList,
83 CyclesInSpan,
86 HardLimited,
89 StereoBlindSpot,
92 BandEdgeDrop,
96 LineProminence,
99 TooFewFrames,
102 NoBandEdge,
106 TempoPeak,
110 BeatPeriodicity,
115 SparsePeriodicity,
121 OnsetSharpness,
126 OnsetsPerBeat,
131 BpmExact,
133 VoicedFrames,
135 PolyphonicLine,
138 MotionSpan,
141 FitResidual,
144 PitchJitter,
148 SineShare,
150 ContourKurtosis,
154 AttackTime,
157 CurveBound,
160 NoteFrames,
164 RelativeSpread,
168 UpperBoundShare,
172 CountAgreement,
175 AttackHidesJoin,
177 NoAttackRead,
179 LevelKeptAtJoin,
182 StemEdgeAtJoin,
185 TailWeight,
187 TailLight,
190 TailResidual,
192 TailSmooth,
195 TailBand,
197 TailWobble,
200 ZeroMedian,
203}
204
205#[derive(Clone, Copy, Debug, PartialEq)]
210pub enum Signal {
211 Count(u64),
213 Number(f64),
215 Flag(bool),
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
229#[serde(deny_unknown_fields)]
230pub struct Reason {
231 code: ReasonCode,
232 value: Signal,
233}
234
235impl Reason {
236 pub fn new(code: ReasonCode, value: Signal) -> Result<Self, ConfidenceError> {
243 let value = match value {
244 Signal::Number(number) => Signal::Number(check_signal(number).map_err(Kind::Signal)?),
245 other => other,
246 };
247 Ok(Self { code, value })
248 }
249
250 #[must_use]
252 pub fn code(self) -> ReasonCode {
253 self.code
254 }
255
256 #[must_use]
258 pub fn value(self) -> Signal {
259 self.value
260 }
261}
262
263fn check_signal(number: f64) -> Result<f64, RangeError> {
266 if number.is_finite() {
267 Ok(number + 0.0)
268 } else {
269 Err(RangeError::new(
270 "Reason signal",
271 number,
272 crate::units::Range::Finite,
273 ))
274 }
275}
276
277impl Serialize for Signal {
278 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
279 match *self {
280 Self::Count(count) => serializer.serialize_u64(count),
281 Self::Number(number) => serializer.serialize_f64(number),
282 Self::Flag(flag) => serializer.serialize_bool(flag),
283 }
284 }
285}
286
287impl<'de> Deserialize<'de> for Signal {
288 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
289 deserializer.deserialize_any(SignalVisitor)
290 }
291}
292
293struct SignalVisitor;
294
295impl Visitor<'_> for SignalVisitor {
296 type Value = Signal;
297
298 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 write!(f, "a count, a finite number or a bool")
300 }
301
302 fn visit_u64<E: de::Error>(self, count: u64) -> Result<Signal, E> {
303 Ok(Signal::Count(count))
304 }
305
306 fn visit_i64<E: de::Error>(self, number: i64) -> Result<Signal, E> {
308 if let Ok(count) = u64::try_from(number) {
309 return Ok(Signal::Count(count));
310 }
311 i32::try_from(number)
312 .map(|number| Signal::Number(f64::from(number)))
313 .map_err(|_| E::invalid_value(de::Unexpected::Signed(number), &self))
314 }
315
316 fn visit_f64<E: de::Error>(self, number: f64) -> Result<Signal, E> {
317 check_signal(number).map(Signal::Number).map_err(E::custom)
318 }
319
320 fn visit_bool<E: de::Error>(self, flag: bool) -> Result<Signal, E> {
321 Ok(Signal::Flag(flag))
322 }
323}
324
325pub trait Spread: Copy + fmt::Debug + PartialEq + sealed::Spread {}
330
331#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub enum NoSpread {}
336
337mod sealed {
338 use super::ConfidenceError;
339
340 pub trait Spread: Sized {
341 fn to_f64(self) -> f64;
342 fn from_f64(value: f64) -> Result<Self, ConfidenceError>;
343 }
344}
345
346macro_rules! spread {
347 ($($unit:ty),+) => {$(
348 impl sealed::Spread for $unit {
349 fn to_f64(self) -> f64 {
350 self.get()
351 }
352
353 fn from_f64(value: f64) -> Result<Self, ConfidenceError> {
354 Ok(Self::new(value).map_err(Kind::Spread)?)
355 }
356 }
357
358 impl Spread for $unit {}
359 )+};
360}
361
362spread!(
363 Hz,
364 Seconds,
365 Semitones,
366 Cents,
367 Octaves,
368 Decibels,
369 Normalized,
370 MidiPitch,
371 Correlation,
372 Degrees,
373 Ratio
374);
375
376impl<P: Polarity> sealed::Spread for Percent<P> {
377 fn to_f64(self) -> f64 {
378 self.get()
379 }
380
381 fn from_f64(value: f64) -> Result<Self, ConfidenceError> {
382 Ok(Self::new(value).map_err(Kind::Spread)?)
383 }
384}
385
386impl<P: Polarity> Spread for Percent<P> {}
387
388impl sealed::Spread for NoSpread {
389 fn to_f64(self) -> f64 {
390 match self {}
391 }
392
393 fn from_f64(_: f64) -> Result<Self, ConfidenceError> {
394 Err(Kind::SpreadNotAllowed.into())
395 }
396}
397
398impl Spread for NoSpread {}
399
400#[derive(Clone, Debug, PartialEq)]
428pub struct Confidence<S: Spread> {
429 state: State<S>,
430 reasons: Vec<Reason>,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq)]
434enum State<S> {
435 Estimate { level: Level, spread: Option<S> },
436 NoEstimate,
437}
438
439impl<S: Spread> Confidence<S> {
440 pub fn estimate(
447 level: Level,
448 spread: Option<S>,
449 reasons: Vec<Reason>,
450 ) -> Result<Self, ConfidenceError> {
451 if let Some(spread) = spread {
452 crate::units::Range::AtLeast(0.0)
453 .check("Confidence spread", spread.to_f64())
454 .map_err(Kind::Spread)?;
455 }
456 Self::with_reasons(State::Estimate { level, spread }, reasons)
457 }
458
459 pub fn no_estimate(reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
466 Self::with_reasons(State::NoEstimate, reasons)
467 }
468
469 fn with_reasons(state: State<S>, reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
470 if reasons.is_empty() {
471 return Err(Kind::NoReasons.into());
472 }
473 Ok(Self { state, reasons })
474 }
475
476 #[must_use]
478 pub fn is_estimate(&self) -> bool {
479 matches!(self.state, State::Estimate { .. })
480 }
481
482 #[must_use]
484 pub fn level(&self) -> Option<Level> {
485 match self.state {
486 State::Estimate { level, .. } => Some(level),
487 State::NoEstimate => None,
488 }
489 }
490
491 #[must_use]
493 pub fn spread(&self) -> Option<S> {
494 match self.state {
495 State::Estimate { spread, .. } => spread,
496 State::NoEstimate => None,
497 }
498 }
499
500 #[must_use]
502 pub fn reasons(&self) -> &[Reason] {
503 &self.reasons
504 }
505}
506
507#[derive(Serialize)]
508struct WireOut<'a> {
509 #[serde(skip_serializing_if = "Option::is_none")]
510 level: Option<Level>,
511 reasons: &'a [Reason],
512 #[serde(skip_serializing_if = "Option::is_none")]
513 spread: Option<f64>,
514 estimate: bool,
515}
516
517#[derive(Deserialize)]
518#[serde(deny_unknown_fields)]
519struct WireIn {
520 level: Option<Level>,
521 reasons: Vec<Reason>,
522 spread: Option<f64>,
523 estimate: bool,
524}
525
526impl<S: Spread> Serialize for Confidence<S> {
527 fn serialize<Ser: Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
528 WireOut {
529 level: self.level(),
530 reasons: &self.reasons,
531 spread: self.spread().map(sealed::Spread::to_f64),
532 estimate: self.is_estimate(),
533 }
534 .serialize(serializer)
535 }
536}
537
538impl<'de, S: Spread> Deserialize<'de> for Confidence<S> {
539 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
540 let wire = WireIn::deserialize(deserializer)?;
541 Self::from_wire(wire).map_err(de::Error::custom)
542 }
543}
544
545impl<S: Spread> Confidence<S> {
546 fn from_wire(wire: WireIn) -> Result<Self, ConfidenceError> {
547 match (wire.estimate, wire.level) {
548 (true, Some(level)) => {
549 let spread = wire.spread.map(S::from_f64).transpose()?;
550 Self::estimate(level, spread, wire.reasons)
551 }
552 (true, None) => Err(Kind::EstimateWithoutLevel.into()),
553 (false, Some(_)) => Err(Kind::LevelWithoutEstimate.into()),
554 (false, None) if wire.spread.is_some() => Err(Kind::SpreadWithoutEstimate.into()),
555 (false, None) => Self::no_estimate(wire.reasons),
556 }
557 }
558}
559
560#[derive(Clone, Debug, PartialEq)]
564pub struct ConfidenceError(Kind);
565
566#[derive(Clone, Debug, PartialEq)]
567enum Kind {
568 NoReasons,
569 Spread(RangeError),
570 Signal(RangeError),
571 SpreadNotAllowed,
572 EstimateWithoutLevel,
573 LevelWithoutEstimate,
574 SpreadWithoutEstimate,
575}
576
577impl From<Kind> for ConfidenceError {
578 fn from(kind: Kind) -> Self {
579 Self(kind)
580 }
581}
582
583impl fmt::Display for ConfidenceError {
584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585 match &self.0 {
586 Kind::NoReasons => write!(
587 f,
588 "Confidence has no reasons: add at least one reason code with its signal value"
589 ),
590 Kind::Spread(error) | Kind::Signal(error) => write!(f, "{error}"),
591 Kind::SpreadNotAllowed => write!(
592 f,
593 "Confidence has a spread, but this estimate has none: remove \"spread\""
594 ),
595 Kind::EstimateWithoutLevel => write!(
596 f,
597 "Confidence with \"estimate\": true has no level: \
598 add \"level\" or set \"estimate\": false"
599 ),
600 Kind::LevelWithoutEstimate => write!(
601 f,
602 "Confidence with \"estimate\": false has a level: \
603 remove \"level\" or set \"estimate\": true"
604 ),
605 Kind::SpreadWithoutEstimate => write!(
606 f,
607 "Confidence with \"estimate\": false has a spread: \
608 remove \"spread\" or set \"estimate\": true"
609 ),
610 }
611 }
612}
613
614impl Error for ConfidenceError {
615 fn source(&self) -> Option<&(dyn Error + 'static)> {
616 match &self.0 {
617 Kind::Spread(error) | Kind::Signal(error) => Some(error),
618 _ => None,
619 }
620 }
621}
622
623#[derive(Clone, Debug, PartialEq)]
640pub struct Estimate<V, S: Spread> {
641 value: Option<V>,
642 confidence: Confidence<S>,
643}
644
645impl<V, S: Spread> Estimate<V, S> {
646 pub fn new(
652 value: V,
653 level: Level,
654 spread: Option<S>,
655 reasons: Vec<Reason>,
656 ) -> Result<Self, ConfidenceError> {
657 Ok(Self {
658 value: Some(value),
659 confidence: Confidence::estimate(level, spread, reasons)?,
660 })
661 }
662
663 pub fn none(reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
669 Ok(Self {
670 value: None,
671 confidence: Confidence::no_estimate(reasons)?,
672 })
673 }
674
675 #[must_use]
677 pub fn value(&self) -> Option<&V> {
678 self.value.as_ref()
679 }
680
681 #[must_use]
683 pub fn confidence(&self) -> &Confidence<S> {
684 &self.confidence
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use proptest::prelude::*;
692 use serde_json::json;
693
694 fn frames(count: u64) -> Reason {
695 Reason::new(ReasonCode::ActiveFrames, Signal::Count(count)).unwrap()
696 }
697
698 fn octaves(value: f64) -> Octaves {
699 Octaves::new(value).unwrap()
700 }
701
702 #[test]
703 fn estimate_serializes_in_the_report_layout() {
704 let confidence =
705 Confidence::estimate(Level::Medium, Some(octaves(0.3)), vec![frames(412)]).unwrap();
706 assert_eq!(
707 serde_json::to_value(&confidence).unwrap(),
708 json!({"level": "medium", "reasons": [{"code": "active_frames", "value": 412}], "spread": 0.3, "estimate": true})
709 );
710 assert_eq!(
711 serde_json::to_string(&confidence).unwrap(),
712 r#"{"level":"medium","reasons":[{"code":"active_frames","value":412}],"spread":0.3,"estimate":true}"#
713 );
714 }
715
716 #[test]
717 fn estimate_without_spread_leaves_the_field_out() {
718 let confidence = Confidence::<Cents>::estimate(Level::High, None, vec![frames(1)]).unwrap();
719 assert_eq!(
720 serde_json::to_value(&confidence).unwrap(),
721 json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "estimate": true})
722 );
723 }
724
725 #[test]
726 fn no_estimate_has_no_level_no_spread_and_a_reason() {
727 let reason = Reason::new(ReasonCode::TooFewNotes, Signal::Count(2)).unwrap();
728 let confidence = Confidence::<Octaves>::no_estimate(vec![reason]).unwrap();
729 assert!(!confidence.is_estimate());
730 assert_eq!(confidence.level(), None);
731 assert_eq!(confidence.spread(), None);
732 assert_eq!(confidence.reasons(), &[reason]);
733 let value = serde_json::to_value(&confidence).unwrap();
734 assert_eq!(
735 value,
736 json!({"reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false})
737 );
738 let back: Confidence<Octaves> = serde_json::from_value(value).unwrap();
739 assert_eq!(back, confidence);
740 }
741
742 #[test]
743 fn level_reasons_and_signals_stay_together() {
744 let reasons = vec![
745 frames(412),
746 Reason::new(ReasonCode::OctaveUncertain, Signal::Flag(true)).unwrap(),
747 ];
748 let confidence =
749 Confidence::estimate(Level::Low, Some(octaves(1.5)), reasons.clone()).unwrap();
750 assert!(confidence.is_estimate());
751 assert_eq!(confidence.level(), Some(Level::Low));
752 assert_eq!(confidence.spread(), Some(octaves(1.5)));
753 assert_eq!(confidence.reasons(), reasons.as_slice());
754 assert_eq!(confidence.reasons()[1].code(), ReasonCode::OctaveUncertain);
755 assert_eq!(confidence.reasons()[1].value(), Signal::Flag(true));
756 }
757
758 #[test]
759 fn every_state_needs_a_reason() {
760 let error = Confidence::<Octaves>::estimate(Level::High, None, Vec::new()).unwrap_err();
761 assert_eq!(
762 error.to_string(),
763 "Confidence has no reasons: add at least one reason code with its signal value"
764 );
765 assert!(error.source().is_none());
766 assert!(Confidence::<Octaves>::no_estimate(Vec::new()).is_err());
767 let json = json!({"reasons": [], "estimate": false});
768 assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
769 }
770
771 #[test]
772 fn spread_in_percent_serializes_as_a_number() {
773 let spread = Percent::<crate::units::Unipolar>::new(12.5).unwrap();
774 let confidence = Confidence::estimate(Level::Low, Some(spread), vec![frames(3)]).unwrap();
775 let value = serde_json::to_value(&confidence).unwrap();
776 assert_eq!(value["spread"], json!(12.5));
777 assert_eq!(
778 serde_json::from_value::<Confidence<_>>(value).unwrap(),
779 confidence
780 );
781 }
782
783 #[test]
784 fn negative_spread_is_rejected() {
785 let error =
786 Confidence::estimate(Level::High, Some(octaves(-0.3)), vec![frames(1)]).unwrap_err();
787 assert_eq!(
788 error.to_string(),
789 "Confidence spread out of range: got -0.3, expected a finite value of at least 0.0"
790 );
791 assert!(error.source().is_some());
792 assert!(Confidence::estimate(Level::High, Some(octaves(0.0)), vec![frames(1)]).is_ok());
793 }
794
795 #[test]
796 fn nan_and_infinite_spread_cannot_be_built() {
797 assert!(Octaves::new(f64::NAN).is_err());
799 assert!(Octaves::new(f64::INFINITY).is_err());
800 let json = r#"{"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": 1e999, "estimate": true}"#;
801 assert!(serde_json::from_str::<Confidence<Octaves>>(json).is_err());
802 }
803
804 #[test]
805 fn json_negative_or_out_of_range_spread_is_rejected() {
806 let json = json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": -0.3, "estimate": true});
807 let message = serde_json::from_value::<Confidence<Octaves>>(json)
808 .unwrap_err()
809 .to_string();
810 assert!(
811 message.contains("Confidence spread out of range"),
812 "{message}"
813 );
814 let json = json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": 1.5, "estimate": true});
815 let message = serde_json::from_value::<Confidence<Normalized>>(json)
816 .unwrap_err()
817 .to_string();
818 assert!(message.contains("Normalized out of range"), "{message}");
819 }
820
821 #[test]
822 fn json_level_on_a_no_estimate_state_is_rejected() {
823 let json = json!({"level": "high", "reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false});
824 assert_eq!(
825 serde_json::from_value::<Confidence<Octaves>>(json)
826 .unwrap_err()
827 .to_string(),
828 "Confidence with \"estimate\": false has a level: \
829 remove \"level\" or set \"estimate\": true"
830 );
831 }
832
833 #[test]
834 fn json_spread_on_a_no_estimate_state_is_rejected() {
835 let json = json!({"reasons": [{"code": "too_few_notes", "value": 2}], "spread": 0.3, "estimate": false});
836 assert_eq!(
837 serde_json::from_value::<Confidence<Octaves>>(json)
838 .unwrap_err()
839 .to_string(),
840 "Confidence with \"estimate\": false has a spread: \
841 remove \"spread\" or set \"estimate\": true"
842 );
843 }
844
845 #[test]
846 fn json_estimate_without_a_level_is_rejected() {
847 let json = json!({"reasons": [{"code": "active_frames", "value": 412}], "estimate": true});
848 assert_eq!(
849 serde_json::from_value::<Confidence<Octaves>>(json)
850 .unwrap_err()
851 .to_string(),
852 "Confidence with \"estimate\": true has no level: \
853 add \"level\" or set \"estimate\": false"
854 );
855 }
856
857 #[test]
858 fn json_spread_for_an_estimate_without_spread_is_rejected() {
859 let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}], "spread": 0.3, "estimate": true});
860 assert_eq!(
861 serde_json::from_value::<Confidence<NoSpread>>(json)
862 .unwrap_err()
863 .to_string(),
864 "Confidence has a spread, but this estimate has none: remove \"spread\""
865 );
866 }
867
868 #[test]
869 fn json_unknown_field_and_missing_estimate_are_rejected() {
870 let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}], "estimate": true, "score": 0.5});
871 assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
872 let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}]});
873 assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
874 let json = json!({"code": "active_frames", "value": 1, "weight": 2});
875 assert!(serde_json::from_value::<Reason>(json).is_err());
876 }
877
878 #[test]
879 fn unknown_reason_code_and_level_are_rejected() {
880 assert!(serde_json::from_value::<Reason>(json!({"code": "vibes", "value": 1})).is_err());
881 assert!(serde_json::from_value::<Level>(json!("certain")).is_err());
882 }
883
884 #[test]
885 fn signals_read_from_json_by_kind() {
886 let read = |value| serde_json::from_value::<Signal>(value).unwrap();
887 assert_eq!(read(json!(412)), Signal::Count(412));
888 assert_eq!(read(json!(-3)), Signal::Number(-3.0));
889 assert_eq!(read(json!(2.5)), Signal::Number(2.5));
890 assert_eq!(read(json!(true)), Signal::Flag(true));
891 assert!(serde_json::from_value::<Signal>(json!(i64::MIN)).is_err());
892 assert_eq!(
893 serde_json::from_value::<Signal>(json!("412"))
894 .unwrap_err()
895 .to_string(),
896 "invalid type: string \"412\", expected a count, a finite number or a bool"
897 );
898 assert!(serde_json::from_str::<Signal>("1e999").is_err());
899 }
900
901 #[test]
902 fn a_signal_must_be_finite() {
903 let error = Reason::new(ReasonCode::ActiveFrames, Signal::Number(f64::NAN)).unwrap_err();
904 assert_eq!(
905 error.to_string(),
906 "Reason signal out of range: got NaN, expected a finite value"
907 );
908 assert!(error.source().is_some());
909 assert!(Reason::new(ReasonCode::ActiveFrames, Signal::Number(f64::INFINITY)).is_err());
910 assert!(Reason::new(ReasonCode::ActiveFrames, Signal::Number(-2.5)).is_ok());
911 }
912
913 #[test]
914 fn a_signal_stores_negative_zero_as_positive_zero() {
915 let reason = Reason::new(ReasonCode::ActiveFrames, Signal::Number(-0.0)).unwrap();
916 let Signal::Number(number) = reason.value() else {
917 panic!("{reason:?}");
918 };
919 assert!(number.is_sign_positive());
920 assert_eq!(
921 serde_json::to_string(&reason).unwrap(),
922 r#"{"code":"active_frames","value":0.0}"#
923 );
924 let read: Signal = serde_json::from_str("-0.0").unwrap();
925 assert!(matches!(read, Signal::Number(number) if number.is_sign_positive()));
926 }
927
928 #[test]
929 fn every_reason_code_has_a_distinct_snake_case_name() {
930 let names: Vec<String> = ReasonCode::ALL
931 .iter()
932 .map(|code| serde_json::to_string(code).unwrap())
933 .collect();
934 let mut unique = names.clone();
935 unique.sort();
936 unique.dedup();
937 assert_eq!(unique.len(), names.len());
938 assert!(names.contains(&"\"octave_uncertain\"".to_owned()));
939 }
940
941 #[test]
942 fn levels_order_low_to_high() {
943 assert!(Level::Low < Level::Medium && Level::Medium < Level::High);
944 assert_eq!(Level::High.min(Level::Low), Level::Low);
945 }
946
947 fn finite() -> impl Strategy<Value = f64> {
949 prop::num::f64::NORMAL | prop::num::f64::SUBNORMAL | prop::num::f64::ZERO
950 }
951
952 fn reason() -> impl Strategy<Value = Reason> {
953 let code = prop::sample::select(ReasonCode::ALL);
954 let signal = prop_oneof![
955 any::<u64>().prop_map(Signal::Count),
956 finite().prop_map(Signal::Number),
957 any::<bool>().prop_map(Signal::Flag),
958 ];
959 (code, signal).prop_map(|(code, signal)| Reason::new(code, signal).unwrap())
960 }
961
962 fn confidence() -> impl Strategy<Value = Confidence<Cents>> {
963 let level = prop_oneof![Just(Level::Low), Just(Level::Medium), Just(Level::High)];
964 let spread = prop::option::of(finite().prop_map(|v| Cents::new(v.abs()).unwrap()));
965 let reasons = prop::collection::vec(reason(), 1..4);
966 (any::<bool>(), level, spread, reasons).prop_map(|(estimate, level, spread, reasons)| {
967 if estimate {
968 Confidence::estimate(level, spread, reasons).unwrap()
969 } else {
970 Confidence::no_estimate(reasons).unwrap()
971 }
972 })
973 }
974
975 proptest! {
976 #[test]
977 fn every_state_round_trips(confidence in confidence()) {
978 let json = serde_json::to_string(&confidence).unwrap();
979 let back: Confidence<Cents> = serde_json::from_str(&json).unwrap();
980 prop_assert_eq!(back, confidence);
981 }
982 }
983}