rs_stats/
error.rs

1//! # Error Types
2//!
3//! This module defines the error types used throughout the rs-stats library.
4//! All errors are structured and provide context about what went wrong.
5
6use thiserror::Error;
7
8/// Main error type for the rs-stats library
9///
10/// This enum represents all possible errors that can occur in the library.
11/// Each variant includes a message providing context about the error.
12///
13/// # Examples
14///
15/// ```rust
16/// use rs_stats::error::{StatsError, StatsResult};
17///
18/// fn example() -> StatsResult<f64> {
19///     Err(StatsError::InvalidInput {
20///         message: "Value must be positive".to_string(),
21///     })
22/// }
23/// ```
24#[derive(Error, Debug, Clone, PartialEq)]
25pub enum StatsError {
26    /// Invalid input parameters provided to a function
27    #[error("Invalid input: {message}")]
28    InvalidInput {
29        /// Human-readable error message
30        message: String,
31    },
32
33    /// Type conversion failure
34    #[error("Conversion error: {message}")]
35    ConversionError {
36        /// Human-readable error message
37        message: String,
38    },
39
40    /// Empty data provided when data is required
41    #[error("Empty data: {message}")]
42    EmptyData {
43        /// Human-readable error message
44        message: String,
45    },
46
47    /// Dimension mismatch between arrays/vectors
48    #[error("Dimension mismatch: {message}")]
49    DimensionMismatch {
50        /// Human-readable error message
51        message: String,
52    },
53
54    /// Numerical computation error (overflow, underflow, NaN, etc.)
55    #[error("Numerical error: {message}")]
56    NumericalError {
57        /// Human-readable error message
58        message: String,
59    },
60
61    /// Model not fitted/trained before use
62    #[error("Model not fitted: {message}")]
63    NotFitted {
64        /// Human-readable error message
65        message: String,
66    },
67
68    /// Invalid parameter value
69    #[error("Invalid parameter: {message}")]
70    InvalidParameter {
71        /// Human-readable error message
72        message: String,
73    },
74
75    /// Index out of bounds
76    #[error("Index out of bounds: {message}")]
77    IndexOutOfBounds {
78        /// Human-readable error message
79        message: String,
80    },
81
82    /// Division by zero or similar mathematical error
83    #[error("Mathematical error: {message}")]
84    MathematicalError {
85        /// Human-readable error message
86        message: String,
87    },
88}
89
90/// Convenience type alias for Result with StatsError
91///
92/// This is the standard return type for functions that can fail in the rs-stats library.
93///
94/// # Examples
95///
96/// ```rust
97/// use rs_stats::error::StatsResult;
98///
99/// fn might_fail() -> StatsResult<f64> {
100///     Ok(42.0)
101/// }
102/// ```
103pub type StatsResult<T> = Result<T, StatsError>;
104
105impl StatsError {
106    /// Create an InvalidInput error
107    pub fn invalid_input<S: Into<String>>(message: S) -> Self {
108        StatsError::InvalidInput {
109            message: message.into(),
110        }
111    }
112
113    /// Create a ConversionError
114    pub fn conversion_error<S: Into<String>>(message: S) -> Self {
115        StatsError::ConversionError {
116            message: message.into(),
117        }
118    }
119
120    /// Create an EmptyData error
121    pub fn empty_data<S: Into<String>>(message: S) -> Self {
122        StatsError::EmptyData {
123            message: message.into(),
124        }
125    }
126
127    /// Create a DimensionMismatch error
128    pub fn dimension_mismatch<S: Into<String>>(message: S) -> Self {
129        StatsError::DimensionMismatch {
130            message: message.into(),
131        }
132    }
133
134    /// Create a NumericalError
135    pub fn numerical_error<S: Into<String>>(message: S) -> Self {
136        StatsError::NumericalError {
137            message: message.into(),
138        }
139    }
140
141    /// Create a NotFitted error
142    pub fn not_fitted<S: Into<String>>(message: S) -> Self {
143        StatsError::NotFitted {
144            message: message.into(),
145        }
146    }
147
148    /// Create an InvalidParameter error
149    pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
150        StatsError::InvalidParameter {
151            message: message.into(),
152        }
153    }
154
155    /// Create an IndexOutOfBounds error
156    pub fn index_out_of_bounds<S: Into<String>>(message: S) -> Self {
157        StatsError::IndexOutOfBounds {
158            message: message.into(),
159        }
160    }
161
162    /// Create a MathematicalError
163    pub fn mathematical_error<S: Into<String>>(message: S) -> Self {
164        StatsError::MathematicalError {
165            message: message.into(),
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    /// Test Display implementation for all variants
175    #[test]
176    fn test_all_variants_display() {
177        let cases = vec![
178            (StatsError::invalid_input("msg"), "Invalid input: msg"),
179            (StatsError::conversion_error("msg"), "Conversion error: msg"),
180            (StatsError::empty_data("msg"), "Empty data: msg"),
181            (
182                StatsError::dimension_mismatch("msg"),
183                "Dimension mismatch: msg",
184            ),
185            (StatsError::numerical_error("msg"), "Numerical error: msg"),
186            (StatsError::not_fitted("msg"), "Model not fitted: msg"),
187            (
188                StatsError::invalid_parameter("msg"),
189                "Invalid parameter: msg",
190            ),
191            (
192                StatsError::index_out_of_bounds("msg"),
193                "Index out of bounds: msg",
194            ),
195            (
196                StatsError::mathematical_error("msg"),
197                "Mathematical error: msg",
198            ),
199        ];
200
201        for (err, expected) in cases {
202            assert_eq!(err.to_string(), expected, "Display format mismatch");
203        }
204    }
205
206    /// Test equality between errors
207    #[test]
208    fn test_error_equality() {
209        let err1 = StatsError::invalid_input("message");
210        let err2 = StatsError::invalid_input("message");
211        let err3 = StatsError::invalid_input("different");
212        let err4 = StatsError::conversion_error("message");
213
214        assert_eq!(err1, err2, "Same variant and message should be equal");
215        assert_ne!(err1, err3, "Different messages should not be equal");
216        assert_ne!(err1, err4, "Different variants should not be equal");
217    }
218
219    /// Test Clone implementation
220    #[test]
221    fn test_error_clone() {
222        let err = StatsError::conversion_error("test");
223        let cloned = err.clone();
224        assert_eq!(err, cloned);
225    }
226
227    /// Test StatsResult type alias
228    #[test]
229    fn test_stats_result() {
230        let ok: StatsResult<f64> = Ok(42.0);
231        assert_eq!(ok.unwrap(), 42.0);
232
233        let err: StatsResult<f64> = Err(StatsError::invalid_input("test"));
234        assert!(err.is_err());
235        assert_eq!(err.unwrap_err(), StatsError::invalid_input("test"));
236    }
237
238    /// Test helper methods return correct variants
239    #[test]
240    fn test_helper_methods() {
241        assert!(matches!(
242            StatsError::invalid_input("msg"),
243            StatsError::InvalidInput { .. }
244        ));
245        assert!(matches!(
246            StatsError::conversion_error("msg"),
247            StatsError::ConversionError { .. }
248        ));
249        assert!(matches!(
250            StatsError::empty_data("msg"),
251            StatsError::EmptyData { .. }
252        ));
253        assert!(matches!(
254            StatsError::dimension_mismatch("msg"),
255            StatsError::DimensionMismatch { .. }
256        ));
257        assert!(matches!(
258            StatsError::numerical_error("msg"),
259            StatsError::NumericalError { .. }
260        ));
261        assert!(matches!(
262            StatsError::not_fitted("msg"),
263            StatsError::NotFitted { .. }
264        ));
265        assert!(matches!(
266            StatsError::invalid_parameter("msg"),
267            StatsError::InvalidParameter { .. }
268        ));
269        assert!(matches!(
270            StatsError::index_out_of_bounds("msg"),
271            StatsError::IndexOutOfBounds { .. }
272        ));
273        assert!(matches!(
274            StatsError::mathematical_error("msg"),
275            StatsError::MathematicalError { .. }
276        ));
277    }
278
279    /// Test Into<String> conversion for helper methods
280    #[test]
281    fn test_into_string_conversion() {
282        // Test with &str
283        let err1 = StatsError::invalid_input("string slice");
284        assert_eq!(err1.to_string(), "Invalid input: string slice");
285
286        // Test with String
287        let err2 = StatsError::invalid_input("owned string".to_string());
288        assert_eq!(err2.to_string(), "Invalid input: owned string");
289    }
290
291    /// Test error propagation with ? operator
292    #[test]
293    fn test_error_propagation() {
294        fn might_fail() -> StatsResult<f64> {
295            Err(StatsError::invalid_input("test"))
296        }
297
298        fn propagate() -> StatsResult<f64> {
299            might_fail()?;
300            Ok(42.0)
301        }
302
303        let result = propagate();
304        assert!(result.is_err());
305        assert!(matches!(
306            result.unwrap_err(),
307            StatsError::InvalidInput { .. }
308        ));
309    }
310
311    /// Test pattern matching on error variants
312    #[test]
313    fn test_pattern_matching() {
314        let err = StatsError::conversion_error("failed");
315
316        match err {
317            StatsError::ConversionError { message } => {
318                assert_eq!(message, "failed");
319            }
320            _ => panic!("Wrong variant matched"),
321        }
322    }
323
324    /// Test Debug implementation
325    #[test]
326    fn test_debug_implementation() {
327        let err = StatsError::invalid_input("test");
328        let debug_str = format!("{:?}", err);
329        assert!(debug_str.contains("InvalidInput"));
330        assert!(debug_str.contains("test"));
331    }
332}