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#[derive(Debug, Default, PartialEq, Eq, Clone)]
19pub struct Amount<'ctx> {
20 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 #[inline(always)]
111 pub fn zero() -> Self {
112 Self::default()
113 }
114
115 pub fn from_value(commodity: CommodityTag<'ctx>, amount: Decimal) -> Self {
117 Self::zero() + SingleAmount::from_value(commodity, amount)
118 }
119
120 pub fn from_values(values: BTreeMap<CommodityTag<'ctx>, Decimal>) -> Self {
122 Self { values }
123 }
124
125 pub fn into_values(self) -> BTreeMap<CommodityTag<'ctx>, Decimal> {
127 self.values
128 }
129
130 pub fn iter(&self) -> impl Iterator<Item = SingleAmount<'ctx>> + '_ {
132 AmountIter(self.values.iter())
133 }
134
135 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 pub fn is_absolute_zero(&self) -> bool {
151 self.values.is_empty()
152 }
153
154 pub fn is_zero(&self) -> bool {
156 self.values.iter().all(|(_, v)| v.is_zero())
157 }
158
159 pub fn remove_zero_entries(&mut self) {
163 self.values.retain(|_, v| !v.is_zero());
164 }
165
166 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 fn get_part(&self, commodity: CommodityTag<'ctx>) -> Decimal {
185 self.values.get(&commodity).copied().unwrap_or_default()
186 }
187
188 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 pub fn round(mut self, ctx: &ReportContext) -> Self {
203 self.round_mut(ctx);
204 self
205 }
206
207 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 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 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 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 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 }
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)), ]),
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}