ledger_rs_lib/
parser.rs

1/*!
2 * Parser with iterators
3 *
4 * Parses string tokens into model entities (Account, Transaction, Post, Amount...)
5 *
6 * The main idea here is to minimize memory allocations.
7 * The parsing is done in functions, not objects.
8 * Each parser will provide an iterator over the tokens it recognizes, i.e. Xact parser
9 * will iterate over the Xact header items: date, payee, note.
10 * Post parser provides an iterator over Account, Amount. Amount parser provides
11 * sign, quantity, symbol, price.
12 * Iterator returns None if a token is not present.
13 *
14 * Tokens are then handled by lexer, which creates instances of Structs and populates
15 * the collections in the Journal.
16 * It also creates links among the models. This functionality is from finalize() function.
17 */
18use 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
48/// Parses ISO-formatted date string, like 2023-07-23
49pub(crate) fn parse_date(date_str: &str) -> NaiveDate {
50    // todo: support more date formats?
51
52    NaiveDate::parse_from_str(date_str, ISO_DATE_FORMAT).expect("date parsed")
53}
54
55/// Create DateTime from date string only.
56pub 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
68/// Parse amount parts (quantity, commodity), i.e. "25", "AUD".
69/// Returns Amount.
70/// Panics if parsing fails.
71pub fn parse_amount_parts(
72    quantity: &str,
73    commodity: &str,
74    journal: &mut Journal,
75) -> Option<Amount> {
76    // Create Commodity, add to collection
77    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        // To avoid allocation, reuse the String variable.
98        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                    // end of file
116                    break;
117                }
118                Ok(_) => {
119                    // Remove the trailing newline characters
120                    // let trimmed = &line.trim_end();
121
122                    match self.read_next_directive() {
123                        Ok(_) => (), // continue
124                        Err(err) => {
125                            log::error!("Error: {:?}", err);
126                            println!("Error: {:?}", err);
127                            break;
128                        }
129                    };
130                }
131            }
132
133            // clear the buffer before reading the next line.
134            self.buffer.clear();
135        }
136    }
137
138    fn read_next_directive(&mut self) -> Result<(), String> {
139        // if self.buffer.is_empty() {
140        //     return Ok(());
141        // }
142        // let length = self.buffer.len();
143        // log::debug!("line length: {:?}", length);
144        if self.buffer == "\r\n" || self.buffer == "\n" {
145            return Ok(());
146        }
147
148        // determine what the line is
149        match self.buffer.chars().peekable().peek().unwrap() {
150            // comments
151            ';' | '#' | '*' | '|' => {
152                // ignore
153                return Ok(());
154            }
155
156            '-' => {
157                // option_directive
158            }
159
160            '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
161                // Starts with date/number.
162                let _ = self.xact_directive();
163            }
164
165            ' ' | '\t' => {
166                todo!("complete")
167            }
168
169            // The rest
170            c => {
171                // 4.7.2 command directives
172
173                // if !general_directive()
174                match c {
175                    // ACDNY
176                    'P' => {
177                        // price
178                        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    /// textual.cc
194    /// bool instance_t::general_directive(char *line)
195    fn general_directive(&self) -> bool {
196        // todo: skip if (*p == '@' || *p == '!')
197
198        // split directive and argument
199        let mut iter = self.buffer.split_whitespace();
200        let Some(directive) = iter.next() else { panic!("no directive?") };
201        let argument = iter.next();
202
203        // todo: check arguments for directives that require one
204        // match directive {
205        //     "comment" | "end" | "python" | "test" | "year" | "Y" => {
206        //         //
207        //         // Year can also be specified with one letter?
208        //         ()
209        //     }
210        //     _ => {
211        //         panic!("The directive {:?} requires an argument!", directive);
212        //     }
213        // }
214
215        match directive.chars().peekable().peek().unwrap() {
216            'a' => {
217                todo!("a");
218            }
219
220            // bcde
221            '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            // ptvy
233            _ => {
234                todo!("handle")
235            }
236        }
237
238        // lookup(DIRECTIVE, self.buffer)
239
240        false
241    }
242
243    fn price_xact_directive(&mut self) {
244        // pass on to the commodity pool
245        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        // Add xact to the journal
255        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        // Read the Xact contents (Posts, Comments, etc.)
264        // Read until separator (empty line).
265        loop {
266            self.buffer.clear(); // empty the buffer before reading
267            match self.reader.read_line(&mut self.buffer) {
268                Err(e) => {
269                    println!("Error: {:?}", e);
270                    break;
271                }
272                Ok(0) => {
273                    // end of file
274                    log::debug!("0-length buffer");
275                    break;
276                }
277                Ok(_) => {
278                    if self.buffer.is_empty() {
279                        // panic!("Unexpected whitespace at the beginning of line!")
280                        todo!("Check what happens here")
281                    }
282
283                    // parse line
284                    match self.buffer.chars().peekable().peek() {
285                        Some(' ') => {
286                            // valid line, starts with space.
287                            let input = self.buffer.trim_start();
288
289                            // if the line is blank after trimming, exit (end the transaction).
290                            if input.is_empty() {
291                                break;
292                            }
293
294                            // Process the Xact content line. Could be a Comment or a Post.
295                            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                            // empty line "\r\n". Exit.
306                            break;
307                        }
308                        _ => {
309                            panic!("should not happen")
310                        }
311                    }
312                }
313            }
314
315            // empty the buffer before exiting.
316            self.buffer.clear();
317        }
318
319        // "finalize" transaction
320        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 (line[0] != '/' && line[0] != '\\' && line[0] != '~')
329        if argument.starts_with('/') || argument.starts_with('\\') || argument.starts_with('~') {
330            filename = PathBuf::from_str(argument).unwrap();
331        } else {
332            // relative path
333            // TODO: get the parent path?
334            // dir = parent_path()
335            // if (parent_path.empty())
336            // else, use current directory
337            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                // let base = filename.file_name();
347
348                // TODO: read file
349                // read_into_journal(source, journal)
350                todo!("read file")
351            }
352        }
353
354        if !file_found {
355            panic!("Include file not found");
356        }
357    }
358
359    /// Parses the trailing note from the buffer.
360    /// xact_index = The index of the current transaction, being parsed.
361    /// The note is added either to the transaction or the last post, based on it's position.
362    ///
363    fn parse_trailing_note(&mut self, xact_ptr: *mut Xact) {
364        // This is a trailing note, and possibly a metadata info tag
365        // It is added to the previous element (xact/post).
366
367        let note = self.buffer.trim_start();
368        // The note starts with the comment character `;`.
369        let note = note[1..].trim();
370        if note.is_empty() {
371            return;
372        }
373
374        // let xact = self.journal.xacts.get_mut(xact_index).unwrap();
375        let xact: &mut Xact;
376        unsafe {
377            xact = &mut *xact_ptr;
378        }
379        if xact.posts.is_empty() {
380            // The first comment. Add to the xact.
381            // let xact_mut = self.journal.xacts.get_mut(xact_index).unwrap();
382            xact.add_note(note);
383        } else {
384            // Post comment. Add to the previous posting.
385            // let last_post_index = xact.posts.last().unwrap();
386            let last_post = xact.posts.last_mut().unwrap();
387            // let post = self.journal.get_post_mut(*last_post_index);
388            last_post.add_note(note);
389        }
390    }
391}
392
393/// Parses Post from the buffer, adds it to the Journal and links
394/// to Xact, Account, etc.
395fn parse_post(input: &str, xact_ptr: *const Xact, journal: &mut Journal) -> Result<(), Error> {
396    let tokens = scanner::scan_post(input);
397
398    // TODO: Make this more like Ledger now that we have pointers.
399
400    // Create Account, add to collection
401    let account_ptr = journal.register_account(tokens.account).unwrap();
402
403    // create amount
404    let amount_opt = parse_amount_parts(tokens.quantity, tokens.symbol, journal);
405
406    // parse and add annotations.
407    {
408        let annotation = Annotation::parse(
409            tokens.price_date,
410            tokens.price_quantity,
411            tokens.price_commodity,
412            journal,
413        )?;
414
415        // TODO: if the cost price is total (not per unit)
416        // details.price /= amount
417
418        // store annotation
419        journal
420            .commodity_pool
421            .find_or_create(tokens.symbol, Some(annotation));
422    }
423
424    // handle cost (2nd amount)
425    let cost_option = parse_cost(&tokens, &amount_opt, journal);
426
427    // note
428    // TODO: parse note
429    let note = None;
430
431    // Create Post, link Xact, Account, Commodity
432    let post_ref: &Post;
433    {
434        let post: Post;
435        post = Post::new(account_ptr, xact_ptr, amount_opt, cost_option, note);
436
437        // add Post to Xact.
438        // let xact = journal.xacts.get_mut(xact_ptr).unwrap();
439        let xact: &mut Xact;
440        unsafe {
441            xact = &mut *(xact_ptr.cast_mut());
442        }
443        // xact.post_indices.push(post_index);
444        post_ref = xact.add_post(post);
445    }
446
447    // add Post to Account.posts
448    {
449        //let account = journal.accounts.get_mut(account_ptr).unwrap();
450        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    // parse cost (per-unit vs total)
467    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        // per-unit cost
475        let mut cost_val = cost;
476        cost_val *= amount.unwrap();
477        cost = cost_val;
478    }
479    // Total cost is already the end-value.
480
481    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    /// Enable this test again when the functionality is complete
492    //#[test]
493    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    /// A transaction record, after which comes a line with spaces only.
505    /// This should be parseable.
506    #[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        // Act
519        parser.parse();
520
521        // Assert
522        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        // Act
543        super::read_into_journal(cursor, &mut journal);
544
545        // Assert
546        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        // Act
572        read_into_journal(cursor, &mut journal);
573
574        // Assert
575        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        // Act
599        parser::read_into_journal(cursor, &mut journal);
600
601        // Assert
602
603        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 exp_account = journal.get
610        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 post_2 = xact.posts.iter().nth(1).unwrap();
616        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
633        // Xact
634        assert_eq!(1, journal.xacts.len());
635
636        if let Some(xact) = journal.xacts.first() {
637            // assert!(xact.is_some());
638
639            assert_eq!("Supermarket", xact.payee);
640
641            // Posts
642            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        // Arrange
657        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        // Act
666        read_into_journal(cursor, &mut journal);
667
668        // Assert
669
670        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        // post 1
676        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        // amount
682        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        // cost
688        assert_eq!(200, cost1.quantity.into());
689        assert_eq!("EUR", cost1.get_commodity().unwrap().symbol);
690
691        // post 2
692        let p2 = &posts[1];
693        assert_eq!("Assets", journal.get_account(p2.account).name);
694        // amount
695        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        // Act
717        read_into_journal(cursor, &mut journal);
718
719        // Assert
720        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        // Act
741        parse_file(file_path, &mut j);
742
743        // Assert
744        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        // price
748        assert_eq!(2, j.commodity_pool.commodity_history.node_count());
749        assert_eq!(1, j.commodity_pool.commodity_history.edge_count());
750        // Check price: 10 VEUR @ 12.75 EUR
751        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        // let existing_key = price.keys().nth(0).unwrap();
758        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        // Act
769        parse_file(file_path, &mut j);
770
771        // Assert the price of "10 VEUR @ 12.75 EUR" must to be 127.50 EUR
772        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
794        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        // Assert
814        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        // Act
859
860        let _ = parse_post("  Assets  20 EUR", xact_ptr, &mut journal);
861
862        // Assert
863        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        // commodity
871        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        // Act
884        let _ = parse_post("  Assets  -20 EUR", xact_ptr, &mut journal);
885
886        // Assert
887        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        // Arrange
899        let mut journal = setup();
900        let xact_ptr = &journal.xacts[0] as *const Xact;
901
902        // Act
903        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
909        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        // Arrange
916        let mut journal = setup();
917        let xact_ptr = &journal.xacts[0] as *const Xact;
918
919        // Act
920        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
926        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 c1 = Commodity::new("EUR");
945        let left = Amount::new(10.into(), None);
946        // let c2 = Commodity::new("EUR");
947        let right = Amount::new(15.into(), None);
948
949        let actual = left + right;
950
951        assert_eq!(Quantity::from(25), actual.quantity);
952        // assert!(actual.commodity.is_some());
953        // assert_eq!("EUR", actual.commodity.unwrap().symbol);
954    }
955
956    #[test]
957    fn test_add_assign() {
958        // let c1 = Commodity::new("EUR");
959        let mut actual = Amount::new(21.into(), None);
960        // let c2 = Commodity::new("EUR");
961        let other = Amount::new(13.into(), None);
962
963        // actual += addition;
964        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        // assert_eq!(None, actual.commodity);
976    }
977}