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 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)]
|
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug)]
|
||||||
pub struct Euros {
|
pub struct Euros {
|
||||||
|
|
@ -12,6 +21,18 @@ impl Euros {
|
||||||
Euros { value_as_cents }
|
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 {
|
pub const fn as_cents(self) -> i64 {
|
||||||
self.value_as_cents
|
self.value_as_cents
|
||||||
}
|
}
|
||||||
|
|
@ -106,4 +127,37 @@ mod tests {
|
||||||
let euros = Euros::from_cents_as_i64(-123_00);
|
let euros = Euros::from_cents_as_i64(-123_00);
|
||||||
assert_eq!(format!("{}", euros), "-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