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