1use crate::{
4 parse::{parse_ledger, ParseError, ParseOptions},
5 syntax::{self, display::DisplayContext},
6};
7
8use std::io::{Read, Write};
9
10#[derive(thiserror::Error, Debug)]
12pub enum FormatError {
13 #[error("failed to perform IO")]
14 IO(#[from] std::io::Error),
15 #[error("failed to parse the file")]
16 Parse(#[from] ParseError),
17 #[error("recursive format isn't supported yet")]
19 UnsupportedRecursiveFormat,
20}
21
22#[derive(Debug, Default)]
24pub struct FormatOptions {
25 recursive: bool,
26}
27
28impl FormatOptions {
29 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn recursive(&mut self, recursive: bool) -> &mut Self {
38 self.recursive = recursive;
39 self
40 }
41
42 pub fn format<R, W>(&self, r: &mut R, w: &mut W) -> Result<(), FormatError>
44 where
45 R: Read,
46 W: Write,
47 {
48 if self.recursive {
50 return Err(FormatError::UnsupportedRecursiveFormat);
51 }
52 let mut buf = String::new();
53 r.read_to_string(&mut buf)?;
54 let ctx = DisplayContext::default();
56 for parsed in parse_ledger(&ParseOptions::default(), &buf) {
57 let (_, entry): (_, syntax::plain::LedgerEntry) = parsed?;
58 writeln!(w, "{}", ctx.as_display(&entry))?;
59 }
60 Ok(())
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 use indoc::indoc;
69 use pretty_assertions::assert_eq;
70
71 #[test]
72 fn format_succeeds_transaction_without_lot_price() {
73 let input = indoc! {"
74 ; Top
75 ; level
76 #comment
77 %can
78 |have several prefixes.
79
80 ; second
81 ; round
82
83 account Foo\t
84 alias Bar\t
85 note これは何でしょうか
86 alias Baz
87
88 commodity USD\t
89 \talias 米ドル\t
90 \talias $\t
91
92 apply tag foo
93 apply tag key: value
94 apply tag key:: 10 USD
95
96 end apply tag
97
98 end apply tag
99 end apply tag
100
101 include path/to/other.ledger
102
103 2021/03/12 Opening Balance ; initial balance
104 Assets:Bank = 1000 CHF
105 Equity
106
107 2021/05/14 !(#txn-1) My Grocery
108 Expenses:Grocery\t10 CHF
109 Expenses:Commissions 1 USD @ 0.98 CHF ; Payee: My Card
110 ; My card took commission
111 ; :financial:経済:
112 Assets:Bank -20 CHF=1CHF
113 Expenses:Household = 0
114 Assets:Complex (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
115 Assets:Broker -2 SPINX (bought before Xmas) {100 USD} [2010/12/23] @ 10000 USD
116 Liabilities:Comma 5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF
117 "};
118 let want = indoc! {"
121 ; Top
122 ; level
123 ;comment
124 ;can
125 ;have several prefixes.
126
127 ; second
128 ; round
129
130 account Foo
131 alias Bar
132 note これは何でしょうか
133 alias Baz
134
135 commodity USD
136 alias 米ドル
137 alias $
138
139 apply tag foo
140
141 apply tag key: value
142
143 apply tag key:: 10 USD
144
145 end apply tag
146
147 end apply tag
148
149 end apply tag
150
151 include path/to/other.ledger
152
153 2021/03/12 Opening Balance
154 ; initial balance
155 Assets:Bank = 1000 CHF
156 Equity
157
158 2021/05/14 ! (#txn-1) My Grocery
159 Expenses:Grocery 10 CHF
160 Expenses:Commissions 1 USD @ 0.98 CHF
161 ; Payee: My Card
162 ; My card took commission
163 ; :financial:経済:
164 Assets:Bank -20 CHF = 1 CHF
165 Expenses:Household = 0
166 Assets:Complex (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
167 Assets:Broker -2 SPINX {100 USD} [2010/12/23] (bought before Xmas) @ 10000 USD
168 Liabilities:Comma 5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF
169
170 "};
171 let mut output = Vec::new();
172 let mut r = input.as_bytes();
173
174 FormatOptions::new()
175 .format(&mut r, &mut output)
176 .expect("format() should succeeds");
177 let got = std::str::from_utf8(&output).expect("output should be valid UTF-8");
178 assert_eq!(want, got);
179 }
180}