Add Euros::from_euros_as_decimal with a typed error
Converting a Decimal amount into integer cents can fail two ways: the value carries sub-cent precision, or it does not fit in an i64 once scaled. Both are reported through EurosError rather than silently truncating, since a money type that rounds without being asked is the wrong kind of convenient. The scale check demands exactly 2 rather than at most 2, so callers have to be explicit about the precision they are handing over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0cf311bdf4
commit
3ab4a4064a
1 changed files with 55 additions and 1 deletions
|
|
@ -1,6 +1,15 @@
|
|||
use std::fmt;
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Error, Clone)]
|
||||
pub enum EurosError {
|
||||
#[error("Can only create from decimal with scale = 2: {0}")]
|
||||
WrongDecimalScale(Decimal),
|
||||
#[error("Decimal cannot be represented as i64: {0}")]
|
||||
DecimalIsTooBig(Decimal),
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug)]
|
||||
pub struct Euros {
|
||||
|
|
@ -12,6 +21,18 @@ impl Euros {
|
|||
Euros { value_as_cents }
|
||||
}
|
||||
|
||||
pub fn from_euros_as_decimal(value_as_euros: Decimal) -> Result<Self, EurosError> {
|
||||
// Euros should explicitely have scale 2 to validate cents precision
|
||||
if value_as_euros.scale() != 2 {
|
||||
Err(EurosError::WrongDecimalScale(value_as_euros))
|
||||
} else {
|
||||
(value_as_euros * Decimal::ONE_HUNDRED)
|
||||
.to_i64()
|
||||
.map(Euros::from_cents_as_i64)
|
||||
.ok_or(EurosError::DecimalIsTooBig(value_as_euros))
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_cents(self) -> i64 {
|
||||
self.value_as_cents
|
||||
}
|
||||
|
|
@ -106,4 +127,37 @@ mod tests {
|
|||
let euros = Euros::from_cents_as_i64(-123_00);
|
||||
assert_eq!(format!("{}", euros), "-123.00 €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_euros_as_decimal_fails_if_scale_is_larger_than_2() {
|
||||
let dec = Decimal::new(3141, 3);
|
||||
let ret = Euros::from_euros_as_decimal(dec);
|
||||
assert!(ret.is_err());
|
||||
assert_eq!(ret.unwrap_err(), EurosError::WrongDecimalScale(dec));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_euros_as_decimal_fails_if_scale_is_lower_than_2() {
|
||||
let dec = Decimal::new(3141, 1);
|
||||
let ret = Euros::from_euros_as_decimal(dec);
|
||||
assert!(ret.is_err());
|
||||
assert_eq!(ret.unwrap_err(), EurosError::WrongDecimalScale(dec));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_euros_as_decimal_yields_correct_result_if_scale_is_2() {
|
||||
let dec = Decimal::new(3141, 2);
|
||||
let ret = Euros::from_euros_as_decimal(dec);
|
||||
assert!(ret.is_ok());
|
||||
assert_eq!(ret.unwrap().as_cents(), 3141);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_euros_as_decimal_yields_error_with_decimal_too_big() {
|
||||
let mut dec = Decimal::from(u64::MAX);
|
||||
dec.rescale(2);
|
||||
let ret = Euros::from_euros_as_decimal(dec);
|
||||
assert!(ret.is_err());
|
||||
assert_eq!(ret.unwrap_err(), EurosError::DecimalIsTooBig(dec));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue