1use chrono::NaiveDate;
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
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 = HashMap<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}
107
108impl Posting {
109 #[must_use]
111 pub fn new(account: impl Into<InternedStr>, units: Amount) -> Self {
112 Self {
113 account: account.into(),
114 units: Some(IncompleteAmount::Complete(units)),
115 cost: None,
116 price: None,
117 flag: None,
118 meta: Metadata::new(),
119 }
120 }
121
122 #[must_use]
124 pub fn with_incomplete(account: impl Into<InternedStr>, units: IncompleteAmount) -> Self {
125 Self {
126 account: account.into(),
127 units: Some(units),
128 cost: None,
129 price: None,
130 flag: None,
131 meta: Metadata::new(),
132 }
133 }
134
135 #[must_use]
137 pub fn auto(account: impl Into<InternedStr>) -> Self {
138 Self {
139 account: account.into(),
140 units: None,
141 cost: None,
142 price: None,
143 flag: None,
144 meta: Metadata::new(),
145 }
146 }
147
148 #[must_use]
150 pub fn amount(&self) -> Option<&Amount> {
151 self.units.as_ref().and_then(|u| u.as_amount())
152 }
153
154 #[must_use]
156 pub fn with_cost(mut self, cost: CostSpec) -> Self {
157 self.cost = Some(cost);
158 self
159 }
160
161 #[must_use]
163 pub fn with_price(mut self, price: PriceAnnotation) -> Self {
164 self.price = Some(price);
165 self
166 }
167
168 #[must_use]
170 pub const fn with_flag(mut self, flag: char) -> Self {
171 self.flag = Some(flag);
172 self
173 }
174
175 #[must_use]
177 pub const fn has_units(&self) -> bool {
178 self.units.is_some()
179 }
180}
181
182impl fmt::Display for Posting {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 write!(f, " ")?;
185 if let Some(flag) = self.flag {
186 write!(f, "{flag} ")?;
187 }
188 write!(f, "{}", self.account)?;
189 if let Some(units) = &self.units {
190 write!(f, " {units}")?;
191 }
192 if let Some(cost) = &self.cost {
193 write!(f, " {cost}")?;
194 }
195 if let Some(price) = &self.price {
196 write!(f, " {price}")?;
197 }
198 Ok(())
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[cfg_attr(
208 feature = "rkyv",
209 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
210)]
211pub enum PriceAnnotation {
212 Unit(Amount),
214 Total(Amount),
216 UnitIncomplete(IncompleteAmount),
218 TotalIncomplete(IncompleteAmount),
220 UnitEmpty,
222 TotalEmpty,
224}
225
226impl PriceAnnotation {
227 #[must_use]
229 pub const fn amount(&self) -> Option<&Amount> {
230 match self {
231 Self::Unit(a) | Self::Total(a) => Some(a),
232 Self::UnitIncomplete(ia) | Self::TotalIncomplete(ia) => ia.as_amount(),
233 Self::UnitEmpty | Self::TotalEmpty => None,
234 }
235 }
236
237 #[must_use]
239 pub const fn is_unit(&self) -> bool {
240 matches!(
241 self,
242 Self::Unit(_) | Self::UnitIncomplete(_) | Self::UnitEmpty
243 )
244 }
245}
246
247impl fmt::Display for PriceAnnotation {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 Self::Unit(a) => write!(f, "@ {a}"),
251 Self::Total(a) => write!(f, "@@ {a}"),
252 Self::UnitIncomplete(ia) => write!(f, "@ {ia}"),
253 Self::TotalIncomplete(ia) => write!(f, "@@ {ia}"),
254 Self::UnitEmpty => write!(f, "@"),
255 Self::TotalEmpty => write!(f, "@@"),
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
265pub enum DirectivePriority {
266 Open = 0,
268 Commodity = 1,
270 Pad = 2,
272 Balance = 3,
274 Transaction = 4,
276 Note = 5,
278 Document = 6,
280 Event = 7,
282 Query = 8,
284 Price = 9,
286 Close = 10,
288 Custom = 11,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[cfg_attr(
295 feature = "rkyv",
296 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
297)]
298pub enum Directive {
299 Transaction(Transaction),
301 Balance(Balance),
303 Open(Open),
305 Close(Close),
307 Commodity(Commodity),
309 Pad(Pad),
311 Event(Event),
313 Query(Query),
315 Note(Note),
317 Document(Document),
319 Price(Price),
321 Custom(Custom),
323}
324
325impl Directive {
326 #[must_use]
328 pub const fn date(&self) -> NaiveDate {
329 match self {
330 Self::Transaction(t) => t.date,
331 Self::Balance(b) => b.date,
332 Self::Open(o) => o.date,
333 Self::Close(c) => c.date,
334 Self::Commodity(c) => c.date,
335 Self::Pad(p) => p.date,
336 Self::Event(e) => e.date,
337 Self::Query(q) => q.date,
338 Self::Note(n) => n.date,
339 Self::Document(d) => d.date,
340 Self::Price(p) => p.date,
341 Self::Custom(c) => c.date,
342 }
343 }
344
345 #[must_use]
347 pub const fn meta(&self) -> &Metadata {
348 match self {
349 Self::Transaction(t) => &t.meta,
350 Self::Balance(b) => &b.meta,
351 Self::Open(o) => &o.meta,
352 Self::Close(c) => &c.meta,
353 Self::Commodity(c) => &c.meta,
354 Self::Pad(p) => &p.meta,
355 Self::Event(e) => &e.meta,
356 Self::Query(q) => &q.meta,
357 Self::Note(n) => &n.meta,
358 Self::Document(d) => &d.meta,
359 Self::Price(p) => &p.meta,
360 Self::Custom(c) => &c.meta,
361 }
362 }
363
364 #[must_use]
366 pub const fn is_transaction(&self) -> bool {
367 matches!(self, Self::Transaction(_))
368 }
369
370 #[must_use]
372 pub const fn as_transaction(&self) -> Option<&Transaction> {
373 match self {
374 Self::Transaction(t) => Some(t),
375 _ => None,
376 }
377 }
378
379 #[must_use]
381 pub const fn type_name(&self) -> &'static str {
382 match self {
383 Self::Transaction(_) => "transaction",
384 Self::Balance(_) => "balance",
385 Self::Open(_) => "open",
386 Self::Close(_) => "close",
387 Self::Commodity(_) => "commodity",
388 Self::Pad(_) => "pad",
389 Self::Event(_) => "event",
390 Self::Query(_) => "query",
391 Self::Note(_) => "note",
392 Self::Document(_) => "document",
393 Self::Price(_) => "price",
394 Self::Custom(_) => "custom",
395 }
396 }
397
398 #[must_use]
402 pub const fn priority(&self) -> DirectivePriority {
403 match self {
404 Self::Open(_) => DirectivePriority::Open,
405 Self::Commodity(_) => DirectivePriority::Commodity,
406 Self::Pad(_) => DirectivePriority::Pad,
407 Self::Balance(_) => DirectivePriority::Balance,
408 Self::Transaction(_) => DirectivePriority::Transaction,
409 Self::Note(_) => DirectivePriority::Note,
410 Self::Document(_) => DirectivePriority::Document,
411 Self::Event(_) => DirectivePriority::Event,
412 Self::Query(_) => DirectivePriority::Query,
413 Self::Price(_) => DirectivePriority::Price,
414 Self::Close(_) => DirectivePriority::Close,
415 Self::Custom(_) => DirectivePriority::Custom,
416 }
417 }
418}
419
420pub fn sort_directives(directives: &mut [Directive]) {
425 directives.sort_by(|a, b| {
426 a.date()
428 .cmp(&b.date())
429 .then_with(|| a.priority().cmp(&b.priority()))
431 });
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439#[cfg_attr(
440 feature = "rkyv",
441 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
442)]
443pub struct Transaction {
444 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
446 pub date: NaiveDate,
447 pub flag: char,
449 #[cfg_attr(feature = "rkyv", rkyv(with = AsOptionInternedStr))]
451 pub payee: Option<InternedStr>,
452 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
454 pub narration: InternedStr,
455 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
457 pub tags: Vec<InternedStr>,
458 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
460 pub links: Vec<InternedStr>,
461 pub meta: Metadata,
463 pub postings: Vec<Posting>,
465}
466
467impl Transaction {
468 #[must_use]
470 pub fn new(date: NaiveDate, narration: impl Into<InternedStr>) -> Self {
471 Self {
472 date,
473 flag: '*',
474 payee: None,
475 narration: narration.into(),
476 tags: Vec::new(),
477 links: Vec::new(),
478 meta: Metadata::new(),
479 postings: Vec::new(),
480 }
481 }
482
483 #[must_use]
485 pub const fn with_flag(mut self, flag: char) -> Self {
486 self.flag = flag;
487 self
488 }
489
490 #[must_use]
492 pub fn with_payee(mut self, payee: impl Into<InternedStr>) -> Self {
493 self.payee = Some(payee.into());
494 self
495 }
496
497 #[must_use]
499 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
500 self.tags.push(tag.into());
501 self
502 }
503
504 #[must_use]
506 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
507 self.links.push(link.into());
508 self
509 }
510
511 #[must_use]
513 pub fn with_posting(mut self, posting: Posting) -> Self {
514 self.postings.push(posting);
515 self
516 }
517
518 #[must_use]
520 pub const fn is_complete(&self) -> bool {
521 self.flag == '*'
522 }
523
524 #[must_use]
526 pub const fn is_incomplete(&self) -> bool {
527 self.flag == '!'
528 }
529
530 #[must_use]
533 pub const fn is_pending(&self) -> bool {
534 self.flag == '!'
535 }
536
537 #[must_use]
539 pub const fn is_pad_generated(&self) -> bool {
540 self.flag == 'P'
541 }
542
543 #[must_use]
545 pub const fn is_summarization(&self) -> bool {
546 self.flag == 'S'
547 }
548
549 #[must_use]
551 pub const fn is_transfer(&self) -> bool {
552 self.flag == 'T'
553 }
554
555 #[must_use]
557 pub const fn is_conversion(&self) -> bool {
558 self.flag == 'C'
559 }
560
561 #[must_use]
563 pub const fn is_unrealized(&self) -> bool {
564 self.flag == 'U'
565 }
566
567 #[must_use]
569 pub const fn is_return(&self) -> bool {
570 self.flag == 'R'
571 }
572
573 #[must_use]
575 pub const fn is_merge(&self) -> bool {
576 self.flag == 'M'
577 }
578
579 #[must_use]
581 pub const fn is_bookmarked(&self) -> bool {
582 self.flag == '#'
583 }
584
585 #[must_use]
587 pub const fn needs_investigation(&self) -> bool {
588 self.flag == '?'
589 }
590
591 #[must_use]
593 pub const fn is_valid_flag(flag: char) -> bool {
594 matches!(
595 flag,
596 '*' | '!' | 'P' | 'S' | 'T' | 'C' | 'U' | 'R' | 'M' | '#' | '?' | '%' | '&'
597 )
598 }
599}
600
601impl fmt::Display for Transaction {
602 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603 write!(f, "{} {} ", self.date, self.flag)?;
604 if let Some(payee) = &self.payee {
605 write!(f, "\"{payee}\" ")?;
606 }
607 write!(f, "\"{}\"", self.narration)?;
608 for tag in &self.tags {
609 write!(f, " #{tag}")?;
610 }
611 for link in &self.links {
612 write!(f, " ^{link}")?;
613 }
614 for posting in &self.postings {
615 write!(f, "\n{posting}")?;
616 }
617 Ok(())
618 }
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(
626 feature = "rkyv",
627 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
628)]
629pub struct Balance {
630 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
632 pub date: NaiveDate,
633 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
635 pub account: InternedStr,
636 pub amount: Amount,
638 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsDecimal>))]
640 pub tolerance: Option<Decimal>,
641 pub meta: Metadata,
643}
644
645impl Balance {
646 #[must_use]
648 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, amount: Amount) -> Self {
649 Self {
650 date,
651 account: account.into(),
652 amount,
653 tolerance: None,
654 meta: Metadata::new(),
655 }
656 }
657
658 #[must_use]
660 pub const fn with_tolerance(mut self, tolerance: Decimal) -> Self {
661 self.tolerance = Some(tolerance);
662 self
663 }
664
665 #[must_use]
667 pub fn with_meta(mut self, meta: Metadata) -> Self {
668 self.meta = meta;
669 self
670 }
671}
672
673impl fmt::Display for Balance {
674 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675 write!(f, "{} balance {} {}", self.date, self.account, self.amount)?;
676 if let Some(tol) = self.tolerance {
677 write!(f, " ~ {tol}")?;
678 }
679 Ok(())
680 }
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
687#[cfg_attr(
688 feature = "rkyv",
689 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
690)]
691pub struct Open {
692 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
694 pub date: NaiveDate,
695 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
697 pub account: InternedStr,
698 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsInternedStr>))]
700 pub currencies: Vec<InternedStr>,
701 pub booking: Option<String>,
703 pub meta: Metadata,
705}
706
707impl Open {
708 #[must_use]
710 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
711 Self {
712 date,
713 account: account.into(),
714 currencies: Vec::new(),
715 booking: None,
716 meta: Metadata::new(),
717 }
718 }
719
720 #[must_use]
722 pub fn with_currencies(mut self, currencies: Vec<InternedStr>) -> Self {
723 self.currencies = currencies;
724 self
725 }
726
727 #[must_use]
729 pub fn with_booking(mut self, booking: impl Into<String>) -> Self {
730 self.booking = Some(booking.into());
731 self
732 }
733
734 #[must_use]
736 pub fn with_meta(mut self, meta: Metadata) -> Self {
737 self.meta = meta;
738 self
739 }
740}
741
742impl fmt::Display for Open {
743 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
744 write!(f, "{} open {}", self.date, self.account)?;
745 if !self.currencies.is_empty() {
746 let currencies: Vec<&str> = self.currencies.iter().map(InternedStr::as_str).collect();
747 write!(f, " {}", currencies.join(","))?;
748 }
749 if let Some(booking) = &self.booking {
750 write!(f, " \"{booking}\"")?;
751 }
752 Ok(())
753 }
754}
755
756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
760#[cfg_attr(
761 feature = "rkyv",
762 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
763)]
764pub struct Close {
765 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
767 pub date: NaiveDate,
768 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
770 pub account: InternedStr,
771 pub meta: Metadata,
773}
774
775impl Close {
776 #[must_use]
778 pub fn new(date: NaiveDate, account: impl Into<InternedStr>) -> Self {
779 Self {
780 date,
781 account: account.into(),
782 meta: Metadata::new(),
783 }
784 }
785
786 #[must_use]
788 pub fn with_meta(mut self, meta: Metadata) -> Self {
789 self.meta = meta;
790 self
791 }
792}
793
794impl fmt::Display for Close {
795 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796 write!(f, "{} close {}", self.date, self.account)
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 Commodity {
809 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
811 pub date: NaiveDate,
812 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
814 pub currency: InternedStr,
815 pub meta: Metadata,
817}
818
819impl Commodity {
820 #[must_use]
822 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>) -> Self {
823 Self {
824 date,
825 currency: currency.into(),
826 meta: Metadata::new(),
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 Commodity {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 write!(f, "{} commodity {}", self.date, self.currency)
841 }
842}
843
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
849#[cfg_attr(
850 feature = "rkyv",
851 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
852)]
853pub struct Pad {
854 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
856 pub date: NaiveDate,
857 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
859 pub account: InternedStr,
860 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
862 pub source_account: InternedStr,
863 pub meta: Metadata,
865}
866
867impl Pad {
868 #[must_use]
870 pub fn new(
871 date: NaiveDate,
872 account: impl Into<InternedStr>,
873 source_account: impl Into<InternedStr>,
874 ) -> Self {
875 Self {
876 date,
877 account: account.into(),
878 source_account: source_account.into(),
879 meta: Metadata::new(),
880 }
881 }
882
883 #[must_use]
885 pub fn with_meta(mut self, meta: Metadata) -> Self {
886 self.meta = meta;
887 self
888 }
889}
890
891impl fmt::Display for Pad {
892 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893 write!(
894 f,
895 "{} pad {} {}",
896 self.date, self.account, self.source_account
897 )
898 }
899}
900
901#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905#[cfg_attr(
906 feature = "rkyv",
907 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
908)]
909pub struct Event {
910 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
912 pub date: NaiveDate,
913 pub event_type: String,
915 pub value: String,
917 pub meta: Metadata,
919}
920
921impl Event {
922 #[must_use]
924 pub fn new(date: NaiveDate, event_type: impl Into<String>, value: impl Into<String>) -> Self {
925 Self {
926 date,
927 event_type: event_type.into(),
928 value: value.into(),
929 meta: Metadata::new(),
930 }
931 }
932
933 #[must_use]
935 pub fn with_meta(mut self, meta: Metadata) -> Self {
936 self.meta = meta;
937 self
938 }
939}
940
941impl fmt::Display for Event {
942 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
943 write!(
944 f,
945 "{} event \"{}\" \"{}\"",
946 self.date, self.event_type, self.value
947 )
948 }
949}
950
951#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
955#[cfg_attr(
956 feature = "rkyv",
957 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
958)]
959pub struct Query {
960 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
962 pub date: NaiveDate,
963 pub name: String,
965 pub query: String,
967 pub meta: Metadata,
969}
970
971impl Query {
972 #[must_use]
974 pub fn new(date: NaiveDate, name: impl Into<String>, query: impl Into<String>) -> Self {
975 Self {
976 date,
977 name: name.into(),
978 query: query.into(),
979 meta: Metadata::new(),
980 }
981 }
982
983 #[must_use]
985 pub fn with_meta(mut self, meta: Metadata) -> Self {
986 self.meta = meta;
987 self
988 }
989}
990
991impl fmt::Display for Query {
992 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
993 write!(
994 f,
995 "{} query \"{}\" \"{}\"",
996 self.date, self.name, self.query
997 )
998 }
999}
1000
1001#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1005#[cfg_attr(
1006 feature = "rkyv",
1007 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1008)]
1009pub struct Note {
1010 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1012 pub date: NaiveDate,
1013 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1015 pub account: InternedStr,
1016 pub comment: String,
1018 pub meta: Metadata,
1020}
1021
1022impl Note {
1023 #[must_use]
1025 pub fn new(
1026 date: NaiveDate,
1027 account: impl Into<InternedStr>,
1028 comment: impl Into<String>,
1029 ) -> Self {
1030 Self {
1031 date,
1032 account: account.into(),
1033 comment: comment.into(),
1034 meta: Metadata::new(),
1035 }
1036 }
1037
1038 #[must_use]
1040 pub fn with_meta(mut self, meta: Metadata) -> Self {
1041 self.meta = meta;
1042 self
1043 }
1044}
1045
1046impl fmt::Display for Note {
1047 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1048 write!(
1049 f,
1050 "{} note {} \"{}\"",
1051 self.date, self.account, self.comment
1052 )
1053 }
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1060#[cfg_attr(
1061 feature = "rkyv",
1062 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1063)]
1064pub struct Document {
1065 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1067 pub date: NaiveDate,
1068 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1070 pub account: InternedStr,
1071 pub path: String,
1073 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1075 pub tags: Vec<InternedStr>,
1076 #[cfg_attr(feature = "rkyv", rkyv(with = AsVecInternedStr))]
1078 pub links: Vec<InternedStr>,
1079 pub meta: Metadata,
1081}
1082
1083impl Document {
1084 #[must_use]
1086 pub fn new(date: NaiveDate, account: impl Into<InternedStr>, path: impl Into<String>) -> Self {
1087 Self {
1088 date,
1089 account: account.into(),
1090 path: path.into(),
1091 tags: Vec::new(),
1092 links: Vec::new(),
1093 meta: Metadata::new(),
1094 }
1095 }
1096
1097 #[must_use]
1099 pub fn with_tag(mut self, tag: impl Into<InternedStr>) -> Self {
1100 self.tags.push(tag.into());
1101 self
1102 }
1103
1104 #[must_use]
1106 pub fn with_link(mut self, link: impl Into<InternedStr>) -> Self {
1107 self.links.push(link.into());
1108 self
1109 }
1110
1111 #[must_use]
1113 pub fn with_meta(mut self, meta: Metadata) -> Self {
1114 self.meta = meta;
1115 self
1116 }
1117}
1118
1119impl fmt::Display for Document {
1120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121 write!(
1122 f,
1123 "{} document {} \"{}\"",
1124 self.date, self.account, self.path
1125 )
1126 }
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1133#[cfg_attr(
1134 feature = "rkyv",
1135 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1136)]
1137pub struct Price {
1138 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1140 pub date: NaiveDate,
1141 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
1143 pub currency: InternedStr,
1144 pub amount: Amount,
1146 pub meta: Metadata,
1148}
1149
1150impl Price {
1151 #[must_use]
1153 pub fn new(date: NaiveDate, currency: impl Into<InternedStr>, amount: Amount) -> Self {
1154 Self {
1155 date,
1156 currency: currency.into(),
1157 amount,
1158 meta: Metadata::new(),
1159 }
1160 }
1161
1162 #[must_use]
1164 pub fn with_meta(mut self, meta: Metadata) -> Self {
1165 self.meta = meta;
1166 self
1167 }
1168}
1169
1170impl fmt::Display for Price {
1171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1172 write!(f, "{} price {} {}", self.date, self.currency, self.amount)
1173 }
1174}
1175
1176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1180#[cfg_attr(
1181 feature = "rkyv",
1182 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
1183)]
1184pub struct Custom {
1185 #[cfg_attr(feature = "rkyv", rkyv(with = AsNaiveDate))]
1187 pub date: NaiveDate,
1188 pub custom_type: String,
1190 pub values: Vec<MetaValue>,
1192 pub meta: Metadata,
1194}
1195
1196impl Custom {
1197 #[must_use]
1199 pub fn new(date: NaiveDate, custom_type: impl Into<String>) -> Self {
1200 Self {
1201 date,
1202 custom_type: custom_type.into(),
1203 values: Vec::new(),
1204 meta: Metadata::new(),
1205 }
1206 }
1207
1208 #[must_use]
1210 pub fn with_value(mut self, value: MetaValue) -> Self {
1211 self.values.push(value);
1212 self
1213 }
1214
1215 #[must_use]
1217 pub fn with_meta(mut self, meta: Metadata) -> Self {
1218 self.meta = meta;
1219 self
1220 }
1221}
1222
1223impl fmt::Display for Custom {
1224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1225 write!(f, "{} custom \"{}\"", self.date, self.custom_type)?;
1226 for value in &self.values {
1227 write!(f, " {value}")?;
1228 }
1229 Ok(())
1230 }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use rust_decimal_macros::dec;
1237
1238 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
1239 NaiveDate::from_ymd_opt(year, month, day).unwrap()
1240 }
1241
1242 #[test]
1243 fn test_transaction() {
1244 let txn = Transaction::new(date(2024, 1, 15), "Grocery shopping")
1245 .with_payee("Whole Foods")
1246 .with_flag('*')
1247 .with_tag("food")
1248 .with_posting(Posting::new(
1249 "Expenses:Food",
1250 Amount::new(dec!(50.00), "USD"),
1251 ))
1252 .with_posting(Posting::auto("Assets:Checking"));
1253
1254 assert_eq!(txn.flag, '*');
1255 assert_eq!(txn.payee.as_deref(), Some("Whole Foods"));
1256 assert_eq!(txn.postings.len(), 2);
1257 assert!(txn.is_complete());
1258 }
1259
1260 #[test]
1261 fn test_balance() {
1262 let bal = Balance::new(
1263 date(2024, 1, 1),
1264 "Assets:Checking",
1265 Amount::new(dec!(1000.00), "USD"),
1266 );
1267
1268 assert_eq!(bal.account, "Assets:Checking");
1269 assert_eq!(bal.amount.number, dec!(1000.00));
1270 }
1271
1272 #[test]
1273 fn test_open() {
1274 let open = Open::new(date(2024, 1, 1), "Assets:Bank:Checking")
1275 .with_currencies(vec!["USD".into()])
1276 .with_booking("FIFO");
1277
1278 assert_eq!(open.currencies, vec![InternedStr::from("USD")]);
1279 assert_eq!(open.booking, Some("FIFO".to_string()));
1280 }
1281
1282 #[test]
1283 fn test_directive_date() {
1284 let txn = Transaction::new(date(2024, 1, 15), "Test");
1285 let dir = Directive::Transaction(txn);
1286
1287 assert_eq!(dir.date(), date(2024, 1, 15));
1288 assert!(dir.is_transaction());
1289 assert_eq!(dir.type_name(), "transaction");
1290 }
1291
1292 #[test]
1293 fn test_posting_display() {
1294 let posting = Posting::new("Assets:Checking", Amount::new(dec!(100.00), "USD"));
1295 let s = format!("{posting}");
1296 assert!(s.contains("Assets:Checking"));
1297 assert!(s.contains("100.00 USD"));
1298 }
1299
1300 #[test]
1301 fn test_transaction_display() {
1302 let txn = Transaction::new(date(2024, 1, 15), "Test transaction")
1303 .with_payee("Test Payee")
1304 .with_posting(Posting::new(
1305 "Expenses:Test",
1306 Amount::new(dec!(50.00), "USD"),
1307 ))
1308 .with_posting(Posting::auto("Assets:Cash"));
1309
1310 let s = format!("{txn}");
1311 assert!(s.contains("2024-01-15"));
1312 assert!(s.contains("Test Payee"));
1313 assert!(s.contains("Test transaction"));
1314 }
1315
1316 #[test]
1317 fn test_directive_priority() {
1318 assert!(DirectivePriority::Open < DirectivePriority::Transaction);
1320 assert!(DirectivePriority::Pad < DirectivePriority::Balance);
1321 assert!(DirectivePriority::Balance < DirectivePriority::Transaction);
1322 assert!(DirectivePriority::Transaction < DirectivePriority::Close);
1323 assert!(DirectivePriority::Price < DirectivePriority::Close);
1324 }
1325
1326 #[test]
1327 fn test_sort_directives_by_date() {
1328 let mut directives = vec![
1329 Directive::Transaction(Transaction::new(date(2024, 1, 15), "Third")),
1330 Directive::Transaction(Transaction::new(date(2024, 1, 1), "First")),
1331 Directive::Transaction(Transaction::new(date(2024, 1, 10), "Second")),
1332 ];
1333
1334 sort_directives(&mut directives);
1335
1336 assert_eq!(directives[0].date(), date(2024, 1, 1));
1337 assert_eq!(directives[1].date(), date(2024, 1, 10));
1338 assert_eq!(directives[2].date(), date(2024, 1, 15));
1339 }
1340
1341 #[test]
1342 fn test_sort_directives_by_type_same_date() {
1343 let mut directives = vec![
1345 Directive::Close(Close::new(date(2024, 1, 1), "Assets:Bank")),
1346 Directive::Transaction(Transaction::new(date(2024, 1, 1), "Payment")),
1347 Directive::Open(Open::new(date(2024, 1, 1), "Assets:Bank")),
1348 Directive::Balance(Balance::new(
1349 date(2024, 1, 1),
1350 "Assets:Bank",
1351 Amount::new(dec!(0), "USD"),
1352 )),
1353 ];
1354
1355 sort_directives(&mut directives);
1356
1357 assert_eq!(directives[0].type_name(), "open");
1358 assert_eq!(directives[1].type_name(), "balance");
1359 assert_eq!(directives[2].type_name(), "transaction");
1360 assert_eq!(directives[3].type_name(), "close");
1361 }
1362
1363 #[test]
1364 fn test_sort_directives_pad_before_balance() {
1365 let mut directives = vec![
1367 Directive::Balance(Balance::new(
1368 date(2024, 1, 1),
1369 "Assets:Bank",
1370 Amount::new(dec!(1000), "USD"),
1371 )),
1372 Directive::Pad(Pad::new(
1373 date(2024, 1, 1),
1374 "Assets:Bank",
1375 "Equity:Opening-Balances",
1376 )),
1377 ];
1378
1379 sort_directives(&mut directives);
1380
1381 assert_eq!(directives[0].type_name(), "pad");
1382 assert_eq!(directives[1].type_name(), "balance");
1383 }
1384
1385 #[test]
1386 fn test_transaction_flags() {
1387 let make_txn = |flag: char| Transaction::new(date(2024, 1, 15), "Test").with_flag(flag);
1388
1389 assert!(make_txn('*').is_complete());
1391 assert!(make_txn('!').is_incomplete());
1392 assert!(make_txn('!').is_pending());
1393
1394 assert!(make_txn('P').is_pad_generated());
1396 assert!(make_txn('S').is_summarization());
1397 assert!(make_txn('T').is_transfer());
1398 assert!(make_txn('C').is_conversion());
1399 assert!(make_txn('U').is_unrealized());
1400 assert!(make_txn('R').is_return());
1401 assert!(make_txn('M').is_merge());
1402 assert!(make_txn('#').is_bookmarked());
1403 assert!(make_txn('?').needs_investigation());
1404
1405 assert!(!make_txn('*').is_pending());
1407 assert!(!make_txn('!').is_complete());
1408 assert!(!make_txn('*').is_pad_generated());
1409 }
1410
1411 #[test]
1412 fn test_is_valid_flag() {
1413 for flag in [
1415 '*', '!', 'P', 'S', 'T', 'C', 'U', 'R', 'M', '#', '?', '%', '&',
1416 ] {
1417 assert!(
1418 Transaction::is_valid_flag(flag),
1419 "Flag '{flag}' should be valid"
1420 );
1421 }
1422
1423 for flag in ['x', 'X', '0', ' ', 'a', 'Z'] {
1425 assert!(
1426 !Transaction::is_valid_flag(flag),
1427 "Flag '{flag}' should be invalid"
1428 );
1429 }
1430 }
1431}