ledger_parser/
serializer.rs

1use crate::model::*;
2use std::io;
3
4#[non_exhaustive]
5pub struct SerializerSettings {
6    pub indent: String,
7    pub eol: String,
8
9    pub transaction_date_format: String,
10    pub commodity_date_format: String,
11
12    /// Should single line posting comments be printed on the same line as the posting?
13    pub posting_comments_sameline: bool,
14}
15
16impl SerializerSettings {
17    pub fn with_indent(mut self, indent: &str) -> Self {
18        indent.clone_into(&mut self.indent);
19        self
20    }
21
22    pub fn with_eol(mut self, eol: &str) -> Self {
23        eol.clone_into(&mut self.eol);
24        self
25    }
26}
27
28impl Default for SerializerSettings {
29    fn default() -> Self {
30        Self {
31            indent: "  ".to_owned(),
32            eol: "\n".to_owned(),
33            transaction_date_format: "%Y-%m-%d".to_owned(),
34            commodity_date_format: "%Y-%m-%d %H:%M:%S".to_owned(),
35            posting_comments_sameline: false,
36        }
37    }
38}
39
40pub trait Serializer {
41    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
42    where
43        W: io::Write;
44
45    fn to_string_pretty(&self, settings: &SerializerSettings) -> String {
46        let mut res = Vec::new();
47        self.write(&mut res, settings).unwrap();
48        return std::str::from_utf8(&res).unwrap().to_owned();
49    }
50}
51
52impl Serializer for Ledger {
53    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
54    where
55        W: io::Write,
56    {
57        for item in &self.items {
58            item.write(writer, settings)?;
59        }
60        Ok(())
61    }
62}
63
64impl Serializer for LedgerItem {
65    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
66    where
67        W: io::Write,
68    {
69        match self {
70            LedgerItem::EmptyLine => write!(writer, "{}", settings.eol)?,
71            LedgerItem::LineComment(comment) => write!(writer, "; {}{}", comment, settings.eol)?,
72            LedgerItem::Transaction(transaction) => {
73                transaction.write(writer, settings)?;
74                write!(writer, "{}", settings.eol)?;
75            }
76            LedgerItem::CommodityPrice(commodity_price) => {
77                commodity_price.write(writer, settings)?;
78                write!(writer, "{}", settings.eol)?;
79            }
80            LedgerItem::Include(file) => write!(writer, "include {}{}", file, settings.eol)?,
81        }
82        Ok(())
83    }
84}
85
86impl Serializer for Transaction {
87    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
88    where
89        W: io::Write,
90    {
91        write!(
92            writer,
93            "{}",
94            self.date.format(&settings.transaction_date_format)
95        )?;
96
97        if let Some(effective_date) = self.effective_date {
98            write!(
99                writer,
100                "={}",
101                effective_date.format(&settings.transaction_date_format)
102            )?;
103        }
104
105        if let Some(ref status) = self.status {
106            write!(writer, " ")?;
107            status.write(writer, settings)?;
108        }
109
110        if let Some(ref code) = self.code {
111            write!(writer, " ({})", code)?;
112        }
113
114        // for the None case, ledger would print "<Unspecified payee>"
115        if let Some(ref description) = self.description {
116            if !description.is_empty() {
117                write!(writer, " {}", description)?;
118            }
119        }
120
121        if let Some(ref comment) = self.comment {
122            for comment in comment.split('\n') {
123                write!(writer, "{}{}; {}", settings.eol, settings.indent, comment)?;
124            }
125        }
126
127        for tag in &self.posting_metadata.tags {
128            write!(writer, "{}{}; {}", settings.eol, settings.indent, tag.name)?;
129            if let Some(ref value) = tag.value {
130                write!(writer, ": {}", value)?;
131            };
132        }
133
134        for posting in &self.postings {
135            write!(writer, "{}{}", settings.eol, settings.indent)?;
136            posting.write(writer, settings)?;
137        }
138
139        Ok(())
140    }
141}
142
143impl Serializer for TransactionStatus {
144    fn write<W>(&self, writer: &mut W, _settings: &SerializerSettings) -> Result<(), io::Error>
145    where
146        W: io::Write,
147    {
148        match self {
149            TransactionStatus::Pending => write!(writer, "!"),
150            TransactionStatus::Cleared => write!(writer, "*"),
151        }
152    }
153}
154
155impl Serializer for Posting {
156    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
157    where
158        W: io::Write,
159    {
160        if let Some(ref status) = self.status {
161            status.write(writer, settings)?;
162            write!(writer, " ")?;
163        }
164
165        match self.reality {
166            Reality::Real => write!(writer, "{}", self.account)?,
167            Reality::BalancedVirtual => write!(writer, "[{}]", self.account)?,
168            Reality::UnbalancedVirtual => write!(writer, "({})", self.account)?,
169        }
170
171        if self.amount.is_some() || self.balance.is_some() {
172            write!(writer, "{}", settings.indent)?;
173        }
174
175        if let Some(ref amount) = self.amount {
176            amount.write(writer, settings)?;
177        }
178
179        if let Some(ref balance) = self.balance {
180            write!(writer, " = ")?;
181            balance.write(writer, settings)?;
182        }
183
184        for tag in &self.metadata.tags {
185            write!(writer, "{}; {}", settings.indent, tag.name)?;
186            if let Some(ref value) = tag.value {
187                write!(writer, ": {}", value)?;
188            };
189        }
190
191        if let Some(ref comment) = self.comment {
192            if !comment.contains('\n') && settings.posting_comments_sameline {
193                write!(writer, "{}; {}", settings.indent, comment)?;
194            } else {
195                for comment in comment.split('\n') {
196                    write!(writer, "{}{}; {}", settings.eol, settings.indent, comment)?;
197                }
198            }
199        }
200
201        Ok(())
202    }
203}
204
205impl Serializer for PostingAmount {
206    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
207    where
208        W: io::Write,
209    {
210        self.amount.write(writer, settings)?;
211
212        if let Some(ref lot_price) = self.lot_price {
213            match lot_price {
214                Price::Unit(amount) => {
215                    write!(writer, " {{")?;
216                    amount.write(writer, settings)?;
217                    write!(writer, "}}")?;
218                }
219                Price::Total(amount) => {
220                    write!(writer, " {{{{")?;
221                    amount.write(writer, settings)?;
222                    write!(writer, "}}}}")?;
223                }
224            }
225        }
226
227        if let Some(ref lot_price) = self.price {
228            match lot_price {
229                Price::Unit(amount) => {
230                    write!(writer, " @ ")?;
231                    amount.write(writer, settings)?;
232                }
233                Price::Total(amount) => {
234                    write!(writer, " @@ ")?;
235                    amount.write(writer, settings)?;
236                }
237            }
238        }
239
240        Ok(())
241    }
242}
243
244impl Serializer for Amount {
245    fn write<W>(&self, writer: &mut W, _settings: &SerializerSettings) -> Result<(), io::Error>
246    where
247        W: io::Write,
248    {
249        match self.commodity.position {
250            CommodityPosition::Left => write!(writer, "{}{}", self.commodity.name, self.quantity),
251            CommodityPosition::Right => write!(writer, "{} {}", self.quantity, self.commodity.name),
252        }
253    }
254}
255
256impl Serializer for Balance {
257    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
258    where
259        W: io::Write,
260    {
261        match self {
262            Balance::Zero => write!(writer, "0"),
263            Balance::Amount(ref balance) => balance.write(writer, settings),
264        }
265    }
266}
267
268impl Serializer for CommodityPrice {
269    fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
270    where
271        W: io::Write,
272    {
273        write!(
274            writer,
275            "P {} {} ",
276            self.datetime.format(&settings.commodity_date_format),
277            self.commodity_name
278        )?;
279        self.amount.write(writer, settings)?;
280        Ok(())
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn serialize_transaction() {
290        let ledger = crate::parse(
291            r#"2018/10/01    (123)     Payee 123
292  TEST:ABC 123        $1.20
293    TEST:DEF 123"#,
294        )
295        .expect("parsing test transaction");
296
297        let mut buf = Vec::new();
298        ledger
299            .write(&mut buf, &SerializerSettings::default())
300            .expect("serializing test transaction");
301
302        assert_eq!(
303            String::from_utf8(buf).unwrap(),
304            r#"2018-10-01 (123) Payee 123
305  TEST:ABC 123  $1.20
306  TEST:DEF 123
307"#
308        );
309    }
310
311    #[test]
312    fn serialize_with_custom_date_format() {
313        let ledger = crate::parse(
314            r#"2018-10-01    (123)     Payee 123
315  TEST:ABC 123        $1.20
316    TEST:DEF 123"#,
317        )
318        .expect("parsing test transaction");
319
320        let mut buf = Vec::new();
321        ledger
322            .write(
323                &mut buf,
324                &SerializerSettings {
325                    transaction_date_format: "%Y/%m/%d".to_owned(),
326                    ..SerializerSettings::default()
327                },
328            )
329            .expect("serializing test transaction");
330
331        assert_eq!(
332            String::from_utf8(buf).unwrap(),
333            r#"2018/10/01 (123) Payee 123
334  TEST:ABC 123  $1.20
335  TEST:DEF 123
336"#
337        );
338    }
339
340    #[test]
341    fn serialize_tags() {
342        let ledger = crate::parse(
343            r#"2018-10-01   (123)    Payee 123
344  ;   Tag1:   Foo bar
345  TEST:ABC 123       $1.20      ;   Tag2:  Fizz bazz
346  TEST:DEF 123"#,
347        )
348        .expect("parsing test transaction");
349
350        let mut buf = Vec::new();
351        ledger
352            .write(&mut buf, &SerializerSettings::default())
353            .expect("serializing test transaction");
354
355        assert_eq!(
356            String::from_utf8(buf).unwrap(),
357            r#"2018-10-01 (123) Payee 123
358  ; Tag1: Foo bar
359  TEST:ABC 123  $1.20  ; Tag2: Fizz bazz
360  TEST:DEF 123
361"#
362        );
363    }
364
365    #[test]
366    fn serialize_posting_comments_sameline() {
367        let ledger = crate::parse(
368            r#"2018-10-01 Payee 123
369  TEST:ABC 123  $1.20
370  ; This is a one-line comment
371  TEST:DEF 123
372  ; This is a two-
373  ; line comment"#,
374        )
375        .expect("parsing test transaction");
376
377        let mut buf = Vec::new();
378        ledger
379            .write(
380                &mut buf,
381                &SerializerSettings {
382                    posting_comments_sameline: true,
383                    ..SerializerSettings::default()
384                },
385            )
386            .expect("serializing test transaction");
387
388        assert_eq!(
389            String::from_utf8(buf).unwrap(),
390            r#"2018-10-01 Payee 123
391  TEST:ABC 123  $1.20  ; This is a one-line comment
392  TEST:DEF 123
393  ; This is a two-
394  ; line comment
395"#
396        );
397    }
398}