1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! format functionalities of Ledger format files.

use crate::{
    parse::{parse_ledger, ParseError},
    repl::display::DisplayContext,
};

use std::io::{Read, Write};

/// Error occured during Format.
#[derive(thiserror::Error, Debug)]
pub enum FormatError {
    #[error("failed to perform IO")]
    IO(#[from] std::io::Error),
    #[error("failed to parse the file")]
    Parse(#[from] Box<ParseError>),
    // TODO: Remove this once supported.
    #[error("recursive format isn't supported yet")]
    UnsupportedRecursiveFormat,
}

/// Options to control format functionalities.
#[derive(Debug, Default)]
pub struct FormatOptions {
    recursive: bool,
}

impl FormatOptions {
    /// Create a default FormatOptions instance.
    /// All options are initially set to `false`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets `recursive` option to given value.
    /// If `recursive` is set to `true`, it'll try to follow `include` directive.
    pub fn recursive(&mut self, recursive: bool) -> &mut Self {
        self.recursive = recursive;
        self
    }

    /// Formats given `Read` instance and write it back to `Write`.
    pub fn format<R, W>(&self, r: &mut R, w: &mut W) -> Result<(), FormatError>
    where
        R: Read,
        W: Write,
    {
        if self.recursive {
            return Err(FormatError::UnsupportedRecursiveFormat);
        }
        let mut buf = String::new();
        r.read_to_string(&mut buf)?;
        // TODO: Grab DisplayContext externally, or from LedgerEntry.
        let ctx = DisplayContext::default();
        for txn in parse_ledger(&buf) {
            let txn = txn.map_err(Box::new)?;
            writeln!(w, "{}", ctx.as_display(&txn))?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use indoc::indoc;
    use pretty_assertions::assert_eq;

    #[test]
    fn format_succeeds_transaction_without_lot_price() {
        let input = indoc! {"
            ; Top
            ; level
            #comment
            %can
            |have several prefixes.

            ; second
            ; round

            account  Foo\t
             alias Bar\t
               note これは何でしょうか
              alias Baz

            commodity  USD\t
             \talias 米ドル\t
             \talias $\t

            apply    tag   foo
            apply tag key: value
            apply tag key:: 10 USD

            end  apply   tag

            end apply tag
            end apply tag

            include        path/to/other.ledger

            2021/03/12 Opening Balance  ; initial balance
             Assets:Bank     = 1000 CHF
             Equity

            2021/05/14 !(#txn-1) My Grocery
                Expenses:Grocery\t10 CHF
                Expenses:Commissions    1 USD   @ 0.98 CHF ; Payee: My Card
                ; My card took commission
                ; :financial:経済:
                Assets:Bank  -20 CHF=1CHF
                Expenses:Household  = 0
                Assets:Complex  (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
                Assets:Broker  -2 SPINX (bought before Xmas) {100 USD} [2010/12/23] @ 10000 USD
                Liabilities:Comma      5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF
        "};
        // TODO: 1. guess commodity width if not available.
        // TOOD: 2. remove trailing space on non-commodity value.
        let want = indoc! {"
            ; Top
            ; level
            ;comment
            ;can
            ;have several prefixes.

            ; second
            ; round

            account Foo
                alias Bar
                note これは何でしょうか
                alias Baz

            commodity USD
                alias 米ドル
                alias $

            apply tag foo

            apply tag key: value

            apply tag key:: 10 USD

            end apply tag

            end apply tag

            end apply tag

            include path/to/other.ledger

            2021/03/12 Opening Balance
                ; initial balance
                Assets:Bank                                          = 1000 CHF
                Equity

            2021/05/14 ! (#txn-1) My Grocery
                Expenses:Grocery                              10 CHF
                Expenses:Commissions                           1 USD @ 0.98 CHF
                ; Payee: My Card
                ; My card took commission
                ; :financial:経済:
                Assets:Bank                                  -20 CHF = 1 CHF
                Expenses:Household                               = 0
                Assets:Complex                        (-10 * 2.1 $) @ (1 $ + 1 $) = 2.5 $
                Assets:Broker                                 -2 SPINX {100 USD} [2010/12/23] (bought before Xmas) @ 10000 USD
                Liabilities:Comma                       5,678.00 CHF @ 1,000,000 JPYRIN = -123,456.12 CHF

        "};
        let mut output = Vec::new();
        let mut r = input.as_bytes();

        FormatOptions::new()
            .format(&mut r, &mut output)
            .expect("format() should succeeds");
        let got = std::str::from_utf8(&output).expect("output should be valid UTF-8");
        assert_eq!(want, got);
    }
}