Skip to main content

okane_core/report/eval/
amount.rs

1use std::{
2    collections::{btree_map, BTreeMap},
3    fmt::Display,
4    iter::FusedIterator,
5    ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
6};
7
8use rust_decimal::Decimal;
9
10use crate::report::{
11    commodity::{CommodityStore, CommodityTag},
12    context::ReportContext,
13};
14
15use super::{error::EvalError, PostingAmount, SingleAmount};
16
17/// Amount with multiple commodities, or simple zero.
18#[derive(Debug, Default, PartialEq, Eq, Clone)]
19pub struct Amount<'ctx> {
20    // if values.len == zero, then it'll be completely zero.
21    // TODO: Consider optimizing for small number of commodities,
22    // as most of the case it needs to be just a few elements.
23    values: BTreeMap<CommodityTag<'ctx>, Decimal>,
24}
25
26impl<'ctx> TryFrom<Amount<'ctx>> for SingleAmount<'ctx> {
27    type Error = EvalError<'ctx>;
28
29    fn try_from(value: Amount<'ctx>) -> Result<Self, Self::Error> {
30        SingleAmount::try_from(&value)
31    }
32}
33
34impl<'ctx> TryFrom<Amount<'ctx>> for PostingAmount<'ctx> {
35    type Error = EvalError<'ctx>;
36
37    fn try_from(value: Amount<'ctx>) -> Result<Self, Self::Error> {
38        PostingAmount::try_from(&value)
39    }
40}
41
42impl<'ctx> TryFrom<&Amount<'ctx>> for SingleAmount<'ctx> {
43    type Error = EvalError<'ctx>;
44
45    fn try_from(value: &Amount<'ctx>) -> Result<Self, Self::Error> {
46        let (commodity, value) = value
47            .values
48            .iter()
49            .next()
50            .ok_or(EvalError::SingleAmountRequired)?;
51        Ok(SingleAmount {
52            value: *value,
53            commodity: *commodity,
54        })
55    }
56}
57
58impl<'ctx> TryFrom<&Amount<'ctx>> for PostingAmount<'ctx> {
59    type Error = EvalError<'ctx>;
60
61    fn try_from(value: &Amount<'ctx>) -> Result<Self, Self::Error> {
62        if value.values.len() > 1 {
63            Err(EvalError::PostingAmountRequired)
64        } else {
65            Ok(value
66                .values
67                .iter()
68                .next()
69                .map(|(commodity, value)| {
70                    PostingAmount::Single(SingleAmount {
71                        value: *value,
72                        commodity: *commodity,
73                    })
74                })
75                .unwrap_or_default())
76        }
77    }
78}
79
80impl<'ctx> From<PostingAmount<'ctx>> for Amount<'ctx> {
81    fn from(value: PostingAmount<'ctx>) -> Self {
82        match value {
83            PostingAmount::Zero => Amount::zero(),
84            PostingAmount::Single(single_amount) => single_amount.into(),
85        }
86    }
87}
88
89impl<'ctx> From<SingleAmount<'ctx>> for Amount<'ctx> {
90    fn from(value: SingleAmount<'ctx>) -> Self {
91        Amount::from_value(value.commodity, value.value)
92    }
93}
94
95impl<'ctx> FromIterator<(CommodityTag<'ctx>, Decimal)> for Amount<'ctx> {
96    fn from_iter<T>(iter: T) -> Self
97    where
98        T: IntoIterator<Item = (CommodityTag<'ctx>, Decimal)>,
99    {
100        let mut ret = Self::zero();
101        for (commodity, value) in iter.into_iter() {
102            ret += SingleAmount::from_value(commodity, value);
103        }
104        ret
105    }
106}
107
108impl<'ctx> Amount<'ctx> {
109    /// Creates an [`Amount`] with zero value.
110    #[inline(always)]
111    pub fn zero() -> Self {
112        Self::default()
113    }
114
115    /// Creates an [`Amount`] with single value and commodity.
116    pub fn from_value(commodity: CommodityTag<'ctx>, amount: Decimal) -> Self {
117        Self::zero() + SingleAmount::from_value(commodity, amount)
118    }
119
120    /// Creates an [`Amount`] from a set of values in [`BTreeMap`].
121    pub fn from_values(values: BTreeMap<CommodityTag<'ctx>, Decimal>) -> Self {
122        Self { values }
123    }
124
125    /// Takes out the instance and returns map from commodity to its value.
126    pub fn into_values(self) -> BTreeMap<CommodityTag<'ctx>, Decimal> {
127        self.values
128    }
129
130    /// Returns an iterator over its amount.
131    pub fn iter(&self) -> impl Iterator<Item = SingleAmount<'ctx>> + '_ {
132        AmountIter(self.values.iter())
133    }
134
135    /// Returns an object to print the amount as inline.
136    ///
137    /// The commodity is ordered by the appearing order, and deterministic.
138    pub fn as_inline_display<'a>(&'a self, ctx: &'a ReportContext<'ctx>) -> impl Display + 'a + 'ctx
139    where
140        'a: 'ctx,
141    {
142        InlinePrintAmount {
143            commodity_store: &ctx.commodities,
144            amount: self,
145        }
146    }
147
148    /// Returns `true` if this is 'non-commoditized zero', which is used to assert
149    /// the account balance is completely zero.
150    pub fn is_absolute_zero(&self) -> bool {
151        self.values.is_empty()
152    }
153
154    /// Returns `true` if this is zero, including zero commodities.
155    pub fn is_zero(&self) -> bool {
156        self.values.iter().all(|(_, v)| v.is_zero())
157    }
158
159    /// Removes zero values, useful when callers doesn't care zero value.
160    /// However, if caller must distinguish `0` and `0 commodity`,
161    /// caller must not use this method.
162    pub fn remove_zero_entries(&mut self) {
163        self.values.retain(|_, v| !v.is_zero());
164    }
165
166    /// Replace the amount of the particular commodity, and returns the previous amount for the commodity.
167    /// E.g. (100 USD + 100 EUR).set_partial(200, USD) returns 100.
168    /// Note this method removes the given commodity if value is zero,
169    /// so only meant for [`Balance`].
170    pub(crate) fn set_partial(&mut self, amount: SingleAmount<'ctx>) -> SingleAmount<'ctx> {
171        let value = if amount.value.is_zero() {
172            self.values.remove(&amount.commodity)
173        } else {
174            self.values.insert(amount.commodity, amount.value)
175        }
176        .unwrap_or_default();
177        SingleAmount {
178            value,
179            commodity: amount.commodity,
180        }
181    }
182
183    /// Returns the amount of the particular commodity.
184    fn get_part(&self, commodity: CommodityTag<'ctx>) -> Decimal {
185        self.values.get(&commodity).copied().unwrap_or_default()
186    }
187
188    /// Returns pair of commodity amount, if the amount contains exactly 2 commodities.
189    /// Otherwise returns None.
190    pub fn maybe_pair(&self) -> Option<(SingleAmount<'ctx>, SingleAmount<'ctx>)> {
191        if self.values.len() != 2 {
192            return None;
193        }
194        let ((c1, v1), (c2, v2)) = self.values.iter().zip(self.values.iter().skip(1)).next()?;
195        Some((
196            SingleAmount::from_value(*c1, *v1),
197            SingleAmount::from_value(*c2, *v2),
198        ))
199    }
200
201    /// Rounds the given Amount and returns the new instance.
202    pub fn round(mut self, ctx: &ReportContext) -> Self {
203        self.round_mut(ctx);
204        self
205    }
206
207    /// Rounds the Amount in-place with the given context provided precision.
208    pub fn round_mut(&mut self, ctx: &ReportContext) {
209        for (k, v) in self.values.iter_mut() {
210            match ctx.commodities.get_decimal_point(*k) {
211                None => (),
212                Some(dp) => {
213                    let updated = v.round_dp_with_strategy(
214                        dp,
215                        rust_decimal::RoundingStrategy::MidpointNearestEven,
216                    );
217                    *v = updated;
218                }
219            }
220        }
221    }
222
223    /// Creates negated instance.
224    pub fn negate(mut self) -> Self {
225        for (_, v) in self.values.iter_mut() {
226            v.set_sign_positive(!v.is_sign_positive())
227        }
228        self
229    }
230
231    /// Run division with error checking.
232    pub fn check_div(mut self, rhs: Decimal) -> Result<Self, EvalError<'ctx>> {
233        if rhs.is_zero() {
234            return Err(EvalError::DivideByZero);
235        }
236        for (_, v) in self.values.iter_mut() {
237            *v = v.checked_div(rhs).ok_or(EvalError::NumberOverflow)?;
238        }
239        Ok(self)
240    }
241
242    /// Checks if the amount is matching with the given [`PostingAmount`] balance,
243    /// Returns the diff (expected - actual), or None if those are consistent.
244    ///
245    /// Consistent means
246    ///
247    /// *   If the given balance is zero, then the amount must be zero.
248    /// *   If the given balance is a value with commodity,
249    ///     then the amount should be equal to given value only on the commodity.
250    pub(crate) fn assert_balance(&self, expected: &PostingAmount<'ctx>) -> Self {
251        match expected {
252            PostingAmount::Zero => {
253                if self.is_zero() {
254                    Self::zero()
255                } else {
256                    -self.clone()
257                }
258            }
259            PostingAmount::Single(single) => {
260                let diff = single.value - self.get_part(single.commodity);
261                if diff.is_zero() {
262                    Self::zero()
263                } else {
264                    Self::from_value(single.commodity, diff)
265                }
266            }
267        }
268    }
269}
270
271#[derive(Debug)]
272struct AmountIter<'a, 'ctx>(btree_map::Iter<'a, CommodityTag<'ctx>, Decimal>);
273
274impl<'ctx> Iterator for AmountIter<'_, 'ctx> {
275    type Item = SingleAmount<'ctx>;
276
277    fn next(&mut self) -> Option<Self::Item> {
278        self.0.next().map(|(c, v)| SingleAmount::from_value(*c, *v))
279    }
280}
281
282impl FusedIterator for AmountIter<'_, '_> {}
283
284#[derive(Debug)]
285struct InlinePrintAmount<'a, 'ctx> {
286    commodity_store: &'a CommodityStore<'ctx>,
287    amount: &'a Amount<'ctx>,
288}
289
290impl Display for InlinePrintAmount<'_, '_> {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        let vs = &self.amount.values;
293        if vs.len() <= 1 {
294            return match vs.iter().next() {
295                Some((c, v)) => {
296                    write!(f, "{} {}", v, c.to_str_lossy(self.commodity_store))
297                }
298                None => write!(f, "0"),
299            };
300        }
301        // wrap in () for 2 or more commodities case.
302        write!(f, "(")?;
303        for (i, (c, v)) in vs.iter().enumerate() {
304            let mut v = *v;
305            if i != 0 {
306                if v.is_sign_negative() {
307                    v.set_sign_negative(false);
308                    write!(f, " - ")?;
309                } else {
310                    write!(f, " + ")?;
311                }
312            }
313            write!(f, "{} {}", v, c.to_str_lossy(self.commodity_store))?;
314        }
315        write!(f, ")")
316    }
317}
318
319impl Neg for Amount<'_> {
320    type Output = Self;
321
322    fn neg(self) -> Self::Output {
323        self.negate()
324    }
325}
326
327impl<'ctx> Add<&'_ Amount<'ctx>> for Amount<'ctx> {
328    type Output = Self;
329
330    fn add(mut self, rhs: &Self) -> Self::Output {
331        self += rhs;
332        self
333    }
334}
335
336impl Add for Amount<'_> {
337    type Output = Self;
338
339    fn add(self, rhs: Self) -> Self::Output {
340        self + &rhs
341    }
342}
343
344impl<'ctx> AddAssign<&'_ Amount<'ctx>> for Amount<'ctx> {
345    fn add_assign(&mut self, rhs: &Amount<'ctx>) {
346        for (&c, &v2) in &rhs.values {
347            *self.values.entry(c).or_insert(Decimal::ZERO) += v2;
348            // we should retain the value even if zero,
349            // as (0 USD + 0 EUR) are different from 0 or (0 USD + 0 USD).
350        }
351    }
352}
353
354impl AddAssign for Amount<'_> {
355    fn add_assign(&mut self, rhs: Self) {
356        *self += &rhs;
357    }
358}
359
360impl<'ctx> Add<SingleAmount<'ctx>> for Amount<'ctx> {
361    type Output = Amount<'ctx>;
362
363    fn add(mut self, rhs: SingleAmount<'ctx>) -> Self::Output {
364        self += rhs;
365        self
366    }
367}
368
369impl<'ctx> AddAssign<SingleAmount<'ctx>> for Amount<'ctx> {
370    fn add_assign(&mut self, rhs: SingleAmount<'ctx>) {
371        let curr = self.values.entry(rhs.commodity).or_default();
372        *curr += rhs.value;
373    }
374}
375
376impl<'ctx> AddAssign<PostingAmount<'ctx>> for Amount<'ctx> {
377    fn add_assign(&mut self, rhs: PostingAmount<'ctx>) {
378        match rhs {
379            PostingAmount::Zero => (),
380            PostingAmount::Single(single) => *self += single,
381        }
382    }
383}
384
385impl<'ctx> Sub<&'_ Amount<'ctx>> for Amount<'ctx> {
386    type Output = Self;
387
388    fn sub(mut self, rhs: &Self) -> Self::Output {
389        self -= rhs;
390        self
391    }
392}
393
394impl Sub for Amount<'_> {
395    type Output = Self;
396
397    fn sub(self, rhs: Self) -> Self::Output {
398        self - &rhs
399    }
400}
401
402impl<'ctx> SubAssign<&'_ Amount<'ctx>> for Amount<'ctx> {
403    fn sub_assign(&mut self, rhs: &Amount<'ctx>) {
404        for (&c, &v2) in &rhs.values {
405            *self.values.entry(c).or_insert(Decimal::ZERO) -= v2;
406        }
407    }
408}
409
410impl SubAssign for Amount<'_> {
411    fn sub_assign(&mut self, rhs: Self) {
412        *self -= &rhs;
413    }
414}
415
416impl Mul<Decimal> for Amount<'_> {
417    type Output = Self;
418
419    fn mul(mut self, rhs: Decimal) -> Self::Output {
420        self *= rhs;
421        self
422    }
423}
424
425impl MulAssign<Decimal> for Amount<'_> {
426    fn mul_assign(&mut self, rhs: Decimal) {
427        for (_, mut v) in self.values.iter_mut() {
428            v *= rhs;
429        }
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    use bumpalo::Bump;
438    use maplit::btreemap;
439    use pretty_assertions::assert_eq;
440    use pretty_decimal::PrettyDecimal;
441    use rust_decimal_macros::dec;
442
443    use crate::report::ReportContext;
444
445    #[test]
446    fn test_default() {
447        let arena = Bump::new();
448        let ctx = ReportContext::new(&arena);
449        let amount = Amount::default();
450        assert_eq!(format!("{}", amount.as_inline_display(&ctx)), "0")
451    }
452
453    #[test]
454    fn test_from_value() {
455        let arena = Bump::new();
456        let mut ctx = ReportContext::new(&arena);
457        let jpy = ctx.commodities.ensure("JPY");
458        let amount = Amount::from_value(jpy, dec!(123.45));
459        assert_eq!(format!("{}", amount.as_inline_display(&ctx)), "123.45 JPY")
460    }
461
462    #[test]
463    fn test_from_values() {
464        let arena = Bump::new();
465        let mut ctx = ReportContext::new(&arena);
466        let jpy = ctx.commodities.ensure("JPY");
467        let chf = ctx.commodities.ensure("CHF");
468
469        let amount = Amount::from_iter([(jpy, dec!(10)), (chf, dec!(1))]);
470        assert_eq!(
471            amount.into_values(),
472            btreemap! {jpy => dec!(10), chf => dec!(1)},
473        );
474
475        let amount = Amount::from_iter([(jpy, dec!(10)), (jpy, dec!(1))]);
476        assert_eq!(amount.into_values(), btreemap! {jpy => dec!(11)});
477
478        let amount = Amount::from_iter([(jpy, dec!(10)), (jpy, dec!(-10))]);
479        assert_eq!(amount.into_values(), btreemap! {jpy => dec!(0)});
480    }
481
482    #[test]
483    fn test_is_absolute_zero() {
484        let arena = Bump::new();
485        let mut ctx = ReportContext::new(&arena);
486        let jpy = ctx.commodities.ensure("JPY");
487        let usd = ctx.commodities.ensure("USD");
488
489        assert!(Amount::default().is_absolute_zero());
490        assert!(!Amount::from_value(jpy, dec!(0)).is_absolute_zero());
491
492        let mut amount = Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0))]);
493        assert!(
494            !amount.is_absolute_zero(),
495            "{}",
496            amount.as_inline_display(&ctx)
497        );
498
499        amount.remove_zero_entries();
500        assert!(
501            amount.is_absolute_zero(),
502            "{}",
503            amount.as_inline_display(&ctx)
504        );
505    }
506
507    #[test]
508    fn test_is_zero() {
509        let arena = Bump::new();
510        let mut ctx = ReportContext::new(&arena);
511        let jpy = ctx.commodities.ensure("JPY");
512        let usd = ctx.commodities.ensure("USD");
513
514        assert!(Amount::default().is_zero());
515        assert!(Amount::from_value(jpy, dec!(0)).is_zero());
516        assert!(Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0))]).is_zero());
517
518        assert!(!Amount::from_value(jpy, dec!(1)).is_zero());
519        assert!(!Amount::from_iter([(jpy, dec!(0)), (usd, dec!(1))]).is_zero());
520    }
521
522    #[test]
523    fn test_neg() {
524        let arena = Bump::new();
525        let mut ctx = ReportContext::new(&arena);
526        let jpy = ctx.commodities.ensure("JPY");
527        let usd = ctx.commodities.ensure("USD");
528
529        assert_eq!(-Amount::zero(), Amount::zero());
530        assert_eq!(
531            -Amount::from_value(jpy, dec!(100)),
532            Amount::from_value(jpy, dec!(-100))
533        );
534        assert_eq!(
535            -Amount::from_iter([(jpy, dec!(100)), (usd, dec!(-20.35))]),
536            Amount::from_iter([(jpy, dec!(-100)), (usd, dec!(20.35))]),
537        );
538    }
539
540    #[test]
541    fn test_add_amount() {
542        let arena = Bump::new();
543        let mut ctx = ReportContext::new(&arena);
544        let jpy = ctx.commodities.ensure("JPY");
545        let usd = ctx.commodities.ensure("USD");
546        let eur = ctx.commodities.ensure("EUR");
547        let chf = ctx.commodities.ensure("CHF");
548
549        let zero_plus_zero = Amount::zero() + Amount::zero();
550        assert_eq!(zero_plus_zero, Amount::zero());
551
552        assert_eq!(
553            Amount::from_value(jpy, dec!(1)) + Amount::zero(),
554            Amount::from_value(jpy, dec!(1)),
555        );
556        assert_eq!(
557            Amount::zero() + Amount::from_value(jpy, dec!(1)),
558            Amount::from_value(jpy, dec!(1)),
559        );
560        assert_eq!(
561            Amount::from_iter([
562                (jpy, dec!(123.00)),
563                (usd, dec!(456.0)),
564                (eur, dec!(7.89)),
565                (chf, dec!(0)), // 0 CHF retained
566            ]),
567            Amount::from_value(jpy, dec!(123.45))
568                + Amount::from_value(jpy, dec!(-0.45))
569                + Amount::from_value(usd, dec!(456))
570                + Amount::from_value(usd, dec!(0.0))
571                + -Amount::from_value(chf, dec!(100))
572                + Amount::from_value(eur, dec!(7.89))
573                + Amount::from_value(chf, dec!(100)),
574        );
575
576        assert_eq!(
577            Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0)), (chf, dec!(0))]),
578            Amount::from_iter([(jpy, dec!(1)), (usd, dec!(2)), (chf, dec!(3))])
579                + Amount::from_iter([(jpy, dec!(-1)), (usd, dec!(-2)), (chf, dec!(-3))])
580        );
581    }
582
583    #[test]
584    fn test_add_single_amount() {
585        let arena = Bump::new();
586        let mut ctx = ReportContext::new(&arena);
587        let jpy = ctx.commodities.ensure("JPY");
588        let usd = ctx.commodities.ensure("USD");
589
590        let amount = Amount::zero() + SingleAmount::from_value(usd, dec!(0));
591        assert_eq!(amount, Amount::from_value(usd, dec!(0)));
592
593        assert_eq!(
594            Amount::zero() + SingleAmount::from_value(jpy, dec!(1)),
595            Amount::from_value(jpy, dec!(1)),
596        );
597    }
598
599    #[test]
600    fn test_sub() {
601        let arena = Bump::new();
602        let mut ctx = ReportContext::new(&arena);
603        let jpy = ctx.commodities.ensure("JPY");
604        let usd = ctx.commodities.ensure("USD");
605        let eur = ctx.commodities.ensure("EUR");
606        let chf = ctx.commodities.ensure("CHF");
607
608        let zero_minus_zero = Amount::zero() - Amount::zero();
609        assert_eq!(zero_minus_zero, Amount::zero());
610
611        assert_eq!(
612            Amount::from_value(jpy, dec!(1)) - Amount::zero(),
613            Amount::from_value(jpy, dec!(1)),
614        );
615        assert_eq!(
616            Amount::zero() - Amount::from_value(jpy, dec!(1)),
617            Amount::from_value(jpy, dec!(-1)),
618        );
619        assert_eq!(
620            Amount::from_iter([
621                (jpy, dec!(12345)),
622                (eur, dec!(-200)),
623                (chf, dec!(13.3)),
624                (usd, dec!(0))
625            ]),
626            Amount::from_iter([(jpy, dec!(12345)), (usd, dec!(56.78))])
627                - Amount::from_iter([(usd, dec!(56.780)), (eur, dec!(200)), (chf, dec!(-13.3)),]),
628        );
629
630        let mut val = Amount::zero();
631
632        val -= Amount::from_value(jpy, dec!(1));
633        assert_eq!(val, Amount::from_value(jpy, dec!(-1)));
634
635        val -= &Amount::from_value(jpy, dec!(1));
636        assert_eq!(val, Amount::from_value(jpy, dec!(-2)));
637    }
638
639    fn eps() -> Decimal {
640        Decimal::try_from_i128_with_scale(1, 28).unwrap()
641    }
642
643    #[test]
644    fn test_mul() {
645        let arena = Bump::new();
646        let mut ctx = ReportContext::new(&arena);
647        let jpy = ctx.commodities.ensure("JPY");
648        let eur = ctx.commodities.ensure("EUR");
649        let chf = ctx.commodities.ensure("CHF");
650
651        assert_eq!(Amount::zero() * dec!(5), Amount::zero());
652        assert_eq!(
653            Amount::from_value(jpy, dec!(1)) * Decimal::ZERO,
654            Amount::from_value(jpy, dec!(0)),
655        );
656        assert_eq!(
657            Amount::from_value(jpy, dec!(123)) * dec!(3),
658            Amount::from_value(jpy, dec!(369)),
659        );
660        assert_eq!(
661            Amount::from_iter([(jpy, dec!(10081)), (eur, dec!(200)), (chf, dec!(-13.3))])
662                * dec!(-0.5),
663            Amount::from_iter([(jpy, dec!(-5040.5)), (eur, dec!(-100.0)), (chf, dec!(6.65))]),
664        );
665        assert_eq!(
666            Amount::from_value(jpy, eps()) * eps(),
667            Amount::from_value(jpy, dec!(0))
668        );
669    }
670
671    #[test]
672    fn test_check_div() {
673        let arena = Bump::new();
674        let mut ctx = ReportContext::new(&arena);
675        let jpy = ctx.commodities.ensure("JPY");
676        let eur = ctx.commodities.ensure("EUR");
677        let chf = ctx.commodities.ensure("CHF");
678
679        assert_eq!(Amount::zero().check_div(dec!(5)).unwrap(), Amount::zero());
680        assert_eq!(
681            Amount::zero().check_div(dec!(0)).unwrap_err(),
682            EvalError::DivideByZero
683        );
684
685        assert_eq!(
686            Amount::from_value(jpy, dec!(50))
687                .check_div(dec!(4))
688                .unwrap(),
689            Amount::from_value(jpy, dec!(12.5))
690        );
691
692        assert_eq!(
693            Amount::from_value(jpy, Decimal::MAX)
694                .check_div(eps())
695                .unwrap_err(),
696            EvalError::NumberOverflow
697        );
698
699        assert_eq!(
700            Amount::from_value(jpy, eps())
701                .check_div(Decimal::MAX)
702                .unwrap(),
703            Amount::from_value(jpy, dec!(0))
704        );
705
706        assert_eq!(
707            Amount::from_iter([(jpy, dec!(810)), (eur, dec!(-100.0)), (chf, dec!(6.66))])
708                .check_div(dec!(3))
709                .unwrap(),
710            Amount::from_iter([
711                (jpy, dec!(270)),
712                (eur, dec!(-33.333333333333333333333333333)),
713                (chf, dec!(2.22))
714            ]),
715        );
716    }
717
718    #[test]
719    fn test_round() {
720        let arena = Bump::new();
721        let mut ctx = ReportContext::new(&arena);
722        let jpy = ctx.commodities.ensure("JPY");
723        let eur = ctx.commodities.ensure("EUR");
724        let chf = ctx.commodities.ensure("CHF");
725
726        ctx.commodities
727            .set_format(jpy, PrettyDecimal::comma3dot(dec!(12345)));
728        ctx.commodities
729            .set_format(eur, PrettyDecimal::plain(dec!(123.45)));
730        ctx.commodities
731            .set_format(chf, PrettyDecimal::comma3dot(dec!(123.450)));
732
733        assert_eq!(Amount::zero(), Amount::zero().round(&ctx));
734
735        assert_eq!(
736            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.00)), (chf, dec!(6.660))]),
737            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.0)), (chf, dec!(6.66))])
738                .round(&ctx),
739        );
740
741        assert_eq!(
742            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.02)), (chf, dec!(6.666))]),
743            Amount::from_iter([
744                (jpy, dec!(812.5)),
745                (eur, dec!(-100.015)),
746                (chf, dec!(6.6665))
747            ])
748            .round(&ctx),
749        );
750    }
751
752    #[test]
753    fn test_to_string() {
754        let arena = Bump::new();
755        let mut ctx = ReportContext::new(&arena);
756        let jpy = ctx.commodities.ensure("JPY");
757        let chf = ctx.commodities.ensure("CHF");
758
759        assert_eq!("0", Amount::default().as_inline_display(&ctx).to_string());
760
761        assert_eq!(
762            "10 JPY",
763            Amount::from_value(jpy, dec!(10))
764                .as_inline_display(&ctx)
765                .to_string()
766        );
767
768        assert_eq!(
769            "(10 JPY + 1 CHF)",
770            Amount::from_iter([(jpy, dec!(10)), (chf, dec!(1))])
771                .as_inline_display(&ctx)
772                .to_string()
773        );
774
775        assert_eq!(
776            "(-10 JPY - 1 CHF)",
777            Amount::from_iter([(jpy, dec!(-10)), (chf, dec!(-1))])
778                .as_inline_display(&ctx)
779                .to_string()
780        );
781    }
782}