1mod amount;
7mod directives;
8mod helpers;
9mod transaction;
10
11pub(crate) use amount::{format_amount, format_cost_spec, format_price_annotation};
12pub(crate) use directives::{
13 format_balance, format_close, format_commodity, format_custom, format_document, format_event,
14 format_note, format_open, format_pad, format_price, format_query,
15};
16pub use helpers::escape_string;
17pub(crate) use helpers::format_meta_value;
18pub(crate) use transaction::{format_incomplete_amount, format_transaction};
19
20use crate::Directive;
21
22#[derive(Debug, Clone)]
24pub struct FormatConfig {
25 pub amount_column: usize,
27 pub indent: String,
29}
30
31impl Default for FormatConfig {
32 fn default() -> Self {
33 Self {
34 amount_column: 60,
35 indent: " ".to_string(),
36 }
37 }
38}
39
40impl FormatConfig {
41 #[must_use]
43 pub fn with_column(column: usize) -> Self {
44 Self {
45 amount_column: column,
46 ..Default::default()
47 }
48 }
49
50 #[must_use]
52 pub fn with_indent(indent_width: usize) -> Self {
53 let indent = " ".repeat(indent_width);
54 Self {
55 indent,
56 ..Default::default()
57 }
58 }
59
60 #[must_use]
62 pub fn new(column: usize, indent_width: usize) -> Self {
63 let indent = " ".repeat(indent_width);
64 Self {
65 amount_column: column,
66 indent,
67 }
68 }
69}
70
71pub fn format_directive(directive: &Directive, config: &FormatConfig) -> String {
73 match directive {
74 Directive::Transaction(txn) => format_transaction(txn, config),
75 Directive::Balance(bal) => format_balance(bal, config),
76 Directive::Open(open) => format_open(open, config),
77 Directive::Close(close) => format_close(close, config),
78 Directive::Commodity(comm) => format_commodity(comm, config),
79 Directive::Pad(pad) => format_pad(pad, config),
80 Directive::Event(event) => format_event(event, config),
81 Directive::Query(query) => format_query(query, config),
82 Directive::Note(note) => format_note(note, config),
83 Directive::Document(doc) => format_document(doc, config),
84 Directive::Price(price) => format_price(price, config),
85 Directive::Custom(custom) => format_custom(custom, config),
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::transaction::format_posting;
92 use super::*;
93 use crate::{
94 Amount, Balance, Close, Commodity, CostSpec, Custom, Directive, Document, Event,
95 IncompleteAmount, MetaValue, Metadata, NaiveDate, Note, Open, Pad, Posting, Price,
96 PriceAnnotation, Query, Transaction,
97 };
98 use rust_decimal_macros::dec;
99
100 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
101 NaiveDate::from_ymd_opt(year, month, day).unwrap()
102 }
103
104 #[test]
105 fn test_format_simple_transaction() {
106 let txn = Transaction::new(date(2024, 1, 15), "Morning coffee")
107 .with_flag('*')
108 .with_payee("Coffee Shop")
109 .with_posting(Posting::new(
110 "Expenses:Food:Coffee",
111 Amount::new(dec!(5.00), "USD"),
112 ))
113 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-5.00), "USD")));
114
115 let config = FormatConfig::with_column(50);
116 let formatted = format_transaction(&txn, &config);
117
118 assert!(formatted.contains("2024-01-15 * \"Coffee Shop\" \"Morning coffee\""));
119 assert!(formatted.contains("Expenses:Food:Coffee"));
120 assert!(formatted.contains("5.00 USD"));
121 }
122
123 #[test]
124 fn test_format_balance() {
125 let bal = Balance::new(
126 date(2024, 1, 1),
127 "Assets:Bank",
128 Amount::new(dec!(1000.00), "USD"),
129 );
130 let config = FormatConfig::default();
131 let formatted = format_balance(&bal, &config);
132 assert_eq!(formatted, "2024-01-01 balance Assets:Bank 1000.00 USD\n");
133 }
134
135 #[test]
136 fn test_format_open() {
137 let open = Open {
138 date: date(2024, 1, 1),
139 account: "Assets:Bank:Checking".into(),
140 currencies: vec!["USD".into(), "EUR".into()],
141 booking: None,
142 meta: Default::default(),
143 };
144 let config = FormatConfig::default();
145 let formatted = format_open(&open, &config);
146 assert_eq!(formatted, "2024-01-01 open Assets:Bank:Checking USD,EUR\n");
147 }
148
149 #[test]
150 fn test_escape_string() {
151 assert_eq!(escape_string("hello"), "hello");
152 assert_eq!(escape_string("say \"hi\""), "say \\\"hi\\\"");
153 assert_eq!(escape_string("line1\nline2"), "line1\\nline2");
154 }
155
156 #[test]
161 fn test_escape_string_combined() {
162 assert_eq!(
164 escape_string("path\\to\\file\n\"quoted\""),
165 "path\\\\to\\\\file\\n\\\"quoted\\\""
166 );
167 }
168
169 #[test]
170 fn test_escape_string_backslash_quote() {
171 assert_eq!(escape_string("\\\""), "\\\\\\\"");
173 }
174
175 #[test]
176 fn test_escape_string_empty() {
177 assert_eq!(escape_string(""), "");
178 }
179
180 #[test]
181 fn test_escape_string_unicode() {
182 assert_eq!(escape_string("café résumé"), "café résumé");
183 assert_eq!(escape_string("日本語"), "日本語");
184 assert_eq!(escape_string("emoji 🎉"), "emoji 🎉");
185 }
186
187 #[test]
188 fn test_format_meta_value_string() {
189 let val = MetaValue::String("hello world".to_string());
190 assert_eq!(format_meta_value(&val), "\"hello world\"");
191 }
192
193 #[test]
194 fn test_format_meta_value_string_with_quotes() {
195 let val = MetaValue::String("say \"hello\"".to_string());
196 assert_eq!(format_meta_value(&val), "\"say \\\"hello\\\"\"");
197 }
198
199 #[test]
200 fn test_format_meta_value_account() {
201 let val = MetaValue::Account("Assets:Bank:Checking".to_string());
202 assert_eq!(format_meta_value(&val), "Assets:Bank:Checking");
203 }
204
205 #[test]
206 fn test_format_meta_value_currency() {
207 let val = MetaValue::Currency("USD".to_string());
208 assert_eq!(format_meta_value(&val), "USD");
209 }
210
211 #[test]
212 fn test_format_meta_value_tag() {
213 let val = MetaValue::Tag("trip-2024".to_string());
214 assert_eq!(format_meta_value(&val), "#trip-2024");
215 }
216
217 #[test]
218 fn test_format_meta_value_link() {
219 let val = MetaValue::Link("invoice-123".to_string());
220 assert_eq!(format_meta_value(&val), "^invoice-123");
221 }
222
223 #[test]
224 fn test_format_meta_value_date() {
225 let val = MetaValue::Date(date(2024, 6, 15));
226 assert_eq!(format_meta_value(&val), "2024-06-15");
227 }
228
229 #[test]
230 fn test_format_meta_value_number() {
231 let val = MetaValue::Number(dec!(123.456));
232 assert_eq!(format_meta_value(&val), "123.456");
233 }
234
235 #[test]
236 fn test_format_meta_value_amount() {
237 let val = MetaValue::Amount(Amount::new(dec!(99.99), "USD"));
238 assert_eq!(format_meta_value(&val), "99.99 USD");
239 }
240
241 #[test]
242 fn test_format_meta_value_bool_true() {
243 let val = MetaValue::Bool(true);
244 assert_eq!(format_meta_value(&val), "TRUE");
245 }
246
247 #[test]
248 fn test_format_meta_value_bool_false() {
249 let val = MetaValue::Bool(false);
250 assert_eq!(format_meta_value(&val), "FALSE");
251 }
252
253 #[test]
254 fn test_format_meta_value_none() {
255 let val = MetaValue::None;
256 assert_eq!(format_meta_value(&val), "");
257 }
258
259 #[test]
260 fn test_format_cost_spec_per_unit() {
261 let spec = CostSpec {
262 number_per: Some(dec!(150.00)),
263 number_total: None,
264 currency: Some("USD".into()),
265 date: None,
266 label: None,
267 merge: false,
268 };
269 assert_eq!(format_cost_spec(&spec), "{150.00 USD}");
270 }
271
272 #[test]
273 fn test_format_cost_spec_total() {
274 let spec = CostSpec {
275 number_per: None,
276 number_total: Some(dec!(1500.00)),
277 currency: Some("USD".into()),
278 date: None,
279 label: None,
280 merge: false,
281 };
282 assert_eq!(format_cost_spec(&spec), "{{1500.00 USD}}");
283 }
284
285 #[test]
286 fn test_format_cost_spec_with_date() {
287 let spec = CostSpec {
288 number_per: Some(dec!(150.00)),
289 number_total: None,
290 currency: Some("USD".into()),
291 date: Some(date(2024, 1, 15)),
292 label: None,
293 merge: false,
294 };
295 assert_eq!(format_cost_spec(&spec), "{150.00 USD, 2024-01-15}");
296 }
297
298 #[test]
299 fn test_format_cost_spec_with_label() {
300 let spec = CostSpec {
301 number_per: Some(dec!(150.00)),
302 number_total: None,
303 currency: Some("USD".into()),
304 date: None,
305 label: Some("lot-a".to_string()),
306 merge: false,
307 };
308 assert_eq!(format_cost_spec(&spec), "{150.00 USD, \"lot-a\"}");
309 }
310
311 #[test]
312 fn test_format_cost_spec_with_merge() {
313 let spec = CostSpec {
314 number_per: Some(dec!(150.00)),
315 number_total: None,
316 currency: Some("USD".into()),
317 date: None,
318 label: None,
319 merge: true,
320 };
321 assert_eq!(format_cost_spec(&spec), "{150.00 USD, *}");
322 }
323
324 #[test]
325 fn test_format_cost_spec_all_fields() {
326 let spec = CostSpec {
327 number_per: Some(dec!(150.00)),
328 number_total: None,
329 currency: Some("USD".into()),
330 date: Some(date(2024, 1, 15)),
331 label: Some("lot-a".to_string()),
332 merge: true,
333 };
334 assert_eq!(
335 format_cost_spec(&spec),
336 "{150.00 USD, 2024-01-15, \"lot-a\", *}"
337 );
338 }
339
340 #[test]
341 fn test_format_cost_spec_empty() {
342 let spec = CostSpec {
343 number_per: None,
344 number_total: None,
345 currency: None,
346 date: None,
347 label: None,
348 merge: false,
349 };
350 assert_eq!(format_cost_spec(&spec), "{}");
351 }
352
353 #[test]
354 fn test_format_price_annotation_unit() {
355 let price = PriceAnnotation::Unit(Amount::new(dec!(150.00), "USD"));
356 assert_eq!(format_price_annotation(&price), "@ 150.00 USD");
357 }
358
359 #[test]
360 fn test_format_price_annotation_total() {
361 let price = PriceAnnotation::Total(Amount::new(dec!(1500.00), "USD"));
362 assert_eq!(format_price_annotation(&price), "@@ 1500.00 USD");
363 }
364
365 #[test]
366 fn test_format_price_annotation_unit_incomplete() {
367 let price = PriceAnnotation::UnitIncomplete(IncompleteAmount::NumberOnly(dec!(150.00)));
368 assert_eq!(format_price_annotation(&price), "@ 150.00");
369 }
370
371 #[test]
372 fn test_format_price_annotation_total_incomplete() {
373 let price = PriceAnnotation::TotalIncomplete(IncompleteAmount::CurrencyOnly("USD".into()));
374 assert_eq!(format_price_annotation(&price), "@@ USD");
375 }
376
377 #[test]
378 fn test_format_price_annotation_unit_empty() {
379 let price = PriceAnnotation::UnitEmpty;
380 assert_eq!(format_price_annotation(&price), "@");
381 }
382
383 #[test]
384 fn test_format_price_annotation_total_empty() {
385 let price = PriceAnnotation::TotalEmpty;
386 assert_eq!(format_price_annotation(&price), "@@");
387 }
388
389 #[test]
390 fn test_format_incomplete_amount_complete() {
391 let amount = IncompleteAmount::Complete(Amount::new(dec!(100.50), "EUR"));
392 assert_eq!(format_incomplete_amount(&amount), "100.50 EUR");
393 }
394
395 #[test]
396 fn test_format_incomplete_amount_number_only() {
397 let amount = IncompleteAmount::NumberOnly(dec!(42.00));
398 assert_eq!(format_incomplete_amount(&amount), "42.00");
399 }
400
401 #[test]
402 fn test_format_incomplete_amount_currency_only() {
403 let amount = IncompleteAmount::CurrencyOnly("BTC".into());
404 assert_eq!(format_incomplete_amount(&amount), "BTC");
405 }
406
407 #[test]
408 fn test_format_close() {
409 let close = Close {
410 date: date(2024, 12, 31),
411 account: "Assets:OldAccount".into(),
412 meta: Default::default(),
413 };
414 let config = FormatConfig::default();
415 let formatted = format_close(&close, &config);
416 assert_eq!(formatted, "2024-12-31 close Assets:OldAccount\n");
417 }
418
419 #[test]
420 fn test_format_commodity() {
421 let comm = Commodity {
422 date: date(2024, 1, 1),
423 currency: "BTC".into(),
424 meta: Default::default(),
425 };
426 let config = FormatConfig::default();
427 let formatted = format_commodity(&comm, &config);
428 assert_eq!(formatted, "2024-01-01 commodity BTC\n");
429 }
430
431 #[test]
432 fn test_format_pad() {
433 let pad = Pad {
434 date: date(2024, 1, 15),
435 account: "Assets:Checking".into(),
436 source_account: "Equity:Opening-Balances".into(),
437 meta: Default::default(),
438 };
439 let config = FormatConfig::default();
440 let formatted = format_pad(&pad, &config);
441 assert_eq!(
442 formatted,
443 "2024-01-15 pad Assets:Checking Equity:Opening-Balances\n"
444 );
445 }
446
447 #[test]
448 fn test_format_event() {
449 let event = Event {
450 date: date(2024, 6, 1),
451 event_type: "location".to_string(),
452 value: "New York".to_string(),
453 meta: Default::default(),
454 };
455 let config = FormatConfig::default();
456 let formatted = format_event(&event, &config);
457 assert_eq!(formatted, "2024-06-01 event \"location\" \"New York\"\n");
458 }
459
460 #[test]
461 fn test_format_event_with_quotes() {
462 let event = Event {
463 date: date(2024, 6, 1),
464 event_type: "quote".to_string(),
465 value: "He said \"hello\"".to_string(),
466 meta: Default::default(),
467 };
468 let config = FormatConfig::default();
469 let formatted = format_event(&event, &config);
470 assert_eq!(
471 formatted,
472 "2024-06-01 event \"quote\" \"He said \\\"hello\\\"\"\n"
473 );
474 }
475
476 #[test]
477 fn test_format_query() {
478 let query = Query {
479 date: date(2024, 1, 1),
480 name: "monthly_expenses".to_string(),
481 query: "SELECT account, sum(position) WHERE account ~ 'Expenses'".to_string(),
482 meta: Default::default(),
483 };
484 let config = FormatConfig::default();
485 let formatted = format_query(&query, &config);
486 assert!(formatted.contains("query \"monthly_expenses\""));
487 assert!(formatted.contains("SELECT account"));
488 }
489
490 #[test]
491 fn test_format_note() {
492 let note = Note {
493 date: date(2024, 3, 15),
494 account: "Assets:Bank".into(),
495 comment: "Called the bank about fee".to_string(),
496 meta: Default::default(),
497 };
498 let config = FormatConfig::default();
499 let formatted = format_note(¬e, &config);
500 assert_eq!(
501 formatted,
502 "2024-03-15 note Assets:Bank \"Called the bank about fee\"\n"
503 );
504 }
505
506 #[test]
507 fn test_format_document() {
508 let doc = Document {
509 date: date(2024, 2, 10),
510 account: "Assets:Bank".into(),
511 path: "/docs/statement-2024-02.pdf".to_string(),
512 tags: vec![],
513 links: vec![],
514 meta: Default::default(),
515 };
516 let config = FormatConfig::default();
517 let formatted = format_document(&doc, &config);
518 assert_eq!(
519 formatted,
520 "2024-02-10 document Assets:Bank \"/docs/statement-2024-02.pdf\"\n"
521 );
522 }
523
524 #[test]
525 fn test_format_price() {
526 let price = Price {
527 date: date(2024, 1, 15),
528 currency: "AAPL".into(),
529 amount: Amount::new(dec!(185.50), "USD"),
530 meta: Default::default(),
531 };
532 let config = FormatConfig::default();
533 let formatted = format_price(&price, &config);
534 assert_eq!(formatted, "2024-01-15 price AAPL 185.50 USD\n");
535 }
536
537 #[test]
538 fn test_format_custom() {
539 let custom = Custom {
540 date: date(2024, 1, 1),
541 custom_type: "budget".to_string(),
542 values: vec![],
543 meta: Default::default(),
544 };
545 let config = FormatConfig::default();
546 let formatted = format_custom(&custom, &config);
547 assert_eq!(formatted, "2024-01-01 custom \"budget\"\n");
548 }
549
550 #[test]
553 fn test_issue_573_format_custom_with_values() {
554 let custom = Custom {
556 date: date(2024, 1, 1),
557 custom_type: "fava-option".to_string(),
558 values: vec![
559 MetaValue::String("language".to_string()),
560 MetaValue::String("en".to_string()),
561 ],
562 meta: Default::default(),
563 };
564 let config = FormatConfig::default();
565 let formatted = format_custom(&custom, &config);
566 assert_eq!(
567 formatted,
568 "2024-01-01 custom \"fava-option\" \"language\" \"en\"\n"
569 );
570 }
571
572 #[test]
573 fn test_format_custom_with_mixed_values() {
574 let custom = Custom {
576 date: date(2024, 3, 15),
577 custom_type: "budget".to_string(),
578 values: vec![
579 MetaValue::Account("Expenses:Food".to_string()),
580 MetaValue::Amount(Amount::new(dec!(500), "USD")),
581 MetaValue::String("monthly".to_string()),
582 ],
583 meta: Default::default(),
584 };
585 let config = FormatConfig::default();
586 let formatted = format_custom(&custom, &config);
587 assert_eq!(
588 formatted,
589 "2024-03-15 custom \"budget\" Expenses:Food 500 USD \"monthly\"\n"
590 );
591 }
592
593 #[test]
594 fn test_format_open_with_booking() {
595 let open = Open {
596 date: date(2024, 1, 1),
597 account: "Assets:Brokerage".into(),
598 currencies: vec!["USD".into()],
599 booking: Some("FIFO".to_string()),
600 meta: Default::default(),
601 };
602 let config = FormatConfig::default();
603 let formatted = format_open(&open, &config);
604 assert_eq!(formatted, "2024-01-01 open Assets:Brokerage USD \"FIFO\"\n");
605 }
606
607 #[test]
608 fn test_format_open_no_currencies() {
609 let open = Open {
610 date: date(2024, 1, 1),
611 account: "Assets:Misc".into(),
612 currencies: vec![],
613 booking: None,
614 meta: Default::default(),
615 };
616 let config = FormatConfig::default();
617 let formatted = format_open(&open, &config);
618 assert_eq!(formatted, "2024-01-01 open Assets:Misc\n");
619 }
620
621 #[test]
622 fn test_format_balance_with_tolerance() {
623 let bal = Balance {
624 date: date(2024, 1, 1),
625 account: "Assets:Bank".into(),
626 amount: Amount::new(dec!(1000.00), "USD"),
627 tolerance: Some(dec!(0.01)),
628 meta: Default::default(),
629 };
630 let config = FormatConfig::default();
631 let formatted = format_balance(&bal, &config);
632 assert_eq!(
633 formatted,
634 "2024-01-01 balance Assets:Bank 1000.00 USD ~ 0.01\n"
635 );
636 }
637
638 #[test]
639 fn test_format_transaction_with_tags() {
640 let txn = Transaction::new(date(2024, 1, 15), "Dinner")
641 .with_flag('*')
642 .with_tag("trip-2024")
643 .with_tag("food")
644 .with_posting(Posting::new(
645 "Expenses:Food",
646 Amount::new(dec!(50.00), "USD"),
647 ))
648 .with_posting(Posting::new(
649 "Assets:Cash",
650 Amount::new(dec!(-50.00), "USD"),
651 ));
652
653 let config = FormatConfig::default();
654 let formatted = format_transaction(&txn, &config);
655
656 assert!(formatted.contains("#trip-2024"));
657 assert!(formatted.contains("#food"));
658 }
659
660 #[test]
661 fn test_format_transaction_with_links() {
662 let txn = Transaction::new(date(2024, 1, 15), "Invoice payment")
663 .with_flag('*')
664 .with_link("invoice-123")
665 .with_posting(Posting::new(
666 "Income:Freelance",
667 Amount::new(dec!(-1000.00), "USD"),
668 ))
669 .with_posting(Posting::new(
670 "Assets:Bank",
671 Amount::new(dec!(1000.00), "USD"),
672 ));
673
674 let config = FormatConfig::default();
675 let formatted = format_transaction(&txn, &config);
676
677 assert!(formatted.contains("^invoice-123"));
678 }
679
680 #[test]
681 fn test_format_transaction_with_metadata() {
682 let mut meta = Metadata::default();
683 meta.insert(
684 "filename".to_string(),
685 MetaValue::String("receipt.pdf".to_string()),
686 );
687 meta.insert("verified".to_string(), MetaValue::Bool(true));
688
689 let txn = Transaction {
690 date: date(2024, 1, 15),
691 flag: '*',
692 payee: None,
693 narration: "Purchase".into(),
694 tags: vec![],
695 links: vec![],
696 postings: vec![],
697 meta,
698 trailing_comments: Vec::new(),
699 };
700
701 let config = FormatConfig::default();
702 let formatted = format_transaction(&txn, &config);
703
704 assert!(formatted.contains("filename: \"receipt.pdf\""));
705 assert!(formatted.contains("verified: TRUE"));
706 }
707
708 #[test]
709 fn test_format_posting_with_flag() {
710 let mut posting = Posting::new("Expenses:Unknown", Amount::new(dec!(100.00), "USD"));
711 posting.flag = Some('!');
712
713 let config = FormatConfig::default();
714 let formatted = format_posting(&posting, &config);
715
716 assert!(formatted.contains("! Expenses:Unknown"));
717 }
718
719 #[test]
720 fn test_format_posting_no_units() {
721 let posting = Posting {
722 flag: None,
723 account: "Assets:Bank".into(),
724 units: None,
725 cost: None,
726 price: None,
727 meta: Default::default(),
728 comments: Vec::new(),
729 trailing_comments: Vec::new(),
730 };
731
732 let config = FormatConfig::default();
733 let formatted = format_posting(&posting, &config);
734
735 assert!(formatted.contains("Assets:Bank"));
736 assert!(!formatted.contains("USD"));
738 }
739
740 #[test]
741 fn test_format_config_with_column() {
742 let config = FormatConfig::with_column(80);
743 assert_eq!(config.amount_column, 80);
744 assert_eq!(config.indent, " ");
745 }
746
747 #[test]
748 fn test_format_config_with_indent() {
749 let config = FormatConfig::with_indent(4);
750 assert_eq!(config.indent, " ");
751 }
752
753 #[test]
754 fn test_format_config_new() {
755 let config = FormatConfig::new(70, 3);
756 assert_eq!(config.amount_column, 70);
757 assert_eq!(config.indent, " ");
758 }
759
760 #[test]
761 fn test_format_posting_long_account_name() {
762 let posting = Posting::new(
763 "Assets:Bank:Checking:Primary:Joint:Savings:Emergency:Fund:Extra:Long",
764 Amount::new(dec!(100.00), "USD"),
765 );
766
767 let config = FormatConfig::with_column(50);
768 let formatted = format_posting(&posting, &config);
769
770 assert!(formatted.contains(" 100.00 USD"));
772 }
773
774 #[test]
775 fn test_format_posting_with_cost_and_price() {
776 let posting = Posting {
777 flag: None,
778 account: "Assets:Brokerage".into(),
779 units: Some(IncompleteAmount::Complete(Amount::new(dec!(10), "AAPL"))),
780 cost: Some(CostSpec {
781 number_per: Some(dec!(150.00)),
782 number_total: None,
783 currency: Some("USD".into()),
784 date: Some(date(2024, 1, 15)),
785 label: None,
786 merge: false,
787 }),
788 price: Some(PriceAnnotation::Unit(Amount::new(dec!(155.00), "USD"))),
789 meta: Default::default(),
790 comments: Vec::new(),
791 trailing_comments: Vec::new(),
792 };
793
794 let config = FormatConfig::default();
795 let formatted = format_posting(&posting, &config);
796
797 assert!(formatted.contains("10 AAPL"));
798 assert!(formatted.contains("{150.00 USD, 2024-01-15}"));
799 assert!(formatted.contains("@ 155.00 USD"));
800 }
801
802 #[test]
803 fn test_format_directive_all_types() {
804 let config = FormatConfig::default();
805
806 let txn = Transaction::new(date(2024, 1, 1), "Test")
808 .with_flag('*')
809 .with_posting(Posting::new("Expenses:Test", Amount::new(dec!(1), "USD")))
810 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-1), "USD")));
811 let formatted = format_directive(&Directive::Transaction(txn), &config);
812 assert!(formatted.contains("2024-01-01"));
813
814 let bal = Balance::new(
816 date(2024, 1, 1),
817 "Assets:Bank",
818 Amount::new(dec!(100), "USD"),
819 );
820 let formatted = format_directive(&Directive::Balance(bal), &config);
821 assert!(formatted.contains("balance"));
822
823 let open = Open {
825 date: date(2024, 1, 1),
826 account: "Assets:Test".into(),
827 currencies: vec![],
828 booking: None,
829 meta: Default::default(),
830 };
831 let formatted = format_directive(&Directive::Open(open), &config);
832 assert!(formatted.contains("open"));
833
834 let close = Close {
836 date: date(2024, 1, 1),
837 account: "Assets:Test".into(),
838 meta: Default::default(),
839 };
840 let formatted = format_directive(&Directive::Close(close), &config);
841 assert!(formatted.contains("close"));
842
843 let comm = Commodity {
845 date: date(2024, 1, 1),
846 currency: "BTC".into(),
847 meta: Default::default(),
848 };
849 let formatted = format_directive(&Directive::Commodity(comm), &config);
850 assert!(formatted.contains("commodity"));
851
852 let pad = Pad {
854 date: date(2024, 1, 1),
855 account: "Assets:A".into(),
856 source_account: "Equity:B".into(),
857 meta: Default::default(),
858 };
859 let formatted = format_directive(&Directive::Pad(pad), &config);
860 assert!(formatted.contains("pad"));
861
862 let event = Event {
864 date: date(2024, 1, 1),
865 event_type: "test".to_string(),
866 value: "value".to_string(),
867 meta: Default::default(),
868 };
869 let formatted = format_directive(&Directive::Event(event), &config);
870 assert!(formatted.contains("event"));
871
872 let query = Query {
874 date: date(2024, 1, 1),
875 name: "test".to_string(),
876 query: "SELECT *".to_string(),
877 meta: Default::default(),
878 };
879 let formatted = format_directive(&Directive::Query(query), &config);
880 assert!(formatted.contains("query"));
881
882 let note = Note {
884 date: date(2024, 1, 1),
885 account: "Assets:Bank".into(),
886 comment: "test".to_string(),
887 meta: Default::default(),
888 };
889 let formatted = format_directive(&Directive::Note(note), &config);
890 assert!(formatted.contains("note"));
891
892 let doc = Document {
894 date: date(2024, 1, 1),
895 account: "Assets:Bank".into(),
896 path: "/path".to_string(),
897 tags: vec![],
898 links: vec![],
899 meta: Default::default(),
900 };
901 let formatted = format_directive(&Directive::Document(doc), &config);
902 assert!(formatted.contains("document"));
903
904 let price = Price {
906 date: date(2024, 1, 1),
907 currency: "AAPL".into(),
908 amount: Amount::new(dec!(150), "USD"),
909 meta: Default::default(),
910 };
911 let formatted = format_directive(&Directive::Price(price), &config);
912 assert!(formatted.contains("price"));
913
914 let custom = Custom {
916 date: date(2024, 1, 1),
917 custom_type: "test".to_string(),
918 values: vec![],
919 meta: Default::default(),
920 };
921 let formatted = format_directive(&Directive::Custom(custom), &config);
922 assert!(formatted.contains("custom"));
923 }
924
925 #[test]
926 fn test_format_amount_negative() {
927 let amount = Amount::new(dec!(-100.50), "USD");
928 assert_eq!(format_amount(&amount), "-100.50 USD");
929 }
930
931 #[test]
932 fn test_format_amount_zero() {
933 let amount = Amount::new(dec!(0), "EUR");
934 assert_eq!(format_amount(&amount), "0 EUR");
935 }
936
937 #[test]
938 fn test_format_amount_large_number() {
939 let amount = Amount::new(dec!(1234567890.12), "USD");
940 assert_eq!(format_amount(&amount), "1234567890.12 USD");
941 }
942
943 #[test]
944 fn test_format_amount_small_decimal() {
945 let amount = Amount::new(dec!(0.00001), "BTC");
946 assert_eq!(format_amount(&amount), "0.00001 BTC");
947 }
948
949 #[test]
950 fn test_format_transaction_with_inline_comment() {
951 let config = FormatConfig::default();
952
953 let mut posting = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
955 posting.comments = vec!["; This is an inline comment".to_string()];
956
957 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
958 .with_flag('*')
959 .with_posting(posting)
960 .with_posting(Posting::new("Assets:Bank", Amount::new(dec!(-50), "USD")));
961
962 let formatted = format_transaction(&txn, &config);
963
964 assert!(
966 formatted.contains("; This is an inline comment"),
967 "Formatted transaction should contain inline comment: {formatted}"
968 );
969 let comment_pos = formatted.find("; This is an inline comment").unwrap();
971 let expenses_pos = formatted.find("Expenses:Food").unwrap();
972 assert!(
973 comment_pos < expenses_pos,
974 "Comment should appear before the posting"
975 );
976 }
977
978 #[test]
980 fn test_issue_364_format_all_comment_types() {
981 let config = FormatConfig::default();
982
983 let mut posting1 = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
985 posting1.comments = vec!["; Pre-comment 1".to_string(), "; Pre-comment 2".to_string()];
986 posting1.trailing_comments = vec!["; trailing on posting".to_string()];
987
988 let mut posting2 = Posting::new("Assets:Bank", Amount::new(dec!(-50), "USD"));
990 posting2.comments = vec!["; Comment before second posting".to_string()];
991
992 let mut txn = Transaction::new(date(2024, 1, 15), "Test transaction")
994 .with_flag('*')
995 .with_posting(posting1)
996 .with_posting(posting2);
997 txn.trailing_comments = vec![
998 "; Transaction trailing 1".to_string(),
999 "; Transaction trailing 2".to_string(),
1000 ];
1001
1002 let formatted = format_transaction(&txn, &config);
1003
1004 let lines: Vec<&str> = formatted.lines().collect();
1006
1007 assert!(lines[0].contains("2024-01-15 * \"Test transaction\""));
1009
1010 assert_eq!(lines[1].trim(), "; Pre-comment 1");
1012 assert_eq!(lines[2].trim(), "; Pre-comment 2");
1013
1014 assert!(lines[3].contains("Expenses:Food"));
1016 assert!(lines[3].contains("; trailing on posting"));
1017
1018 assert_eq!(lines[4].trim(), "; Comment before second posting");
1020
1021 assert!(lines[5].contains("Assets:Bank"));
1023
1024 assert_eq!(lines[6].trim(), "; Transaction trailing 1");
1026 assert_eq!(lines[7].trim(), "; Transaction trailing 2");
1027 }
1028
1029 #[test]
1031 fn test_issue_364_trailing_comment_on_posting_line() {
1032 let config = FormatConfig::default();
1033
1034 let mut posting = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
1035 posting.trailing_comments = vec!["; This goes on same line".to_string()];
1036
1037 let txn = Transaction::new(date(2024, 1, 15), "Test")
1038 .with_flag('*')
1039 .with_posting(posting)
1040 .with_posting(Posting::auto("Assets:Bank"));
1041
1042 let formatted = format_transaction(&txn, &config);
1043
1044 for line in formatted.lines() {
1046 if line.contains("Expenses:Food") {
1047 assert!(
1048 line.contains("; This goes on same line"),
1049 "Trailing comment should be on same line as posting: {line}"
1050 );
1051 break;
1052 }
1053 }
1054 }
1055}