1use crate::Directive;
6
7pub const DEFAULT_CURRENCIES: &[&str] = &["USD", "EUR", "GBP"];
9
10pub fn extract_accounts(directives: &[Directive]) -> Vec<String> {
12 extract_accounts_iter(directives.iter())
13}
14
15pub fn extract_accounts_iter<'a>(directives: impl Iterator<Item = &'a Directive>) -> Vec<String> {
22 let mut accounts = Vec::new();
23
24 for directive in directives {
25 match directive {
26 Directive::Open(open) => accounts.push(open.account.to_string()),
27 Directive::Close(close) => accounts.push(close.account.to_string()),
28 Directive::Balance(bal) => accounts.push(bal.account.to_string()),
29 Directive::Pad(pad) => {
30 accounts.push(pad.account.to_string());
31 accounts.push(pad.source_account.to_string());
32 }
33 Directive::Transaction(txn) => {
34 for posting in &txn.postings {
35 accounts.push(posting.account.to_string());
36 }
37 }
38 _ => {}
39 }
40 }
41
42 accounts.sort();
43 accounts.dedup();
44 accounts
45}
46
47pub fn extract_currencies(directives: &[Directive]) -> Vec<String> {
51 extract_currencies_iter(directives.iter())
52}
53
54pub fn extract_currencies_iter<'a>(directives: impl Iterator<Item = &'a Directive>) -> Vec<String> {
56 let mut currencies = Vec::new();
57
58 for directive in directives {
59 match directive {
60 Directive::Open(open) => {
61 for currency in &open.currencies {
62 currencies.push(currency.to_string());
63 }
64 }
65 Directive::Commodity(comm) => currencies.push(comm.currency.to_string()),
66 Directive::Balance(bal) => currencies.push(bal.amount.currency.to_string()),
67 Directive::Transaction(txn) => {
68 for posting in &txn.postings {
69 if let Some(ref units) = posting.units
70 && let Some(currency) = units.currency()
71 {
72 currencies.push(currency.to_string());
73 }
74 }
75 }
76 _ => {}
77 }
78 }
79
80 for currency in DEFAULT_CURRENCIES {
81 currencies.push((*currency).to_string());
82 }
83
84 currencies.sort();
85 currencies.dedup();
86 currencies
87}
88
89pub fn extract_payees(directives: &[Directive]) -> Vec<String> {
91 extract_payees_iter(directives.iter())
92}
93
94pub fn extract_payees_iter<'a>(directives: impl Iterator<Item = &'a Directive>) -> Vec<String> {
96 let mut payees = Vec::new();
97
98 for directive in directives {
99 if let Directive::Transaction(txn) = directive
100 && let Some(ref payee) = txn.payee
101 {
102 payees.push(payee.to_string());
103 }
104 }
105
106 payees.sort();
107 payees.dedup();
108 payees
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::{Amount, Balance, Commodity, Open, Pad, Posting, Transaction};
115 use chrono::NaiveDate;
116
117 fn date(y: i32, m: u32, d: u32) -> NaiveDate {
118 NaiveDate::from_ymd_opt(y, m, d).unwrap()
119 }
120
121 fn test_directives() -> Vec<Directive> {
122 vec![
123 Directive::Open(Open {
124 date: date(2024, 1, 1),
125 account: "Assets:Cash".into(),
126 currencies: vec!["USD".into(), "EUR".into()],
127 booking: None,
128 meta: Default::default(),
129 }),
130 Directive::Open(Open {
131 date: date(2024, 1, 1),
132 account: "Expenses:Food".into(),
133 currencies: vec![],
134 booking: None,
135 meta: Default::default(),
136 }),
137 Directive::Commodity(Commodity {
138 date: date(2024, 1, 1),
139 currency: "BTC".into(),
140 meta: Default::default(),
141 }),
142 Directive::Pad(Pad {
143 date: date(2024, 1, 2),
144 account: "Assets:Cash".into(),
145 source_account: "Equity:Opening".into(),
146 meta: Default::default(),
147 }),
148 Directive::Balance(Balance {
149 date: date(2024, 1, 3),
150 account: "Assets:Cash".into(),
151 amount: Amount::new(rust_decimal_macros::dec!(100), "CHF"),
152 tolerance: None,
153 meta: Default::default(),
154 }),
155 Directive::Transaction(Transaction {
156 date: date(2024, 1, 4),
157 flag: '*',
158 payee: Some("Corner Store".into()),
159 narration: "Groceries".into(),
160 tags: vec![],
161 links: vec![],
162 meta: Default::default(),
163 postings: vec![
164 Posting {
165 account: "Expenses:Food".into(),
166 units: Some(crate::IncompleteAmount::from(Amount::new(
167 rust_decimal_macros::dec!(25),
168 "USD",
169 ))),
170 cost: None,
171 price: None,
172 flag: None,
173 meta: Default::default(),
174 comments: vec![],
175 trailing_comments: vec![],
176 },
177 Posting {
178 account: "Assets:Cash".into(),
179 units: None,
180 cost: None,
181 price: None,
182 flag: None,
183 meta: Default::default(),
184 comments: vec![],
185 trailing_comments: vec![],
186 },
187 ],
188 trailing_comments: vec![],
189 }),
190 Directive::Transaction(Transaction {
191 date: date(2024, 1, 5),
192 flag: '*',
193 payee: Some("Coffee Shop".into()),
194 narration: "Coffee".into(),
195 tags: vec![],
196 links: vec![],
197 meta: Default::default(),
198 postings: vec![],
199 trailing_comments: vec![],
200 }),
201 ]
202 }
203
204 #[test]
205 fn test_empty_directives() {
206 let empty: Vec<Directive> = vec![];
207 assert!(extract_accounts(&empty).is_empty());
208 assert_eq!(extract_currencies(&empty).len(), DEFAULT_CURRENCIES.len());
209 assert!(extract_payees(&empty).is_empty());
210 }
211
212 #[test]
213 fn test_extract_accounts_from_directives() {
214 let directives = test_directives();
215 let accounts = extract_accounts(&directives);
216 assert_eq!(
217 accounts,
218 vec![
219 "Assets:Cash".to_string(),
220 "Equity:Opening".to_string(),
221 "Expenses:Food".to_string(),
222 ]
223 );
224 }
225
226 #[test]
227 fn test_extract_currencies_from_directives() {
228 let directives = test_directives();
229 let currencies = extract_currencies(&directives);
230 assert!(currencies.contains(&"BTC".to_string()));
232 assert!(currencies.contains(&"CHF".to_string()));
233 assert!(currencies.contains(&"EUR".to_string()));
234 assert!(currencies.contains(&"GBP".to_string()));
235 assert!(currencies.contains(&"USD".to_string()));
236 }
237
238 #[test]
239 fn test_extract_payees_from_directives() {
240 let directives = test_directives();
241 let payees = extract_payees(&directives);
242 assert_eq!(
243 payees,
244 vec!["Coffee Shop".to_string(), "Corner Store".to_string()]
245 );
246 }
247
248 #[test]
249 fn test_default_currencies_not_duplicated() {
250 let directives = test_directives();
252 let currencies = extract_currencies(&directives);
253 assert_eq!(
254 currencies.iter().filter(|c| *c == "USD").count(),
255 1,
256 "USD should appear exactly once"
257 );
258 }
259
260 #[test]
261 fn test_iter_variant_matches_slice_variant() {
262 let directives = test_directives();
263 assert_eq!(
264 extract_accounts(&directives),
265 extract_accounts_iter(directives.iter())
266 );
267 assert_eq!(
268 extract_currencies(&directives),
269 extract_currencies_iter(directives.iter())
270 );
271 assert_eq!(
272 extract_payees(&directives),
273 extract_payees_iter(directives.iter())
274 );
275 }
276}