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}