mod code;
pub use code::{CurrencyCode, ParseCurrencyCodeError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CurrencyError {
Mismatch {
left: CurrencyCode,
right: CurrencyCode,
},
Overflow,
}
impl std::fmt::Display for CurrencyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CurrencyError::Mismatch { left, right } => {
write!(f, "currency mismatch: {left} vs {right}")
}
CurrencyError::Overflow => f.write_str("currency arithmetic overflow"),
}
}
}
impl std::error::Error for CurrencyError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Currency {
code: CurrencyCode,
amount_minor: i64,
}
impl Currency {
#[must_use]
pub const fn new(code: CurrencyCode, amount_minor: i64) -> Self {
Self { code, amount_minor }
}
#[must_use]
pub const fn zero(code: CurrencyCode) -> Self {
Self::new(code, 0)
}
#[must_use]
pub const fn code(self) -> CurrencyCode {
self.code
}
#[must_use]
pub const fn amount_minor(self) -> i64 {
self.amount_minor
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.amount_minor == 0
}
#[must_use]
pub const fn checked_abs(self) -> Option<Self> {
match self.amount_minor.checked_abs() {
Some(v) => Some(Self::new(self.code, v)),
None => None,
}
}
#[must_use]
pub const fn checked_negate(self) -> Option<Self> {
match self.amount_minor.checked_neg() {
Some(v) => Some(Self::new(self.code, v)),
None => None,
}
}
pub fn checked_add(self, other: Self) -> Result<Self, CurrencyError> {
if self.code != other.code {
return Err(CurrencyError::Mismatch {
left: self.code,
right: other.code,
});
}
self.amount_minor
.checked_add(other.amount_minor)
.map(|v| Self::new(self.code, v))
.ok_or(CurrencyError::Overflow)
}
pub fn partial_cmp_amount(self, other: Self) -> Result<std::cmp::Ordering, CurrencyError> {
if self.code != other.code {
return Err(CurrencyError::Mismatch {
left: self.code,
right: other.code,
});
}
Ok(self.amount_minor.cmp(&other.amount_minor))
}
pub fn from_major_units(code: CurrencyCode, major: i64) -> Result<Self, CurrencyError> {
let factor = 10_i64
.checked_pow(code.exponent())
.ok_or(CurrencyError::Overflow)?;
major
.checked_mul(factor)
.map(|minor| Self::new(code, minor))
.ok_or(CurrencyError::Overflow)
}
pub fn to_major_units(self) -> Result<i64, CurrencyError> {
let factor = 10_i64
.checked_pow(self.code.exponent())
.ok_or(CurrencyError::Overflow)?;
if factor == 0 {
return Err(CurrencyError::Overflow);
}
Ok(self.amount_minor / factor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_shape() {
let c = Currency::new(CurrencyCode::Usd, 12345);
let v = serde_json::to_value(c).unwrap();
assert_eq!(
v,
serde_json::json!({ "code": "USD", "amount_minor": 12345 })
);
let back: Currency = serde_json::from_value(v).unwrap();
assert_eq!(back, c);
}
#[test]
fn rejects_unknown_code_on_deserialize() {
let v = serde_json::json!({ "code": "ZZZ", "amount_minor": 1 });
assert!(serde_json::from_value::<Currency>(v).is_err());
}
#[test]
fn add_mismatch_and_major() {
let a = Currency::new(CurrencyCode::Usd, 100);
let b = Currency::new(CurrencyCode::Eur, 100);
assert!(matches!(
a.checked_add(b),
Err(CurrencyError::Mismatch { .. })
));
let from_major = Currency::from_major_units(CurrencyCode::Usd, 12).unwrap();
assert_eq!(from_major.amount_minor(), 1200);
assert_eq!(from_major.to_major_units().unwrap(), 12);
let jpy = Currency::from_major_units(CurrencyCode::Jpy, 100).unwrap();
assert_eq!(jpy.amount_minor(), 100);
}
}