Skip to main content

optionstratlib_core/error/
decimal.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 25/12/24
5******************************************************************************/
6//! Target crate (ADR-0001 D6, roadmap M1-14): **core**. Owns `DecimalError`.
7
8use rust_decimal::Decimal;
9use thiserror::Error;
10
11/// # Decimal Error Management
12///
13/// Represents errors that can occur during decimal operations in financial calculations.
14/// This enum provides a structured way to handle various error conditions that may arise
15/// when working with decimal values, including validation, arithmetic operations,
16/// conversions, and precision issues.
17///
18/// # Use Cases
19///
20/// * Financial calculations requiring strict decimal precision
21/// * Currency and monetary value operations
22/// * Option pricing models where precision is critical
23/// * Risk management calculations
24///
25/// # Error Propagation
26///
27/// These errors are typically wrapped in `DecimalResult` and propagated through
28/// the application's calculation pipeline.
29///
30/// # Variants
31///
32/// * `InvalidValue` - Handles errors when a value cannot be represented as a valid decimal
33/// * `ArithmeticError` - Handles errors during mathematical operations
34/// * `ConversionError` - Handles errors when converting between different decimal representations
35/// * `OutOfBounds` - Handles errors when a value exceeds defined limits
36/// * `InvalidPrecision` - Handles errors related to decimal precision settings
37///
38/// # Example Usage
39///
40/// ```rust
41/// use optionstratlib_core::error::DecimalError;
42///
43/// fn validate_decimal(value: f64) -> Result<(), DecimalError> {
44///     if value.is_nan() {
45///         return Err(DecimalError::InvalidValue {
46///             value,
47///             reason: "Value cannot be NaN".to_string(),
48///         });
49///     }
50///     
51///     if value < 0.0 || value > 100.0 {
52///         return Err(DecimalError::OutOfBounds {
53///             value,
54///             min: 0.0,
55///             max: 100.0,
56///         });
57///     }
58///     
59///     Ok(())
60/// }
61/// ```
62#[derive(Error, Debug)]
63pub enum DecimalError {
64    /// Error when attempting to create a decimal from an invalid value
65    ///
66    /// Occurs when a value cannot be properly represented as a decimal,
67    /// such as when it's NaN, infinity, or otherwise unsuitable for
68    /// financial calculations.
69    #[error("Invalid decimal value {value}: {reason}")]
70    InvalidValue {
71        /// The problematic value that caused the error
72        value: f64,
73        /// Detailed explanation of why the value is invalid
74        reason: String,
75    },
76
77    /// Error when performing decimal arithmetic operations
78    ///
79    /// Occurs during mathematical operations such as addition, subtraction,
80    /// multiplication, or division when the operation cannot be completed
81    /// correctly (e.g., division by zero, overflow).
82    #[error("Decimal arithmetic error during {operation}: {reason}")]
83    ArithmeticError {
84        /// The operation that failed (e.g., "addition", "division")
85        operation: String,
86        /// Detailed explanation of why the operation failed
87        reason: String,
88    },
89
90    /// Error when converting between decimal types
91    ///
92    /// Occurs when a decimal value cannot be correctly converted from one
93    /// representation to another, such as between different precision levels
94    /// or between different decimal formats.
95    #[error("Failed to convert decimal from {from_type} to {to_type}: {reason}")]
96    ConversionError {
97        /// The source type being converted from
98        from_type: String,
99        /// The destination type being converted to
100        to_type: String,
101        /// Detailed explanation of why the conversion failed
102        reason: String,
103    },
104
105    /// Error when a decimal value exceeds its bounds
106    ///
107    /// Occurs when a decimal value falls outside of acceptable minimum
108    /// or maximum values for a specific calculation context.
109    #[error("Decimal value {value} is out of bounds (min: {min}, max: {max})")]
110    OutOfBounds {
111        /// The value that is out of bounds
112        value: f64,
113        /// The minimum acceptable value
114        min: f64,
115        /// The maximum acceptable value
116        max: f64,
117    },
118
119    /// Error when decimal precision is invalid
120    ///
121    /// Occurs when an operation specifies or results in an invalid precision
122    /// level that cannot be properly handled.
123    #[error("Invalid decimal precision {precision}: {reason}")]
124    InvalidPrecision {
125        /// The problematic precision value
126        precision: i32,
127        /// Detailed explanation of why the precision is invalid
128        reason: String,
129    },
130
131    /// Expiration-date conversion error surfaced during decimal operations.
132    #[error(transparent)]
133    ExpirationDate(expiration_date::error::ExpirationDateError),
134
135    /// Error when a `Decimal` arithmetic operation overflows the representable range.
136    ///
137    /// Emitted by the crate-private `d_add` / `d_sub` / `d_mul` / `d_div` helpers
138    /// in [`crate::model::decimal`] when the underlying
139    /// `Decimal::checked_*` call returns `None`. The `operation` tag is a
140    /// short, static identifier describing the call-site (for example
141    /// `"pricing::black_scholes::discount_strike"`) so failures can be
142    /// traced back to their kernel without walking the call stack.
143    ///
144    /// The operands are captured verbatim for post-mortem debugging and
145    /// are included in the formatted error output (see the `#[error]`
146    /// template below). This `DecimalError` variant is propagated across
147    /// domain modules through the existing `#[from]` cascade, so the
148    /// operand values will appear in any log or wrapper-error string
149    /// produced downstream. Callers that need to redact them for
150    /// production logging should match on the `Overflow` variant
151    /// explicitly and reformat rather than rely on the default `Display`.
152    #[error("Decimal {operation} overflow: {lhs} op {rhs}")]
153    Overflow {
154        /// Short static tag identifying the call-site.
155        operation: &'static str,
156        /// Left-hand operand of the failed arithmetic.
157        lhs: Decimal,
158        /// Right-hand operand of the failed arithmetic.
159        rhs: Decimal,
160    },
161}
162
163/// A specialized `Result` type for decimal calculation operations.
164///
165/// This type alias provides a convenient shorthand for operations that can result in a
166/// `DecimalError`. It helps improve code readability and reduces boilerplate when working
167/// with decimal calculations throughout the library.
168///
169/// # Type Parameters
170///
171/// * `T` - The successful result type of the operation
172///
173/// # Examples
174///
175/// ```rust
176/// use optionstratlib_core::error::{DecimalError, DecimalResult};
177///
178/// fn divide(a: f64, b: f64) -> DecimalResult<f64> {
179///     if b == 0.0 {
180///         Err(DecimalError::ArithmeticError {
181///             operation: "division".to_string(),
182///             reason: "division by zero".to_string(),
183///         })
184///     } else {
185///         Ok(a / b)
186///     }
187/// }
188/// ```
189///
190/// # Usage Context
191///
192/// This type is primarily used in the financial calculations and decimal handling
193/// components of the library, where precise decimal operations are critical and
194/// error handling needs to be consistent and well-structured.
195///
196/// # Related Types
197///
198/// * `DecimalError` - The error type representing various decimal operation failures
199pub type DecimalResult<T> = Result<T, DecimalError>;
200
201/// Helper methods for creating common decimal errors
202///
203/// This implementation provides convenient factory methods to create
204/// standardized instances of `DecimalError` for common error scenarios
205/// in decimal operations. These methods help maintain consistency in
206/// error creation across the codebase and simplify the construction of
207/// descriptive error instances.
208///
209/// # Example
210///
211/// ```rust
212/// use optionstratlib_core::error::DecimalError;
213/// // Creating an invalid value error
214/// let err = DecimalError::invalid_value(12.34, "Value exceeds maximum allowed");
215///
216/// // Creating an arithmetic error
217/// let div_err = DecimalError::arithmetic_error("division", "Division by zero");
218/// ```
219impl DecimalError {
220    /// Creates a new `InvalidValue` error
221    ///
222    /// Used when a decimal value fails validation due to being outside
223    /// accepted ranges or otherwise inappropriate for the context.
224    ///
225    /// # Parameters
226    ///
227    /// * `value` - The problematic floating-point value
228    /// * `reason` - Explanation of why the value is invalid
229    ///
230    /// # Returns
231    ///
232    /// A new `DecimalError::InvalidValue` instance
233    #[must_use]
234    #[cold]
235    #[inline(never)]
236    pub fn invalid_value(value: f64, reason: &str) -> Self {
237        DecimalError::InvalidValue {
238            value,
239            reason: reason.to_string(),
240        }
241    }
242
243    /// Creates a new `ArithmeticError` error
244    ///
245    /// Used when a mathematical operation on decimal values fails, such as
246    /// division by zero, overflow, or underflow.
247    ///
248    /// # Parameters
249    ///
250    /// * `operation` - The name of the operation that failed (e.g., "addition", "division")
251    /// * `reason` - Explanation of why the operation failed
252    ///
253    /// # Returns
254    ///
255    /// A new `DecimalError::ArithmeticError` instance
256    #[cold]
257    #[inline(never)]
258    #[must_use]
259    pub fn arithmetic_error(operation: &str, reason: &str) -> Self {
260        DecimalError::ArithmeticError {
261            operation: operation.to_string(),
262            reason: reason.to_string(),
263        }
264    }
265
266    /// Creates a new `ConversionError` error
267    ///
268    /// Used when conversion between decimal types or from/to other number
269    /// types fails due to compatibility or range issues.
270    ///
271    /// # Parameters
272    ///
273    /// * `from_type` - The source type being converted from
274    /// * `to_type` - The destination type being converted to
275    /// * `reason` - Explanation of why the conversion failed
276    ///
277    /// # Returns
278    ///
279    /// A new `DecimalError::ConversionError` instance
280    #[cold]
281    #[inline(never)]
282    #[must_use]
283    pub fn conversion_error(from_type: &str, to_type: &str, reason: &str) -> Self {
284        DecimalError::ConversionError {
285            from_type: from_type.to_string(),
286            to_type: to_type.to_string(),
287            reason: reason.to_string(),
288        }
289    }
290
291    /// Creates a new `OutOfBounds` error
292    ///
293    /// Used when a decimal value falls outside of specified minimum and maximum bounds.
294    ///
295    /// # Parameters
296    ///
297    /// * `value` - The out-of-bounds floating-point value
298    /// * `min` - The lower bound (inclusive) of the valid range
299    /// * `max` - The upper bound (inclusive) of the valid range
300    ///
301    /// # Returns
302    ///
303    /// A new `DecimalError::OutOfBounds` instance
304    #[cold]
305    #[inline(never)]
306    #[must_use]
307    pub fn out_of_bounds(value: f64, min: f64, max: f64) -> Self {
308        DecimalError::OutOfBounds { value, min, max }
309    }
310
311    /// Creates a new `InvalidPrecision` error
312    ///
313    /// Used when a specified decimal precision is invalid, such as being negative,
314    /// too large, or otherwise inappropriate for the context.
315    ///
316    /// # Parameters
317    ///
318    /// * `precision` - The problematic precision value
319    /// * `reason` - Explanation of why the precision is invalid
320    ///
321    /// # Returns
322    ///
323    /// A new `DecimalError::InvalidPrecision` instance
324    #[cold]
325    #[inline(never)]
326    #[must_use]
327    pub fn invalid_precision(precision: i32, reason: &str) -> Self {
328        DecimalError::InvalidPrecision {
329            precision,
330            reason: reason.to_string(),
331        }
332    }
333
334    /// Creates a new `Overflow` error for a failed checked arithmetic operation.
335    ///
336    /// Used by the crate-private `d_add` / `d_sub` / `d_mul` / `d_div` helpers
337    /// to surface an overflow of the underlying `Decimal::checked_*` call
338    /// with full operand context.
339    ///
340    /// # Parameters
341    ///
342    /// * `operation` - Short static tag identifying the call-site
343    ///   (for example `"pricing::black_scholes::discount_strike"`).
344    /// * `lhs` - Left-hand operand of the failed arithmetic.
345    /// * `rhs` - Right-hand operand of the failed arithmetic.
346    ///
347    /// # Returns
348    ///
349    /// A new `DecimalError::Overflow` instance.
350    #[cold]
351    #[inline(never)]
352    #[must_use]
353    pub fn overflow(operation: &'static str, lhs: Decimal, rhs: Decimal) -> Self {
354        DecimalError::Overflow {
355            operation,
356            lhs,
357            rhs,
358        }
359    }
360}
361
362impl From<expiration_date::error::ExpirationDateError> for DecimalError {
363    #[inline]
364    fn from(err: expiration_date::error::ExpirationDateError) -> Self {
365        DecimalError::ExpirationDate(err)
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_invalid_value_error() {
375        let error = DecimalError::invalid_value(-1.0, "Value cannot be negative");
376        assert!(matches!(error, DecimalError::InvalidValue { .. }));
377        assert!(error.to_string().contains("cannot be negative"));
378    }
379
380    #[test]
381    fn test_arithmetic_error() {
382        let error = DecimalError::arithmetic_error("division", "Division by zero");
383        assert!(matches!(error, DecimalError::ArithmeticError { .. }));
384        assert!(error.to_string().contains("Division by zero"));
385    }
386
387    #[test]
388    fn test_conversion_error() {
389        let error = DecimalError::conversion_error("f64", "Decimal", "Value out of range");
390        assert!(matches!(error, DecimalError::ConversionError { .. }));
391        assert!(error.to_string().contains("out of range"));
392    }
393
394    #[test]
395    fn test_out_of_bounds_error() {
396        let error = DecimalError::out_of_bounds(150.0, 0.0, 100.0);
397        assert!(matches!(error, DecimalError::OutOfBounds { .. }));
398        assert!(error.to_string().contains("150"));
399    }
400
401    #[test]
402    fn test_invalid_precision_error() {
403        let error = DecimalError::invalid_precision(-1, "Precision must be non-negative");
404        assert!(matches!(error, DecimalError::InvalidPrecision { .. }));
405        assert!(error.to_string().contains("non-negative"));
406    }
407
408    #[test]
409    fn test_overflow_error() {
410        let error = DecimalError::overflow("test::site", Decimal::MAX, Decimal::MAX);
411        assert!(matches!(error, DecimalError::Overflow { .. }));
412        let rendered = error.to_string();
413        assert!(rendered.contains("test::site"));
414        assert!(rendered.contains("overflow"));
415    }
416
417    #[test]
418    fn test_overflow_fields() {
419        let lhs = Decimal::MAX;
420        let rhs = Decimal::from(2);
421        if let DecimalError::Overflow {
422            operation,
423            lhs: captured_lhs,
424            rhs: captured_rhs,
425        } = DecimalError::overflow("test::mul", lhs, rhs)
426        {
427            assert_eq!(operation, "test::mul");
428            assert_eq!(captured_lhs, lhs);
429            assert_eq!(captured_rhs, rhs);
430        } else {
431            panic!("expected Overflow variant");
432        }
433    }
434}