u_analytics/error.rs
1//! Refusals shared by the functions of more than one module.
2
3use std::ops::RangeInclusive;
4
5/// Why a time-series statistic was refused: [`crate::correlation::acf`],
6/// [`crate::correlation::pacf`], [`crate::testing::adf_test`] and
7/// [`crate::testing::kpss_test`].
8///
9/// The data is checked before the options, and the options before any
10/// computation: a refusal of an option never hides a refusal of the data,
11/// and [`TimeSeriesError::NoVariation`] / [`TimeSeriesError::DegenerateRegression`]
12/// only follow options that were in range.
13#[derive(Debug, Clone, Copy, PartialEq)]
14#[non_exhaustive]
15pub enum TimeSeriesError {
16 /// The series holds a NaN or an infinity.
17 ValueNotFinite {
18 /// Position of the first such value.
19 index: usize,
20 },
21 /// Fewer observations than the statistic is defined for.
22 TooFewObservations {
23 /// The fewest it takes.
24 needed: usize,
25 /// How many were given.
26 got: usize,
27 },
28 /// An option outside its domain, which depends on the number of
29 /// observations: `min` and `max` are both inclusive.
30 OptionOutOfRange {
31 /// The option's name, spelled as the wire formats spell it
32 /// (`max_lag`, `max_lags`, `lags`).
33 option: &'static str,
34 /// The value given (a count as a float).
35 got: f64,
36 /// Inclusive lower bound.
37 min: f64,
38 /// Inclusive upper bound.
39 max: f64,
40 },
41 /// The series does not vary (about its level, or its trend, for the KPSS
42 /// test) beyond the rounding of its own size, so the statistic divides
43 /// by zero.
44 NoVariation,
45 /// The ADF test regression is singular, or fits the differences exactly
46 /// (a deterministic series), so the t-ratio is undefined.
47 DegenerateRegression,
48}
49
50impl core::fmt::Display for TimeSeriesError {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 match self {
53 TimeSeriesError::ValueNotFinite { index } => {
54 write!(f, "the value at index {index} is not finite")
55 }
56 TimeSeriesError::TooFewObservations { needed, got } => {
57 write!(f, "at least {needed} values are needed, got {got}")
58 }
59 TimeSeriesError::OptionOutOfRange {
60 option,
61 got,
62 min,
63 max,
64 } => write!(f, "`{option}` must be in [{min}, {max}] (got {got})"),
65 TimeSeriesError::NoVariation => {
66 write!(f, "the series does not vary, so the statistic is undefined")
67 }
68 TimeSeriesError::DegenerateRegression => write!(
69 f,
70 "the test regression is singular, or an exact fit of the differences \
71 (a deterministic series), so the t-ratio is undefined"
72 ),
73 }
74 }
75}
76
77impl std::error::Error for TimeSeriesError {}
78
79/// The series' own checks, in the order every time-series statistic applies
80/// them: a non-finite value, then too few values.
81pub(crate) fn check_series(data: &[f64], needed: usize) -> Result<(), TimeSeriesError> {
82 if let Some(index) = data.iter().position(|v| !v.is_finite()) {
83 return Err(TimeSeriesError::ValueNotFinite { index });
84 }
85 if data.len() < needed {
86 return Err(TimeSeriesError::TooFewObservations {
87 needed,
88 got: data.len(),
89 });
90 }
91 Ok(())
92}
93
94/// Refuses a count option outside `domain`.
95pub(crate) fn check_option(
96 option: &'static str,
97 value: usize,
98 domain: &RangeInclusive<usize>,
99) -> Result<usize, TimeSeriesError> {
100 if domain.contains(&value) {
101 Ok(value)
102 } else {
103 Err(TimeSeriesError::out_of(option, value as f64, domain))
104 }
105}
106
107impl TimeSeriesError {
108 /// `option` set to `got`, outside `domain` -- the refusal a caller that
109 /// reads the option itself (negative, or past what a count holds) gives
110 /// against a domain such as [`crate::correlation::acf_max_lag_domain`].
111 pub fn out_of(option: &'static str, got: f64, domain: &RangeInclusive<usize>) -> Self {
112 TimeSeriesError::OptionOutOfRange {
113 option,
114 got,
115 min: *domain.start() as f64,
116 max: *domain.end() as f64,
117 }
118 }
119}