1use chrono::NaiveDate;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::fmt;
23
24use crate::{Amount, CostSpec, IncompleteAmount};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum MetaValue {
29 String(String),
31 Account(String),
33 Currency(String),
35 Tag(String),
37 Link(String),
39 Date(NaiveDate),
41 Number(Decimal),
43 Bool(bool),
45 Amount(Amount),
47 None,
49}
50
51impl fmt::Display for MetaValue {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::String(s) => write!(f, "\"{s}\""),
55 Self::Account(a) => write!(f, "{a}"),
56 Self::Currency(c) => write!(f, "{c}"),
57 Self::Tag(t) => write!(f, "#{t}"),
58 Self::Link(l) => write!(f, "^{l}"),
59 Self::Date(d) => write!(f, "{d}"),
60 Self::Number(n) => write!(f, "{n}"),
61 Self::Bool(b) => write!(f, "{b}"),
62 Self::Amount(a) => write!(f, "{a}"),
63 Self::None => write!(f, "None"),
64 }
65 }
66}
67
68pub type Metadata = HashMap<String, MetaValue>;
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct Posting {
82 pub account: String,
84 pub units: Option<IncompleteAmount>,
86 pub cost: Option<CostSpec>,
88 pub price: Option<PriceAnnotation>,
90 pub flag: Option<char>,
92 pub meta: Metadata,
94}
95
96impl Posting {
97 #[must_use]
99 pub fn new(account: impl Into<String>, units: Amount) -> Self {
100 Self {
101 account: account.into(),
102 units: Some(IncompleteAmount::Complete(units)),
103 cost: None,
104 price: None,
105 flag: None,
106 meta: Metadata::new(),
107 }
108 }
109
110 #[must_use]
112 pub fn with_incomplete(account: impl Into<String>, units: IncompleteAmount) -> Self {
113 Self {
114 account: account.into(),
115 units: Some(units),
116 cost: None,
117 price: None,
118 flag: None,
119 meta: Metadata::new(),
120 }
121 }
122
123 #[must_use]
125 pub fn auto(account: impl Into<String>) -> Self {
126 Self {
127 account: account.into(),
128 units: None,
129 cost: None,
130 price: None,
131 flag: None,
132 meta: Metadata::new(),
133 }
134 }
135
136 #[must_use]
138 pub fn amount(&self) -> Option<&Amount> {
139 self.units.as_ref().and_then(|u| u.as_amount())
140 }
141
142 #[must_use]
144 pub fn with_cost(mut self, cost: CostSpec) -> Self {
145 self.cost = Some(cost);
146 self
147 }
148
149 #[must_use]
151 pub fn with_price(mut self, price: PriceAnnotation) -> Self {
152 self.price = Some(price);
153 self
154 }
155
156 #[must_use]
158 pub const fn with_flag(mut self, flag: char) -> Self {
159 self.flag = Some(flag);
160 self
161 }
162
163 #[must_use]
165 pub const fn has_units(&self) -> bool {
166 self.units.is_some()
167 }
168}
169
170impl fmt::Display for Posting {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, " ")?;
173 if let Some(flag) = self.flag {
174 write!(f, "{flag} ")?;
175 }
176 write!(f, "{}", self.account)?;
177 if let Some(units) = &self.units {
178 write!(f, " {units}")?;
179 }
180 if let Some(cost) = &self.cost {
181 write!(f, " {cost}")?;
182 }
183 if let Some(price) = &self.price {
184 write!(f, " {price}")?;
185 }
186 Ok(())
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub enum PriceAnnotation {
196 Unit(Amount),
198 Total(Amount),
200 UnitIncomplete(IncompleteAmount),
202 TotalIncomplete(IncompleteAmount),
204 UnitEmpty,
206 TotalEmpty,
208}
209
210impl PriceAnnotation {
211 #[must_use]
213 pub const fn amount(&self) -> Option<&Amount> {
214 match self {
215 Self::Unit(a) | Self::Total(a) => Some(a),
216 Self::UnitIncomplete(ia) | Self::TotalIncomplete(ia) => ia.as_amount(),
217 Self::UnitEmpty | Self::TotalEmpty => None,
218 }
219 }
220
221 #[must_use]
223 pub const fn is_unit(&self) -> bool {
224 matches!(
225 self,
226 Self::Unit(_) | Self::UnitIncomplete(_) | Self::UnitEmpty
227 )
228 }
229}
230
231impl fmt::Display for PriceAnnotation {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 Self::Unit(a) => write!(f, "@ {a}"),
235 Self::Total(a) => write!(f, "@@ {a}"),
236 Self::UnitIncomplete(ia) => write!(f, "@ {ia}"),
237 Self::TotalIncomplete(ia) => write!(f, "@@ {ia}"),
238 Self::UnitEmpty => write!(f, "@"),
239 Self::TotalEmpty => write!(f, "@@"),
240 }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
249pub enum DirectivePriority {
250 Open = 0,
252 Commodity = 1,
254 Pad = 2,
256 Balance = 3,
258 Transaction = 4,
260 Note = 5,
262 Document = 6,
264 Event = 7,
266 Query = 8,
268 Price = 9,
270 Close = 10,
272 Custom = 11,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub enum Directive {
279 Transaction(Transaction),
281 Balance(Balance),
283 Open(Open),
285 Close(Close),
287 Commodity(Commodity),
289 Pad(Pad),
291 Event(Event),
293 Query(Query),
295 Note(Note),
297 Document(Document),
299 Price(Price),
301 Custom(Custom),
303}
304
305impl Directive {
306 #[must_use]
308 pub const fn date(&self) -> NaiveDate {
309 match self {
310 Self::Transaction(t) => t.date,
311 Self::Balance(b) => b.date,
312 Self::Open(o) => o.date,
313 Self::Close(c) => c.date,
314 Self::Commodity(c) => c.date,
315 Self::Pad(p) => p.date,
316 Self::Event(e) => e.date,
317 Self::Query(q) => q.date,
318 Self::Note(n) => n.date,
319 Self::Document(d) => d.date,
320 Self::Price(p) => p.date,
321 Self::Custom(c) => c.date,
322 }
323 }
324
325 #[must_use]
327 pub const fn meta(&self) -> &Metadata {
328 match self {
329 Self::Transaction(t) => &t.meta,
330 Self::Balance(b) => &b.meta,
331 Self::Open(o) => &o.meta,
332 Self::Close(c) => &c.meta,
333 Self::Commodity(c) => &c.meta,
334 Self::Pad(p) => &p.meta,
335 Self::Event(e) => &e.meta,
336 Self::Query(q) => &q.meta,
337 Self::Note(n) => &n.meta,
338 Self::Document(d) => &d.meta,
339 Self::Price(p) => &p.meta,
340 Self::Custom(c) => &c.meta,
341 }
342 }
343
344 #[must_use]
346 pub const fn is_transaction(&self) -> bool {
347 matches!(self, Self::Transaction(_))
348 }
349
350 #[must_use]
352 pub const fn as_transaction(&self) -> Option<&Transaction> {
353 match self {
354 Self::Transaction(t) => Some(t),
355 _ => None,
356 }
357 }
358
359 #[must_use]
361 pub const fn type_name(&self) -> &'static str {
362 match self {
363 Self::Transaction(_) => "transaction",
364 Self::Balance(_) => "balance",
365 Self::Open(_) => "open",
366 Self::Close(_) => "close",
367 Self::Commodity(_) => "commodity",
368 Self::Pad(_) => "pad",
369 Self::Event(_) => "event",
370 Self::Query(_) => "query",
371 Self::Note(_) => "note",
372 Self::Document(_) => "document",
373 Self::Price(_) => "price",
374 Self::Custom(_) => "custom",
375 }
376 }
377
378 #[must_use]
382 pub const fn priority(&self) -> DirectivePriority {
383 match self {
384 Self::Open(_) => DirectivePriority::Open,
385 Self::Commodity(_) => DirectivePriority::Commodity,
386 Self::Pad(_) => DirectivePriority::Pad,
387 Self::Balance(_) => DirectivePriority::Balance,
388 Self::Transaction(_) => DirectivePriority::Transaction,
389 Self::Note(_) => DirectivePriority::Note,
390 Self::Document(_) => DirectivePriority::Document,
391 Self::Event(_) => DirectivePriority::Event,
392 Self::Query(_) => DirectivePriority::Query,
393 Self::Price(_) => DirectivePriority::Price,
394 Self::Close(_) => DirectivePriority::Close,
395 Self::Custom(_) => DirectivePriority::Custom,
396 }
397 }
398}
399
400pub fn sort_directives(directives: &mut [Directive]) {
405 directives.sort_by(|a, b| {
406 a.date()
408 .cmp(&b.date())
409 .then_with(|| a.priority().cmp(&b.priority()))
411 });
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct Transaction {
420 pub date: NaiveDate,
422 pub flag: char,
424 pub payee: Option<String>,
426 pub narration: String,
428 pub tags: Vec<String>,
430 pub links: Vec<String>,
432 pub meta: Metadata,
434 pub postings: Vec<Posting>,
436}
437
438impl Transaction {
439 #[must_use]
441 pub fn new(date: NaiveDate, narration: impl Into<String>) -> Self {
442 Self {
443 date,
444 flag: '*',
445 payee: None,
446 narration: narration.into(),
447 tags: Vec::new(),
448 links: Vec::new(),
449 meta: Metadata::new(),
450 postings: Vec::new(),
451 }
452 }
453
454 #[must_use]
456 pub const fn with_flag(mut self, flag: char) -> Self {
457 self.flag = flag;
458 self
459 }
460
461 #[must_use]
463 pub fn with_payee(mut self, payee: impl Into<String>) -> Self {
464 self.payee = Some(payee.into());
465 self
466 }
467
468 #[must_use]
470 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
471 self.tags.push(tag.into());
472 self
473 }
474
475 #[must_use]
477 pub fn with_link(mut self, link: impl Into<String>) -> Self {
478 self.links.push(link.into());
479 self
480 }
481
482 #[must_use]
484 pub fn with_posting(mut self, posting: Posting) -> Self {
485 self.postings.push(posting);
486 self
487 }
488
489 #[must_use]
491 pub const fn is_complete(&self) -> bool {
492 self.flag == '*'
493 }
494
495 #[must_use]
497 pub const fn is_incomplete(&self) -> bool {
498 self.flag == '!'
499 }
500}
501
502impl fmt::Display for Transaction {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 write!(f, "{} {} ", self.date, self.flag)?;
505 if let Some(payee) = &self.payee {
506 write!(f, "\"{payee}\" ")?;
507 }
508 write!(f, "\"{}\"", self.narration)?;
509 for tag in &self.tags {
510 write!(f, " #{tag}")?;
511 }
512 for link in &self.links {
513 write!(f, " ^{link}")?;
514 }
515 for posting in &self.postings {
516 write!(f, "\n{posting}")?;
517 }
518 Ok(())
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526pub struct Balance {
527 pub date: NaiveDate,
529 pub account: String,
531 pub amount: Amount,
533 pub tolerance: Option<Decimal>,
535 pub meta: Metadata,
537}
538
539impl Balance {
540 #[must_use]
542 pub fn new(date: NaiveDate, account: impl Into<String>, amount: Amount) -> Self {
543 Self {
544 date,
545 account: account.into(),
546 amount,
547 tolerance: None,
548 meta: Metadata::new(),
549 }
550 }
551
552 #[must_use]
554 pub const fn with_tolerance(mut self, tolerance: Decimal) -> Self {
555 self.tolerance = Some(tolerance);
556 self
557 }
558}
559
560impl fmt::Display for Balance {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 write!(f, "{} balance {} {}", self.date, self.account, self.amount)?;
563 if let Some(tol) = self.tolerance {
564 write!(f, " ~ {tol}")?;
565 }
566 Ok(())
567 }
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct Open {
575 pub date: NaiveDate,
577 pub account: String,
579 pub currencies: Vec<String>,
581 pub booking: Option<String>,
583 pub meta: Metadata,
585}
586
587impl Open {
588 #[must_use]
590 pub fn new(date: NaiveDate, account: impl Into<String>) -> Self {
591 Self {
592 date,
593 account: account.into(),
594 currencies: Vec::new(),
595 booking: None,
596 meta: Metadata::new(),
597 }
598 }
599
600 #[must_use]
602 pub fn with_currencies(mut self, currencies: Vec<String>) -> Self {
603 self.currencies = currencies;
604 self
605 }
606
607 #[must_use]
609 pub fn with_booking(mut self, booking: impl Into<String>) -> Self {
610 self.booking = Some(booking.into());
611 self
612 }
613}
614
615impl fmt::Display for Open {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 write!(f, "{} open {}", self.date, self.account)?;
618 if !self.currencies.is_empty() {
619 write!(f, " {}", self.currencies.join(","))?;
620 }
621 if let Some(booking) = &self.booking {
622 write!(f, " \"{booking}\"")?;
623 }
624 Ok(())
625 }
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632pub struct Close {
633 pub date: NaiveDate,
635 pub account: String,
637 pub meta: Metadata,
639}
640
641impl Close {
642 #[must_use]
644 pub fn new(date: NaiveDate, account: impl Into<String>) -> Self {
645 Self {
646 date,
647 account: account.into(),
648 meta: Metadata::new(),
649 }
650 }
651}
652
653impl fmt::Display for Close {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 write!(f, "{} close {}", self.date, self.account)
656 }
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663pub struct Commodity {
664 pub date: NaiveDate,
666 pub currency: String,
668 pub meta: Metadata,
670}
671
672impl Commodity {
673 #[must_use]
675 pub fn new(date: NaiveDate, currency: impl Into<String>) -> Self {
676 Self {
677 date,
678 currency: currency.into(),
679 meta: Metadata::new(),
680 }
681 }
682}
683
684impl fmt::Display for Commodity {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 write!(f, "{} commodity {}", self.date, self.currency)
687 }
688}
689
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
695pub struct Pad {
696 pub date: NaiveDate,
698 pub account: String,
700 pub source_account: String,
702 pub meta: Metadata,
704}
705
706impl Pad {
707 #[must_use]
709 pub fn new(
710 date: NaiveDate,
711 account: impl Into<String>,
712 source_account: impl Into<String>,
713 ) -> Self {
714 Self {
715 date,
716 account: account.into(),
717 source_account: source_account.into(),
718 meta: Metadata::new(),
719 }
720 }
721}
722
723impl fmt::Display for Pad {
724 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
725 write!(
726 f,
727 "{} pad {} {}",
728 self.date, self.account, self.source_account
729 )
730 }
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737pub struct Event {
738 pub date: NaiveDate,
740 pub event_type: String,
742 pub value: String,
744 pub meta: Metadata,
746}
747
748impl Event {
749 #[must_use]
751 pub fn new(date: NaiveDate, event_type: impl Into<String>, value: impl Into<String>) -> Self {
752 Self {
753 date,
754 event_type: event_type.into(),
755 value: value.into(),
756 meta: Metadata::new(),
757 }
758 }
759}
760
761impl fmt::Display for Event {
762 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
763 write!(
764 f,
765 "{} event \"{}\" \"{}\"",
766 self.date, self.event_type, self.value
767 )
768 }
769}
770
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
775pub struct Query {
776 pub date: NaiveDate,
778 pub name: String,
780 pub query: String,
782 pub meta: Metadata,
784}
785
786impl Query {
787 #[must_use]
789 pub fn new(date: NaiveDate, name: impl Into<String>, query: impl Into<String>) -> Self {
790 Self {
791 date,
792 name: name.into(),
793 query: query.into(),
794 meta: Metadata::new(),
795 }
796 }
797}
798
799impl fmt::Display for Query {
800 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801 write!(
802 f,
803 "{} query \"{}\" \"{}\"",
804 self.date, self.name, self.query
805 )
806 }
807}
808
809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
813pub struct Note {
814 pub date: NaiveDate,
816 pub account: String,
818 pub comment: String,
820 pub meta: Metadata,
822}
823
824impl Note {
825 #[must_use]
827 pub fn new(date: NaiveDate, account: impl Into<String>, comment: impl Into<String>) -> Self {
828 Self {
829 date,
830 account: account.into(),
831 comment: comment.into(),
832 meta: Metadata::new(),
833 }
834 }
835}
836
837impl fmt::Display for Note {
838 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
839 write!(
840 f,
841 "{} note {} \"{}\"",
842 self.date, self.account, self.comment
843 )
844 }
845}
846
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
851pub struct Document {
852 pub date: NaiveDate,
854 pub account: String,
856 pub path: String,
858 pub tags: Vec<String>,
860 pub links: Vec<String>,
862 pub meta: Metadata,
864}
865
866impl Document {
867 #[must_use]
869 pub fn new(date: NaiveDate, account: impl Into<String>, path: impl Into<String>) -> Self {
870 Self {
871 date,
872 account: account.into(),
873 path: path.into(),
874 tags: Vec::new(),
875 links: Vec::new(),
876 meta: Metadata::new(),
877 }
878 }
879}
880
881impl fmt::Display for Document {
882 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
883 write!(
884 f,
885 "{} document {} \"{}\"",
886 self.date, self.account, self.path
887 )
888 }
889}
890
891#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
895pub struct Price {
896 pub date: NaiveDate,
898 pub currency: String,
900 pub amount: Amount,
902 pub meta: Metadata,
904}
905
906impl Price {
907 #[must_use]
909 pub fn new(date: NaiveDate, currency: impl Into<String>, amount: Amount) -> Self {
910 Self {
911 date,
912 currency: currency.into(),
913 amount,
914 meta: Metadata::new(),
915 }
916 }
917}
918
919impl fmt::Display for Price {
920 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
921 write!(f, "{} price {} {}", self.date, self.currency, self.amount)
922 }
923}
924
925#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
929pub struct Custom {
930 pub date: NaiveDate,
932 pub custom_type: String,
934 pub values: Vec<MetaValue>,
936 pub meta: Metadata,
938}
939
940impl Custom {
941 #[must_use]
943 pub fn new(date: NaiveDate, custom_type: impl Into<String>) -> Self {
944 Self {
945 date,
946 custom_type: custom_type.into(),
947 values: Vec::new(),
948 meta: Metadata::new(),
949 }
950 }
951
952 #[must_use]
954 pub fn with_value(mut self, value: MetaValue) -> Self {
955 self.values.push(value);
956 self
957 }
958}
959
960impl fmt::Display for Custom {
961 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
962 write!(f, "{} custom \"{}\"", self.date, self.custom_type)?;
963 for value in &self.values {
964 write!(f, " {value}")?;
965 }
966 Ok(())
967 }
968}
969
970#[cfg(test)]
971mod tests {
972 use super::*;
973 use rust_decimal_macros::dec;
974
975 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
976 NaiveDate::from_ymd_opt(year, month, day).unwrap()
977 }
978
979 #[test]
980 fn test_transaction() {
981 let txn = Transaction::new(date(2024, 1, 15), "Grocery shopping")
982 .with_payee("Whole Foods")
983 .with_flag('*')
984 .with_tag("food")
985 .with_posting(Posting::new(
986 "Expenses:Food",
987 Amount::new(dec!(50.00), "USD"),
988 ))
989 .with_posting(Posting::auto("Assets:Checking"));
990
991 assert_eq!(txn.flag, '*');
992 assert_eq!(txn.payee, Some("Whole Foods".to_string()));
993 assert_eq!(txn.postings.len(), 2);
994 assert!(txn.is_complete());
995 }
996
997 #[test]
998 fn test_balance() {
999 let bal = Balance::new(
1000 date(2024, 1, 1),
1001 "Assets:Checking",
1002 Amount::new(dec!(1000.00), "USD"),
1003 );
1004
1005 assert_eq!(bal.account, "Assets:Checking");
1006 assert_eq!(bal.amount.number, dec!(1000.00));
1007 }
1008
1009 #[test]
1010 fn test_open() {
1011 let open = Open::new(date(2024, 1, 1), "Assets:Bank:Checking")
1012 .with_currencies(vec!["USD".to_string()])
1013 .with_booking("FIFO");
1014
1015 assert_eq!(open.currencies, vec!["USD"]);
1016 assert_eq!(open.booking, Some("FIFO".to_string()));
1017 }
1018
1019 #[test]
1020 fn test_directive_date() {
1021 let txn = Transaction::new(date(2024, 1, 15), "Test");
1022 let dir = Directive::Transaction(txn);
1023
1024 assert_eq!(dir.date(), date(2024, 1, 15));
1025 assert!(dir.is_transaction());
1026 assert_eq!(dir.type_name(), "transaction");
1027 }
1028
1029 #[test]
1030 fn test_posting_display() {
1031 let posting = Posting::new("Assets:Checking", Amount::new(dec!(100.00), "USD"));
1032 let s = format!("{posting}");
1033 assert!(s.contains("Assets:Checking"));
1034 assert!(s.contains("100.00 USD"));
1035 }
1036
1037 #[test]
1038 fn test_transaction_display() {
1039 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
1040 .with_payee("Test Payee")
1041 .with_posting(Posting::new(
1042 "Expenses:Test",
1043 Amount::new(dec!(50.00), "USD"),
1044 ))
1045 .with_posting(Posting::auto("Assets:Cash"));
1046
1047 let s = format!("{txn}");
1048 assert!(s.contains("2024-01-15"));
1049 assert!(s.contains("Test Payee"));
1050 assert!(s.contains("Test transaction"));
1051 }
1052
1053 #[test]
1054 fn test_directive_priority() {
1055 assert!(DirectivePriority::Open < DirectivePriority::Transaction);
1057 assert!(DirectivePriority::Pad < DirectivePriority::Balance);
1058 assert!(DirectivePriority::Balance < DirectivePriority::Transaction);
1059 assert!(DirectivePriority::Transaction < DirectivePriority::Close);
1060 assert!(DirectivePriority::Price < DirectivePriority::Close);
1061 }
1062
1063 #[test]
1064 fn test_sort_directives_by_date() {
1065 let mut directives = vec![
1066 Directive::Transaction(Transaction::new(date(2024, 1, 15), "Third")),
1067 Directive::Transaction(Transaction::new(date(2024, 1, 1), "First")),
1068 Directive::Transaction(Transaction::new(date(2024, 1, 10), "Second")),
1069 ];
1070
1071 sort_directives(&mut directives);
1072
1073 assert_eq!(directives[0].date(), date(2024, 1, 1));
1074 assert_eq!(directives[1].date(), date(2024, 1, 10));
1075 assert_eq!(directives[2].date(), date(2024, 1, 15));
1076 }
1077
1078 #[test]
1079 fn test_sort_directives_by_type_same_date() {
1080 let mut directives = vec![
1082 Directive::Close(Close::new(date(2024, 1, 1), "Assets:Bank")),
1083 Directive::Transaction(Transaction::new(date(2024, 1, 1), "Payment")),
1084 Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
1085 Directive::Balance(Balance::new(
1086 date(2024, 1, 1),
1087 "Assets:Bank",
1088 Amount::new(dec!(0), "USD"),
1089 )),
1090 ];
1091
1092 sort_directives(&mut directives);
1093
1094 assert_eq!(directives[0].type_name(), "open");
1095 assert_eq!(directives[1].type_name(), "balance");
1096 assert_eq!(directives[2].type_name(), "transaction");
1097 assert_eq!(directives[3].type_name(), "close");
1098 }
1099
1100 #[test]
1101 fn test_sort_directives_pad_before_balance() {
1102 let mut directives = vec![
1104 Directive::Balance(Balance::new(
1105 date(2024, 1, 1),
1106 "Assets:Bank",
1107 Amount::new(dec!(1000), "USD"),
1108 )),
1109 Directive::Pad(Pad::new(
1110 date(2024, 1, 1),
1111 "Assets:Bank",
1112 "Equity:Opening-Balances",
1113 )),
1114 ];
1115
1116 sort_directives(&mut directives);
1117
1118 assert_eq!(directives[0].type_name(), "pad");
1119 assert_eq!(directives[1].type_name(), "balance");
1120 }
1121}