Skip to main content

rustledger_core/format/
mod.rs

1//! Beancount file formatter.
2//!
3//! Provides pretty-printing for beancount directives with configurable
4//! amount alignment.
5
6mod 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/// Formatter configuration.
23#[derive(Debug, Clone)]
24pub struct FormatConfig {
25    /// Column to align amounts to (default: 60).
26    pub amount_column: usize,
27    /// Indentation for postings.
28    pub indent: String,
29    /// Indentation for metadata.
30    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    /// Create a new config with the specified amount column.
45    #[must_use]
46    pub fn with_column(column: usize) -> Self {
47        Self {
48            amount_column: column,
49            ..Default::default()
50        }
51    }
52
53    /// Create a new config with the specified indent width.
54    #[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    /// Create a new config with both column and indent settings.
66    #[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
78/// Format a directive to a string.
79pub 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    // ====================================================================
164    // Phase 2: Additional Coverage Tests for Format Functions
165    // ====================================================================
166
167    #[test]
168    fn test_escape_string_combined() {
169        // Test escaping with quotes + backslash + newline combined
170        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        // Backslash followed by quote
179        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(&note, &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        // No amount should appear
698        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        // Should have at least 2 spaces between account and amount
734        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        // Transaction
768        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        // Balance
776        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        // Open
785        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        // Close
796        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        // Commodity
805        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        // Pad
814        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        // Event
824        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        // Query
834        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        // Note
844        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        // Document
854        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        // Price
866        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        // Custom
876        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}