1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_amount::{Amount, AmountError};
8use use_currency::CurrencyCode;
9
10pub mod prelude {
12 pub use crate::{CurrencyMismatch, Money, MoneyError};
13}
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Money {
18 amount: Amount,
19 currency: CurrencyCode,
20}
21
22impl Money {
23 #[must_use]
25 pub const fn new(amount: Amount, currency: CurrencyCode) -> Self {
26 Self { amount, currency }
27 }
28
29 #[must_use]
31 pub const fn amount(&self) -> Amount {
32 self.amount
33 }
34
35 #[must_use]
37 pub const fn currency(&self) -> &CurrencyCode {
38 &self.currency
39 }
40
41 #[must_use]
43 pub const fn is_zero(&self) -> bool {
44 self.amount.is_zero()
45 }
46
47 pub fn checked_add(&self, other: &Self) -> Result<Self, MoneyError> {
54 self.ensure_same_currency(other)?;
55 Ok(Self::new(
56 self.amount
57 .checked_add(other.amount)
58 .map_err(MoneyError::Amount)?,
59 self.currency.clone(),
60 ))
61 }
62
63 pub fn checked_sub(&self, other: &Self) -> Result<Self, MoneyError> {
70 self.ensure_same_currency(other)?;
71 Ok(Self::new(
72 self.amount
73 .checked_sub(other.amount)
74 .map_err(MoneyError::Amount)?,
75 self.currency.clone(),
76 ))
77 }
78
79 fn ensure_same_currency(&self, other: &Self) -> Result<(), MoneyError> {
80 if self.currency == other.currency {
81 Ok(())
82 } else {
83 Err(MoneyError::CurrencyMismatch(CurrencyMismatch {
84 expected: self.currency.clone(),
85 actual: other.currency.clone(),
86 }))
87 }
88 }
89}
90
91impl fmt::Display for Money {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(formatter, "{} {}", self.amount, self.currency)
94 }
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct CurrencyMismatch {
100 expected: CurrencyCode,
101 actual: CurrencyCode,
102}
103
104impl CurrencyMismatch {
105 #[must_use]
107 pub const fn expected(&self) -> &CurrencyCode {
108 &self.expected
109 }
110
111 #[must_use]
113 pub const fn actual(&self) -> &CurrencyCode {
114 &self.actual
115 }
116}
117
118impl fmt::Display for CurrencyMismatch {
119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(
121 formatter,
122 "currency mismatch: expected {}, got {}",
123 self.expected, self.actual
124 )
125 }
126}
127
128impl Error for CurrencyMismatch {}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum MoneyError {
133 CurrencyMismatch(CurrencyMismatch),
135 Amount(AmountError),
137}
138
139impl fmt::Display for MoneyError {
140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141 match self {
142 Self::CurrencyMismatch(error) => error.fmt(formatter),
143 Self::Amount(error) => error.fmt(formatter),
144 }
145 }
146}
147
148impl Error for MoneyError {
149 fn source(&self) -> Option<&(dyn Error + 'static)> {
150 match self {
151 Self::CurrencyMismatch(error) => Some(error),
152 Self::Amount(error) => Some(error),
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use use_amount::Amount;
160 use use_currency::CurrencyCode;
161
162 use super::{Money, MoneyError};
163
164 #[test]
165 fn adds_and_subtracts_same_currency_money() -> Result<(), Box<dyn std::error::Error>> {
166 let usd = CurrencyCode::new("USD")?;
167 let left = Money::new(Amount::from_minor_units(10_000, 2)?, usd.clone());
168 let right = Money::new(Amount::from_minor_units(2_500, 2)?, usd);
169
170 assert_eq!(left.checked_add(&right)?.amount().minor_units(), 12_500);
171 assert_eq!(left.checked_sub(&right)?.amount().minor_units(), 7_500);
172 assert_eq!(left.to_string(), "100.00 USD");
173 Ok(())
174 }
175
176 #[test]
177 fn rejects_currency_mismatch() -> Result<(), Box<dyn std::error::Error>> {
178 let usd = Money::new(Amount::from_minor_units(100, 2)?, CurrencyCode::new("USD")?);
179 let eur = Money::new(Amount::from_minor_units(100, 2)?, CurrencyCode::new("EUR")?);
180
181 let error = usd.checked_add(&eur).expect_err("currencies should differ");
182 assert!(matches!(error, MoneyError::CurrencyMismatch(_)));
183 Ok(())
184 }
185}