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