1use crate::{
4 parse::{ParseError, ParseOptions, parse_ledger},
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 write!(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 fn format_str(input: &str) -> String {
72 let mut output = Vec::new();
73 FormatOptions::new()
74 .format(&mut input.as_bytes(), &mut output)
75 .expect("format() should succeeds");
76 String::from_utf8(output).expect("output should be valid UTF-8")
77 }
78
79 #[test]
80 fn format_keeps_comment_attached_to_the_next_entry() {
81 let input = indoc! {"
82 ; Explains the transaction below.
83 2021/03/12 Opening Balance
84 Assets:Bank = 1000 CHF
85 Equity
86 ; Explains the transaction above.
87
88 ; Standalone comment, separated by blank lines.
89
90 2021/03/13 Grocery
91 Expenses:Grocery 10 CHF
92 Assets:Bank
93 "};
94
95 assert_eq!(input, format_str(input));
96 }
97
98 #[test]
99 fn format_collapses_consecutive_blank_lines() {
100 let input = indoc! {"
101 include a.ledger
102
103
104
105 include b.ledger
106 "};
107 let want = indoc! {"
108 include a.ledger
109
110 include b.ledger
111 "};
112
113 assert_eq!(want, format_str(input));
114 }
115
116 #[test]
117 fn format_is_idempotent() {
118 let input = indoc! {"
119 ; Top
120 #comment
121
122 account Foo\t
123 alias Bar
124
125 apply tag foo
126 end apply tag
127
128 2021/05/14 !(#txn-1) My Grocery
129 Expenses:Grocery\t10 CHF
130 Assets:Bank -10 CHF
131 "};
132
133 let once = format_str(input);
134 assert_eq!(once, format_str(&once));
135 }
136
137 #[test]
138 fn format_succeeds_transaction_without_lot_price() {
139 let input = indoc! {"
140 ; Top
141 ; level
142 #comment
143 %can
144 |have several prefixes.
145
146 ; second
147 ; round
148
149 account Foo\t
150 alias Bar\t
151 note これは何でしょうか
152 alias Baz
153
154 commodity USD\t
155 \talias 米ドル\t
156 \talias $\t
157
158 apply tag foo
159
160 apply tag key: value
161 apply tag key:: 10 USD
162
163 end apply tag
164 ; key:: 10 USD
165
166 end apply tag
167 end apply tag
168
169 include path/to/other.ledger
170
171 2021/03/12 Opening Balance ; initial balance
172 Assets:Bank = 1000 CHF
173 Equity
174
175 2021/05/14 !(#txn-1) My Grocery
176 Expenses:Grocery\t10 CHF
177 Expenses:Commissions 1 USD @ 0.98 CHF ; Payee: My Card
178 ; My card took commission
179 ; :financial:経済:
180 Assets:Bank -20 CHF=1CHF
181 Expenses:Household = 0
182 Assets:Complex (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
183 Assets:Broker -2 SPINX (bought before Xmas) {100 USD} [2010/12/23] @ 10000 USD
184 Liabilities:Comma 5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF
185 "};
186 let want = indoc! {"
189 ; Top
190 ; level
191 ;comment
192 ;can
193 ;have several prefixes.
194
195 ; second
196 ; round
197
198 account Foo
199 alias Bar
200 note これは何でしょうか
201 alias Baz
202
203 commodity USD
204 alias 米ドル
205 alias $
206
207 apply tag foo
208
209 apply tag key: value
210 apply tag key:: 10 USD
211
212 end apply tag
213 ; key:: 10 USD
214
215 end apply tag
216 end apply tag
217
218 include path/to/other.ledger
219
220 2021/03/12 Opening Balance
221 ; initial balance
222 Assets:Bank = 1000 CHF
223 Equity
224
225 2021/05/14 ! (#txn-1) My Grocery
226 Expenses:Grocery 10 CHF
227 Expenses:Commissions 1 USD @ 0.98 CHF
228 ; Payee: My Card
229 ; My card took commission
230 ; :financial:経済:
231 Assets:Bank -20 CHF = 1 CHF
232 Expenses:Household = 0
233 Assets:Complex (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
234 Assets:Broker -2 SPINX {100 USD} [2010/12/23] (bought before Xmas) @ 10000 USD
235 Liabilities:Comma 5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF
236 "};
237 let mut output = Vec::new();
238 let mut r = input.as_bytes();
239
240 FormatOptions::new()
241 .format(&mut r, &mut output)
242 .expect("format() should succeeds");
243 let got = std::str::from_utf8(&output).expect("output should be valid UTF-8");
244 assert_eq!(want, got);
245 }
246}