1use std::fmt;
70
71pub type MoneyResult<T> = Result<T, MoneyError>;
86
87#[derive(Debug, Clone, PartialEq)]
91pub enum MoneyError {
92 CurrencyMismatch {
97 expected: &'static str,
99 found: &'static str,
101 context: String,
103 },
104
105 ConversionRateMissing {
107 from: &'static str,
109 to: &'static str,
111 },
112
113 PrecisionError {
118 currency: &'static str,
120 expected: u8,
122 actual: u32,
124 suggestion: String,
126 },
127
128 InvalidAmount {
130 reason: String,
132 currency: Option<&'static str>,
134 },
135
136 ParseError {
138 input: String,
140 expected_currency: Option<&'static str>,
142 reason: String,
144 },
145
146 RoundingError {
148 currency: &'static str,
150 reason: String,
152 },
153
154 InvalidRate {
156 value: String,
158 reason: String,
160 },
161
162 Overflow {
164 operation: String,
166 currency: &'static str,
168 },
169
170 Underflow {
172 operation: String,
174 currency: &'static str,
176 },
177}
178
179impl MoneyError {
180 pub fn suggestion(&self) -> &str {
195 match self {
196 MoneyError::CurrencyMismatch { .. } => {
197 "Ensure both amounts use the same currency, or use explicit conversion with a Rate"
198 }
199 MoneyError::ConversionRateMissing { .. } => {
200 "Provide a Rate instance for the currency conversion"
201 }
202 MoneyError::PrecisionError { suggestion, .. } => suggestion,
203 MoneyError::InvalidAmount { .. } => "Check that the amount is a valid finite number",
204 MoneyError::ParseError { .. } => {
205 "Ensure the input string is in a valid format (e.g., '12.34' or '$12.34 USD')"
206 }
207 MoneyError::RoundingError { .. } => {
208 "Try a different rounding mode or check the amount precision"
209 }
210 MoneyError::InvalidRate { .. } => "Exchange rates must be positive, finite numbers",
211 MoneyError::Overflow { .. } => {
212 "Use smaller values or check for logical errors in calculations"
213 }
214 MoneyError::Underflow { .. } => {
215 "Use larger values or check for logical errors in calculations"
216 }
217 }
218 }
219
220 pub fn currency(&self) -> Option<&'static str> {
222 match self {
223 MoneyError::CurrencyMismatch { expected, .. } => Some(expected),
224 MoneyError::ConversionRateMissing { from, .. } => Some(from),
225 MoneyError::PrecisionError { currency, .. } => Some(currency),
226 MoneyError::InvalidAmount { currency, .. } => *currency,
227 MoneyError::ParseError {
228 expected_currency, ..
229 } => *expected_currency,
230 MoneyError::RoundingError { currency, .. } => Some(currency),
231 MoneyError::InvalidRate { .. } => None,
232 MoneyError::Overflow { currency, .. } => Some(currency),
233 MoneyError::Underflow { currency, .. } => Some(currency),
234 }
235 }
236}
237
238impl fmt::Display for MoneyError {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 MoneyError::CurrencyMismatch {
242 expected,
243 found,
244 context,
245 } => {
246 write!(
247 f,
248 "Currency mismatch: expected {}, found {} ({})",
249 expected, found, context
250 )
251 }
252 MoneyError::ConversionRateMissing { from, to } => {
253 write!(f, "No conversion rate available from {} to {}", from, to)
254 }
255 MoneyError::PrecisionError {
256 currency,
257 expected,
258 actual,
259 ..
260 } => {
261 write!(
262 f,
263 "Precision error for {}: expected {} decimal places, found {}",
264 currency, expected, actual
265 )
266 }
267 MoneyError::InvalidAmount { reason, currency } => {
268 if let Some(curr) = currency {
269 write!(f, "Invalid amount for {}: {}", curr, reason)
270 } else {
271 write!(f, "Invalid amount: {}", reason)
272 }
273 }
274 MoneyError::ParseError {
275 input,
276 expected_currency,
277 reason,
278 } => {
279 if let Some(curr) = expected_currency {
280 write!(f, "Failed to parse '{}' as {}: {}", input, curr, reason)
281 } else {
282 write!(f, "Failed to parse '{}': {}", input, reason)
283 }
284 }
285 MoneyError::RoundingError { currency, reason } => {
286 write!(f, "Rounding error for {}: {}", currency, reason)
287 }
288 MoneyError::InvalidRate { value, reason } => {
289 write!(f, "Invalid exchange rate '{}': {}", value, reason)
290 }
291 MoneyError::Overflow {
292 operation,
293 currency,
294 } => {
295 write!(
296 f,
297 "Arithmetic overflow in {} operation for {}",
298 operation, currency
299 )
300 }
301 MoneyError::Underflow {
302 operation,
303 currency,
304 } => {
305 write!(
306 f,
307 "Arithmetic underflow in {} operation for {}",
308 operation, currency
309 )
310 }
311 }
312 }
313}
314
315impl std::error::Error for MoneyError {
316 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
317 None
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_currency_mismatch_display() {
328 let error = MoneyError::CurrencyMismatch {
329 expected: "USD",
330 found: "EUR",
331 context: "addition".to_string(),
332 };
333
334 assert_eq!(
335 error.to_string(),
336 "Currency mismatch: expected USD, found EUR (addition)"
337 );
338 }
339
340 #[test]
341 fn test_conversion_rate_missing_display() {
342 let error = MoneyError::ConversionRateMissing {
343 from: "USD",
344 to: "JPY",
345 };
346
347 assert_eq!(
348 error.to_string(),
349 "No conversion rate available from USD to JPY"
350 );
351 }
352
353 #[test]
354 fn test_precision_error_display() {
355 let error = MoneyError::PrecisionError {
356 currency: "USD",
357 expected: 2,
358 actual: 5,
359 suggestion: "Use normalize() or round()".to_string(),
360 };
361
362 assert_eq!(
363 error.to_string(),
364 "Precision error for USD: expected 2 decimal places, found 5"
365 );
366 }
367
368 #[test]
369 fn test_invalid_amount_display() {
370 let error = MoneyError::InvalidAmount {
371 reason: "Value is NaN".to_string(),
372 currency: Some("EUR"),
373 };
374
375 assert_eq!(error.to_string(), "Invalid amount for EUR: Value is NaN");
376 }
377
378 #[test]
379 fn test_parse_error_display() {
380 let error = MoneyError::ParseError {
381 input: "not a number".to_string(),
382 expected_currency: Some("USD"),
383 reason: "Contains non-numeric characters".to_string(),
384 };
385
386 assert_eq!(
387 error.to_string(),
388 "Failed to parse 'not a number' as USD: Contains non-numeric characters"
389 );
390 }
391
392 #[test]
393 fn test_invalid_rate_display() {
394 let error = MoneyError::InvalidRate {
395 value: "0.0".to_string(),
396 reason: "Rate must be positive".to_string(),
397 };
398
399 assert_eq!(
400 error.to_string(),
401 "Invalid exchange rate '0.0': Rate must be positive"
402 );
403 }
404
405 #[test]
406 fn test_overflow_display() {
407 let error = MoneyError::Overflow {
408 operation: "multiplication".to_string(),
409 currency: "BTC",
410 };
411
412 assert_eq!(
413 error.to_string(),
414 "Arithmetic overflow in multiplication operation for BTC"
415 );
416 }
417
418 #[test]
419 fn test_suggestion() {
420 let error = MoneyError::CurrencyMismatch {
421 expected: "USD",
422 found: "EUR",
423 context: "test".to_string(),
424 };
425
426 assert!(error.suggestion().contains("same currency"));
427 }
428
429 #[test]
430 fn test_currency_extraction() {
431 let error = MoneyError::PrecisionError {
432 currency: "USD",
433 expected: 2,
434 actual: 5,
435 suggestion: "test".to_string(),
436 };
437
438 assert_eq!(error.currency(), Some("USD"));
439 }
440
441 #[test]
442 fn test_error_trait_implementation() {
443 let error = MoneyError::InvalidAmount {
444 reason: "test".to_string(),
445 currency: None,
446 };
447
448 let _: &dyn std::error::Error = &error;
450 }
451
452 #[test]
453 fn test_money_result_alias() {
454 fn example() -> MoneyResult<i32> {
455 Ok(42)
456 }
457
458 assert_eq!(example().unwrap(), 42);
459 }
460
461 #[test]
462 fn test_error_clone() {
463 let error = MoneyError::InvalidRate {
464 value: "0".to_string(),
465 reason: "test".to_string(),
466 };
467
468 let cloned = error.clone();
469 assert_eq!(error, cloned);
470 }
471
472 #[test]
473 fn test_error_debug() {
474 let error = MoneyError::InvalidAmount {
475 reason: "test".to_string(),
476 currency: Some("USD"),
477 };
478
479 let debug_str = format!("{:?}", error);
480 assert!(debug_str.contains("InvalidAmount"));
481 }
482}