hledger_parser/directive/transaction/
simple.rs1use 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}