1use core::panic;
19use std::{
20 env,
21 io::{BufRead, BufReader, Read},
22 path::PathBuf,
23 str::FromStr,
24 todo,
25};
26
27use anyhow::Error;
28use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
29
30use crate::{
31 amount::{Amount, Quantity},
32 annotate::Annotation,
33 journal::Journal,
34 post::Post,
35 scanner::{self, PostTokens},
36 xact::Xact,
37};
38
39pub const ISO_DATE_FORMAT: &str = "%Y-%m-%d";
40pub const ISO_TIME_FORMAT: &str = "%H:%M:%S";
41
42pub(crate) fn read_into_journal<T: Read>(source: T, journal: &mut Journal) {
43 let mut parser = Parser::new(source, journal);
44
45 parser.parse();
46}
47
48pub(crate) fn parse_date(date_str: &str) -> NaiveDate {
50 NaiveDate::parse_from_str(date_str, ISO_DATE_FORMAT).expect("date parsed")
53}
54
55pub fn parse_datetime(iso_str: &str) -> Result<NaiveDateTime, anyhow::Error> {
57 Ok(NaiveDateTime::new(
58 NaiveDate::parse_from_str(iso_str, ISO_DATE_FORMAT)?,
59 NaiveTime::MIN,
60 ))
61}
62
63pub fn parse_amount(amount_str: &str, journal: &mut Journal) -> Option<Amount> {
64 let (tokens, _) = scanner::scan_amount(amount_str);
65 parse_amount_parts(tokens.quantity, tokens.symbol, journal)
66}
67
68pub fn parse_amount_parts(
72 quantity: &str,
73 commodity: &str,
74 journal: &mut Journal,
75) -> Option<Amount> {
76 let commodity_ptr = journal.commodity_pool.find_or_create(commodity, None);
78
79 if let Some(quantity) = Quantity::from_str(quantity) {
80 Some(Amount::new(quantity, Some(commodity_ptr)))
81 } else {
82 None
83 }
84
85}
86
87pub(crate) struct Parser<'j, T: Read> {
88 pub journal: &'j mut Journal,
89
90 reader: BufReader<T>,
91 buffer: String,
92}
93
94impl<'j, T: Read> Parser<'j, T> {
95 pub fn new(source: T, journal: &'j mut Journal) -> Self {
96 let reader = BufReader::new(source);
97 let buffer = String::new();
99
100 Self {
101 reader,
102 buffer,
103 journal,
104 }
105 }
106
107 pub fn parse(&mut self) {
108 loop {
109 match self.reader.read_line(&mut self.buffer) {
110 Err(err) => {
111 println!("Error: {:?}", err);
112 break;
113 }
114 Ok(0) => {
115 break;
117 }
118 Ok(_) => {
119 match self.read_next_directive() {
123 Ok(_) => (), Err(err) => {
125 log::error!("Error: {:?}", err);
126 println!("Error: {:?}", err);
127 break;
128 }
129 };
130 }
131 }
132
133 self.buffer.clear();
135 }
136 }
137
138 fn read_next_directive(&mut self) -> Result<(), String> {
139 if self.buffer == "\r\n" || self.buffer == "\n" {
145 return Ok(());
146 }
147
148 match self.buffer.chars().peekable().peek().unwrap() {
150 ';' | '#' | '*' | '|' => {
152 return Ok(());
154 }
155
156 '-' => {
157 }
159
160 '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
161 let _ = self.xact_directive();
163 }
164
165 ' ' | '\t' => {
166 todo!("complete")
167 }
168
169 c => {
171 match c {
175 'P' => {
177 self.price_xact_directive();
179 }
180
181 c => {
182 log::warn!("not handled: {:?}", c);
183 todo!("handle other directives");
184 }
185 }
186 todo!("the rest")
187 }
188 }
189
190 Ok(())
191 }
192
193 fn general_directive(&self) -> bool {
196 let mut iter = self.buffer.split_whitespace();
200 let Some(directive) = iter.next() else { panic!("no directive?") };
201 let argument = iter.next();
202
203 match directive.chars().peekable().peek().unwrap() {
216 'a' => {
217 todo!("a");
218 }
219
220 'i' => match self.buffer.as_str() {
222 "include" => {
223 self.include_directive(argument.unwrap());
224 return true;
225 }
226 "import" => {
227 todo!("import directive")
228 }
229 _ => (),
230 },
231
232 _ => {
234 todo!("handle")
235 }
236 }
237
238 false
241 }
242
243 fn price_xact_directive(&mut self) {
244 self.journal
246 .commodity_pool
247 .parse_price_directive(&self.buffer);
248 }
249
250 fn create_xact(&mut self) -> *const Xact {
251 let tokens = scanner::tokenize_xact_header(&self.buffer);
252 let xact = Xact::create(tokens[0], tokens[1], tokens[2], tokens[3]);
253
254 let new_ref = self.journal.add_xact(xact);
256
257 new_ref
258 }
259
260 fn xact_directive(&mut self) -> Result<(), Error> {
261 let xact_ptr = self.create_xact() as *mut Xact;
262
263 loop {
266 self.buffer.clear(); match self.reader.read_line(&mut self.buffer) {
268 Err(e) => {
269 println!("Error: {:?}", e);
270 break;
271 }
272 Ok(0) => {
273 log::debug!("0-length buffer");
275 break;
276 }
277 Ok(_) => {
278 if self.buffer.is_empty() {
279 todo!("Check what happens here")
281 }
282
283 match self.buffer.chars().peekable().peek() {
285 Some(' ') => {
286 let input = self.buffer.trim_start();
288
289 if input.is_empty() {
291 break;
292 }
293
294 match input.chars().peekable().peek() {
296 Some(';') => {
297 self.parse_trailing_note(xact_ptr);
298 }
299 _ => {
300 parse_post(input, xact_ptr, &mut self.journal)?;
301 }
302 }
303 }
304 Some('\r') | Some('\n') => {
305 break;
307 }
308 _ => {
309 panic!("should not happen")
310 }
311 }
312 }
313 }
314
315 self.buffer.clear();
317 }
318
319 crate::xact::finalize(xact_ptr, &mut self.journal);
321
322 Ok(())
323 }
324
325 fn include_directive(&self, argument: &str) {
326 let mut filename: PathBuf;
327
328 if argument.starts_with('/') || argument.starts_with('\\') || argument.starts_with('~') {
330 filename = PathBuf::from_str(argument).unwrap();
331 } else {
332 filename = env::current_dir().unwrap();
338
339 filename.set_file_name(argument);
340 }
341
342 let mut file_found = false;
343 let parent_path = filename.parent().unwrap();
344 if parent_path.exists() {
345 if filename.is_file() {
346 todo!("read file")
351 }
352 }
353
354 if !file_found {
355 panic!("Include file not found");
356 }
357 }
358
359 fn parse_trailing_note(&mut self, xact_ptr: *mut Xact) {
364 let note = self.buffer.trim_start();
368 let note = note[1..].trim();
370 if note.is_empty() {
371 return;
372 }
373
374 let xact: &mut Xact;
376 unsafe {
377 xact = &mut *xact_ptr;
378 }
379 if xact.posts.is_empty() {
380 xact.add_note(note);
383 } else {
384 let last_post = xact.posts.last_mut().unwrap();
387 last_post.add_note(note);
389 }
390 }
391}
392
393fn parse_post(input: &str, xact_ptr: *const Xact, journal: &mut Journal) -> Result<(), Error> {
396 let tokens = scanner::scan_post(input);
397
398 let account_ptr = journal.register_account(tokens.account).unwrap();
402
403 let amount_opt = parse_amount_parts(tokens.quantity, tokens.symbol, journal);
405
406 {
408 let annotation = Annotation::parse(
409 tokens.price_date,
410 tokens.price_quantity,
411 tokens.price_commodity,
412 journal,
413 )?;
414
415 journal
420 .commodity_pool
421 .find_or_create(tokens.symbol, Some(annotation));
422 }
423
424 let cost_option = parse_cost(&tokens, &amount_opt, journal);
426
427 let note = None;
430
431 let post_ref: &Post;
433 {
434 let post: Post;
435 post = Post::new(account_ptr, xact_ptr, amount_opt, cost_option, note);
436
437 let xact: &mut Xact;
440 unsafe {
441 xact = &mut *(xact_ptr.cast_mut());
442 }
443 post_ref = xact.add_post(post);
445 }
446
447 {
449 let account = journal.get_account_mut(account_ptr);
451 account.posts.push(post_ref);
452 }
453
454 Ok(())
455}
456
457fn parse_cost(
458 tokens: &PostTokens,
459 amount: &Option<Amount>,
460 journal: &mut Journal,
461) -> Option<Amount> {
462 if tokens.cost_quantity.is_empty() || amount.is_none() {
463 return None;
464 }
465
466 let cost_result = parse_amount_parts(tokens.cost_quantity, tokens.cost_symbol, journal);
468 if cost_result.is_none() {
469 return None;
470 }
471 let mut cost = cost_result.unwrap();
472
473 if tokens.is_per_unit {
474 let mut cost_val = cost;
476 cost_val *= amount.unwrap();
477 cost = cost_val;
478 }
479 Some(cost)
482}
483
484#[cfg(test)]
485mod tests {
486 use std::{io::Cursor, todo};
487
488 use super::Parser;
489 use crate::journal::Journal;
490
491 fn test_general_directive() {
494 let source = Cursor::new("include some-file.ledger");
495 let mut journal = Journal::new();
496
497 let parser = Parser::new(source, &mut journal);
498
499 parser.general_directive();
500
501 todo!("assert")
502 }
503
504 #[test]
507 fn test_xact_with_space_after() {
508 let src = r#";
5092023-05-05 Payee
510 Expenses 25 EUR
511 Assets
512
513"#;
514 let source = Cursor::new(src);
515 let mut journal = Journal::new();
516 let mut parser = Parser::new(source, &mut journal);
517
518 parser.parse();
520
521 assert_eq!(3, journal.master.flatten_account_tree().len());
523 }
524}
525
526#[cfg(test)]
527mod full_tests {
528 use std::io::Cursor;
529
530 use crate::{journal::Journal, parser::read_into_journal};
531
532 #[test]
533 fn test_minimal_parsing() {
534 let input = r#"; Minimal transaction
5352023-04-10 Supermarket
536 Expenses 20
537 Assets
538"#;
539 let cursor = Cursor::new(input);
540 let mut journal = Journal::new();
541
542 super::read_into_journal(cursor, &mut journal);
544
545 assert_eq!(1, journal.xacts.len());
547
548 let xact = journal.xacts.first().unwrap();
549 assert_eq!("Supermarket", xact.payee);
550 assert_eq!(2, xact.posts.len());
551
552 let post1 = &xact.posts[0];
553 assert_eq!("Expenses", journal.get_account(post1.account).name);
554 assert_eq!("20", post1.amount.as_ref().unwrap().quantity.to_string());
555 assert_eq!(None, post1.amount.as_ref().unwrap().get_commodity());
556
557 let post2 = &xact.posts[1];
558 assert_eq!("Assets", journal.get_account(post2.account).name);
559 }
560
561 #[test]
562 fn test_multiple_currencies_one_xact() {
563 let input = r#";
5642023-05-05 Payee
565 Assets:Cash EUR -25 EUR
566 Assets:Cash USD 30 USD
567"#;
568 let cursor = Cursor::new(input);
569 let mut journal = Journal::new();
570
571 read_into_journal(cursor, &mut journal);
573
574 assert_eq!(2, journal.commodity_pool.commodities.len());
576 }
577}
578
579#[cfg(test)]
580mod parser_tests {
581 use std::{assert_eq, io::Cursor};
582
583 use crate::{
584 journal::Journal,
585 parser::{self, read_into_journal},
586 };
587
588 #[test]
589 fn test_minimal_parser() {
590 let input = r#"; Minimal transaction
5912023-04-10 Supermarket
592 Expenses 20
593 Assets
594"#;
595 let cursor = Cursor::new(input);
596 let mut journal = Journal::new();
597
598 parser::read_into_journal(cursor, &mut journal);
600
601 assert_eq!(1, journal.xacts.len());
604
605 let xact = journal.xacts.first().unwrap();
606 assert_eq!("Supermarket", xact.payee);
607 assert_eq!(2, xact.posts.len());
608
609 let post1 = &xact.posts[0];
611 assert_eq!("Expenses", journal.get_account(post1.account).name);
612 assert_eq!("20", post1.amount.as_ref().unwrap().quantity.to_string());
613 assert_eq!(None, post1.amount.as_ref().unwrap().get_commodity());
614
615 let post2 = &xact.posts[1];
617 assert_eq!("Assets", journal.get_account(post2.account).name);
618 }
619
620 #[test]
621 fn test_parse_standard_xact() {
622 let input = r#"; Standard transaction
6232023-04-10 Supermarket
624 Expenses 20 EUR
625 Assets
626"#;
627 let cursor = Cursor::new(input);
628 let mut journal = Journal::new();
629
630 super::read_into_journal(cursor, &mut journal);
631
632 assert_eq!(1, journal.xacts.len());
635
636 if let Some(xact) = journal.xacts.first() {
637 assert_eq!("Supermarket", xact.payee);
640
641 let posts = &xact.posts;
643 assert_eq!(2, posts.len());
644
645 let acc1 = journal.get_account(posts[0].account);
646 assert_eq!("Expenses", acc1.name);
647 let acc2 = journal.get_account(posts[1].account);
648 assert_eq!("Assets", acc2.name);
649 } else {
650 assert!(false);
651 }
652 }
653
654 #[test_log::test]
655 fn test_parse_trade_xact() {
656 let input = r#"; Standard transaction
6582023-04-10 Supermarket
659 Assets:Investment 20 VEUR @ 10 EUR
660 Assets
661"#;
662 let cursor = Cursor::new(input);
663 let mut journal = Journal::new();
664
665 read_into_journal(cursor, &mut journal);
667
668 let xact = journal.xacts.first().unwrap();
671 assert_eq!("Supermarket", xact.payee);
672 let posts = &xact.posts;
673 assert_eq!(2, posts.len());
674
675 let p1 = &posts[0];
677 let account = journal.get_account(p1.account);
678 assert_eq!("Investment", account.name);
679 let parent = journal.get_account(account.parent);
680 assert_eq!("Assets", parent.name);
681 let Some(a1) = &p1.amount else {panic!()};
683 assert_eq!("20", a1.quantity.to_string());
684 let comm1 = a1.get_commodity().unwrap();
685 assert_eq!("VEUR", comm1.symbol);
686 let Some(ref cost1) = p1.cost else { panic!()};
687 assert_eq!(200, cost1.quantity.into());
689 assert_eq!("EUR", cost1.get_commodity().unwrap().symbol);
690
691 let p2 = &posts[1];
693 assert_eq!("Assets", journal.get_account(p2.account).name);
694 let Some(a2) = &p2.amount else {panic!()};
696 assert_eq!("-200", a2.quantity.to_string());
697 let comm2 = a2.get_commodity().unwrap();
698 assert_eq!("EUR", comm2.symbol);
699
700 assert!(p2.cost.is_none());
701 }
702
703 #[test]
704 fn test_parsing_account_tree() {
705 let input = r#"
7062023-05-23 Payee
707 Assets:Eur -20 EUR
708 Assets:USD 30 USD
709 Trading:Eur 20 EUR
710 Trading:Usd -30 USD
711"#;
712
713 let cursor = Cursor::new(input);
714 let mut journal = Journal::new();
715
716 read_into_journal(cursor, &mut journal);
718
719 let xact = &journal.xacts[0];
721 assert_eq!(1, journal.xacts.len());
722 assert_eq!(4, xact.posts.len());
723 assert_eq!(7, journal.master.flatten_account_tree().len());
724 assert_eq!(2, journal.commodity_pool.commodities.len());
725 }
726}
727
728#[cfg(test)]
729mod posting_parsing_tests {
730 use std::io::Cursor;
731
732 use super::Parser;
733 use crate::{amount::Quantity, journal::Journal, parse_file, parser::parse_datetime};
734
735 #[test]
736 fn test_parsing_buy_lot() {
737 let file_path = "tests/lot.ledger";
738 let mut j = Journal::new();
739
740 parse_file(file_path, &mut j);
742
743 assert_eq!(1, j.xacts.len());
745 assert_eq!(4, j.master.flatten_account_tree().len());
746 assert_eq!(2, j.commodity_pool.commodities.len());
747 assert_eq!(2, j.commodity_pool.commodity_history.node_count());
749 assert_eq!(1, j.commodity_pool.commodity_history.edge_count());
750 let price = j
752 .commodity_pool
753 .commodity_history
754 .edge_weight(0.into())
755 .unwrap();
756 let expected_date = parse_datetime("2023-05-01").unwrap();
757 assert!(price.contains_key(&expected_date));
759 let value = price.get(&expected_date).unwrap();
760 assert_eq!(Quantity::from(12.75), *value);
761 }
762
763 #[test]
764 fn test_buy_lot_cost() {
765 let file_path = "tests/lot.ledger";
766 let mut j = Journal::new();
767
768 parse_file(file_path, &mut j);
770
771 let xact = j.xacts.get(0).unwrap();
773 let post = &xact.posts[0];
774 let cost = post.cost.unwrap();
775 assert_eq!(cost.quantity, 127.5.into());
776
777 let eur = j.commodity_pool.find("EUR").unwrap();
778 assert_eq!(cost.commodity, eur);
779 }
780
781 #[test]
782 fn test_parsing_trailing_xact_comment() {
783 let input = r#"2023-03-02 Payee
784 ; this is xact comment
785 Expenses 20 EUR
786 Assets
787"#;
788 let journal = &mut Journal::new();
789 let mut parser = Parser::new(Cursor::new(input), journal);
790
791 parser.parse();
792
793 assert!(journal.xacts[0].note.is_some());
795 assert_eq!(
796 Some("this is xact comment".to_string()),
797 journal.xacts[0].note
798 );
799 }
800
801 #[test]
802 fn test_parsing_trailing_post_comment() {
803 let input = r#"2023-03-02 Payee
804 Expenses 20 EUR
805 ; this is post comment
806 Assets
807"#;
808 let journal = &mut Journal::new();
809 let mut parser = Parser::new(Cursor::new(input), journal);
810
811 parser.parse();
812
813 let xact = &journal.xacts[0];
815 assert!(xact.posts[0].note.is_some());
816 assert!(xact.posts[1].note.is_none());
817 assert_eq!(Some("this is post comment".to_string()), xact.posts[0].note);
818 }
819}
820
821#[cfg(test)]
822mod amount_parsing_tests {
823 use super::Amount;
824 use crate::{amount::Quantity, journal::Journal, parser::parse_post, xact::Xact};
825
826 fn setup() -> Journal {
827 let mut journal = Journal::new();
828 let xact = Xact::create("2023-05-02", "", "Supermarket", "");
829 journal.add_xact(xact);
830
831 journal
832 }
833
834 #[test]
835 fn test_positive_no_commodity() {
836 let expected = Amount::new(20.into(), None);
837 let actual = Amount::new(20.into(), None);
838
839 assert_eq!(expected, actual);
840 }
841
842 #[test]
843 fn test_negative_no_commodity() {
844 let actual = Amount::new((-20).into(), None);
845 let expected = Amount::new((-20).into(), None);
846
847 assert_eq!(expected, actual);
848 }
849
850 #[test]
851 fn test_pos_w_commodity_separated() {
852 const SYMBOL: &str = "EUR";
853 let mut journal = setup();
854 let eur_ptr = journal.commodity_pool.create(SYMBOL, None);
855 let xact_ptr = &journal.xacts[0] as *const Xact;
856 let expected = Amount::new(20.into(), Some(eur_ptr));
857
858 let _ = parse_post(" Assets 20 EUR", xact_ptr, &mut journal);
861
862 let xact = &journal.xacts[0];
864 let post = xact.posts.first().unwrap();
865 let amount = &post.amount.unwrap();
866
867 assert_eq!(&expected.commodity, &amount.commodity);
868 assert_eq!(expected, *amount);
869
870 let c = amount.get_commodity().unwrap();
872 assert_eq!("EUR", c.symbol);
873 }
874
875 #[test]
876 fn test_neg_commodity_separated() {
877 const SYMBOL: &str = "EUR";
878 let mut journal = setup();
879 let eur_ptr = journal.commodity_pool.create(SYMBOL, None);
880 let expected = Amount::new((-20).into(), Some(eur_ptr));
881 let xact_ptr = &journal.xacts[0] as *const Xact;
882
883 let _ = parse_post(" Assets -20 EUR", xact_ptr, &mut journal);
885
886 let xact = &journal.xacts[0];
888 let post = xact.posts.first().unwrap();
889 let Some(amt) = &post.amount else { panic!() };
890 assert_eq!(&expected, amt);
891
892 let commodity = amt.get_commodity().unwrap();
893 assert_eq!("EUR", commodity.symbol);
894 }
895
896 #[test]
897 fn test_full_w_commodity_separated() {
898 let mut journal = setup();
900 let xact_ptr = &journal.xacts[0] as *const Xact;
901
902 let _ = parse_post(" Assets -20000.00 EUR", xact_ptr, &mut journal);
904 let xact = &journal.xacts[0];
905 let post = xact.posts.first().unwrap();
906 let Some(ref amount) = post.amount else { panic!()};
907
908 assert_eq!("-20000.00", amount.quantity.to_string());
910 assert_eq!("EUR", amount.get_commodity().unwrap().symbol);
911 }
912
913 #[test]
914 fn test_full_commodity_first() {
915 let mut journal = setup();
917 let xact_ptr = &journal.xacts[0] as *const Xact;
918
919 let _ = parse_post(" Assets A$-20000.00", xact_ptr, &mut journal);
921 let xact = &journal.xacts[0];
922 let post = xact.posts.first().unwrap();
923 let Some(ref amount) = post.amount else { panic!()};
924
925 assert_eq!("-20000.00", amount.quantity.to_string());
927 assert_eq!("A$", amount.get_commodity().unwrap().symbol);
928 }
929
930 #[test]
931 fn test_quantity_separators() {
932 let input = "-1000000.00";
933 let expected = Quantity::from(-1_000_000);
934
935 let amount = Amount::new(Quantity::from_str(input).unwrap(), None);
936
937 let actual = amount.quantity;
938
939 assert_eq!(expected, actual);
940 }
941
942 #[test]
943 fn test_addition() {
944 let left = Amount::new(10.into(), None);
946 let right = Amount::new(15.into(), None);
948
949 let actual = left + right;
950
951 assert_eq!(Quantity::from(25), actual.quantity);
952 }
955
956 #[test]
957 fn test_add_assign() {
958 let mut actual = Amount::new(21.into(), None);
960 let other = Amount::new(13.into(), None);
962
963 actual.add(&other);
965
966 assert_eq!(Quantity::from(34), actual.quantity);
967 }
968
969 #[test]
970 fn test_copy_from_no_commodity() {
971 let other = Amount::new(10.into(), None);
972 let actual = Amount::copy_from(&other);
973
974 assert_eq!(Quantity::from(10), actual.quantity);
975 }
977}