okane_core/
format.rs

1//! format functionalities of Ledger format files.
2
3use crate::{
4    parse::{parse_ledger, ParseError, ParseOptions},
5    syntax::{self, display::DisplayContext},
6};
7
8use std::io::{Read, Write};
9
10/// Error occured during Format.
11#[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    // TODO: Remove this once supported.
18    #[error("recursive format isn't supported yet")]
19    UnsupportedRecursiveFormat,
20}
21
22/// Options to control format functionalities.
23#[derive(Debug, Default)]
24pub struct FormatOptions {
25    recursive: bool,
26}
27
28impl FormatOptions {
29    /// Create a default FormatOptions instance.
30    /// All options are initially set to `false`.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Sets `recursive` option to given value.
36    /// If `recursive` is set to `true`, it'll try to follow `include` directive.
37    pub fn recursive(&mut self, recursive: bool) -> &mut Self {
38        self.recursive = recursive;
39        self
40    }
41
42    /// Formats given `Read` instance and write it back to `Write`.
43    pub fn format<R, W>(&self, r: &mut R, w: &mut W) -> Result<(), FormatError>
44    where
45        R: Read,
46        W: Write,
47    {
48        // TODO: Implement recursive formatting.
49        if self.recursive {
50            return Err(FormatError::UnsupportedRecursiveFormat);
51        }
52        let mut buf = String::new();
53        r.read_to_string(&mut buf)?;
54        // TODO: Grab DisplayContext externally, or from LedgerEntry.
55        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        // TODO: 1. guess commodity width if not available.
119        // TOOD: 2. remove trailing space on non-commodity value.
120        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}