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]
551 fn test_format_open_with_booking() {
552 let open = Open {
553 date: date(2024, 1, 1),
554 account: "Assets:Brokerage".into(),
555 currencies: vec!["USD".into()],
556 booking: Some("FIFO".to_string()),
557 meta: Default::default(),
558 };
559 let config = FormatConfig::default();
560 let formatted = format_open(&open, &config);
561 assert_eq!(formatted, "2024-01-01 open Assets:Brokerage USD \"FIFO\"\n");
562 }
563
564 #[test]
565 fn test_format_open_no_currencies() {
566 let open = Open {
567 date: date(2024, 1, 1),
568 account: "Assets:Misc".into(),
569 currencies: vec![],
570 booking: None,
571 meta: Default::default(),
572 };
573 let config = FormatConfig::default();
574 let formatted = format_open(&open, &config);
575 assert_eq!(formatted, "2024-01-01 open Assets:Misc\n");
576 }
577
578 #[test]
579 fn test_format_balance_with_tolerance() {
580 let bal = Balance {
581 date: date(2024, 1, 1),
582 account: "Assets:Bank".into(),
583 amount: Amount::new(dec!(1000.00), "USD"),
584 tolerance: Some(dec!(0.01)),
585 meta: Default::default(),
586 };
587 let config = FormatConfig::default();
588 let formatted = format_balance(&bal, &config);
589 assert_eq!(
590 formatted,
591 "2024-01-01 balance Assets:Bank 1000.00 USD ~ 0.01\n"
592 );
593 }
594
595 #[test]
596 fn test_format_transaction_with_tags() {
597 let txn = Transaction::new(date(2024, 1, 15), "Dinner")
598 .with_flag('*')
599 .with_tag("trip-2024")
600 .with_tag("food")
601 .with_posting(Posting::new(
602 "Expenses:Food",
603 Amount::new(dec!(50.00), "USD"),
604 ))
605 .with_posting(Posting::new(
606 "Assets:Cash",
607 Amount::new(dec!(-50.00), "USD"),
608 ));
609
610 let config = FormatConfig::default();
611 let formatted = format_transaction(&txn, &config);
612
613 assert!(formatted.contains("#trip-2024"));
614 assert!(formatted.contains("#food"));
615 }
616
617 #[test]
618 fn test_format_transaction_with_links() {
619 let txn = Transaction::new(date(2024, 1, 15), "Invoice payment")
620 .with_flag('*')
621 .with_link("invoice-123")
622 .with_posting(Posting::new(
623 "Income:Freelance",
624 Amount::new(dec!(-1000.00), "USD"),
625 ))
626 .with_posting(Posting::new(
627 "Assets:Bank",
628 Amount::new(dec!(1000.00), "USD"),
629 ));
630
631 let config = FormatConfig::default();
632 let formatted = format_transaction(&txn, &config);
633
634 assert!(formatted.contains("^invoice-123"));
635 }
636
637 #[test]
638 fn test_format_transaction_with_metadata() {
639 let mut meta = Metadata::default();
640 meta.insert(
641 "filename".to_string(),
642 MetaValue::String("receipt.pdf".to_string()),
643 );
644 meta.insert("verified".to_string(), MetaValue::Bool(true));
645
646 let txn = Transaction {
647 date: date(2024, 1, 15),
648 flag: '*',
649 payee: None,
650 narration: "Purchase".into(),
651 tags: vec![],
652 links: vec![],
653 postings: vec![],
654 meta,
655 trailing_comments: Vec::new(),
656 };
657
658 let config = FormatConfig::default();
659 let formatted = format_transaction(&txn, &config);
660
661 assert!(formatted.contains("filename: \"receipt.pdf\""));
662 assert!(formatted.contains("verified: TRUE"));
663 }
664
665 #[test]
666 fn test_format_posting_with_flag() {
667 let mut posting = Posting::new("Expenses:Unknown", Amount::new(dec!(100.00), "USD"));
668 posting.flag = Some('!');
669
670 let config = FormatConfig::default();
671 let formatted = format_posting(&posting, &config);
672
673 assert!(formatted.contains("! Expenses:Unknown"));
674 }
675
676 #[test]
677 fn test_format_posting_no_units() {
678 let posting = Posting {
679 flag: None,
680 account: "Assets:Bank".into(),
681 units: None,
682 cost: None,
683 price: None,
684 meta: Default::default(),
685 comments: Vec::new(),
686 trailing_comments: Vec::new(),
687 };
688
689 let config = FormatConfig::default();
690 let formatted = format_posting(&posting, &config);
691
692 assert!(formatted.contains("Assets:Bank"));
693 assert!(!formatted.contains("USD"));
695 }
696
697 #[test]
698 fn test_format_config_with_column() {
699 let config = FormatConfig::with_column(80);
700 assert_eq!(config.amount_column, 80);
701 assert_eq!(config.indent, " ");
702 }
703
704 #[test]
705 fn test_format_config_with_indent() {
706 let config = FormatConfig::with_indent(4);
707 assert_eq!(config.indent, " ");
708 }
709
710 #[test]
711 fn test_format_config_new() {
712 let config = FormatConfig::new(70, 3);
713 assert_eq!(config.amount_column, 70);
714 assert_eq!(config.indent, " ");
715 }
716
717 #[test]
718 fn test_format_posting_long_account_name() {
719 let posting = Posting::new(
720 "Assets:Bank:Checking:Primary:Joint:Savings:Emergency:Fund:Extra:Long",
721 Amount::new(dec!(100.00), "USD"),
722 );
723
724 let config = FormatConfig::with_column(50);
725 let formatted = format_posting(&posting, &config);
726
727 assert!(formatted.contains(" 100.00 USD"));
729 }
730
731 #[test]
732 fn test_format_posting_with_cost_and_price() {
733 let posting = Posting {
734 flag: None,
735 account: "Assets:Brokerage".into(),
736 units: Some(IncompleteAmount::Complete(Amount::new(dec!(10), "AAPL"))),
737 cost: Some(CostSpec {
738 number_per: Some(dec!(150.00)),
739 number_total: None,
740 currency: Some("USD".into()),
741 date: Some(date(2024, 1, 15)),
742 label: None,
743 merge: false,
744 }),
745 price: Some(PriceAnnotation::Unit(Amount::new(dec!(155.00), "USD"))),
746 meta: Default::default(),
747 comments: Vec::new(),
748 trailing_comments: Vec::new(),
749 };
750
751 let config = FormatConfig::default();
752 let formatted = format_posting(&posting, &config);
753
754 assert!(formatted.contains("10 AAPL"));
755 assert!(formatted.contains("{150.00 USD, 2024-01-15}"));
756 assert!(formatted.contains("@ 155.00 USD"));
757 }
758
759 #[test]
760 fn test_format_directive_all_types() {
761 let config = FormatConfig::default();
762
763 let txn = Transaction::new(date(2024, 1, 1), "Test")
765 .with_flag('*')
766 .with_posting(Posting::new("Expenses:Test", Amount::new(dec!(1), "USD")))
767 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-1), "USD")));
768 let formatted = format_directive(&Directive::Transaction(txn), &config);
769 assert!(formatted.contains("2024-01-01"));
770
771 let bal = Balance::new(
773 date(2024, 1, 1),
774 "Assets:Bank",
775 Amount::new(dec!(100), "USD"),
776 );
777 let formatted = format_directive(&Directive::Balance(bal), &config);
778 assert!(formatted.contains("balance"));
779
780 let open = Open {
782 date: date(2024, 1, 1),
783 account: "Assets:Test".into(),
784 currencies: vec![],
785 booking: None,
786 meta: Default::default(),
787 };
788 let formatted = format_directive(&Directive::Open(open), &config);
789 assert!(formatted.contains("open"));
790
791 let close = Close {
793 date: date(2024, 1, 1),
794 account: "Assets:Test".into(),
795 meta: Default::default(),
796 };
797 let formatted = format_directive(&Directive::Close(close), &config);
798 assert!(formatted.contains("close"));
799
800 let comm = Commodity {
802 date: date(2024, 1, 1),
803 currency: "BTC".into(),
804 meta: Default::default(),
805 };
806 let formatted = format_directive(&Directive::Commodity(comm), &config);
807 assert!(formatted.contains("commodity"));
808
809 let pad = Pad {
811 date: date(2024, 1, 1),
812 account: "Assets:A".into(),
813 source_account: "Equity:B".into(),
814 meta: Default::default(),
815 };
816 let formatted = format_directive(&Directive::Pad(pad), &config);
817 assert!(formatted.contains("pad"));
818
819 let event = Event {
821 date: date(2024, 1, 1),
822 event_type: "test".to_string(),
823 value: "value".to_string(),
824 meta: Default::default(),
825 };
826 let formatted = format_directive(&Directive::Event(event), &config);
827 assert!(formatted.contains("event"));
828
829 let query = Query {
831 date: date(2024, 1, 1),
832 name: "test".to_string(),
833 query: "SELECT *".to_string(),
834 meta: Default::default(),
835 };
836 let formatted = format_directive(&Directive::Query(query), &config);
837 assert!(formatted.contains("query"));
838
839 let note = Note {
841 date: date(2024, 1, 1),
842 account: "Assets:Bank".into(),
843 comment: "test".to_string(),
844 meta: Default::default(),
845 };
846 let formatted = format_directive(&Directive::Note(note), &config);
847 assert!(formatted.contains("note"));
848
849 let doc = Document {
851 date: date(2024, 1, 1),
852 account: "Assets:Bank".into(),
853 path: "/path".to_string(),
854 tags: vec![],
855 links: vec![],
856 meta: Default::default(),
857 };
858 let formatted = format_directive(&Directive::Document(doc), &config);
859 assert!(formatted.contains("document"));
860
861 let price = Price {
863 date: date(2024, 1, 1),
864 currency: "AAPL".into(),
865 amount: Amount::new(dec!(150), "USD"),
866 meta: Default::default(),
867 };
868 let formatted = format_directive(&Directive::Price(price), &config);
869 assert!(formatted.contains("price"));
870
871 let custom = Custom {
873 date: date(2024, 1, 1),
874 custom_type: "test".to_string(),
875 values: vec![],
876 meta: Default::default(),
877 };
878 let formatted = format_directive(&Directive::Custom(custom), &config);
879 assert!(formatted.contains("custom"));
880 }
881
882 #[test]
883 fn test_format_amount_negative() {
884 let amount = Amount::new(dec!(-100.50), "USD");
885 assert_eq!(format_amount(&amount), "-100.50 USD");
886 }
887
888 #[test]
889 fn test_format_amount_zero() {
890 let amount = Amount::new(dec!(0), "EUR");
891 assert_eq!(format_amount(&amount), "0 EUR");
892 }
893
894 #[test]
895 fn test_format_amount_large_number() {
896 let amount = Amount::new(dec!(1234567890.12), "USD");
897 assert_eq!(format_amount(&amount), "1234567890.12 USD");
898 }
899
900 #[test]
901 fn test_format_amount_small_decimal() {
902 let amount = Amount::new(dec!(0.00001), "BTC");
903 assert_eq!(format_amount(&amount), "0.00001 BTC");
904 }
905
906 #[test]
907 fn test_format_transaction_with_inline_comment() {
908 let config = FormatConfig::default();
909
910 let mut posting = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
912 posting.comments = vec!["; This is an inline comment".to_string()];
913
914 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
915 .with_flag('*')
916 .with_posting(posting)
917 .with_posting(Posting::new("Assets:Bank", Amount::new(dec!(-50), "USD")));
918
919 let formatted = format_transaction(&txn, &config);
920
921 assert!(
923 formatted.contains("; This is an inline comment"),
924 "Formatted transaction should contain inline comment: {formatted}"
925 );
926 let comment_pos = formatted.find("; This is an inline comment").unwrap();
928 let expenses_pos = formatted.find("Expenses:Food").unwrap();
929 assert!(
930 comment_pos < expenses_pos,
931 "Comment should appear before the posting"
932 );
933 }
934
935 #[test]
937 fn test_issue_364_format_all_comment_types() {
938 let config = FormatConfig::default();
939
940 let mut posting1 = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
942 posting1.comments = vec!["; Pre-comment 1".to_string(), "; Pre-comment 2".to_string()];
943 posting1.trailing_comments = vec!["; trailing on posting".to_string()];
944
945 let mut posting2 = Posting::new("Assets:Bank", Amount::new(dec!(-50), "USD"));
947 posting2.comments = vec!["; Comment before second posting".to_string()];
948
949 let mut txn = Transaction::new(date(2024, 1, 15), "Test transaction")
951 .with_flag('*')
952 .with_posting(posting1)
953 .with_posting(posting2);
954 txn.trailing_comments = vec![
955 "; Transaction trailing 1".to_string(),
956 "; Transaction trailing 2".to_string(),
957 ];
958
959 let formatted = format_transaction(&txn, &config);
960
961 let lines: Vec<&str> = formatted.lines().collect();
963
964 assert!(lines[0].contains("2024-01-15 * \"Test transaction\""));
966
967 assert_eq!(lines[1].trim(), "; Pre-comment 1");
969 assert_eq!(lines[2].trim(), "; Pre-comment 2");
970
971 assert!(lines[3].contains("Expenses:Food"));
973 assert!(lines[3].contains("; trailing on posting"));
974
975 assert_eq!(lines[4].trim(), "; Comment before second posting");
977
978 assert!(lines[5].contains("Assets:Bank"));
980
981 assert_eq!(lines[6].trim(), "; Transaction trailing 1");
983 assert_eq!(lines[7].trim(), "; Transaction trailing 2");
984 }
985
986 #[test]
988 fn test_issue_364_trailing_comment_on_posting_line() {
989 let config = FormatConfig::default();
990
991 let mut posting = Posting::new("Expenses:Food", Amount::new(dec!(50), "USD"));
992 posting.trailing_comments = vec!["; This goes on same line".to_string()];
993
994 let txn = Transaction::new(date(2024, 1, 15), "Test")
995 .with_flag('*')
996 .with_posting(posting)
997 .with_posting(Posting::auto("Assets:Bank"));
998
999 let formatted = format_transaction(&txn, &config);
1000
1001 for line in formatted.lines() {
1003 if line.contains("Expenses:Food") {
1004 assert!(
1005 line.contains("; This goes on same line"),
1006 "Trailing comment should be on same line as posting: {line}"
1007 );
1008 break;
1009 }
1010 }
1011 }
1012}