1use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
11
12use crate::intern::InternedStr;
13#[cfg(feature = "rkyv")]
14use crate::intern::{AsDecimal, AsInternedStr};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[cfg_attr(
35 feature = "rkyv",
36 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
37)]
38pub struct Amount {
39 #[cfg_attr(feature = "rkyv", rkyv(with = AsDecimal))]
41 pub number: Decimal,
42 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
44 pub currency: InternedStr,
45}
46
47impl Amount {
48 #[must_use]
50 pub fn new(number: Decimal, currency: impl Into<InternedStr>) -> Self {
51 Self {
52 number,
53 currency: currency.into(),
54 }
55 }
56
57 #[must_use]
59 pub fn zero(currency: impl Into<InternedStr>) -> Self {
60 Self {
61 number: Decimal::ZERO,
62 currency: currency.into(),
63 }
64 }
65
66 #[must_use]
68 pub const fn is_zero(&self) -> bool {
69 self.number.is_zero()
70 }
71
72 #[must_use]
74 pub const fn is_positive(&self) -> bool {
75 self.number.is_sign_positive() && !self.number.is_zero()
76 }
77
78 #[must_use]
80 pub const fn is_negative(&self) -> bool {
81 self.number.is_sign_negative()
82 }
83
84 #[must_use]
86 pub fn abs(&self) -> Self {
87 Self {
88 number: self.number.abs(),
89 currency: self.currency.clone(),
90 }
91 }
92
93 #[must_use]
95 pub const fn scale(&self) -> u32 {
96 self.number.scale()
97 }
98
99 #[must_use]
106 pub fn inferred_tolerance(&self) -> Decimal {
107 Decimal::new(5, self.number.scale() + 1)
109 }
110
111 #[must_use]
113 pub fn is_near_zero(&self, tolerance: Decimal) -> bool {
114 self.number.abs() <= tolerance
115 }
116
117 #[must_use]
121 pub fn is_near(&self, other: &Self, tolerance: Decimal) -> bool {
122 self.currency == other.currency && (self.number - other.number).abs() <= tolerance
123 }
124
125 #[must_use]
146 pub fn eq_with_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
147 self.is_near(other, tolerance)
148 }
149
150 #[must_use]
168 pub fn eq_auto_tolerance(&self, other: &Self) -> bool {
169 if self.currency != other.currency {
170 return false;
171 }
172 let tolerance = self.inferred_tolerance().max(other.inferred_tolerance());
173 (self.number - other.number).abs() <= tolerance
174 }
175
176 #[must_use]
178 pub fn round_dp(&self, dp: u32) -> Self {
179 Self {
180 number: self.number.round_dp(dp),
181 currency: self.currency.clone(),
182 }
183 }
184}
185
186impl fmt::Display for Amount {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "{} {}", self.number, self.currency)
189 }
190}
191
192impl Add for &Amount {
195 type Output = Amount;
196
197 fn add(self, other: &Amount) -> Amount {
198 debug_assert_eq!(
199 self.currency, other.currency,
200 "Cannot add amounts with different currencies"
201 );
202 Amount {
203 number: self.number + other.number,
204 currency: self.currency.clone(),
205 }
206 }
207}
208
209impl Sub for &Amount {
210 type Output = Amount;
211
212 fn sub(self, other: &Amount) -> Amount {
213 debug_assert_eq!(
214 self.currency, other.currency,
215 "Cannot subtract amounts with different currencies"
216 );
217 Amount {
218 number: self.number - other.number,
219 currency: self.currency.clone(),
220 }
221 }
222}
223
224impl Neg for &Amount {
225 type Output = Amount;
226
227 fn neg(self) -> Amount {
228 Amount {
229 number: -self.number,
230 currency: self.currency.clone(),
231 }
232 }
233}
234
235impl Add for Amount {
238 type Output = Self;
239
240 fn add(self, other: Self) -> Self {
241 &self + &other
242 }
243}
244
245impl Sub for Amount {
246 type Output = Self;
247
248 fn sub(self, other: Self) -> Self {
249 &self - &other
250 }
251}
252
253impl Neg for Amount {
254 type Output = Self;
255
256 fn neg(self) -> Self {
257 -&self
258 }
259}
260
261impl AddAssign<&Self> for Amount {
262 fn add_assign(&mut self, other: &Self) {
263 debug_assert_eq!(
264 self.currency, other.currency,
265 "Cannot add amounts with different currencies"
266 );
267 self.number += other.number;
268 }
269}
270
271impl SubAssign<&Self> for Amount {
272 fn sub_assign(&mut self, other: &Self) {
273 debug_assert_eq!(
274 self.currency, other.currency,
275 "Cannot subtract amounts with different currencies"
276 );
277 self.number -= other.number;
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[cfg_attr(
294 feature = "rkyv",
295 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
296)]
297pub enum IncompleteAmount {
298 Complete(Amount),
300 NumberOnly(#[cfg_attr(feature = "rkyv", rkyv(with = AsDecimal))] Decimal),
302 CurrencyOnly(#[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))] InternedStr),
304}
305
306impl IncompleteAmount {
307 #[must_use]
309 pub fn complete(number: Decimal, currency: impl Into<InternedStr>) -> Self {
310 Self::Complete(Amount::new(number, currency))
311 }
312
313 #[must_use]
315 pub const fn number_only(number: Decimal) -> Self {
316 Self::NumberOnly(number)
317 }
318
319 #[must_use]
321 pub fn currency_only(currency: impl Into<InternedStr>) -> Self {
322 Self::CurrencyOnly(currency.into())
323 }
324
325 #[must_use]
327 pub const fn number(&self) -> Option<Decimal> {
328 match self {
329 Self::Complete(a) => Some(a.number),
330 Self::NumberOnly(n) => Some(*n),
331 Self::CurrencyOnly(_) => None,
332 }
333 }
334
335 #[must_use]
337 pub fn currency(&self) -> Option<&str> {
338 match self {
339 Self::Complete(a) => Some(&a.currency),
340 Self::NumberOnly(_) => None,
341 Self::CurrencyOnly(c) => Some(c),
342 }
343 }
344
345 #[must_use]
347 pub const fn is_complete(&self) -> bool {
348 matches!(self, Self::Complete(_))
349 }
350
351 #[must_use]
353 pub const fn as_amount(&self) -> Option<&Amount> {
354 match self {
355 Self::Complete(a) => Some(a),
356 _ => None,
357 }
358 }
359
360 #[must_use]
362 pub fn into_amount(self) -> Option<Amount> {
363 match self {
364 Self::Complete(a) => Some(a),
365 _ => None,
366 }
367 }
368}
369
370impl From<Amount> for IncompleteAmount {
371 fn from(amount: Amount) -> Self {
372 Self::Complete(amount)
373 }
374}
375
376impl fmt::Display for IncompleteAmount {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 match self {
379 Self::Complete(a) => write!(f, "{a}"),
380 Self::NumberOnly(n) => write!(f, "{n}"),
381 Self::CurrencyOnly(c) => write!(f, "{c}"),
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use rust_decimal_macros::dec;
390
391 #[test]
392 fn test_new() {
393 let amount = Amount::new(dec!(100.00), "USD");
394 assert_eq!(amount.number, dec!(100.00));
395 assert_eq!(amount.currency, "USD");
396 }
397
398 #[test]
399 fn test_zero() {
400 let amount = Amount::zero("EUR");
401 assert!(amount.is_zero());
402 assert_eq!(amount.currency, "EUR");
403 }
404
405 #[test]
406 fn test_is_positive_negative() {
407 let pos = Amount::new(dec!(100), "USD");
408 let neg = Amount::new(dec!(-100), "USD");
409 let zero = Amount::zero("USD");
410
411 assert!(pos.is_positive());
412 assert!(!pos.is_negative());
413
414 assert!(!neg.is_positive());
415 assert!(neg.is_negative());
416
417 assert!(!zero.is_positive());
418 assert!(!zero.is_negative());
419 }
420
421 #[test]
422 fn test_add() {
423 let a = Amount::new(dec!(100.00), "USD");
424 let b = Amount::new(dec!(50.00), "USD");
425 let sum = &a + &b;
426 assert_eq!(sum.number, dec!(150.00));
427 assert_eq!(sum.currency, "USD");
428 }
429
430 #[test]
431 fn test_sub() {
432 let a = Amount::new(dec!(100.00), "USD");
433 let b = Amount::new(dec!(50.00), "USD");
434 let diff = &a - &b;
435 assert_eq!(diff.number, dec!(50.00));
436 }
437
438 #[test]
439 fn test_neg() {
440 let a = Amount::new(dec!(100.00), "USD");
441 let neg_a = -&a;
442 assert_eq!(neg_a.number, dec!(-100.00));
443 }
444
445 #[test]
446 fn test_add_assign() {
447 let mut a = Amount::new(dec!(100.00), "USD");
448 let b = Amount::new(dec!(50.00), "USD");
449 a += &b;
450 assert_eq!(a.number, dec!(150.00));
451 }
452
453 #[test]
454 fn test_inferred_tolerance() {
455 let a = Amount::new(dec!(100), "USD");
457 assert_eq!(a.inferred_tolerance(), dec!(0.5));
458
459 let b = Amount::new(dec!(100.00), "USD");
461 assert_eq!(b.inferred_tolerance(), dec!(0.005));
462
463 let c = Amount::new(dec!(100.000), "USD");
465 assert_eq!(c.inferred_tolerance(), dec!(0.0005));
466 }
467
468 #[test]
469 fn test_is_near_zero() {
470 let a = Amount::new(dec!(0.004), "USD");
471 assert!(a.is_near_zero(dec!(0.005)));
472 assert!(!a.is_near_zero(dec!(0.003)));
473 }
474
475 #[test]
476 fn test_is_near() {
477 let a = Amount::new(dec!(100.00), "USD");
478 let b = Amount::new(dec!(100.004), "USD");
479 assert!(a.is_near(&b, dec!(0.005)));
480 assert!(!a.is_near(&b, dec!(0.003)));
481
482 let c = Amount::new(dec!(100.00), "EUR");
484 assert!(!a.is_near(&c, dec!(1.0)));
485 }
486
487 #[test]
488 fn test_display() {
489 let a = Amount::new(dec!(1234.56), "USD");
490 assert_eq!(format!("{a}"), "1234.56 USD");
491 }
492
493 #[test]
494 fn test_abs() {
495 let neg = Amount::new(dec!(-100.00), "USD");
496 let abs = neg.abs();
497 assert_eq!(abs.number, dec!(100.00));
498 }
499
500 #[test]
501 fn test_eq_with_tolerance() {
502 let a = Amount::new(dec!(100.00), "USD");
503 let b = Amount::new(dec!(100.004), "USD");
504
505 assert!(a.eq_with_tolerance(&b, dec!(0.005)));
507 assert!(b.eq_with_tolerance(&a, dec!(0.005)));
508
509 assert!(!a.eq_with_tolerance(&b, dec!(0.003)));
511
512 let c = Amount::new(dec!(100.00), "EUR");
514 assert!(!a.eq_with_tolerance(&c, dec!(1.0)));
515
516 let d = Amount::new(dec!(100.00), "USD");
518 assert!(a.eq_with_tolerance(&d, dec!(0.0)));
519 }
520
521 #[test]
522 #[allow(clippy::many_single_char_names)]
523 fn test_eq_auto_tolerance() {
524 let a = Amount::new(dec!(100.00), "USD");
526 let b = Amount::new(dec!(100.004), "USD");
527
528 assert!(a.eq_auto_tolerance(&b));
530
531 let c = Amount::new(dec!(100.000), "USD");
533 let d = Amount::new(dec!(100.001), "USD");
534
535 assert!(!c.eq_auto_tolerance(&d));
537
538 let e = Amount::new(dec!(100.0004), "USD");
540 assert!(c.eq_auto_tolerance(&e)); let f = Amount::new(dec!(100.00), "EUR");
544 assert!(!a.eq_auto_tolerance(&f));
545 }
546}