1use super::*;
4
5use std::collections::HashMap;
6
7use decoration::AsUndecorated;
8use expr::Visitable;
9
10use pretty_decimal::PrettyDecimal;
11use unicode_width::UnicodeWidthStr;
12
13#[derive(Debug, Default, Clone)]
15pub struct DisplayContext {
16 default_commodity: CommodityDisplayOption,
17 commodity_overrides: HashMap<String, CommodityDisplayOption>,
18}
19
20impl DisplayContext {
21 pub fn new(
23 default_commodity: CommodityDisplayOption,
24 commodity_overrides: HashMap<String, CommodityDisplayOption>,
25 ) -> Self {
26 Self {
27 default_commodity,
28 commodity_overrides,
29 }
30 }
31
32 pub fn as_display<'a, T>(&'a self, value: &'a T) -> WithContext<'a, T>
34 where
35 WithContext<'a, T>: fmt::Display,
36 {
37 WithContext {
38 value,
39 context: self,
40 }
41 }
42
43 pub fn decimal_format(&self, commodity: &str) -> Option<pretty_decimal::Format> {
45 self.commodity_overrides
46 .get(commodity)
47 .and_then(|o| o.format)
48 .or(self.default_commodity.format)
49 }
50
51 pub fn min_scale(&self, commodity: &str) -> Option<u8> {
53 self.commodity_overrides
54 .get(commodity)
55 .and_then(|o| o.min_scale)
56 .or(self.default_commodity.min_scale)
57 }
58}
59
60#[derive(Debug, Default, Clone)]
61pub struct CommodityDisplayOption {
62 pub format: Option<pretty_decimal::Format>,
63 pub min_scale: Option<u8>,
64}
65
66pub struct WithContext<'a, T> {
68 value: &'a T,
69 context: &'a DisplayContext,
70}
71
72impl<'a, T> WithContext<'a, T> {
73 fn pass_context<U>(&self, other: &'a U) -> WithContext<'a, U> {
74 WithContext {
75 value: other,
76 context: self.context,
77 }
78 }
79}
80
81impl<Deco: Decoration> fmt::Display for WithContext<'_, LedgerEntry<'_, Deco>> {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 if self.value.separation == Separation::BlankLine {
84 writeln!(f)?;
85 }
86 self.pass_context(&self.value.statement).fmt(f)
87 }
88}
89
90impl<Deco: Decoration> fmt::Display for WithContext<'_, LedgerStatement<'_, Deco>> {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 match &self.value {
93 LedgerStatement::Txn(txn) => self.pass_context(txn).fmt(f),
94 LedgerStatement::Comment(v) => v.fmt(f),
95 LedgerStatement::ApplyTag(v) => v.fmt(f),
96 LedgerStatement::EndApplyTag => writeln!(f, "end apply tag"),
97 LedgerStatement::Include(v) => v.fmt(f),
98 LedgerStatement::Account(v) => v.fmt(f),
99 LedgerStatement::Commodity(v) => self.pass_context(v).fmt(f),
100 }
101 }
102}
103
104#[derive(Debug)]
105struct LineWrapStr<'a> {
106 prefix: &'static str,
107 content: &'a str,
108}
109
110impl<'a> LineWrapStr<'a> {
111 fn wrap(prefix: &'static str, content: &'a str) -> Self {
112 Self { prefix, content }
113 }
114}
115
116impl fmt::Display for LineWrapStr<'_> {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 for line in self.content.lines() {
119 writeln!(f, "{}{}", self.prefix, line)?;
120 }
121 Ok(())
122 }
123}
124
125impl fmt::Display for TopLevelComment<'_> {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 LineWrapStr::wrap(";", &self.0).fmt(f)
128 }
129}
130
131impl fmt::Display for ApplyTag<'_> {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "apply tag {}", self.key)?;
134 match &self.value {
135 None => writeln!(f),
136 Some(v) => writeln!(f, "{}", v),
137 }
138 }
139}
140
141impl fmt::Display for IncludeFile<'_> {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 writeln!(f, "include {}", self.0)
144 }
145}
146
147impl fmt::Display for AccountDeclaration<'_> {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 writeln!(f, "account {}", self.name)?;
150 for detail in &self.details {
151 detail.fmt(f)?;
152 }
153 Ok(())
154 }
155}
156impl fmt::Display for AccountDetail<'_> {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 AccountDetail::Comment(v) => LineWrapStr::wrap(" ; ", v).fmt(f),
160 AccountDetail::Note(v) => LineWrapStr::wrap(" note ", v).fmt(f),
161 AccountDetail::Alias(v) => writeln!(f, " alias {}", v),
162 }
163 }
164}
165
166impl fmt::Display for WithContext<'_, CommodityDeclaration<'_>> {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 writeln!(f, "commodity {}", self.value.name)?;
169 for detail in &self.value.details {
170 self.pass_context(detail).fmt(f)?;
171 }
172 Ok(())
173 }
174}
175impl fmt::Display for WithContext<'_, CommodityDetail<'_>> {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self.value {
178 CommodityDetail::Comment(v) => LineWrapStr::wrap(" ; ", v).fmt(f),
179 CommodityDetail::Note(v) => LineWrapStr::wrap(" note ", v).fmt(f),
180 CommodityDetail::Alias(v) => writeln!(f, " alias {}", v),
181 CommodityDetail::Format(v) => writeln!(f, " format {}", self.pass_context(v)),
182 }
183 }
184}
185impl<Deco: Decoration> fmt::Display for WithContext<'_, Transaction<'_, Deco>> {
186 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
187 let xact = self.value;
188 write!(f, "{}", xact.date.format("%Y/%m/%d"))?;
189 if let Some(edate) = &xact.effective_date {
190 write!(f, "={}", edate.format("%Y/%m/%d"))?;
191 }
192 write!(f, " {}", print_clear_state(xact.clear_state))?;
193 if let Some(code) = &xact.code {
194 write!(f, "({}) ", code)?;
195 }
196 writeln!(f, "{}", xact.payee)?;
197 for m in &xact.metadata {
198 m.fmt(f)?;
199 }
200 for post in &xact.posts {
201 write!(f, "{}", self.context.as_display(post.as_undecorated()))?;
202 }
203 Ok(())
204 }
205}
206
207const METADATA_PREFIX: &str = " ; ";
208
209impl fmt::Display for Metadata<'_> {
210 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211 match self {
212 Metadata::WordTags(tags) => {
213 f.write_str(METADATA_PREFIX)?;
214 f.write_str(":")?;
215 for tag in tags {
216 write!(f, "{}:", tag)?;
217 }
218 f.write_str("\n")?
219 }
220 Metadata::KeyValueTag { key, value } => {
221 f.write_str(METADATA_PREFIX)?;
222 writeln!(f, "{}{}", key, value)?
223 }
224 Metadata::Comment(s) => LineWrapStr::wrap(METADATA_PREFIX, s).fmt(f)?,
225 };
226 Ok(())
227 }
228}
229
230impl fmt::Display for MetadataValue<'_> {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 MetadataValue::Expr(expr) => write!(f, ":: {}", expr),
234 MetadataValue::Text(text) => write!(f, ": {}", text),
235 }
236 }
237}
238
239impl<Deco: Decoration> fmt::Display for WithContext<'_, Posting<'_, Deco>> {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 let post = self.value;
242 let post_clear = print_clear_state(post.clear_state);
243 write!(f, " {}{}", post_clear, post.account.as_undecorated())?;
244 let account_width = UnicodeWidthStr::width_cjk(post.account.as_undecorated().as_ref())
245 + UnicodeWidthStr::width(post_clear);
246 if let Some(amount) = &post.amount {
247 let mut amount_str = String::new();
248 let alignment = self
249 .pass_context(amount.amount.as_undecorated())
250 .fmt_with_alignment(&mut amount_str)?
251 .absolute();
252 write!(
253 f,
254 "{:>width$}{}",
255 "",
256 amount_str.as_str(),
257 width = get_column(48, account_width + alignment, 2)
258 )?;
259 write!(f, "{}", self.pass_context(&amount.lot))?;
260 if let Some(exchange) = &amount.cost {
261 match exchange.as_undecorated() {
262 Exchange::Rate(v) => write!(f, " @ {}", self.pass_context(v)),
263 Exchange::Total(v) => write!(f, " @@ {}", self.pass_context(v)),
264 }?
265 }
266 }
267 if let Some(balance) = &post.balance {
268 let mut balance_str = String::new();
269 let alignment = self
270 .pass_context(balance.as_undecorated())
271 .fmt_with_alignment(&mut balance_str)?
272 .absolute();
273 let trailing = UnicodeWidthStr::width_cjk(balance_str.as_str()) - alignment;
274 let balance_padding = if post.amount.is_some() {
275 0
276 } else {
277 get_column(50 + trailing, account_width, 2)
278 };
279 write!(
280 f,
281 "{:>width$} {}",
282 " =",
283 self.pass_context(balance.as_undecorated()),
284 width = balance_padding
285 )?;
286 }
287 writeln!(f)?;
288 for m in &post.metadata {
289 m.fmt(f)?;
290 }
291 Ok(())
292 }
293}
294
295impl<Deco: Decoration> fmt::Display for WithContext<'_, Lot<'_, Deco>> {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 if let Some(price) = &self.value.price {
298 match price.as_undecorated() {
299 Exchange::Total(e) => write!(f, " {{{{{}}}}}", self.pass_context(e)),
300 Exchange::Rate(e) => write!(f, " {{{}}}", self.pass_context(e)),
301 }?;
302 }
303 if let Some(date) = &self.value.date {
304 write!(f, " [{}]", date.format("%Y/%m/%d"))?;
305 }
306 if let Some(note) = &self.value.note {
307 write!(f, " ({})", note)?;
308 }
309 Ok(())
310 }
311}
312
313#[derive(Debug, PartialEq, Clone, Copy)]
326enum Alignment {
327 Partial(usize),
330 Complete(usize),
333}
334
335impl Alignment {
336 fn absolute(self) -> usize {
338 match self {
339 Alignment::Complete(x) => x,
340 Alignment::Partial(x) => x,
341 }
342 }
343
344 fn plus(self, prefix_length: usize, suffix_length: usize) -> Alignment {
346 match self {
347 Alignment::Partial(x) => Alignment::Partial(prefix_length + x + suffix_length),
348 Alignment::Complete(x) => Alignment::Complete(prefix_length + x),
349 }
350 }
351}
352
353trait DisplayWithAlignment {
354 fn fmt_with_alignment<W: fmt::Write>(&self, f: &mut W) -> Result<Alignment, fmt::Error>;
355}
356
357impl<T> fmt::Display for WithContext<'_, T>
358where
359 Self: DisplayWithAlignment,
360{
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 self.fmt_with_alignment(f).map(|_| ())
363 }
364}
365
366struct ExprFormatter<'a, W> {
368 writer: &'a mut W,
369 context: &'a DisplayContext,
370}
371
372impl<'i, W: fmt::Write> expr::ExprVisitor<'i> for ExprFormatter<'_, W> {
373 type Output = Alignment;
374 type Error = fmt::Error;
375
376 fn visit_amount(&mut self, amount: &expr::Amount<'i>) -> Result<Alignment, fmt::Error> {
377 let amount_str = rescale(amount, self.context).to_string();
378 if amount.commodity.is_empty() {
380 write!(self.writer, "{}", amount_str)?;
381 return Ok(Alignment::Partial(amount_str.as_str().len()));
382 }
383 write!(self.writer, "{} {}", amount_str, amount.commodity)?;
384 Ok(Alignment::Complete(amount_str.as_str().len()))
386 }
387
388 fn visit_value_expr(&mut self, expr: &expr::ValueExpr<'i>) -> Result<Alignment, fmt::Error> {
389 match expr {
390 expr::ValueExpr::Amount(a) => self.visit_amount(a),
391 expr::ValueExpr::Paren(inner) => {
392 self.writer.write_char('(')?;
393 let alignment = self.visit_expr(inner)?;
394 self.writer.write_char(')')?;
395 Ok(alignment.plus(1, 1))
396 }
397 }
398 }
399
400 fn visit_unary(&mut self, expr: &expr::UnaryOpExpr<'i>) -> Result<Alignment, fmt::Error> {
401 write!(self.writer, "{}", expr.op)?;
402 self.visit_expr(&expr.expr).map(|x| x.plus(1, 0))
403 }
404
405 fn visit_binary(&mut self, expr: &expr::BinaryOpExpr<'i>) -> Result<Alignment, fmt::Error> {
406 let a1 = self.visit_expr(&expr.lhs)?;
407 write!(self.writer, " {} ", expr.op)?;
408 let a2 = self.visit_expr(&expr.rhs)?;
409 Ok(match a1.plus(0, 3) {
410 Alignment::Complete(x) => Alignment::Complete(x),
411 Alignment::Partial(x) => a2.plus(x, 0),
412 })
413 }
414}
415
416macro_rules! display_with_alignment_by_visitor {
417 ($t:ty) => {
418 impl DisplayWithAlignment for WithContext<'_, $t> {
419 fn fmt_with_alignment<W: fmt::Write>(
420 &self,
421 f: &mut W,
422 ) -> Result<Alignment, fmt::Error> {
423 self.value.accept(&mut ExprFormatter {
424 writer: f,
425 context: self.context,
426 })
427 }
428 }
429 };
430}
431
432display_with_alignment_by_visitor!(expr::ValueExpr<'_>);
433display_with_alignment_by_visitor!(expr::Amount<'_>);
434
435fn get_column(colsize: usize, left: usize, padding: usize) -> usize {
438 if left + padding < colsize {
439 colsize - left
440 } else {
441 padding
442 }
443}
444
445fn rescale(x: &expr::Amount, context: &DisplayContext) -> PrettyDecimal {
446 let mut v = x.value;
447 if let Some(min_scale) = context.min_scale(x.commodity.as_ref()) {
448 v.as_mut().normalize_assign();
449 v.rescale(std::cmp::max(min_scale.into(), v.scale()));
450 }
451 match context.decimal_format(x.commodity.as_ref()) {
452 Some(format) => PrettyDecimal::with_format(v.value, Some(format)),
453 None => v,
454 }
455}
456
457fn print_clear_state(v: ClearState) -> &'static str {
458 match v {
459 ClearState::Uncleared => "",
460 ClearState::Cleared => "* ",
461 ClearState::Pending => "! ",
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 use std::collections::HashMap;
470
471 use maplit::hashmap;
472 use pretty_assertions::assert_eq;
473 use rust_decimal::Decimal;
474 use rust_decimal_macros::dec;
475
476 fn amount<'a, T, U>(value: T, commodity: U) -> expr::ValueExpr<'a>
477 where
478 T: Into<Decimal>,
479 U: Into<Cow<'a, str>>,
480 {
481 let value: Decimal = value.into();
482 expr::ValueExpr::Amount(expr::Amount {
483 commodity: commodity.into(),
484 value: PrettyDecimal::unformatted(value),
485 })
486 }
487
488 fn amount_expr<T: Into<Decimal>>(value: T, commodity: &'static str) -> expr::Expr<'static> {
489 let value: Decimal = value.into();
490 expr::Expr::Value(Box::new(amount(value, commodity)))
491 }
492
493 #[test]
494 fn display_ledger_entries_no_txn() {
495 let ctx = DisplayContext::default();
496 assert_eq!(
497 concat!(";this\n", ";is\n", ";a pen pineapple apple pen.\n"),
498 format!(
499 "{}",
500 ctx.as_display(&plain::LedgerStatement::Comment(TopLevelComment(
501 Cow::Borrowed("this\nis\na pen pineapple apple pen."),
502 )))
503 )
504 );
505 assert_eq!(
506 "apply tag foo\n",
507 format!(
508 "{}",
509 ctx.as_display(&tracked::LedgerStatement::ApplyTag(ApplyTag {
510 key: Cow::Borrowed("foo"),
511 value: None
512 })),
513 )
514 );
515 assert_eq!(
516 "apply tag foo: bar\n",
517 format!(
518 "{}",
519 ctx.as_display(&plain::LedgerStatement::ApplyTag(ApplyTag {
520 key: Cow::Borrowed("foo"),
521 value: Some(MetadataValue::Text(Cow::Borrowed("bar")))
522 }))
523 ),
524 );
525 assert_eq!(
526 "apply tag foo:: 100\n",
527 format!(
528 "{}",
529 ctx.as_display(&tracked::LedgerStatement::ApplyTag(ApplyTag {
530 key: Cow::Borrowed("foo"),
531 value: Some(MetadataValue::Expr(Cow::Borrowed("100")))
532 }))
533 ),
534 );
535 assert_eq!(
536 "end apply tag\n",
537 format!("{}", ctx.as_display(&plain::LedgerStatement::EndApplyTag))
538 );
539 }
540
541 #[test]
542 fn display_ledger_entry_emits_blank_line_on_separation() {
543 let ctx = DisplayContext::default();
544 let entry = |separation| plain::LedgerEntry {
545 separation,
546 statement: plain::LedgerStatement::EndApplyTag,
547 };
548 assert_eq!(
549 "end apply tag\n",
550 format!("{}", ctx.as_display(&entry(Separation::Immediate)))
551 );
552 assert_eq!(
553 "\nend apply tag\n",
554 format!("{}", ctx.as_display(&entry(Separation::BlankLine)))
555 );
556 }
557
558 #[test]
559 fn display_txn() {
560 let got = format!(
561 "{}",
562 DisplayContext::default().as_display(&LedgerStatement::Txn(plain::Transaction {
563 date: NaiveDate::from_ymd_opt(2022, 12, 23).unwrap(),
564 effective_date: None,
565 clear_state: ClearState::Uncleared,
566 code: None,
567 payee: Cow::Borrowed("Example Grocery"),
568 posts: vec![Posting {
569 account: Cow::Borrowed("Assets"),
570 clear_state: ClearState::Uncleared,
571 amount: Some(PostingAmount {
572 amount: amount(dec!(123.45), "USD"),
573 cost: None,
574 lot: Lot::default(),
575 }),
576 balance: None,
577 metadata: vec![Metadata::Comment("single-line".into()),],
578 }],
579 metadata: vec![
580 Metadata::Comment("multi\nline\ntext\n".into()),
581 Metadata::Comment("works\nfine".into()),
582 ],
583 }))
584 );
585 let want = concat!(
586 "2022/12/23 Example Grocery\n",
587 " ; multi\n",
588 " ; line\n",
589 " ; text\n",
590 " ; works\n",
591 " ; fine\n",
592 " Assets 123.45 USD\n",
593 " ; single-line\n"
594 );
595 assert_eq!(want, got);
596 }
597
598 #[test]
599 fn posting_non_expr() {
600 let all = Posting {
601 amount: Some(PostingAmount {
602 amount: amount(1234, "USD"),
603 cost: Some(Exchange::Rate(amount(dec!(100.00), "JPY"))),
604 lot: plain::Lot {
605 price: Some(Exchange::Rate(amount(dec!(1.1), "USD"))),
606 date: Some(NaiveDate::from_ymd_opt(2022, 5, 20).unwrap()),
607 note: Some(Cow::Borrowed("printable note")),
608 },
609 }),
610 balance: Some(amount(1234, "USD")),
611 ..Posting::new_untracked("Account")
612 };
613 let costbalance = Posting {
614 amount: Some(PostingAmount {
615 amount: amount(1234, "USD"),
616 cost: Some(Exchange::Rate(amount(100, "JPY"))),
617 lot: plain::Lot::default(),
618 }),
619 balance: Some(amount(1234, "USD")),
620 ..Posting::new_untracked("Account")
621 };
622 let total = Posting {
623 amount: Some(PostingAmount {
624 amount: amount(1234, "USD"),
625 cost: Some(Exchange::Total(amount(100, "JPY"))),
626 lot: plain::Lot::default(),
627 }),
628 ..Posting::new_untracked("Account")
629 };
630 let nocost = Posting {
631 amount: Some(PostingAmount {
632 amount: amount(1234, "USD"),
633 cost: None,
634 lot: plain::Lot::default(),
635 }),
636 balance: Some(amount(1234, "USD")),
637 ..Posting::new_untracked("Account")
638 };
639 let noamount = plain::Posting {
640 amount: None,
641 balance: Some(amount(1234, "USD")),
642 ..Posting::new_untracked("Account")
643 };
644 let zerobalance = plain::Posting {
645 amount: None,
646 balance: Some(amount(0, "")),
647 ..Posting::new_untracked("Account")
648 };
649
650 assert_eq!(
651 concat!(
652 " Account 1234 USD {1.1 USD} [2022/05/20] (printable note) @ 100.00 JPY = 1234 USD\n",
655 " Account 1234 USD @ 100 JPY = 1234 USD\n",
656 " Account 1234 USD @@ 100 JPY\n",
657 " Account 1234 USD = 1234 USD\n",
658 " Account = 1234 USD\n",
659 " Account = 0\n"
661 ),
662 format!(
663 "{}{}{}{}{}{}",
664 DisplayContext::default().as_display(&all),
665 DisplayContext::default().as_display(&costbalance),
666 DisplayContext::default().as_display(&total),
667 DisplayContext::default().as_display(&nocost),
668 DisplayContext::default().as_display(&noamount),
669 DisplayContext::default().as_display(&zerobalance),
670 ),
671 );
672
673 let ctx = DisplayContext::new(
675 CommodityDisplayOption::default(),
676 hashmap! {"USD".to_string() => CommodityDisplayOption {format: Some(pretty_decimal::Format::Comma3Dot), min_scale: Some(4)}},
677 );
678 assert_eq!(
679 concat!(
680 " Account 1,234.0000 USD {1.1000 USD} [2022/05/20] (printable note) @ 100.00 JPY = 1,234.0000 USD\n",
683 " Account 1,234.0000 USD @ 100 JPY = 1,234.0000 USD\n",
684 " Account 1,234.0000 USD @@ 100 JPY\n",
685 " Account 1,234.0000 USD = 1,234.0000 USD\n",
686 " Account = 1,234.0000 USD\n",
687 " Account = 0\n"
688 ),
689 format!(
690 "{}{}{}{}{}{}",
691 ctx.as_display(&all),
692 ctx.as_display(&costbalance),
693 ctx.as_display(&total),
694 ctx.as_display(&nocost),
695 ctx.as_display(&noamount),
696 ctx.as_display(&zerobalance),
697 ),
698 );
699 }
700
701 #[test]
702 fn fmt_posting_comma_3_dot() {
703 let ctx = DisplayContext::default();
704 let large = plain::Posting {
705 amount: Some(
706 expr::ValueExpr::Amount(expr::Amount {
707 commodity: Cow::Borrowed("JPY"),
708 value: PrettyDecimal::comma3dot(dec!(1_234_567)),
709 })
710 .into(),
711 ),
712 ..Posting::new_untracked("Account")
713 };
714 let small = plain::Posting {
715 amount: Some(
716 expr::ValueExpr::Amount(expr::Amount {
717 commodity: Cow::Borrowed("JPY"),
718 value: PrettyDecimal::comma3dot(dec!(0.0011)),
719 })
720 .into(),
721 ),
722 ..Posting::new_untracked("Account")
723 };
724
725 assert_eq!(
726 concat!(
727 " Account 1,234,567 JPY\n",
730 " Account 0.0011 JPY\n",
731 ),
732 format!("{}{}", ctx.as_display(&large), ctx.as_display(&small),),
733 );
734 }
735
736 #[test]
737 fn fmt_with_alignment_simple_amount_without_commodity() {
738 let mut buffer = String::new();
739 let alignment = DisplayContext::default()
740 .as_display(&amount(123i8, ""))
741 .fmt_with_alignment(&mut buffer)
742 .unwrap();
743 assert_eq!("123", buffer.as_str());
744 assert_eq!(Alignment::Partial(3), alignment);
745 }
746
747 #[test]
748 fn fmt_with_alignment_simple_amount_with_commodity() {
749 let mut buffer = String::new();
751 let usd1234 = amount(1234i16, "USD");
752 let alignment = DisplayContext::default()
753 .as_display(&usd1234)
754 .fmt_with_alignment(&mut buffer)
755 .unwrap();
756 assert_eq!("1234 USD", buffer.as_str());
757 assert_eq!(Alignment::Complete(4), alignment);
758
759 buffer.clear();
761 let alignment = DisplayContext::new(
762 CommodityDisplayOption {
763 format: None,
764 min_scale: Some(2),
765 },
766 HashMap::new(),
767 )
768 .as_display(&usd1234)
769 .fmt_with_alignment(&mut buffer)
770 .unwrap();
771 assert_eq!("1234.00 USD", buffer.as_str());
772 assert_eq!(Alignment::Complete(7), alignment);
773
774 buffer.clear();
775 let alignment = DisplayContext::new(
776 CommodityDisplayOption {
777 format: Some(pretty_decimal::Format::Comma3Dot),
778 min_scale: Some(2),
779 },
780 HashMap::new(),
781 )
782 .as_display(&usd1234)
783 .fmt_with_alignment(&mut buffer)
784 .unwrap();
785 assert_eq!("1,234.00 USD", buffer.as_str());
786 assert_eq!(Alignment::Complete(8), alignment);
787 }
788
789 #[test]
790 fn test_fmt_with_alignment_complex_expr() {
791 let expr = expr::ValueExpr::Paren(expr::Expr::Binary(expr::BinaryOpExpr {
793 lhs: Box::new(expr::Expr::Binary(expr::BinaryOpExpr {
794 lhs: Box::new(expr::Expr::Value(Box::new(expr::ValueExpr::Paren(
795 expr::Expr::Binary(expr::BinaryOpExpr {
796 lhs: Box::new(amount_expr(dec!(1.20), "")),
797 op: expr::BinaryOp::Add,
798 rhs: Box::new(amount_expr(dec!(2.67), "")),
799 }),
800 )))),
801 op: expr::BinaryOp::Mul,
802 rhs: Box::new(amount_expr(dec!(3.1), "USD")),
803 })),
804 op: expr::BinaryOp::Add,
805 rhs: Box::new(amount_expr(5i32, "USD")),
806 }));
807 let mut got = String::new();
808 let alignment = DisplayContext::default()
809 .as_display(&expr)
810 .fmt_with_alignment(&mut got)
811 .unwrap();
812 assert_eq!("((1.20 + 2.67) * 3.1 USD + 5 USD)", got.as_str());
813 assert_eq!(Alignment::Complete(20), alignment);
814 }
815}