Skip to main content

mathverse_core/
error.rs

1//! Error taxonomy shared by the whole ecosystem.
2//!
3//! Every failure mode `MathVerse` recognizes is represented as a variant of
4//! [`MathError`]. The [`MathResult<T>`] type alias simplifies return types.
5
6use alloc::string::{String, ToString};
7
8/// Every failure mode `MathVerse` recognizes.
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub enum MathError {
12    /// Input outside the function's domain (e.g. `ln(-1)`).
13    Domain,
14    /// Division or modulus by zero.
15    DivisionByZero,
16    /// A parameter was invalid; carries a human-readable reason.
17    InvalidArgument(&'static str),
18    /// An iterative method failed to converge.
19    NotConverged(&'static str),
20    /// Matrix/vector shapes don't line up.
21    DimensionMismatch,
22    /// Result exceeded the representable range.
23    Overflow,
24    /// Result fell below the representable range.
25    Underflow,
26    /// Matrix is not invertible.
27    Singular,
28    /// Computation exceeded a time or iteration limit.
29    Timeout,
30    /// I/O operation failed.
31    Io,
32    /// Parsing of a string or expression failed.
33    Parse,
34    /// Feature or operation not yet implemented.
35    NotImplemented,
36    /// Value outside a valid range.
37    OutOfRange,
38    /// No solution exists for the given inputs.
39    NoSolution,
40    /// Infinitely many solutions exist.
41    InfiniteSolutions,
42    /// Base of a number system is invalid.
43    InvalidBase,
44    /// Input must be non-negative.
45    NegativeInput,
46}
47
48/// Convenience alias used everywhere.
49pub type MathResult<T> = Result<T, MathError>;
50
51impl MathError {
52    /// Returns a human-readable description of the error.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use mathverse_core::error::MathError;
58    ///
59    /// assert_eq!(MathError::Domain.msg(), "input is outside the function domain");
60    /// assert_eq!(MathError::DivisionByZero.msg(), "division by zero");
61    /// assert_eq!(MathError::Overflow.msg(), "numeric overflow");
62    /// ```
63    #[must_use]
64    pub const fn msg(&self) -> &'static str {
65        match self {
66            Self::Domain => "input is outside the function domain",
67            Self::DivisionByZero => "division by zero",
68            Self::InvalidArgument(m) | Self::NotConverged(m) => m,
69            Self::DimensionMismatch => "dimension mismatch",
70            Self::Overflow => "numeric overflow",
71            Self::Underflow => "numeric underflow",
72            Self::Singular => "singular matrix",
73            Self::Timeout => "computation timed out",
74            Self::Io => "I/O error",
75            Self::Parse => "parse error",
76            Self::NotImplemented => "not implemented",
77            Self::OutOfRange => "value out of range",
78            Self::NoSolution => "no solution exists",
79            Self::InfiniteSolutions => "infinitely many solutions",
80            Self::InvalidBase => "invalid base",
81            Self::NegativeInput => "input must be non-negative",
82        }
83    }
84
85    /// Returns `true` if this error variant carries a custom message.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use mathverse_core::error::MathError;
91    ///
92    /// assert!(MathError::InvalidArgument("bad").has_context());
93    /// assert!(!MathError::Domain.has_context());
94    /// ```
95    #[must_use]
96    pub const fn has_context(&self) -> bool {
97        matches!(self, Self::InvalidArgument(_) | Self::NotConverged(_))
98    }
99}
100
101impl core::fmt::Display for MathError {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        match self {
104            Self::InvalidArgument(msg) => write!(f, "invalid argument: {msg}"),
105            Self::NotConverged(method) => write!(f, "{method} failed to converge"),
106            other => f.write_str(other.msg()),
107        }
108    }
109}
110
111#[cfg(feature = "std")]
112impl std::error::Error for MathError {}
113
114#[allow(clippy::use_self)]
115impl From<MathError> for String {
116    fn from(e: MathError) -> String {
117        e.to_string()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn displays_with_context() {
127        assert_eq!(MathError::DivisionByZero.to_string(), "division by zero");
128        assert_eq!(
129            MathError::InvalidArgument("negative radius").to_string(),
130            "invalid argument: negative radius"
131        );
132        assert_eq!(MathError::Timeout.to_string(), "computation timed out");
133        assert_eq!(MathError::NoSolution.to_string(), "no solution exists");
134        assert_eq!(MathError::Parse.to_string(), "parse error");
135    }
136
137    #[test]
138    fn msg_method() {
139        assert_eq!(MathError::Domain.msg(), "input is outside the function domain");
140        assert_eq!(
141            MathError::InvalidArgument("test").msg(),
142            "test"
143        );
144        assert_eq!(MathError::Overflow.msg(), "numeric overflow");
145    }
146
147    #[test]
148    fn has_context_method() {
149        assert!(MathError::InvalidArgument("x").has_context());
150        assert!(MathError::NotConverged("newton").has_context());
151        assert!(!MathError::Domain.has_context());
152        assert!(!MathError::Overflow.has_context());
153    }
154
155    #[test]
156    fn into_string() {
157        let e: String = MathError::DivisionByZero.into();
158        assert_eq!(e, "division by zero");
159    }
160
161    #[test]
162    fn result_flow() {
163        fn invert(x: f64) -> MathResult<f64> {
164            if x == 0.0 {
165                Err(MathError::DivisionByZero)
166            } else {
167                Ok(1.0 / x)
168            }
169        }
170        assert_eq!(invert(4.0), Ok(0.25));
171        assert_eq!(invert(0.0), Err(MathError::DivisionByZero));
172    }
173}