1use std::collections::HashMap;
4use std::fmt;
5
6use chrono::NaiveDate;
7use pest::iterators::{Pair, Pairs};
8use pyo3::pyclass;
9use rust_decimal::Decimal;
10
11use crate::grammar::Rule;
12
13const BASE_DATE: &str = "0001-01-01";
14pub const DATE_FMT: &str = "%Y-%m-%d";
15
16type Ccy = String;
17pub type Account = String;
18
19pub type CcyBal = HashMap<Ccy, Decimal>;
20pub type AccBal = HashMap<Account, CcyBal>;
21pub type AccStatuses = HashMap<Account, (bool, Vec<Ccy>)>;
22
23#[pyclass]
24#[derive(Clone, Debug)]
25pub struct Options {
26 #[pyo3(get)]
27 pub title: String,
28 #[pyo3(get)]
29 pub operating_currency: String,
30}
31
32impl Default for Options {
33 fn default() -> Self {
34 Self {
35 title: "".to_string(),
36 operating_currency: "".to_string(),
37 }
38 }
39}
40
41impl Options {
42 pub fn update_from_entry(&mut self, entry: Pair<Rule>) {
43 let mut pairs = entry.clone().into_inner();
44 let key = pairs.next().unwrap().as_str();
45 let val = pairs.next().unwrap().as_str().to_string();
46 match key {
47 "title" => self.title = val,
48 "operating_currency" => self.operating_currency = val,
49 _ => panic!("Other options not handled yet"),
50 }
51 }
52}
53
54#[derive(Clone, Debug, Default)]
55pub struct DebugLine {
56 pub line: usize,
57}
58
59impl DebugLine {
60 pub fn new(line: usize) -> Self {
61 Self { line }
62 }
63}
64
65impl PartialEq for DebugLine {
66 fn eq(&self, _: &Self) -> bool {
67 true
68 }
69}
70
71impl fmt::Display for DebugLine {
72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 write!(f, "line:{line}", line = self.line)
74 }
75}
76
77#[derive(Clone, Debug)]
78pub struct Amount {
79 pub number: Decimal,
80 pub ccy: Ccy,
81}
82
83impl PartialEq for Amount {
84 fn eq(&self, other: &Self) -> bool {
85 self.ccy == other.ccy && (self.number - other.number).abs() > Decimal::new(1, 3)
87 }
88}
89impl Eq for Amount {}
90
91impl fmt::Display for Amount {
92 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93 write!(f, "{number} {ccy}", number = self.number, ccy = self.ccy,)
94 }
95}
96
97impl Amount {
98 pub fn new(number: Decimal, ccy: Ccy) -> Self {
99 Self { number, ccy }
100 }
101 pub fn from_entry(entry: Pair<Rule>) -> Self {
102 let mut pairs = entry.clone().into_inner();
103 let mut number: String = pairs.next().unwrap().as_str().to_string();
104 if number.contains(',') {
105 number = number.replace(',', "");
106 }
107 let number: Decimal = match number.parse() {
108 Ok(num) => num,
109 Err(_) => {
110 let (line, _) = entry.line_col();
111 panic!("Un-parseable decimal at line:{line}");
112 }
113 };
114 let ccy = pairs.next().unwrap().as_str().to_string();
115 Self { number, ccy }
116 }
117}
118
119#[derive(Clone, Debug, PartialEq)]
120pub struct ConfigCustom {
121 pub date: NaiveDate,
122 pub debug: DebugLine,
123}
124
125impl ConfigCustom {
126 pub fn from_entry(entry: Pair<Rule>) -> Self {
127 let (line, _) = entry.line_col();
128 let debug = DebugLine { line };
129 let date = NaiveDate::parse_from_str(BASE_DATE, DATE_FMT).unwrap();
130 Self { date, debug }
131 }
132}
133
134impl fmt::Display for ConfigCustom {
135 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136 write!(f, "-- ignore custom")
137 }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub struct Metadata {
142 pub key: String,
143 pub val: String,
144 pub debug: DebugLine,
145}
146
147impl Metadata {
148 pub fn from_entry(entry: Pair<Rule>) -> Self {
149 let mut pairs = entry.clone().into_inner();
150 let key = pairs.next().unwrap().as_str().to_string();
151 let val = pairs.next().unwrap().as_str().to_string();
152 let (line, _) = entry.line_col();
153 let debug = DebugLine { line };
154 Self { key, val, debug }
155 }
156}
157
158impl fmt::Display for Metadata {
159 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160 write!(f, " {key}:{val}", key = self.key, val = self.val,)
161 }
162}
163
164#[derive(Clone, Debug, PartialEq)]
165pub struct Commodity {
166 pub date: NaiveDate,
167 pub ccy: String,
168 pub meta: Vec<Metadata>,
169 pub debug: DebugLine,
170}
171
172impl Commodity {
173 pub fn from_entry(entry: Pair<Rule>) -> Self {
174 let mut pairs = entry.clone().into_inner();
175 let date = pairs.next().unwrap().as_str();
176 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
177 let ccy = pairs.next().unwrap().as_str().to_string();
178 let mut meta: Vec<Metadata> = Vec::new();
179 for pair in pairs {
180 if pair.as_rule() == Rule::metadata {
181 let p = Metadata::from_entry(pair);
182 meta.push(p)
183 }
184 }
185 let (line, _) = entry.line_col();
186 let debug = DebugLine { line };
187 Self {
188 date,
189 ccy,
190 meta,
191 debug,
192 }
193 }
194}
195
196impl fmt::Display for Commodity {
197 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198 let mut meta_string: String = String::new();
199 let m_slice = &self.meta[..];
200 for m in m_slice {
201 let line: &str = &format!("\n{m}");
202 meta_string.push_str(line);
203 }
204 write!(
205 f,
206 "{date} commodity {ccy}{meta}",
207 date = self.date,
208 ccy = self.ccy,
209 meta = meta_string,
210 )
211 }
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub struct Open {
216 pub date: NaiveDate,
217 pub account: Account,
218 pub ccys: Vec<Ccy>,
219 pub meta: Vec<Metadata>,
220 pub debug: DebugLine,
221}
222
223impl Open {
224 pub fn from_entry(entry: Pair<Rule>) -> Self {
225 let mut pairs = entry.clone().into_inner();
226 let date = pairs.next().unwrap().as_str();
227 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
228 let account = pairs.next().unwrap().as_str().to_string();
229 let (line, _) = entry.line_col();
230 let debug = DebugLine { line };
231
232 let mut ccys: Vec<Ccy> = Vec::new();
233 let mut meta: Vec<Metadata> = Vec::new();
234
235 for pair in pairs {
236 match pair.as_rule() {
237 Rule::ccy => {
238 let c = pair.as_str().to_owned();
239 ccys.push(c);
240 }
241 Rule::metadata => {
242 let m = Metadata::from_entry(pair);
243 meta.push(m);
244 }
245 _ => (),
246 }
247 }
248
249 Self {
250 date,
251 account,
252 ccys,
253 meta,
254 debug,
255 }
256 }
257}
258
259impl fmt::Display for Open {
260 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261 write!(
262 f,
263 "{date} {account}",
264 date = self.date,
265 account = self.account,
266 )
267 }
268}
269
270#[derive(Debug, Clone, PartialEq)]
271pub struct Close {
272 pub date: NaiveDate,
273 pub account: Account,
274 pub debug: DebugLine,
276}
277
278impl Close {
279 pub fn from_entry(entry: Pair<Rule>) -> Self {
280 let mut pairs = entry.clone().into_inner();
281 let date = pairs.next().unwrap().as_str();
282 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
283 let account = pairs.next().unwrap().as_str().to_string();
284 let (line, _) = entry.line_col();
285 let debug = DebugLine { line };
286 Self {
287 date,
288 account,
289 debug,
290 }
291 }
292}
293
294impl fmt::Display for Close {
295 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
296 write!(
297 f,
298 "{date} {account}",
299 date = self.date,
300 account = self.account,
301 )
302 }
303}
304
305#[derive(Debug, Clone, PartialEq)]
306pub struct Balance {
307 pub date: NaiveDate,
308 pub account: Account,
309 pub amount: Amount,
310 pub debug: DebugLine,
312}
313
314impl Balance {
315 pub fn from_entry(entry: Pair<Rule>) -> Self {
316 let mut pairs = entry.clone().into_inner();
317 let date = pairs.next().unwrap().as_str();
318 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
319 let account = pairs.next().unwrap().as_str().to_string();
320 let amount_entry = pairs.next().unwrap();
321 let amount = Amount::from_entry(amount_entry);
322 let (line, _) = entry.line_col();
323 let debug = DebugLine { line };
324 Self {
325 date,
326 account,
327 amount,
328 debug,
329 }
330 }
331}
332
333impl fmt::Display for Balance {
334 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
335 write!(
336 f,
337 "{date} {account} {amount}",
338 date = self.date,
339 account = self.account,
340 amount = self.amount,
341 )
342 }
343}
344
345#[derive(Debug, Clone, PartialEq)]
346pub struct Pad {
347 pub date: NaiveDate,
348 pub account_to: Account,
349 pub account_from: Account,
350 pub debug: DebugLine,
352}
353
354impl Pad {
355 pub fn from_entry(entry: Pair<Rule>) -> Self {
356 let mut pairs = entry.clone().into_inner();
357 let date = pairs.next().unwrap().as_str();
358 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
359 let account_to = pairs.next().unwrap().as_str().to_string();
360 let account_from = pairs.next().unwrap().as_str().to_string();
361 let (line, _) = entry.line_col();
362 let debug = DebugLine { line };
363 Self {
364 date,
365 account_to,
366 account_from,
367 debug,
368 }
369 }
370}
371
372impl fmt::Display for Pad {
373 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
374 write!(
375 f,
376 "{date} {account_to} {account_from}",
377 date = self.date,
378 account_to = self.account_to,
379 account_from = self.account_from,
380 )
381 }
382}
383
384#[derive(Clone, Debug, PartialEq)]
385pub struct Price {
386 pub date: NaiveDate,
387 pub commodity: String,
388 pub amount: Amount,
389 pub debug: DebugLine,
391}
392
393impl Price {
394 pub fn from_entry(entry: Pair<Rule>) -> Self {
395 let mut pairs = entry.clone().into_inner();
396 let date = pairs.next().unwrap().as_str();
397 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
398 let commodity = pairs.next().unwrap().as_str().to_string();
399 let amount_entry = pairs.next().unwrap();
400 let amount = Amount::from_entry(amount_entry);
401 let (line, _) = entry.line_col();
402 let debug = DebugLine { line };
403 Self {
404 date,
405 commodity,
406 amount,
407 debug,
408 }
409 }
410}
411
412impl fmt::Display for Price {
413 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
414 write!(
415 f,
416 "{date} {commodity} {amount}",
417 date = self.date,
418 commodity = self.commodity,
419 amount = self.amount,
420 )
421 }
422}
423
424#[derive(Clone, Debug, PartialEq)]
425pub struct Document {
426 pub date: NaiveDate,
427 pub account: Account,
428 pub path: String,
429 pub debug: DebugLine,
431}
432
433impl Document {
434 pub fn from_entry(entry: Pair<Rule>) -> Self {
435 let mut pairs = entry.clone().into_inner();
436 let date = pairs.next().unwrap().as_str();
437 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
438 let account = pairs.next().unwrap().as_str().to_string();
439 let path = pairs.next().unwrap().as_str().to_string();
440 let (line, _) = entry.line_col();
441 let debug = DebugLine { line };
442 Self {
443 date,
444 account,
445 path,
446 debug,
447 }
448 }
449}
450
451impl fmt::Display for Document {
452 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
453 write!(
454 f,
455 "{date} document {account} {path}",
456 date = self.date,
457 account = self.account,
458 path = self.path,
459 )
460 }
461}
462
463#[derive(Clone, Debug, PartialEq)]
464pub struct Note {
465 pub date: NaiveDate,
466 pub account: Account,
467 pub note: String,
468 pub debug: DebugLine,
470}
471
472impl Note {
473 pub fn from_entry(entry: Pair<Rule>) -> Self {
474 let mut pairs = entry.clone().into_inner();
475 let date = pairs.next().unwrap().as_str();
476 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
477 let account = pairs.next().unwrap().as_str().to_string();
478 let note = pairs.next().unwrap().as_str().to_string();
479 let (line, _) = entry.line_col();
480 let debug = DebugLine { line };
481 Self {
482 date,
483 account,
484 note,
485 debug,
486 }
487 }
488}
489
490impl fmt::Display for Note {
491 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
492 write!(
493 f,
494 "{date} note {account} {note}",
495 date = self.date,
496 account = self.account,
497 note = self.note,
498 )
499 }
500}
501
502#[derive(Clone, Debug, PartialEq)]
503pub struct Query {
504 pub date: NaiveDate,
505 pub name: String,
506 pub query: String,
507 pub debug: DebugLine,
508}
509
510impl Query {
511 pub fn from_entry(entry: Pair<Rule>) -> Self {
512 let mut pairs = entry.clone().into_inner();
513 let date = pairs.next().unwrap().as_str();
514 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
515 let name = pairs.next().unwrap().as_str().to_string();
516 let query = pairs.next().unwrap().as_str().to_string();
517 let (line, _) = entry.line_col();
518 let debug = DebugLine { line };
519 Self {
520 date,
521 name,
522 query,
523 debug,
524 }
525 }
526}
527
528impl fmt::Display for Query {
529 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
530 write!(
531 f,
532 "{date} query {name} {query}",
533 date = self.date,
534 name = self.name,
535 query = self.query,
536 )
537 }
538}
539
540#[derive(Clone, Debug, PartialEq)]
541pub struct Posting {
542 pub account: Account,
543 pub amount: Option<Amount>,
544 pub debug: Option<DebugLine>,
545}
546
547impl Posting {
548 pub fn new(account: Account, number: Decimal, ccy: Ccy) -> Self {
549 let amount = Some(Amount { number, ccy });
550 let debug = Default::default();
551 Self {
552 account,
553 amount,
554 debug,
555 }
556 }
557 pub fn from_entry(entry: Pair<Rule>) -> Self {
558 let mut pairs = entry.clone().into_inner();
559 let account = pairs.next().unwrap().as_str().to_string();
560 let amount = if pairs.peek().is_some() {
561 Some(Amount::from_entry(pairs.next().unwrap()))
562 } else {
563 None
564 };
565 let (line, _) = entry.line_col();
566 let debug = Some(DebugLine { line });
567 Self {
568 account,
569 amount,
570 debug,
571 }
572 }
573}
574
575impl fmt::Display for Posting {
576 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
577 let amount_str = match &self.amount {
578 Some(amount) => amount.to_string(),
579 None => String::new(),
580 };
581
582 write!(
583 f,
584 " {account} {amount}",
585 account = self.account,
586 amount = amount_str,
587 )
588 }
589}
590
591#[derive(Debug, Clone, PartialEq)]
592pub struct Transaction {
593 pub date: NaiveDate,
594 pub ty: String,
595 pub payee: Option<String>,
596 pub narration: String,
597 pub tag: Option<String>, pub link: Option<String>, pub postings: Vec<Posting>,
600 pub meta: Vec<Metadata>,
601 pub debug: DebugLine,
602 }
604
605fn get_payee_narration(pairs: &mut Pairs<Rule>) -> (Option<String>, String) {
606 let first_val = pairs.next().unwrap().as_str().to_string();
607 if let Some(pair) = pairs.peek() {
608 if pair.as_rule() == Rule::narration {
609 let narration = pairs.next().unwrap().as_str().to_string();
610 return (Some(first_val), narration);
611 }
612 }
613 (None, first_val)
614}
615
616impl Transaction {
617 pub fn from_entry(entry: Pair<Rule>) -> Self {
618 let mut pairs = entry.clone().into_inner();
619 let date = pairs.next().unwrap().as_str();
620 let date = NaiveDate::parse_from_str(date, DATE_FMT).unwrap();
621 let ty = pairs.next().unwrap().as_str().to_string();
622 let (payee, narration) = get_payee_narration(&mut pairs);
623 let mut postings: Vec<Posting> = Vec::new();
624 let mut meta: Vec<Metadata> = Vec::new();
625 let mut link: Option<String> = None;
626 let mut tag: Option<String> = None;
627 for pair in pairs {
628 match pair.as_rule() {
629 Rule::posting => {
630 postings.push(Posting::from_entry(pair));
631 }
632 Rule::metadata => {
633 meta.push(Metadata::from_entry(pair));
634 }
635 Rule::link => {
636 link = Some(entry.as_str().to_owned());
637 }
638 Rule::tag => {
639 tag = Some(entry.as_str().to_owned());
640 }
641 _ => {
642 let (line, _) = entry.line_col();
643 let debug = DebugLine::new(line);
644 unreachable!("Unexpected entry in Transaction, abort.\n{debug}");
645 }
646 }
647 }
648 let (line, _) = entry.line_col();
649 let debug = DebugLine { line };
650 Self {
651 date,
652 ty,
653 payee,
654 narration,
655 tag,
656 link,
657 postings,
658 meta,
659 debug,
660 }
661 }
662 pub fn from_pad(pad: Pad, amount: Amount) -> Self {
663 let date = pad.date;
664 let ty = String::from("pad");
665 let payee = None;
666 let narration = String::new();
667 let debug: DebugLine = DebugLine::default();
668 let link = None;
669 let tag = None;
670 let amount2 = Some(Amount {
671 number: -amount.clone().number,
672 ccy: amount.clone().ccy,
673 });
674 let amount = Some(amount);
675 let p1 = Posting {
676 account: pad.account_to,
677 amount: amount.clone(),
678 debug: Some(debug.clone()),
679 };
680 let p2 = Posting {
681 account: pad.account_from,
682 amount: amount2,
683 debug: Some(debug.clone()),
684 };
685 let postings = vec![p1, p2];
686 let meta: Vec<Metadata> = Vec::new();
687 Self {
688 date,
689 ty,
690 payee,
691 narration,
692 link,
693 tag,
694 postings,
695 meta,
696 debug: debug.clone(),
697 }
698 }
699}
700
701impl fmt::Display for Transaction {
702 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
703 let payee_str = match &self.payee {
704 Some(payee) => payee.as_str(),
705 None => "",
706 };
707
708 let mut posting_string = String::new();
709 let slice = &self.postings[..];
710 for p in slice {
711 let line: &str = &format!("\n{p}");
712 posting_string.push_str(line);
713 }
714
715 let mut meta_string = String::new();
716 let m_slice = &self.meta[..];
717 for m in m_slice {
718 let line: &str = &format!("\n{m}");
719 meta_string.push_str(line);
720 }
721
722 write!(
723 f,
724 "{date} {ty} {payee} {narration}{meta}{postings}",
725 date = self.date,
726 ty = self.ty,
727 payee = payee_str,
728 narration = self.narration,
729 meta = meta_string,
730 postings = posting_string,
731 )
732 }
733}
734
735#[derive(Clone, Debug)]
738pub enum Directive {
739 ConfigCustom(ConfigCustom),
740 Commodity(Commodity),
741 Open(Open),
742 Close(Close),
743 Balance(Balance),
744 Pad(Pad),
745 Price(Price),
746 Document(Document),
747 Note(Note),
748 Query(Query),
749 Transaction(Transaction),
750}
751
752impl Directive {
753 pub fn date(&self) -> &NaiveDate {
754 match self {
755 Directive::ConfigCustom(d) => &d.date,
756 Directive::Commodity(d) => &d.date,
757 Directive::Open(d) => &d.date,
758 Directive::Close(d) => &d.date,
759 Directive::Balance(d) => &d.date,
760 Directive::Pad(d) => &d.date,
761 Directive::Price(d) => &d.date,
762 Directive::Document(d) => &d.date,
763 Directive::Note(d) => &d.date,
764 Directive::Query(d) => &d.date,
765 Directive::Transaction(d) => &d.date,
766 }
767 }
768 pub fn order(&self) -> i8 {
771 match self {
772 Directive::Open(_) => -2,
773 Directive::Balance(_) => -1,
774 Directive::ConfigCustom(_) => 0,
775 Directive::Commodity(_) => 0,
776 Directive::Pad(_) => 0,
777 Directive::Price(_) => 0,
778 Directive::Transaction(_) => 0,
779 Directive::Document(_) => 1,
780 Directive::Note(_) => 1,
781 Directive::Query(_) => 1,
782 Directive::Close(_) => 2,
783 }
784 }
785}
786
787impl fmt::Display for Directive {
788 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
789 match self {
790 Directive::ConfigCustom(d) => write!(f, "{d}"),
791 Directive::Commodity(d) => write!(f, "{d}"),
792 Directive::Open(d) => write!(f, "{d}"),
793 Directive::Close(d) => write!(f, "{d}"),
794 Directive::Balance(d) => write!(f, "{d}"),
795 Directive::Pad(d) => write!(f, "{d}"),
796 Directive::Price(d) => write!(f, "{d}"),
797 Directive::Document(d) => write!(f, "{d}"),
798 Directive::Note(d) => write!(f, "{d}"),
799 Directive::Query(d) => write!(f, "{d}"),
800 Directive::Transaction(d) => write!(f, "{d}"),
801 }
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use crate::{ledger::Ledger, loader};
809
810 #[test]
811 fn test_open() {
812 let text = r#"2023-01-01 open Assets:Bank GBP"#;
813 let entries = loader::load(&text);
814 let Ledger {
815 dirs,
816 errs: _,
817 opts: _,
818 } = loader::consume(entries);
819 let date = NaiveDate::parse_from_str("2023-01-01", DATE_FMT).unwrap();
820 let a = &Open {
821 date,
822 account: String::from("Assets:Bank"),
823 ccys: vec!["GBP".to_owned()],
824 meta: Vec::new(),
825 debug: DebugLine { line: 2 },
826 };
827 let got = &dirs[0];
828 match got {
829 Directive::Open(i) => {
830 assert!(i == a);
831 }
832 _ => assert!(false, "Found wrong directive type"),
833 }
834 }
835
836 #[test]
837 #[should_panic]
838 fn test_bad_amount() {
839 let text = r#"
840 2023-01-01 price FOO 1,.0.0 BAR
841 "#;
842 let mut entries = loader::load(&text);
843 let entry = entries.next().unwrap();
844 Price::from_entry(entry);
845 }
846}