Skip to main content

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}