1use crate::NaiveDate;
19use rust_decimal::Decimal;
20use rustc_hash::FxHashMap;
21use serde::{Deserialize, Serialize};
22use std::fmt;
23
24use crate::intern::InternedStr;
25#[cfg(feature = "rkyv")]
26use crate::intern::{AsDecimal, AsInternedStr, AsNaiveDate, AsOptionInternedStr, AsVecInternedStr};
27use crate::{Amount, CostSpec, IncompleteAmount};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(
32 feature = "rkyv",
33 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
34)]
35pub enum MetaValue {
36 String(String),
38 Account(String),
40 Currency(String),
42 Tag(String),
44 Link(String),
46 Date(#[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))] NaiveDate),
48 Number(#[cfg_attr(feature = "rkyv", rkyv(with = AsDecimal))] Decimal),
50 Bool(bool),
52 Amount(Amount),
54 None,
56}
57
58impl fmt::Display for MetaValue {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::String(s) => write!(f, "\"{s}\""),
62 Self::Account(a) => write!(f, "{a}"),
63 Self::Currency(c) => write!(f, "{c}"),
64 Self::Tag(t) => write!(f, "#{t}"),
65 Self::Link(l) => write!(f, "^{l}"),
66 Self::Date(d) => write!(f, "{d}"),
67 Self::Number(n) => write!(f, "{n}"),
68 Self::Bool(b) => write!(f, "{b}"),
69 Self::Amount(a) => write!(f, "{a}"),
70 Self::None => write!(f, "None"),
71 }
72 }
73}
74
75pub type Metadata = FxHashMap<String, MetaValue>;
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[cfg_attr(
89 feature = "rkyv",
90 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
91)]
92pub struct Posting {
93 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
95 pub account: InternedStr,
96 pub units: Option<IncompleteAmount>,
98 pub cost: Option<CostSpec>,
100 pub price: Option<PriceAnnotation>,
102 pub flag: Option<char>,
104 pub meta: Metadata,
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub comments: Vec<String>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub trailing_comments: Vec<String>,
112}
113
114impl Posting {
115 #[must_use]
117 pub fn new(account: impl Into<InternedStr>, units: Amount) -> Self {
118 Self {
119 account: account.into(),
120 units: Some(IncompleteAmount::Complete(units)),
121 cost: None,
122 price: None,
123 flag: None,
124 meta: Metadata::default(),
125 comments: Vec::new(),
126 trailing_comments: Vec::new(),
127 }
128 }
129
130 #[must_use]
132 pub fn with_incomplete(account: impl Into<InternedStr>, units: IncompleteAmount) -> Self {
133 Self {
134 account: account.into(),
135 units: Some(units),
136 cost: None,
137 price: None,
138 flag: None,
139 meta: Metadata::default(),
140 comments: Vec::new(),
141 trailing_comments: Vec::new(),
142 }
143 }
144
145 #[must_use]
147 pub fn auto(account: impl Into<InternedStr>) -> Self {
148 Self {
149 account: account.into(),
150 units: None,
151 cost: None,
152 price: None,
153 flag: None,
154 meta: Metadata::default(),
155 comments: Vec::new(),
156 trailing_comments: Vec::new(),
157 }
158 }
159
160 #[must_use]
162 pub fn amount(&self) -> Option<&Amount> {
163 self.units.as_ref().and_then(|u| u.as_amount())
164 }
165
166 #[must_use]
168 pub fn with_cost(mut self, cost: CostSpec) -> Self {
169 self.cost = Some(cost);
170 self
171 }
172
173 #[must_use]
175 pub fn with_price(mut self, price: PriceAnnotation) -> Self {
176 self.price = Some(price);
177 self
178 }
179
180 #[must_use]
182 pub const fn with_flag(mut self, flag: char) -> Self {
183 self.flag = Some(flag);
184 self
185 }
186
187 #[must_use]
189 pub const fn has_units(&self) -> bool {
190 self.units.is_some()
191 }
192}
193
194impl fmt::Display for Posting {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(f, " ")?;
197 if let Some(flag) = self.flag {
198 write!(f, "{flag} ")?;
199 }
200 write!(f, "{}", self.account)?;
201 if let Some(units) = &self.units {
202 write!(f, " {units}")?;
203 }
204 if let Some(cost) = &self.cost {
205 write!(f, " {cost}")?;
206 }
207 if let Some(price) = &self.price {
208 write!(f, " {price}")?;
209 }
210 for (key, value) in &self.meta {
212 write!(f, "\n {key}: {value}")?;
213 }
214 Ok(())
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[cfg_attr(
224 feature = "rkyv",
225 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
226)]
227pub enum PriceAnnotation {
228 Unit(Amount),
230 Total(Amount),
232 UnitIncomplete(IncompleteAmount),
234 TotalIncomplete(IncompleteAmount),
236 UnitEmpty,
238 TotalEmpty,
240}
241
242impl PriceAnnotation {
243 #[must_use]
245 pub const fn amount(&self) -> Option<&Amount> {
246 match self {
247 Self::Unit(a) | Self::Total(a) => Some(a),
248 Self::UnitIncomplete(ia) | Self::TotalIncomplete(ia) => ia.as_amount(),
249 Self::UnitEmpty | Self::TotalEmpty => None,
250 }
251 }
252
253 #[must_use]
255 pub const fn is_unit(&self) -> bool {
256 matches!(
257 self,
258 Self::Unit(_) | Self::UnitIncomplete(_) | Self::UnitEmpty
259 )
260 }
261}
262
263impl fmt::Display for PriceAnnotation {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 match self {
266 Self::Unit(a) => write!(f, "@ {a}"),
267 Self::Total(a) => write!(f, "@@ {a}"),
268 Self::UnitIncomplete(ia) => write!(f, "@ {ia}"),
269 Self::TotalIncomplete(ia) => write!(f, "@@ {ia}"),
270 Self::UnitEmpty => write!(f, "@"),
271 Self::TotalEmpty => write!(f, "@@"),
272 }
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
281pub enum DirectivePriority {
282 Open = 0,
284 Commodity = 1,
286 Pad = 2,
288 Balance = 3,
290 Transaction = 4,
292 Note = 5,
294 Document = 6,
296 Event = 7,
298 Query = 8,
300 Price = 9,
302 Close = 10,
304 Custom = 11,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310#[cfg_attr(
311 feature = "rkyv",
312 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
313)]
314pub enum Directive {
315 Transaction(Transaction),
317 Balance(Balance),
319 Open(Open),
321 Close(Close),
323 Commodity(Commodity),
325 Pad(Pad),
327 Event(Event),
329 Query(Query),
331 Note(Note),
333 Document(Document),
335 Price(Price),
337 Custom(Custom),
339}
340
341impl Directive {
342 #[must_use]
344 pub const fn date(&self) -> NaiveDate {
345 match self {
346 Self::Transaction(t) => t.date,
347 Self::Balance(b) => b.date,
348 Self::Open(o) => o.date,
349 Self::Close(c) => c.date,
350 Self::Commodity(c) => c.date,
351 Self::Pad(p) => p.date,
352 Self::Event(e) => e.date,
353 Self::Query(q) => q.date,
354 Self::Note(n) => n.date,
355 Self::Document(d) => d.date,
356 Self::Price(p) => p.date,
357 Self::Custom(c) => c.date,
358 }
359 }
360
361 #[must_use]
363 pub const fn meta(&self) -> &Metadata {
364 match self {
365 Self::Transaction(t) => &t.meta,
366 Self::Balance(b) => &b.meta,
367 Self::Open(o) => &o.meta,
368 Self::Close(c) => &c.meta,
369 Self::Commodity(c) => &c.meta,
370 Self::Pad(p) => &p.meta,
371 Self::Event(e) => &e.meta,
372 Self::Query(q) => &q.meta,
373 Self::Note(n) => &n.meta,
374 Self::Document(d) => &d.meta,
375 Self::Price(p) => &p.meta,
376 Self::Custom(c) => &c.meta,
377 }
378 }
379
380 #[must_use]
382 pub const fn is_transaction(&self) -> bool {
383 matches!(self, Self::Transaction(_))
384 }
385
386 #[must_use]
388 pub const fn as_transaction(&self) -> Option<&Transaction> {
389 match self {
390 Self::Transaction(t) => Some(t),
391 _ => None,
392 }
393 }
394
395 #[must_use]
397 pub const fn type_name(&self) -> &'static str {
398 match self {
399 Self::Transaction(_) => "transaction",
400 Self::Balance(_) => "balance",
401 Self::Open(_) => "open",
402 Self::Close(_) => "close",
403 Self::Commodity(_) => "commodity",
404 Self::Pad(_) => "pad",
405 Self::Event(_) => "event",
406 Self::Query(_) => "query",
407 Self::Note(_) => "note",
408 Self::Document(_) => "document",
409 Self::Price(_) => "price",
410 Self::Custom(_) => "custom",
411 }
412 }
413
414 #[must_use]
418 pub const fn priority(&self) -> DirectivePriority {
419 match self {
420 Self::Open(_) => DirectivePriority::Open,
421 Self::Commodity(_) => DirectivePriority::Commodity,
422 Self::Pad(_) => DirectivePriority::Pad,
423 Self::Balance(_) => DirectivePriority::Balance,
424 Self::Transaction(_) => DirectivePriority::Transaction,
425 Self::Note(_) => DirectivePriority::Note,
426 Self::Document(_) => DirectivePriority::Document,
427 Self::Event(_) => DirectivePriority::Event,
428 Self::Query(_) => DirectivePriority::Query,
429 Self::Price(_) => DirectivePriority::Price,
430 Self::Close(_) => DirectivePriority::Close,
431 Self::Custom(_) => DirectivePriority::Custom,
432 }
433 }
434
435 #[must_use]
442 pub fn has_cost_reduction(&self) -> bool {
443 if let Self::Transaction(txn) = self {
444 txn.postings.iter().any(|p| {
445 p.cost.is_some()
446 && p.units
447 .as_ref()
448 .and_then(IncompleteAmount::number)
449 .is_some_and(|n| n.is_sign_negative())
450 })
451 } else {
452 false
453 }
454 }
455}
456
457pub fn sort_directives(directives: &mut [Directive]) {
467 directives.sort_by_cached_key(|d| (d.date(), d.priority(), d.has_cost_reduction()));
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
475#[cfg_attr(
476 feature = "rkyv",
477 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
478)]
479pub struct Transaction {
480 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
482 pub date: NaiveDate,
483 pub flag: char,
485 #[cfg_attr(feature = "rkyv", rkyv(with = AsOptionInternedStr))]
487 pub payee: Option<InternedStr>,
488 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
490 pub narration: InternedStr,
491 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
493 pub tags: Vec<InternedStr>,
494 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
496 pub links: Vec<InternedStr>,
497 pub meta: Metadata,
499 pub postings: Vec<Posting>,
501 #[serde(default, skip_serializing_if = "Vec::is_empty")]
503 pub trailing_comments: Vec<String>,
504}
505
506impl Transaction {
507 #[must_use]
509 pub fn new(date: NaiveDate, narration: impl Into<InternedStr>) -> Self {
510 Self {
511 date,
512 flag: '*',
513 payee: None,
514 narration: narration.into(),
515 tags: Vec::new(),
516 links: Vec::new(),
517 meta: Metadata::default(),
518 postings: Vec::new(),
519 trailing_comments: Vec::new(),
520 }
521 }
522
523 #[must_use]
525 pub const fn with_flag(mut self, flag: char) -> Self {
526 self.flag = flag;
527 self
528 }
529
530 #[must_use]
532 pub fn with_payee(mut self, payee: impl Into<InternedStr>) -> Self {
533 self.payee = Some(payee.into());
534 self
535 }
536
537 #[must_use]
539 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
540 self.tags.push(tag.into());
541 self
542 }
543
544 #[must_use]
546 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
547 self.links.push(link.into());
548 self
549 }
550
551 #[must_use]
553 pub fn with_posting(mut self, posting: Posting) -> Self {
554 self.postings.push(posting);
555 self
556 }
557
558 #[must_use]
560 pub const fn is_complete(&self) -> bool {
561 self.flag == '*'
562 }
563
564 #[must_use]
566 pub const fn is_incomplete(&self) -> bool {
567 self.flag == '!'
568 }
569
570 #[must_use]
573 pub const fn is_pending(&self) -> bool {
574 self.flag == '!'
575 }
576
577 #[must_use]
579 pub const fn is_pad_generated(&self) -> bool {
580 self.flag == 'P'
581 }
582
583 #[must_use]
585 pub const fn is_summarization(&self) -> bool {
586 self.flag == 'S'
587 }
588
589 #[must_use]
591 pub const fn is_transfer(&self) -> bool {
592 self.flag == 'T'
593 }
594
595 #[must_use]
597 pub const fn is_conversion(&self) -> bool {
598 self.flag == 'C'
599 }
600
601 #[must_use]
603 pub const fn is_unrealized(&self) -> bool {
604 self.flag == 'U'
605 }
606
607 #[must_use]
609 pub const fn is_return(&self) -> bool {
610 self.flag == 'R'
611 }
612
613 #[must_use]
615 pub const fn is_merge(&self) -> bool {
616 self.flag == 'M'
617 }
618
619 #[must_use]
621 pub const fn is_bookmarked(&self) -> bool {
622 self.flag == '#'
623 }
624
625 #[must_use]
627 pub const fn needs_investigation(&self) -> bool {
628 self.flag == '?'
629 }
630
631 #[must_use]
633 pub const fn is_valid_flag(flag: char) -> bool {
634 matches!(
635 flag,
636 '*' | '!' | 'P' | 'S' | 'T' | 'C' | 'U' | 'R' | 'M' | '#' | '?' | '%' | '&'
637 )
638 }
639}
640
641impl fmt::Display for Transaction {
642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 write!(f, "{} {} ", self.date, self.flag)?;
644 if let Some(payee) = &self.payee {
645 write!(f, "\"{payee}\" ")?;
646 }
647 write!(f, "\"{}\"", self.narration)?;
648 for tag in &self.tags {
649 write!(f, " #{tag}")?;
650 }
651 for link in &self.links {
652 write!(f, " ^{link}")?;
653 }
654 for (key, value) in &self.meta {
656 write!(f, "\n {key}: {value}")?;
657 }
658 for posting in &self.postings {
659 write!(f, "\n{posting}")?;
660 }
661 Ok(())
662 }
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
669#[cfg_attr(
670 feature = "rkyv",
671 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
672)]
673pub struct Balance {
674 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
676 pub date: NaiveDate,
677 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
679 pub account: InternedStr,
680 pub amount: Amount,
682 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsDecimal>))]
684 pub tolerance: Option<Decimal>,
685 pub meta: Metadata,
687}
688
689impl Balance {
690 #[must_use]
692 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, amount: Amount) -> Self {
693 Self {
694 date,
695 account: account.into(),
696 amount,
697 tolerance: None,
698 meta: Metadata::default(),
699 }
700 }
701
702 #[must_use]
704 pub const fn with_tolerance(mut self, tolerance: Decimal) -> Self {
705 self.tolerance = Some(tolerance);
706 self
707 }
708
709 #[must_use]
711 pub fn with_meta(mut self, meta: Metadata) -> Self {
712 self.meta = meta;
713 self
714 }
715}
716
717impl fmt::Display for Balance {
718 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
719 write!(f, "{} balance {} {}", self.date, self.account, self.amount)?;
720 if let Some(tol) = self.tolerance {
721 write!(f, " ~ {tol}")?;
722 }
723 Ok(())
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731#[cfg_attr(
732 feature = "rkyv",
733 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
734)]
735pub struct Open {
736 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
738 pub date: NaiveDate,
739 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
741 pub account: InternedStr,
742 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsInternedStr>))]
744 pub currencies: Vec<InternedStr>,
745 pub booking: Option<String>,
747 pub meta: Metadata,
749}
750
751impl Open {
752 #[must_use]
754 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
755 Self {
756 date,
757 account: account.into(),
758 currencies: Vec::new(),
759 booking: None,
760 meta: Metadata::default(),
761 }
762 }
763
764 #[must_use]
766 pub fn with_currencies(mut self, currencies: Vec<InternedStr>) -> Self {
767 self.currencies = currencies;
768 self
769 }
770
771 #[must_use]
773 pub fn with_booking(mut self, booking: impl Into<String>) -> Self {
774 self.booking = Some(booking.into());
775 self
776 }
777
778 #[must_use]
780 pub fn with_meta(mut self, meta: Metadata) -> Self {
781 self.meta = meta;
782 self
783 }
784}
785
786impl fmt::Display for Open {
787 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788 write!(f, "{} open {}", self.date, self.account)?;
789 if !self.currencies.is_empty() {
790 let currencies: Vec<&str> = self.currencies.iter().map(InternedStr::as_str).collect();
791 write!(f, " {}", currencies.join(","))?;
792 }
793 if let Some(booking) = &self.booking {
794 write!(f, " \"{booking}\"")?;
795 }
796 Ok(())
797 }
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804#[cfg_attr(
805 feature = "rkyv",
806 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
807)]
808pub struct Close {
809 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
811 pub date: NaiveDate,
812 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
814 pub account: InternedStr,
815 pub meta: Metadata,
817}
818
819impl Close {
820 #[must_use]
822 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
823 Self {
824 date,
825 account: account.into(),
826 meta: Metadata::default(),
827 }
828 }
829
830 #[must_use]
832 pub fn with_meta(mut self, meta: Metadata) -> Self {
833 self.meta = meta;
834 self
835 }
836}
837
838impl fmt::Display for Close {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 write!(f, "{} close {}", self.date, self.account)
841 }
842}
843
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[cfg_attr(
849 feature = "rkyv",
850 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
851)]
852pub struct Commodity {
853 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
855 pub date: NaiveDate,
856 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
858 pub currency: InternedStr,
859 pub meta: Metadata,
861}
862
863impl Commodity {
864 #[must_use]
866 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>) -> Self {
867 Self {
868 date,
869 currency: currency.into(),
870 meta: Metadata::default(),
871 }
872 }
873
874 #[must_use]
876 pub fn with_meta(mut self, meta: Metadata) -> Self {
877 self.meta = meta;
878 self
879 }
880}
881
882impl fmt::Display for Commodity {
883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
884 write!(f, "{} commodity {}", self.date, self.currency)
885 }
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
893#[cfg_attr(
894 feature = "rkyv",
895 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
896)]
897pub struct Pad {
898 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
900 pub date: NaiveDate,
901 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
903 pub account: InternedStr,
904 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
906 pub source_account: InternedStr,
907 pub meta: Metadata,
909}
910
911impl Pad {
912 #[must_use]
914 pub fn new(
915 date: NaiveDate,
916 account: impl Into<InternedStr>,
917 source_account: impl Into<InternedStr>,
918 ) -> Self {
919 Self {
920 date,
921 account: account.into(),
922 source_account: source_account.into(),
923 meta: Metadata::default(),
924 }
925 }
926
927 #[must_use]
929 pub fn with_meta(mut self, meta: Metadata) -> Self {
930 self.meta = meta;
931 self
932 }
933}
934
935impl fmt::Display for Pad {
936 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
937 write!(
938 f,
939 "{} pad {} {}",
940 self.date, self.account, self.source_account
941 )
942 }
943}
944
945#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
949#[cfg_attr(
950 feature = "rkyv",
951 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
952)]
953pub struct Event {
954 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
956 pub date: NaiveDate,
957 pub event_type: String,
959 pub value: String,
961 pub meta: Metadata,
963}
964
965impl Event {
966 #[must_use]
968 pub fn new(date: NaiveDate, event_type: impl Into<String>, value: impl Into<String>) -> Self {
969 Self {
970 date,
971 event_type: event_type.into(),
972 value: value.into(),
973 meta: Metadata::default(),
974 }
975 }
976
977 #[must_use]
979 pub fn with_meta(mut self, meta: Metadata) -> Self {
980 self.meta = meta;
981 self
982 }
983}
984
985impl fmt::Display for Event {
986 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
987 write!(
988 f,
989 "{} event \"{}\" \"{}\"",
990 self.date, self.event_type, self.value
991 )
992 }
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
999#[cfg_attr(
1000 feature = "rkyv",
1001 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1002)]
1003pub struct Query {
1004 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1006 pub date: NaiveDate,
1007 pub name: String,
1009 pub query: String,
1011 pub meta: Metadata,
1013}
1014
1015impl Query {
1016 #[must_use]
1018 pub fn new(date: NaiveDate, name: impl Into<String>, query: impl Into<String>) -> Self {
1019 Self {
1020 date,
1021 name: name.into(),
1022 query: query.into(),
1023 meta: Metadata::default(),
1024 }
1025 }
1026
1027 #[must_use]
1029 pub fn with_meta(mut self, meta: Metadata) -> Self {
1030 self.meta = meta;
1031 self
1032 }
1033}
1034
1035impl fmt::Display for Query {
1036 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1037 write!(
1038 f,
1039 "{} query \"{}\" \"{}\"",
1040 self.date, self.name, self.query
1041 )
1042 }
1043}
1044
1045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1049#[cfg_attr(
1050 feature = "rkyv",
1051 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1052)]
1053pub struct Note {
1054 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1056 pub date: NaiveDate,
1057 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1059 pub account: InternedStr,
1060 pub comment: String,
1062 pub meta: Metadata,
1064}
1065
1066impl Note {
1067 #[must_use]
1069 pub fn new(
1070 date: NaiveDate,
1071 account: impl Into<InternedStr>,
1072 comment: impl Into<String>,
1073 ) -> Self {
1074 Self {
1075 date,
1076 account: account.into(),
1077 comment: comment.into(),
1078 meta: Metadata::default(),
1079 }
1080 }
1081
1082 #[must_use]
1084 pub fn with_meta(mut self, meta: Metadata) -> Self {
1085 self.meta = meta;
1086 self
1087 }
1088}
1089
1090impl fmt::Display for Note {
1091 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092 write!(
1093 f,
1094 "{} note {} \"{}\"",
1095 self.date, self.account, self.comment
1096 )
1097 }
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1104#[cfg_attr(
1105 feature = "rkyv",
1106 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1107)]
1108pub struct Document {
1109 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1111 pub date: NaiveDate,
1112 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1114 pub account: InternedStr,
1115 pub path: String,
1117 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1119 pub tags: Vec<InternedStr>,
1120 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1122 pub links: Vec<InternedStr>,
1123 pub meta: Metadata,
1125}
1126
1127impl Document {
1128 #[must_use]
1130 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, path: impl Into<String>) -> Self {
1131 Self {
1132 date,
1133 account: account.into(),
1134 path: path.into(),
1135 tags: Vec::new(),
1136 links: Vec::new(),
1137 meta: Metadata::default(),
1138 }
1139 }
1140
1141 #[must_use]
1143 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
1144 self.tags.push(tag.into());
1145 self
1146 }
1147
1148 #[must_use]
1150 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
1151 self.links.push(link.into());
1152 self
1153 }
1154
1155 #[must_use]
1157 pub fn with_meta(mut self, meta: Metadata) -> Self {
1158 self.meta = meta;
1159 self
1160 }
1161}
1162
1163impl fmt::Display for Document {
1164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1165 write!(
1166 f,
1167 "{} document {} \"{}\"",
1168 self.date, self.account, self.path
1169 )
1170 }
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1177#[cfg_attr(
1178 feature = "rkyv",
1179 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1180)]
1181pub struct Price {
1182 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1184 pub date: NaiveDate,
1185 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1187 pub currency: InternedStr,
1188 pub amount: Amount,
1190 pub meta: Metadata,
1192}
1193
1194impl Price {
1195 #[must_use]
1197 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>, amount: Amount) -> Self {
1198 Self {
1199 date,
1200 currency: currency.into(),
1201 amount,
1202 meta: Metadata::default(),
1203 }
1204 }
1205
1206 #[must_use]
1208 pub fn with_meta(mut self, meta: Metadata) -> Self {
1209 self.meta = meta;
1210 self
1211 }
1212}
1213
1214impl fmt::Display for Price {
1215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1216 write!(f, "{} price {} {}", self.date, self.currency, self.amount)
1217 }
1218}
1219
1220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1224#[cfg_attr(
1225 feature = "rkyv",
1226 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1227)]
1228pub struct Custom {
1229 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1231 pub date: NaiveDate,
1232 pub custom_type: String,
1234 pub values: Vec<MetaValue>,
1236 pub meta: Metadata,
1238}
1239
1240impl Custom {
1241 #[must_use]
1243 pub fn new(date: NaiveDate, custom_type: impl Into<String>) -> Self {
1244 Self {
1245 date,
1246 custom_type: custom_type.into(),
1247 values: Vec::new(),
1248 meta: Metadata::default(),
1249 }
1250 }
1251
1252 #[must_use]
1254 pub fn with_value(mut self, value: MetaValue) -> Self {
1255 self.values.push(value);
1256 self
1257 }
1258
1259 #[must_use]
1261 pub fn with_meta(mut self, meta: Metadata) -> Self {
1262 self.meta = meta;
1263 self
1264 }
1265}
1266
1267impl fmt::Display for Custom {
1268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1269 write!(f, "{} custom \"{}\"", self.date, self.custom_type)?;
1270 for value in &self.values {
1271 write!(f, " {value}")?;
1272 }
1273 Ok(())
1274 }
1275}
1276
1277impl fmt::Display for Directive {
1278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1279 match self {
1280 Self::Transaction(t) => write!(f, "{t}"),
1281 Self::Balance(b) => write!(f, "{b}"),
1282 Self::Open(o) => write!(f, "{o}"),
1283 Self::Close(c) => write!(f, "{c}"),
1284 Self::Commodity(c) => write!(f, "{c}"),
1285 Self::Pad(p) => write!(f, "{p}"),
1286 Self::Event(e) => write!(f, "{e}"),
1287 Self::Query(q) => write!(f, "{q}"),
1288 Self::Note(n) => write!(f, "{n}"),
1289 Self::Document(d) => write!(f, "{d}"),
1290 Self::Price(p) => write!(f, "{p}"),
1291 Self::Custom(c) => write!(f, "{c}"),
1292 }
1293 }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299 use rust_decimal_macros::dec;
1300
1301 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
1302 crate::naive_date(year, month, day).unwrap()
1303 }
1304
1305 #[test]
1306 fn test_transaction() {
1307 let txn = Transaction::new(date(2024, 1, 15), "Grocery shopping")
1308 .with_payee("Whole Foods")
1309 .with_flag('*')
1310 .with_tag("food")
1311 .with_posting(Posting::new(
1312 "Expenses:Food",
1313 Amount::new(dec!(50.00), "USD"),
1314 ))
1315 .with_posting(Posting::auto("Assets:Checking"));
1316
1317 assert_eq!(txn.flag, '*');
1318 assert_eq!(txn.payee.as_deref(), Some("Whole Foods"));
1319 assert_eq!(txn.postings.len(), 2);
1320 assert!(txn.is_complete());
1321 }
1322
1323 #[test]
1324 fn test_balance() {
1325 let bal = Balance::new(
1326 date(2024, 1, 1),
1327 "Assets:Checking",
1328 Amount::new(dec!(1000.00), "USD"),
1329 );
1330
1331 assert_eq!(bal.account, "Assets:Checking");
1332 assert_eq!(bal.amount.number, dec!(1000.00));
1333 }
1334
1335 #[test]
1336 fn test_open() {
1337 let open = Open::new(date(2024, 1, 1), "Assets:Bank:Checking")
1338 .with_currencies(vec!["USD".into()])
1339 .with_booking("FIFO");
1340
1341 assert_eq!(open.currencies, vec![InternedStr::from("USD")]);
1342 assert_eq!(open.booking, Some("FIFO".to_string()));
1343 }
1344
1345 #[test]
1346 fn test_directive_date() {
1347 let txn = Transaction::new(date(2024, 1, 15), "Test");
1348 let dir = Directive::Transaction(txn);
1349
1350 assert_eq!(dir.date(), date(2024, 1, 15));
1351 assert!(dir.is_transaction());
1352 assert_eq!(dir.type_name(), "transaction");
1353 }
1354
1355 #[test]
1356 fn test_posting_display() {
1357 let posting = Posting::new("Assets:Checking", Amount::new(dec!(100.00), "USD"));
1358 let s = format!("{posting}");
1359 assert!(s.contains("Assets:Checking"));
1360 assert!(s.contains("100.00 USD"));
1361 }
1362
1363 #[test]
1364 fn test_transaction_display() {
1365 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
1366 .with_payee("Test Payee")
1367 .with_posting(Posting::new(
1368 "Expenses:Test",
1369 Amount::new(dec!(50.00), "USD"),
1370 ))
1371 .with_posting(Posting::auto("Assets:Cash"));
1372
1373 let s = format!("{txn}");
1374 assert!(s.contains("2024-01-15"));
1375 assert!(s.contains("Test Payee"));
1376 assert!(s.contains("Test transaction"));
1377 }
1378
1379 #[test]
1380 fn test_directive_priority() {
1381 assert!(DirectivePriority::Open < DirectivePriority::Transaction);
1383 assert!(DirectivePriority::Pad < DirectivePriority::Balance);
1384 assert!(DirectivePriority::Balance < DirectivePriority::Transaction);
1385 assert!(DirectivePriority::Transaction < DirectivePriority::Close);
1386 assert!(DirectivePriority::Price < DirectivePriority::Close);
1387 }
1388
1389 #[test]
1390 fn test_sort_directives_by_date() {
1391 let mut directives = vec![
1392 Directive::Transaction(Transaction::new(date(2024, 1, 15), "Third")),
1393 Directive::Transaction(Transaction::new(date(2024, 1, 1), "First")),
1394 Directive::Transaction(Transaction::new(date(2024, 1, 10), "Second")),
1395 ];
1396
1397 sort_directives(&mut directives);
1398
1399 assert_eq!(directives[0].date(), date(2024, 1, 1));
1400 assert_eq!(directives[1].date(), date(2024, 1, 10));
1401 assert_eq!(directives[2].date(), date(2024, 1, 15));
1402 }
1403
1404 #[test]
1405 fn test_sort_directives_by_type_same_date() {
1406 let mut directives = vec![
1408 Directive::Close(Close::new(date(2024, 1, 1), "Assets:Bank")),
1409 Directive::Transaction(Transaction::new(date(2024, 1, 1), "Payment")),
1410 Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
1411 Directive::Balance(Balance::new(
1412 date(2024, 1, 1),
1413 "Assets:Bank",
1414 Amount::new(dec!(0), "USD"),
1415 )),
1416 ];
1417
1418 sort_directives(&mut directives);
1419
1420 assert_eq!(directives[0].type_name(), "open");
1421 assert_eq!(directives[1].type_name(), "balance");
1422 assert_eq!(directives[2].type_name(), "transaction");
1423 assert_eq!(directives[3].type_name(), "close");
1424 }
1425
1426 #[test]
1427 fn test_sort_directives_pad_before_balance() {
1428 let mut directives = vec![
1430 Directive::Balance(Balance::new(
1431 date(2024, 1, 1),
1432 "Assets:Bank",
1433 Amount::new(dec!(1000), "USD"),
1434 )),
1435 Directive::Pad(Pad::new(
1436 date(2024, 1, 1),
1437 "Assets:Bank",
1438 "Equity:Opening-Balances",
1439 )),
1440 ];
1441
1442 sort_directives(&mut directives);
1443
1444 assert_eq!(directives[0].type_name(), "pad");
1445 assert_eq!(directives[1].type_name(), "balance");
1446 }
1447
1448 #[test]
1449 fn test_sort_augmentations_before_reductions_same_date() {
1450 let reduction = Directive::Transaction(
1454 Transaction::new(date(2024, 9, 1), "Transfer Received")
1455 .with_posting(
1456 Posting::new("Assets:AccountB", Amount::new(dec!(11.11), "USD")).with_cost(
1457 CostSpec::empty()
1458 .with_number_per(dec!(0.90))
1459 .with_currency("EUR"),
1460 ),
1461 )
1462 .with_posting(
1463 Posting::new("Assets:Transit", Amount::new(dec!(-11.11), "USD")).with_cost(
1464 CostSpec::empty()
1465 .with_number_per(dec!(0.90))
1466 .with_currency("EUR"),
1467 ),
1468 ),
1469 );
1470
1471 let augmentation = Directive::Transaction(
1472 Transaction::new(date(2024, 9, 1), "Transfer Sent")
1473 .with_posting(Posting::new(
1474 "Assets:AccountA",
1475 Amount::new(dec!(-10.00), "EUR"),
1476 ))
1477 .with_posting(
1478 Posting::new("Assets:Transit", Amount::new(dec!(11.11), "USD")).with_cost(
1479 CostSpec::empty()
1480 .with_number_per(dec!(0.90))
1481 .with_currency("EUR"),
1482 ),
1483 ),
1484 );
1485
1486 let mut directives = vec![reduction, augmentation];
1488 sort_directives(&mut directives);
1489
1490 assert!(
1492 !directives[0].has_cost_reduction(),
1493 "first directive should be augmentation"
1494 );
1495 assert!(
1496 directives[1].has_cost_reduction(),
1497 "second directive should be reduction"
1498 );
1499 }
1500
1501 #[test]
1502 fn test_has_cost_reduction() {
1503 let reduction = Directive::Transaction(
1505 Transaction::new(date(2024, 1, 1), "Sell")
1506 .with_posting(
1507 Posting::new("Assets:Stock", Amount::new(dec!(-10), "AAPL")).with_cost(
1508 CostSpec::empty()
1509 .with_number_per(dec!(150))
1510 .with_currency("USD"),
1511 ),
1512 )
1513 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(1500), "USD"))),
1514 );
1515 assert!(reduction.has_cost_reduction());
1516
1517 let augmentation = Directive::Transaction(
1519 Transaction::new(date(2024, 1, 1), "Buy")
1520 .with_posting(
1521 Posting::new("Assets:Stock", Amount::new(dec!(10), "AAPL")).with_cost(
1522 CostSpec::empty()
1523 .with_number_per(dec!(150))
1524 .with_currency("USD"),
1525 ),
1526 )
1527 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-1500), "USD"))),
1528 );
1529 assert!(!augmentation.has_cost_reduction());
1530
1531 let simple = Directive::Transaction(
1533 Transaction::new(date(2024, 1, 1), "Payment")
1534 .with_posting(Posting::new("Expenses:Food", Amount::new(dec!(50), "USD")))
1535 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-50), "USD"))),
1536 );
1537 assert!(!simple.has_cost_reduction());
1538 }
1539
1540 #[test]
1541 fn test_transaction_flags() {
1542 let make_txn = |flag: char| Transaction::new(date(2024, 1, 15), "Test").with_flag(flag);
1543
1544 assert!(make_txn('*').is_complete());
1546 assert!(make_txn('!').is_incomplete());
1547 assert!(make_txn('!').is_pending());
1548
1549 assert!(make_txn('P').is_pad_generated());
1551 assert!(make_txn('S').is_summarization());
1552 assert!(make_txn('T').is_transfer());
1553 assert!(make_txn('C').is_conversion());
1554 assert!(make_txn('U').is_unrealized());
1555 assert!(make_txn('R').is_return());
1556 assert!(make_txn('M').is_merge());
1557 assert!(make_txn('#').is_bookmarked());
1558 assert!(make_txn('?').needs_investigation());
1559
1560 assert!(!make_txn('*').is_pending());
1562 assert!(!make_txn('!').is_complete());
1563 assert!(!make_txn('*').is_pad_generated());
1564 }
1565
1566 #[test]
1567 fn test_is_valid_flag() {
1568 for flag in [
1570 '*', '!', 'P', 'S', 'T', 'C', 'U', 'R', 'M', '#', '?', '%', '&',
1571 ] {
1572 assert!(
1573 Transaction::is_valid_flag(flag),
1574 "Flag '{flag}' should be valid"
1575 );
1576 }
1577
1578 for flag in ['x', 'X', '0', ' ', 'a', 'Z'] {
1580 assert!(
1581 !Transaction::is_valid_flag(flag),
1582 "Flag '{flag}' should be invalid"
1583 );
1584 }
1585 }
1586
1587 #[test]
1588 fn test_transaction_display_includes_metadata() {
1589 let mut meta = Metadata::default();
1590 meta.insert(
1591 "document".to_string(),
1592 MetaValue::String("myfile.pdf".to_string()),
1593 );
1594
1595 let txn = Transaction {
1596 date: date(2026, 2, 23),
1597 flag: '*',
1598 payee: None,
1599 narration: "Example".into(),
1600 tags: vec![],
1601 links: vec![],
1602 meta,
1603 postings: vec![
1604 Posting::new("Assets:Bank", Amount::new(dec!(-2), "USD")),
1605 Posting::auto("Expenses:Example"),
1606 ],
1607 trailing_comments: Vec::new(),
1608 };
1609
1610 let output = txn.to_string();
1611 assert!(
1612 output.contains("document: \"myfile.pdf\""),
1613 "Transaction Display should include metadata: {output}"
1614 );
1615 assert!(
1616 output.contains("Assets:Bank"),
1617 "Transaction Display should include postings: {output}"
1618 );
1619 }
1620
1621 #[test]
1622 fn test_posting_display_includes_metadata() {
1623 let mut meta = Metadata::default();
1624 meta.insert(
1625 "category".to_string(),
1626 MetaValue::String("groceries".to_string()),
1627 );
1628
1629 let posting = Posting {
1630 account: "Expenses:Food".into(),
1631 units: Some(IncompleteAmount::Complete(Amount::new(dec!(50), "USD"))),
1632 cost: None,
1633 price: None,
1634 flag: None,
1635 meta,
1636 comments: Vec::new(),
1637 trailing_comments: Vec::new(),
1638 };
1639
1640 let output = posting.to_string();
1641 assert!(
1642 output.contains("category: \"groceries\""),
1643 "Posting Display should include metadata: {output}"
1644 );
1645 }
1646
1647 #[test]
1648 fn test_directive_display() {
1649 let txn = Transaction::new(date(2024, 1, 15), "Test transaction");
1651 let dir = Directive::Transaction(txn.clone());
1652
1653 assert_eq!(format!("{dir}"), format!("{txn}"));
1655
1656 let open = Open::new(date(2024, 1, 1), "Assets:Bank");
1658 let dir_open = Directive::Open(open.clone());
1659 assert_eq!(format!("{dir_open}"), format!("{open}"));
1660
1661 let balance = Balance::new(
1662 date(2024, 1, 1),
1663 "Assets:Bank",
1664 Amount::new(dec!(100), "USD"),
1665 );
1666 let dir_balance = Directive::Balance(balance.clone());
1667 assert_eq!(format!("{dir_balance}"), format!("{balance}"));
1668 }
1669}