1use chrono::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
436pub fn sort_directives(directives: &mut [Directive]) {
441 directives.sort_by(|a, b| {
442 a.date()
444 .cmp(&b.date())
445 .then_with(|| a.priority().cmp(&b.priority()))
447 });
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455#[cfg_attr(
456 feature = "rkyv",
457 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
458)]
459pub struct Transaction {
460 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
462 pub date: NaiveDate,
463 pub flag: char,
465 #[cfg_attr(feature = "rkyv", rkyv(with = AsOptionInternedStr))]
467 pub payee: Option<InternedStr>,
468 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
470 pub narration: InternedStr,
471 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
473 pub tags: Vec<InternedStr>,
474 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
476 pub links: Vec<InternedStr>,
477 pub meta: Metadata,
479 pub postings: Vec<Posting>,
481 #[serde(default, skip_serializing_if = "Vec::is_empty")]
483 pub trailing_comments: Vec<String>,
484}
485
486impl Transaction {
487 #[must_use]
489 pub fn new(date: NaiveDate, narration: impl Into<InternedStr>) -> Self {
490 Self {
491 date,
492 flag: '*',
493 payee: None,
494 narration: narration.into(),
495 tags: Vec::new(),
496 links: Vec::new(),
497 meta: Metadata::default(),
498 postings: Vec::new(),
499 trailing_comments: Vec::new(),
500 }
501 }
502
503 #[must_use]
505 pub const fn with_flag(mut self, flag: char) -> Self {
506 self.flag = flag;
507 self
508 }
509
510 #[must_use]
512 pub fn with_payee(mut self, payee: impl Into<InternedStr>) -> Self {
513 self.payee = Some(payee.into());
514 self
515 }
516
517 #[must_use]
519 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
520 self.tags.push(tag.into());
521 self
522 }
523
524 #[must_use]
526 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
527 self.links.push(link.into());
528 self
529 }
530
531 #[must_use]
533 pub fn with_posting(mut self, posting: Posting) -> Self {
534 self.postings.push(posting);
535 self
536 }
537
538 #[must_use]
540 pub const fn is_complete(&self) -> bool {
541 self.flag == '*'
542 }
543
544 #[must_use]
546 pub const fn is_incomplete(&self) -> bool {
547 self.flag == '!'
548 }
549
550 #[must_use]
553 pub const fn is_pending(&self) -> bool {
554 self.flag == '!'
555 }
556
557 #[must_use]
559 pub const fn is_pad_generated(&self) -> bool {
560 self.flag == 'P'
561 }
562
563 #[must_use]
565 pub const fn is_summarization(&self) -> bool {
566 self.flag == 'S'
567 }
568
569 #[must_use]
571 pub const fn is_transfer(&self) -> bool {
572 self.flag == 'T'
573 }
574
575 #[must_use]
577 pub const fn is_conversion(&self) -> bool {
578 self.flag == 'C'
579 }
580
581 #[must_use]
583 pub const fn is_unrealized(&self) -> bool {
584 self.flag == 'U'
585 }
586
587 #[must_use]
589 pub const fn is_return(&self) -> bool {
590 self.flag == 'R'
591 }
592
593 #[must_use]
595 pub const fn is_merge(&self) -> bool {
596 self.flag == 'M'
597 }
598
599 #[must_use]
601 pub const fn is_bookmarked(&self) -> bool {
602 self.flag == '#'
603 }
604
605 #[must_use]
607 pub const fn needs_investigation(&self) -> bool {
608 self.flag == '?'
609 }
610
611 #[must_use]
613 pub const fn is_valid_flag(flag: char) -> bool {
614 matches!(
615 flag,
616 '*' | '!' | 'P' | 'S' | 'T' | 'C' | 'U' | 'R' | 'M' | '#' | '?' | '%' | '&'
617 )
618 }
619}
620
621impl fmt::Display for Transaction {
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 write!(f, "{} {} ", self.date, self.flag)?;
624 if let Some(payee) = &self.payee {
625 write!(f, "\"{payee}\" ")?;
626 }
627 write!(f, "\"{}\"", self.narration)?;
628 for tag in &self.tags {
629 write!(f, " #{tag}")?;
630 }
631 for link in &self.links {
632 write!(f, " ^{link}")?;
633 }
634 for (key, value) in &self.meta {
636 write!(f, "\n {key}: {value}")?;
637 }
638 for posting in &self.postings {
639 write!(f, "\n{posting}")?;
640 }
641 Ok(())
642 }
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649#[cfg_attr(
650 feature = "rkyv",
651 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
652)]
653pub struct Balance {
654 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
656 pub date: NaiveDate,
657 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
659 pub account: InternedStr,
660 pub amount: Amount,
662 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsDecimal>))]
664 pub tolerance: Option<Decimal>,
665 pub meta: Metadata,
667}
668
669impl Balance {
670 #[must_use]
672 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, amount: Amount) -> Self {
673 Self {
674 date,
675 account: account.into(),
676 amount,
677 tolerance: None,
678 meta: Metadata::default(),
679 }
680 }
681
682 #[must_use]
684 pub const fn with_tolerance(mut self, tolerance: Decimal) -> Self {
685 self.tolerance = Some(tolerance);
686 self
687 }
688
689 #[must_use]
691 pub fn with_meta(mut self, meta: Metadata) -> Self {
692 self.meta = meta;
693 self
694 }
695}
696
697impl fmt::Display for Balance {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 write!(f, "{} balance {} {}", self.date, self.account, self.amount)?;
700 if let Some(tol) = self.tolerance {
701 write!(f, " ~ {tol}")?;
702 }
703 Ok(())
704 }
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
711#[cfg_attr(
712 feature = "rkyv",
713 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
714)]
715pub struct Open {
716 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
718 pub date: NaiveDate,
719 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
721 pub account: InternedStr,
722 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsInternedStr>))]
724 pub currencies: Vec<InternedStr>,
725 pub booking: Option<String>,
727 pub meta: Metadata,
729}
730
731impl Open {
732 #[must_use]
734 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
735 Self {
736 date,
737 account: account.into(),
738 currencies: Vec::new(),
739 booking: None,
740 meta: Metadata::default(),
741 }
742 }
743
744 #[must_use]
746 pub fn with_currencies(mut self, currencies: Vec<InternedStr>) -> Self {
747 self.currencies = currencies;
748 self
749 }
750
751 #[must_use]
753 pub fn with_booking(mut self, booking: impl Into<String>) -> Self {
754 self.booking = Some(booking.into());
755 self
756 }
757
758 #[must_use]
760 pub fn with_meta(mut self, meta: Metadata) -> Self {
761 self.meta = meta;
762 self
763 }
764}
765
766impl fmt::Display for Open {
767 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
768 write!(f, "{} open {}", self.date, self.account)?;
769 if !self.currencies.is_empty() {
770 let currencies: Vec<&str> = self.currencies.iter().map(InternedStr::as_str).collect();
771 write!(f, " {}", currencies.join(","))?;
772 }
773 if let Some(booking) = &self.booking {
774 write!(f, " \"{booking}\"")?;
775 }
776 Ok(())
777 }
778}
779
780#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784#[cfg_attr(
785 feature = "rkyv",
786 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
787)]
788pub struct Close {
789 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
791 pub date: NaiveDate,
792 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
794 pub account: InternedStr,
795 pub meta: Metadata,
797}
798
799impl Close {
800 #[must_use]
802 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
803 Self {
804 date,
805 account: account.into(),
806 meta: Metadata::default(),
807 }
808 }
809
810 #[must_use]
812 pub fn with_meta(mut self, meta: Metadata) -> Self {
813 self.meta = meta;
814 self
815 }
816}
817
818impl fmt::Display for Close {
819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820 write!(f, "{} close {}", self.date, self.account)
821 }
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
828#[cfg_attr(
829 feature = "rkyv",
830 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
831)]
832pub struct Commodity {
833 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
835 pub date: NaiveDate,
836 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
838 pub currency: InternedStr,
839 pub meta: Metadata,
841}
842
843impl Commodity {
844 #[must_use]
846 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>) -> Self {
847 Self {
848 date,
849 currency: currency.into(),
850 meta: Metadata::default(),
851 }
852 }
853
854 #[must_use]
856 pub fn with_meta(mut self, meta: Metadata) -> Self {
857 self.meta = meta;
858 self
859 }
860}
861
862impl fmt::Display for Commodity {
863 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864 write!(f, "{} commodity {}", self.date, self.currency)
865 }
866}
867
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
873#[cfg_attr(
874 feature = "rkyv",
875 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
876)]
877pub struct Pad {
878 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
880 pub date: NaiveDate,
881 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
883 pub account: InternedStr,
884 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
886 pub source_account: InternedStr,
887 pub meta: Metadata,
889}
890
891impl Pad {
892 #[must_use]
894 pub fn new(
895 date: NaiveDate,
896 account: impl Into<InternedStr>,
897 source_account: impl Into<InternedStr>,
898 ) -> Self {
899 Self {
900 date,
901 account: account.into(),
902 source_account: source_account.into(),
903 meta: Metadata::default(),
904 }
905 }
906
907 #[must_use]
909 pub fn with_meta(mut self, meta: Metadata) -> Self {
910 self.meta = meta;
911 self
912 }
913}
914
915impl fmt::Display for Pad {
916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917 write!(
918 f,
919 "{} pad {} {}",
920 self.date, self.account, self.source_account
921 )
922 }
923}
924
925#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
929#[cfg_attr(
930 feature = "rkyv",
931 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
932)]
933pub struct Event {
934 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
936 pub date: NaiveDate,
937 pub event_type: String,
939 pub value: String,
941 pub meta: Metadata,
943}
944
945impl Event {
946 #[must_use]
948 pub fn new(date: NaiveDate, event_type: impl Into<String>, value: impl Into<String>) -> Self {
949 Self {
950 date,
951 event_type: event_type.into(),
952 value: value.into(),
953 meta: Metadata::default(),
954 }
955 }
956
957 #[must_use]
959 pub fn with_meta(mut self, meta: Metadata) -> Self {
960 self.meta = meta;
961 self
962 }
963}
964
965impl fmt::Display for Event {
966 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
967 write!(
968 f,
969 "{} event \"{}\" \"{}\"",
970 self.date, self.event_type, self.value
971 )
972 }
973}
974
975#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
979#[cfg_attr(
980 feature = "rkyv",
981 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
982)]
983pub struct Query {
984 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
986 pub date: NaiveDate,
987 pub name: String,
989 pub query: String,
991 pub meta: Metadata,
993}
994
995impl Query {
996 #[must_use]
998 pub fn new(date: NaiveDate, name: impl Into<String>, query: impl Into<String>) -> Self {
999 Self {
1000 date,
1001 name: name.into(),
1002 query: query.into(),
1003 meta: Metadata::default(),
1004 }
1005 }
1006
1007 #[must_use]
1009 pub fn with_meta(mut self, meta: Metadata) -> Self {
1010 self.meta = meta;
1011 self
1012 }
1013}
1014
1015impl fmt::Display for Query {
1016 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1017 write!(
1018 f,
1019 "{} query \"{}\" \"{}\"",
1020 self.date, self.name, self.query
1021 )
1022 }
1023}
1024
1025#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029#[cfg_attr(
1030 feature = "rkyv",
1031 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1032)]
1033pub struct Note {
1034 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1036 pub date: NaiveDate,
1037 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1039 pub account: InternedStr,
1040 pub comment: String,
1042 pub meta: Metadata,
1044}
1045
1046impl Note {
1047 #[must_use]
1049 pub fn new(
1050 date: NaiveDate,
1051 account: impl Into<InternedStr>,
1052 comment: impl Into<String>,
1053 ) -> Self {
1054 Self {
1055 date,
1056 account: account.into(),
1057 comment: comment.into(),
1058 meta: Metadata::default(),
1059 }
1060 }
1061
1062 #[must_use]
1064 pub fn with_meta(mut self, meta: Metadata) -> Self {
1065 self.meta = meta;
1066 self
1067 }
1068}
1069
1070impl fmt::Display for Note {
1071 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1072 write!(
1073 f,
1074 "{} note {} \"{}\"",
1075 self.date, self.account, self.comment
1076 )
1077 }
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1084#[cfg_attr(
1085 feature = "rkyv",
1086 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1087)]
1088pub struct Document {
1089 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1091 pub date: NaiveDate,
1092 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1094 pub account: InternedStr,
1095 pub path: String,
1097 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1099 pub tags: Vec<InternedStr>,
1100 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1102 pub links: Vec<InternedStr>,
1103 pub meta: Metadata,
1105}
1106
1107impl Document {
1108 #[must_use]
1110 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, path: impl Into<String>) -> Self {
1111 Self {
1112 date,
1113 account: account.into(),
1114 path: path.into(),
1115 tags: Vec::new(),
1116 links: Vec::new(),
1117 meta: Metadata::default(),
1118 }
1119 }
1120
1121 #[must_use]
1123 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
1124 self.tags.push(tag.into());
1125 self
1126 }
1127
1128 #[must_use]
1130 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
1131 self.links.push(link.into());
1132 self
1133 }
1134
1135 #[must_use]
1137 pub fn with_meta(mut self, meta: Metadata) -> Self {
1138 self.meta = meta;
1139 self
1140 }
1141}
1142
1143impl fmt::Display for Document {
1144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1145 write!(
1146 f,
1147 "{} document {} \"{}\"",
1148 self.date, self.account, self.path
1149 )
1150 }
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1157#[cfg_attr(
1158 feature = "rkyv",
1159 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1160)]
1161pub struct Price {
1162 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1164 pub date: NaiveDate,
1165 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1167 pub currency: InternedStr,
1168 pub amount: Amount,
1170 pub meta: Metadata,
1172}
1173
1174impl Price {
1175 #[must_use]
1177 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>, amount: Amount) -> Self {
1178 Self {
1179 date,
1180 currency: currency.into(),
1181 amount,
1182 meta: Metadata::default(),
1183 }
1184 }
1185
1186 #[must_use]
1188 pub fn with_meta(mut self, meta: Metadata) -> Self {
1189 self.meta = meta;
1190 self
1191 }
1192}
1193
1194impl fmt::Display for Price {
1195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196 write!(f, "{} price {} {}", self.date, self.currency, self.amount)
1197 }
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1204#[cfg_attr(
1205 feature = "rkyv",
1206 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1207)]
1208pub struct Custom {
1209 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1211 pub date: NaiveDate,
1212 pub custom_type: String,
1214 pub values: Vec<MetaValue>,
1216 pub meta: Metadata,
1218}
1219
1220impl Custom {
1221 #[must_use]
1223 pub fn new(date: NaiveDate, custom_type: impl Into<String>) -> Self {
1224 Self {
1225 date,
1226 custom_type: custom_type.into(),
1227 values: Vec::new(),
1228 meta: Metadata::default(),
1229 }
1230 }
1231
1232 #[must_use]
1234 pub fn with_value(mut self, value: MetaValue) -> Self {
1235 self.values.push(value);
1236 self
1237 }
1238
1239 #[must_use]
1241 pub fn with_meta(mut self, meta: Metadata) -> Self {
1242 self.meta = meta;
1243 self
1244 }
1245}
1246
1247impl fmt::Display for Custom {
1248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1249 write!(f, "{} custom \"{}\"", self.date, self.custom_type)?;
1250 for value in &self.values {
1251 write!(f, " {value}")?;
1252 }
1253 Ok(())
1254 }
1255}
1256
1257impl fmt::Display for Directive {
1258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1259 match self {
1260 Self::Transaction(t) => write!(f, "{t}"),
1261 Self::Balance(b) => write!(f, "{b}"),
1262 Self::Open(o) => write!(f, "{o}"),
1263 Self::Close(c) => write!(f, "{c}"),
1264 Self::Commodity(c) => write!(f, "{c}"),
1265 Self::Pad(p) => write!(f, "{p}"),
1266 Self::Event(e) => write!(f, "{e}"),
1267 Self::Query(q) => write!(f, "{q}"),
1268 Self::Note(n) => write!(f, "{n}"),
1269 Self::Document(d) => write!(f, "{d}"),
1270 Self::Price(p) => write!(f, "{p}"),
1271 Self::Custom(c) => write!(f, "{c}"),
1272 }
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279 use rust_decimal_macros::dec;
1280
1281 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
1282 NaiveDate::from_ymd_opt(year, month, day).unwrap()
1283 }
1284
1285 #[test]
1286 fn test_transaction() {
1287 let txn = Transaction::new(date(2024, 1, 15), "Grocery shopping")
1288 .with_payee("Whole Foods")
1289 .with_flag('*')
1290 .with_tag("food")
1291 .with_posting(Posting::new(
1292 "Expenses:Food",
1293 Amount::new(dec!(50.00), "USD"),
1294 ))
1295 .with_posting(Posting::auto("Assets:Checking"));
1296
1297 assert_eq!(txn.flag, '*');
1298 assert_eq!(txn.payee.as_deref(), Some("Whole Foods"));
1299 assert_eq!(txn.postings.len(), 2);
1300 assert!(txn.is_complete());
1301 }
1302
1303 #[test]
1304 fn test_balance() {
1305 let bal = Balance::new(
1306 date(2024, 1, 1),
1307 "Assets:Checking",
1308 Amount::new(dec!(1000.00), "USD"),
1309 );
1310
1311 assert_eq!(bal.account, "Assets:Checking");
1312 assert_eq!(bal.amount.number, dec!(1000.00));
1313 }
1314
1315 #[test]
1316 fn test_open() {
1317 let open = Open::new(date(2024, 1, 1), "Assets:Bank:Checking")
1318 .with_currencies(vec!["USD".into()])
1319 .with_booking("FIFO");
1320
1321 assert_eq!(open.currencies, vec![InternedStr::from("USD")]);
1322 assert_eq!(open.booking, Some("FIFO".to_string()));
1323 }
1324
1325 #[test]
1326 fn test_directive_date() {
1327 let txn = Transaction::new(date(2024, 1, 15), "Test");
1328 let dir = Directive::Transaction(txn);
1329
1330 assert_eq!(dir.date(), date(2024, 1, 15));
1331 assert!(dir.is_transaction());
1332 assert_eq!(dir.type_name(), "transaction");
1333 }
1334
1335 #[test]
1336 fn test_posting_display() {
1337 let posting = Posting::new("Assets:Checking", Amount::new(dec!(100.00), "USD"));
1338 let s = format!("{posting}");
1339 assert!(s.contains("Assets:Checking"));
1340 assert!(s.contains("100.00 USD"));
1341 }
1342
1343 #[test]
1344 fn test_transaction_display() {
1345 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
1346 .with_payee("Test Payee")
1347 .with_posting(Posting::new(
1348 "Expenses:Test",
1349 Amount::new(dec!(50.00), "USD"),
1350 ))
1351 .with_posting(Posting::auto("Assets:Cash"));
1352
1353 let s = format!("{txn}");
1354 assert!(s.contains("2024-01-15"));
1355 assert!(s.contains("Test Payee"));
1356 assert!(s.contains("Test transaction"));
1357 }
1358
1359 #[test]
1360 fn test_directive_priority() {
1361 assert!(DirectivePriority::Open < DirectivePriority::Transaction);
1363 assert!(DirectivePriority::Pad < DirectivePriority::Balance);
1364 assert!(DirectivePriority::Balance < DirectivePriority::Transaction);
1365 assert!(DirectivePriority::Transaction < DirectivePriority::Close);
1366 assert!(DirectivePriority::Price < DirectivePriority::Close);
1367 }
1368
1369 #[test]
1370 fn test_sort_directives_by_date() {
1371 let mut directives = vec![
1372 Directive::Transaction(Transaction::new(date(2024, 1, 15), "Third")),
1373 Directive::Transaction(Transaction::new(date(2024, 1, 1), "First")),
1374 Directive::Transaction(Transaction::new(date(2024, 1, 10), "Second")),
1375 ];
1376
1377 sort_directives(&mut directives);
1378
1379 assert_eq!(directives[0].date(), date(2024, 1, 1));
1380 assert_eq!(directives[1].date(), date(2024, 1, 10));
1381 assert_eq!(directives[2].date(), date(2024, 1, 15));
1382 }
1383
1384 #[test]
1385 fn test_sort_directives_by_type_same_date() {
1386 let mut directives = vec![
1388 Directive::Close(Close::new(date(2024, 1, 1), "Assets:Bank")),
1389 Directive::Transaction(Transaction::new(date(2024, 1, 1), "Payment")),
1390 Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
1391 Directive::Balance(Balance::new(
1392 date(2024, 1, 1),
1393 "Assets:Bank",
1394 Amount::new(dec!(0), "USD"),
1395 )),
1396 ];
1397
1398 sort_directives(&mut directives);
1399
1400 assert_eq!(directives[0].type_name(), "open");
1401 assert_eq!(directives[1].type_name(), "balance");
1402 assert_eq!(directives[2].type_name(), "transaction");
1403 assert_eq!(directives[3].type_name(), "close");
1404 }
1405
1406 #[test]
1407 fn test_sort_directives_pad_before_balance() {
1408 let mut directives = vec![
1410 Directive::Balance(Balance::new(
1411 date(2024, 1, 1),
1412 "Assets:Bank",
1413 Amount::new(dec!(1000), "USD"),
1414 )),
1415 Directive::Pad(Pad::new(
1416 date(2024, 1, 1),
1417 "Assets:Bank",
1418 "Equity:Opening-Balances",
1419 )),
1420 ];
1421
1422 sort_directives(&mut directives);
1423
1424 assert_eq!(directives[0].type_name(), "pad");
1425 assert_eq!(directives[1].type_name(), "balance");
1426 }
1427
1428 #[test]
1429 fn test_transaction_flags() {
1430 let make_txn = |flag: char| Transaction::new(date(2024, 1, 15), "Test").with_flag(flag);
1431
1432 assert!(make_txn('*').is_complete());
1434 assert!(make_txn('!').is_incomplete());
1435 assert!(make_txn('!').is_pending());
1436
1437 assert!(make_txn('P').is_pad_generated());
1439 assert!(make_txn('S').is_summarization());
1440 assert!(make_txn('T').is_transfer());
1441 assert!(make_txn('C').is_conversion());
1442 assert!(make_txn('U').is_unrealized());
1443 assert!(make_txn('R').is_return());
1444 assert!(make_txn('M').is_merge());
1445 assert!(make_txn('#').is_bookmarked());
1446 assert!(make_txn('?').needs_investigation());
1447
1448 assert!(!make_txn('*').is_pending());
1450 assert!(!make_txn('!').is_complete());
1451 assert!(!make_txn('*').is_pad_generated());
1452 }
1453
1454 #[test]
1455 fn test_is_valid_flag() {
1456 for flag in [
1458 '*', '!', 'P', 'S', 'T', 'C', 'U', 'R', 'M', '#', '?', '%', '&',
1459 ] {
1460 assert!(
1461 Transaction::is_valid_flag(flag),
1462 "Flag '{flag}' should be valid"
1463 );
1464 }
1465
1466 for flag in ['x', 'X', '0', ' ', 'a', 'Z'] {
1468 assert!(
1469 !Transaction::is_valid_flag(flag),
1470 "Flag '{flag}' should be invalid"
1471 );
1472 }
1473 }
1474
1475 #[test]
1476 fn test_transaction_display_includes_metadata() {
1477 let mut meta = Metadata::default();
1478 meta.insert(
1479 "document".to_string(),
1480 MetaValue::String("myfile.pdf".to_string()),
1481 );
1482
1483 let txn = Transaction {
1484 date: date(2026, 2, 23),
1485 flag: '*',
1486 payee: None,
1487 narration: "Example".into(),
1488 tags: vec![],
1489 links: vec![],
1490 meta,
1491 postings: vec![
1492 Posting::new("Assets:Bank", Amount::new(dec!(-2), "USD")),
1493 Posting::auto("Expenses:Example"),
1494 ],
1495 trailing_comments: Vec::new(),
1496 };
1497
1498 let output = txn.to_string();
1499 assert!(
1500 output.contains("document: \"myfile.pdf\""),
1501 "Transaction Display should include metadata: {output}"
1502 );
1503 assert!(
1504 output.contains("Assets:Bank"),
1505 "Transaction Display should include postings: {output}"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_posting_display_includes_metadata() {
1511 let mut meta = Metadata::default();
1512 meta.insert(
1513 "category".to_string(),
1514 MetaValue::String("groceries".to_string()),
1515 );
1516
1517 let posting = Posting {
1518 account: "Expenses:Food".into(),
1519 units: Some(IncompleteAmount::Complete(Amount::new(dec!(50), "USD"))),
1520 cost: None,
1521 price: None,
1522 flag: None,
1523 meta,
1524 comments: Vec::new(),
1525 trailing_comments: Vec::new(),
1526 };
1527
1528 let output = posting.to_string();
1529 assert!(
1530 output.contains("category: \"groceries\""),
1531 "Posting Display should include metadata: {output}"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_directive_display() {
1537 let txn = Transaction::new(date(2024, 1, 15), "Test transaction");
1539 let dir = Directive::Transaction(txn.clone());
1540
1541 assert_eq!(format!("{dir}"), format!("{txn}"));
1543
1544 let open = Open::new(date(2024, 1, 1), "Assets:Bank");
1546 let dir_open = Directive::Open(open.clone());
1547 assert_eq!(format!("{dir_open}"), format!("{open}"));
1548
1549 let balance = Balance::new(
1550 date(2024, 1, 1),
1551 "Assets:Bank",
1552 Amount::new(dec!(100), "USD"),
1553 );
1554 let dir_balance = Directive::Balance(balance.clone());
1555 assert_eq!(format!("{dir_balance}"), format!("{balance}"));
1556 }
1557}