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