Skip to main content

okane_core/report/eval/
amount.rs

1use std::{
2    collections::{BTreeMap, btree_map},
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::{PostingAmount, SingleAmount, error::EvalError};
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.scale(*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.values_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.values_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    #[inline]
346    fn add_assign(&mut self, rhs: &Amount<'ctx>) {
347        for (&c, &v2) in &rhs.values {
348            *self.values.entry(c).or_insert(Decimal::ZERO) += v2;
349            // we should retain the value even if zero,
350            // as (0 USD + 0 EUR) are different from 0 or (0 USD + 0 USD).
351        }
352    }
353}
354
355impl AddAssign for Amount<'_> {
356    fn add_assign(&mut self, rhs: Self) {
357        *self += &rhs;
358    }
359}
360
361impl<'ctx> Add<SingleAmount<'ctx>> for Amount<'ctx> {
362    type Output = Amount<'ctx>;
363
364    fn add(mut self, rhs: SingleAmount<'ctx>) -> Self::Output {
365        self += rhs;
366        self
367    }
368}
369
370impl<'ctx> AddAssign<SingleAmount<'ctx>> for Amount<'ctx> {
371    fn add_assign(&mut self, rhs: SingleAmount<'ctx>) {
372        let curr = self.values.entry(rhs.commodity).or_default();
373        *curr += rhs.value;
374    }
375}
376
377impl<'ctx> AddAssign<PostingAmount<'ctx>> for Amount<'ctx> {
378    fn add_assign(&mut self, rhs: PostingAmount<'ctx>) {
379        match rhs {
380            PostingAmount::Zero => (),
381            PostingAmount::Single(single) => *self += single,
382        }
383    }
384}
385
386impl<'ctx> Sub<&'_ Amount<'ctx>> for Amount<'ctx> {
387    type Output = Self;
388
389    fn sub(mut self, rhs: &Self) -> Self::Output {
390        self -= rhs;
391        self
392    }
393}
394
395impl Sub for Amount<'_> {
396    type Output = Self;
397
398    fn sub(self, rhs: Self) -> Self::Output {
399        self - &rhs
400    }
401}
402
403impl<'ctx> SubAssign<&'_ Amount<'ctx>> for Amount<'ctx> {
404    fn sub_assign(&mut self, rhs: &Amount<'ctx>) {
405        for (&c, &v2) in &rhs.values {
406            *self.values.entry(c).or_insert(Decimal::ZERO) -= v2;
407        }
408    }
409}
410
411impl SubAssign for Amount<'_> {
412    fn sub_assign(&mut self, rhs: Self) {
413        *self -= &rhs;
414    }
415}
416
417impl Mul<Decimal> for Amount<'_> {
418    type Output = Self;
419
420    fn mul(mut self, rhs: Decimal) -> Self::Output {
421        self *= rhs;
422        self
423    }
424}
425
426impl MulAssign<Decimal> for Amount<'_> {
427    fn mul_assign(&mut self, rhs: Decimal) {
428        for mut v in self.values.values_mut() {
429            v *= rhs;
430        }
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    use bumpalo::Bump;
439    use maplit::btreemap;
440    use pretty_assertions::assert_eq;
441    use pretty_decimal::PrettyDecimal;
442    use rust_decimal_macros::dec;
443
444    use crate::report::ReportContext;
445
446    #[test]
447    fn test_default() {
448        let arena = Bump::new();
449        let ctx = ReportContext::new(&arena);
450        let amount = Amount::default();
451        assert_eq!(format!("{}", amount.as_inline_display(&ctx)), "0")
452    }
453
454    #[test]
455    fn test_from_value() {
456        let arena = Bump::new();
457        let mut ctx = ReportContext::new(&arena);
458        let jpy = ctx.commodities.ensure("JPY");
459        let amount = Amount::from_value(jpy, dec!(123.45));
460        assert_eq!(format!("{}", amount.as_inline_display(&ctx)), "123.45 JPY")
461    }
462
463    #[test]
464    fn test_from_values() {
465        let arena = Bump::new();
466        let mut ctx = ReportContext::new(&arena);
467        let jpy = ctx.commodities.ensure("JPY");
468        let chf = ctx.commodities.ensure("CHF");
469
470        let amount = Amount::from_iter([(jpy, dec!(10)), (chf, dec!(1))]);
471        assert_eq!(
472            amount.into_values(),
473            btreemap! {jpy => dec!(10), chf => dec!(1)},
474        );
475
476        let amount = Amount::from_iter([(jpy, dec!(10)), (jpy, dec!(1))]);
477        assert_eq!(amount.into_values(), btreemap! {jpy => dec!(11)});
478
479        let amount = Amount::from_iter([(jpy, dec!(10)), (jpy, dec!(-10))]);
480        assert_eq!(amount.into_values(), btreemap! {jpy => dec!(0)});
481    }
482
483    #[test]
484    fn test_is_absolute_zero() {
485        let arena = Bump::new();
486        let mut ctx = ReportContext::new(&arena);
487        let jpy = ctx.commodities.ensure("JPY");
488        let usd = ctx.commodities.ensure("USD");
489
490        assert!(Amount::default().is_absolute_zero());
491        assert!(!Amount::from_value(jpy, dec!(0)).is_absolute_zero());
492
493        let mut amount = Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0))]);
494        assert!(
495            !amount.is_absolute_zero(),
496            "{}",
497            amount.as_inline_display(&ctx)
498        );
499
500        amount.remove_zero_entries();
501        assert!(
502            amount.is_absolute_zero(),
503            "{}",
504            amount.as_inline_display(&ctx)
505        );
506    }
507
508    #[test]
509    fn test_is_zero() {
510        let arena = Bump::new();
511        let mut ctx = ReportContext::new(&arena);
512        let jpy = ctx.commodities.ensure("JPY");
513        let usd = ctx.commodities.ensure("USD");
514
515        assert!(Amount::default().is_zero());
516        assert!(Amount::from_value(jpy, dec!(0)).is_zero());
517        assert!(Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0))]).is_zero());
518
519        assert!(!Amount::from_value(jpy, dec!(1)).is_zero());
520        assert!(!Amount::from_iter([(jpy, dec!(0)), (usd, dec!(1))]).is_zero());
521    }
522
523    #[test]
524    fn test_neg() {
525        let arena = Bump::new();
526        let mut ctx = ReportContext::new(&arena);
527        let jpy = ctx.commodities.ensure("JPY");
528        let usd = ctx.commodities.ensure("USD");
529
530        assert_eq!(-Amount::zero(), Amount::zero());
531        assert_eq!(
532            -Amount::from_value(jpy, dec!(100)),
533            Amount::from_value(jpy, dec!(-100))
534        );
535        assert_eq!(
536            -Amount::from_iter([(jpy, dec!(100)), (usd, dec!(-20.35))]),
537            Amount::from_iter([(jpy, dec!(-100)), (usd, dec!(20.35))]),
538        );
539    }
540
541    #[test]
542    fn test_add_amount() {
543        let arena = Bump::new();
544        let mut ctx = ReportContext::new(&arena);
545        let jpy = ctx.commodities.ensure("JPY");
546        let usd = ctx.commodities.ensure("USD");
547        let eur = ctx.commodities.ensure("EUR");
548        let chf = ctx.commodities.ensure("CHF");
549
550        let zero_plus_zero = Amount::zero() + Amount::zero();
551        assert_eq!(zero_plus_zero, Amount::zero());
552
553        assert_eq!(
554            Amount::from_value(jpy, dec!(1)) + Amount::zero(),
555            Amount::from_value(jpy, dec!(1)),
556        );
557        assert_eq!(
558            Amount::zero() + Amount::from_value(jpy, dec!(1)),
559            Amount::from_value(jpy, dec!(1)),
560        );
561        assert_eq!(
562            Amount::from_iter([
563                (jpy, dec!(123.00)),
564                (usd, dec!(456.0)),
565                (eur, dec!(7.89)),
566                (chf, dec!(0)), // 0 CHF retained
567            ]),
568            Amount::from_value(jpy, dec!(123.45))
569                + Amount::from_value(jpy, dec!(-0.45))
570                + Amount::from_value(usd, dec!(456))
571                + Amount::from_value(usd, dec!(0.0))
572                + -Amount::from_value(chf, dec!(100))
573                + Amount::from_value(eur, dec!(7.89))
574                + Amount::from_value(chf, dec!(100)),
575        );
576
577        assert_eq!(
578            Amount::from_iter([(jpy, dec!(0)), (usd, dec!(0)), (chf, dec!(0))]),
579            Amount::from_iter([(jpy, dec!(1)), (usd, dec!(2)), (chf, dec!(3))])
580                + Amount::from_iter([(jpy, dec!(-1)), (usd, dec!(-2)), (chf, dec!(-3))])
581        );
582    }
583
584    #[test]
585    fn test_add_single_amount() {
586        let arena = Bump::new();
587        let mut ctx = ReportContext::new(&arena);
588        let jpy = ctx.commodities.ensure("JPY");
589        let usd = ctx.commodities.ensure("USD");
590
591        let amount = Amount::zero() + SingleAmount::from_value(usd, dec!(0));
592        assert_eq!(amount, Amount::from_value(usd, dec!(0)));
593
594        assert_eq!(
595            Amount::zero() + SingleAmount::from_value(jpy, dec!(1)),
596            Amount::from_value(jpy, dec!(1)),
597        );
598    }
599
600    #[test]
601    fn test_sub() {
602        let arena = Bump::new();
603        let mut ctx = ReportContext::new(&arena);
604        let jpy = ctx.commodities.ensure("JPY");
605        let usd = ctx.commodities.ensure("USD");
606        let eur = ctx.commodities.ensure("EUR");
607        let chf = ctx.commodities.ensure("CHF");
608
609        let zero_minus_zero = Amount::zero() - Amount::zero();
610        assert_eq!(zero_minus_zero, Amount::zero());
611
612        assert_eq!(
613            Amount::from_value(jpy, dec!(1)) - Amount::zero(),
614            Amount::from_value(jpy, dec!(1)),
615        );
616        assert_eq!(
617            Amount::zero() - Amount::from_value(jpy, dec!(1)),
618            Amount::from_value(jpy, dec!(-1)),
619        );
620        assert_eq!(
621            Amount::from_iter([
622                (jpy, dec!(12345)),
623                (eur, dec!(-200)),
624                (chf, dec!(13.3)),
625                (usd, dec!(0))
626            ]),
627            Amount::from_iter([(jpy, dec!(12345)), (usd, dec!(56.78))])
628                - Amount::from_iter([(usd, dec!(56.780)), (eur, dec!(200)), (chf, dec!(-13.3)),]),
629        );
630
631        let mut val = Amount::zero();
632
633        val -= Amount::from_value(jpy, dec!(1));
634        assert_eq!(val, Amount::from_value(jpy, dec!(-1)));
635
636        val -= &Amount::from_value(jpy, dec!(1));
637        assert_eq!(val, Amount::from_value(jpy, dec!(-2)));
638    }
639
640    fn eps() -> Decimal {
641        Decimal::try_from_i128_with_scale(1, 28).unwrap()
642    }
643
644    #[test]
645    fn test_mul() {
646        let arena = Bump::new();
647        let mut ctx = ReportContext::new(&arena);
648        let jpy = ctx.commodities.ensure("JPY");
649        let eur = ctx.commodities.ensure("EUR");
650        let chf = ctx.commodities.ensure("CHF");
651
652        assert_eq!(Amount::zero() * dec!(5), Amount::zero());
653        assert_eq!(
654            Amount::from_value(jpy, dec!(1)) * Decimal::ZERO,
655            Amount::from_value(jpy, dec!(0)),
656        );
657        assert_eq!(
658            Amount::from_value(jpy, dec!(123)) * dec!(3),
659            Amount::from_value(jpy, dec!(369)),
660        );
661        assert_eq!(
662            Amount::from_iter([(jpy, dec!(10081)), (eur, dec!(200)), (chf, dec!(-13.3))])
663                * dec!(-0.5),
664            Amount::from_iter([(jpy, dec!(-5040.5)), (eur, dec!(-100.0)), (chf, dec!(6.65))]),
665        );
666        assert_eq!(
667            Amount::from_value(jpy, eps()) * eps(),
668            Amount::from_value(jpy, dec!(0))
669        );
670    }
671
672    #[test]
673    fn test_check_div() {
674        let arena = Bump::new();
675        let mut ctx = ReportContext::new(&arena);
676        let jpy = ctx.commodities.ensure("JPY");
677        let eur = ctx.commodities.ensure("EUR");
678        let chf = ctx.commodities.ensure("CHF");
679
680        assert_eq!(Amount::zero().check_div(dec!(5)).unwrap(), Amount::zero());
681        assert_eq!(
682            Amount::zero().check_div(dec!(0)).unwrap_err(),
683            EvalError::DivideByZero
684        );
685
686        assert_eq!(
687            Amount::from_value(jpy, dec!(50))
688                .check_div(dec!(4))
689                .unwrap(),
690            Amount::from_value(jpy, dec!(12.5))
691        );
692
693        assert_eq!(
694            Amount::from_value(jpy, Decimal::MAX)
695                .check_div(eps())
696                .unwrap_err(),
697            EvalError::NumberOverflow
698        );
699
700        assert_eq!(
701            Amount::from_value(jpy, eps())
702                .check_div(Decimal::MAX)
703                .unwrap(),
704            Amount::from_value(jpy, dec!(0))
705        );
706
707        assert_eq!(
708            Amount::from_iter([(jpy, dec!(810)), (eur, dec!(-100.0)), (chf, dec!(6.66))])
709                .check_div(dec!(3))
710                .unwrap(),
711            Amount::from_iter([
712                (jpy, dec!(270)),
713                (eur, dec!(-33.333333333333333333333333333)),
714                (chf, dec!(2.22))
715            ]),
716        );
717    }
718
719    #[test]
720    fn test_round() {
721        let arena = Bump::new();
722        let mut ctx = ReportContext::new(&arena);
723        let jpy = ctx.commodities.ensure("JPY");
724        let eur = ctx.commodities.ensure("EUR");
725        let chf = ctx.commodities.ensure("CHF");
726
727        ctx.commodities
728            .set_format(jpy, PrettyDecimal::comma3dot(dec!(12345)));
729        ctx.commodities
730            .set_format(eur, PrettyDecimal::plain(dec!(123.45)));
731        ctx.commodities
732            .set_format(chf, PrettyDecimal::comma3dot(dec!(123.450)));
733
734        assert_eq!(Amount::zero(), Amount::zero().round(&ctx));
735
736        assert_eq!(
737            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.00)), (chf, dec!(6.660))]),
738            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.0)), (chf, dec!(6.66))])
739                .round(&ctx),
740        );
741
742        assert_eq!(
743            Amount::from_iter([(jpy, dec!(812)), (eur, dec!(-100.02)), (chf, dec!(6.666))]),
744            Amount::from_iter([
745                (jpy, dec!(812.5)),
746                (eur, dec!(-100.015)),
747                (chf, dec!(6.6665))
748            ])
749            .round(&ctx),
750        );
751    }
752
753    #[test]
754    fn test_to_string() {
755        let arena = Bump::new();
756        let mut ctx = ReportContext::new(&arena);
757        let jpy = ctx.commodities.ensure("JPY");
758        let chf = ctx.commodities.ensure("CHF");
759
760        assert_eq!("0", Amount::default().as_inline_display(&ctx).to_string());
761
762        assert_eq!(
763            "10 JPY",
764            Amount::from_value(jpy, dec!(10))
765                .as_inline_display(&ctx)
766                .to_string()
767        );
768
769        assert_eq!(
770            "(10 JPY + 1 CHF)",
771            Amount::from_iter([(jpy, dec!(10)), (chf, dec!(1))])
772                .as_inline_display(&ctx)
773                .to_string()
774        );
775
776        assert_eq!(
777            "(-10 JPY - 1 CHF)",
778            Amount::from_iter([(jpy, dec!(-10)), (chf, dec!(-1))])
779                .as_inline_display(&ctx)
780                .to_string()
781        );
782    }
783}