hledger_parser/directive/transaction/
simple.rs

1use chumsky::prelude::*;
2
3use crate::component::date::simple::date;
4use crate::component::whitespace::whitespace;
5use crate::directive::transaction::header::header;
6use crate::directive::transaction::posting::{posting, Posting};
7use crate::directive::transaction::status::Status;
8use crate::state::State;
9
10#[derive(Clone, Debug, PartialEq)]
11pub struct Transaction {
12    pub date: chrono::NaiveDate,
13    pub status: Option<Status>,
14    pub code: Option<String>,
15    pub payee: String,
16    pub description: Option<String>,
17    pub postings: Vec<Posting>,
18}
19
20pub fn transaction<'a>(
21) -> impl Parser<'a, &'a str, Transaction, extra::Full<Rich<'a, char>, State, ()>> {
22    let header = date()
23        .then_ignore(whitespace().repeated())
24        .then(header().or_not());
25
26    header
27        .then(
28            posting()
29                .separated_by(text::newline())
30                .allow_leading()
31                .collect::<Vec<_>>(),
32        )
33        .map(|((date, header), postings)| Transaction {
34            date,
35            status: header.as_ref().and_then(|h| h.status.clone()),
36            code: header.as_ref().and_then(|h| h.code.clone()),
37            payee: header.as_ref().map_or(String::new(), |h| h.payee.clone()),
38            description: header.as_ref().and_then(|h| h.description.clone()),
39            postings,
40        })
41}
42
43#[cfg(test)]
44mod tests {
45    use rust_decimal::Decimal;
46
47    use crate::component::amount::Amount;
48
49    use super::*;
50
51    #[test]
52    fn full() {
53        let result = transaction()
54            .then_ignore(end())
55            .parse(
56                "2008/01/01 * (123) salary | january ; transaction comment
57                                                 ; same comment second line
58    assets:bank:checking   $1  ; posting comment
59                               ; same comment second line
60    income:salary  ",
61            )
62            .into_result();
63        assert_eq!(
64            result,
65            Ok(Transaction {
66                date: chrono::NaiveDate::from_ymd_opt(2008, 1, 1).unwrap(),
67                code: Some(String::from("123")),
68                status: Some(Status::Cleared),
69                payee: String::from("salary"),
70                description: Some(String::from("january ")),
71                postings: vec![
72                    Posting {
73                        status: None,
74                        account_name: vec![
75                            String::from("assets"),
76                            String::from("bank"),
77                            String::from("checking"),
78                        ],
79                        amount: Some(Amount {
80                            quantity: Decimal::new(1, 0),
81                            commodity: String::from("$"),
82                        }),
83                        price: None,
84                        assertion: None,
85                        is_virtual: false,
86                    },
87                    Posting {
88                        status: None,
89                        account_name: vec![String::from("income"), String::from("salary"),],
90                        amount: None,
91                        price: None,
92                        assertion: None,
93                        is_virtual: false,
94                    }
95                ],
96            })
97        );
98    }
99
100    #[test]
101    fn simple() {
102        let result = transaction()
103            .then_ignore(end())
104            .parse(
105                "2008/01/01 salary
106    assets:bank:checking   $1
107    income:salary  ",
108            )
109            .into_result();
110        assert_eq!(
111            result,
112            Ok(Transaction {
113                date: chrono::NaiveDate::from_ymd_opt(2008, 1, 1).unwrap(),
114                code: None,
115                status: None,
116                payee: String::from("salary"),
117                description: None,
118                postings: vec![
119                    Posting {
120                        status: None,
121                        account_name: vec![
122                            String::from("assets"),
123                            String::from("bank"),
124                            String::from("checking"),
125                        ],
126                        amount: Some(Amount {
127                            quantity: Decimal::new(1, 0),
128                            commodity: String::from("$"),
129                        }),
130                        price: None,
131                        assertion: None,
132                        is_virtual: false,
133                    },
134                    Posting {
135                        status: None,
136                        account_name: vec![String::from("income"), String::from("salary")],
137                        amount: None,
138                        price: None,
139                        assertion: None,
140                        is_virtual: false,
141                    }
142                ],
143            })
144        );
145    }
146
147    #[test]
148    fn just_date() {
149        let result = transaction()
150            .then_ignore(end())
151            .parse("2008/1/1")
152            .into_result();
153        assert_eq!(
154            result,
155            Ok(Transaction {
156                date: chrono::NaiveDate::from_ymd_opt(2008, 1, 1).unwrap(),
157                code: None,
158                status: None,
159                payee: String::new(),
160                description: None,
161                postings: vec![],
162            })
163        );
164    }
165}