okane_core/
syntax.rs

1//! Provides Ledger file format syntax representation.
2
3pub mod decoration;
4pub mod display;
5pub mod expr;
6pub mod plain;
7pub mod pretty_decimal;
8pub mod tracked;
9
10use std::{borrow::Cow, fmt};
11
12use bounded_static::ToStatic;
13use chrono::NaiveDate;
14use derive_where::derive_where;
15
16#[cfg(test)]
17use bounded_static::ToBoundedStatic;
18
19use decoration::Decoration;
20
21/// Top-level entry of the LedgerFile.
22#[derive_where(Debug, PartialEq, Eq)]
23pub enum LedgerEntry<'i, Deco: Decoration> {
24    /// Transaction
25    Txn(Transaction<'i, Deco>),
26    /// Comment, not limited to one-line oppose to `Metadata`.
27    Comment(TopLevelComment<'i>),
28    /// Apply tag directive.
29    ApplyTag(ApplyTag<'i>),
30    /// "end apply tag" directive.
31    EndApplyTag,
32    /// "include" directive.
33    Include(IncludeFile<'i>),
34    /// "account" directive.
35    Account(AccountDeclaration<'i>),
36    /// "commodity" directive.
37    Commodity(CommodityDeclaration<'i>),
38}
39
40impl<'i> LedgerEntry<'i, plain::Ident> {
41    #[cfg(test)]
42    pub(crate) fn to_static(&self) -> LedgerEntry<'static, plain::Ident> {
43        match self {
44            LedgerEntry::Txn(v) => LedgerEntry::Txn(v.to_static()),
45            LedgerEntry::Comment(v) => LedgerEntry::Comment(v.to_static()),
46            LedgerEntry::ApplyTag(v) => LedgerEntry::ApplyTag(v.to_static()),
47            LedgerEntry::EndApplyTag => LedgerEntry::EndApplyTag,
48            LedgerEntry::Include(v) => LedgerEntry::Include(v.to_static()),
49            LedgerEntry::Account(v) => LedgerEntry::Account(v.to_static()),
50            LedgerEntry::Commodity(v) => LedgerEntry::Commodity(v.to_static()),
51        }
52    }
53}
54
55/// Top-level comment. OK to have multi-line comment.
56#[derive(Debug, PartialEq, Eq, ToStatic)]
57pub struct TopLevelComment<'i>(pub Cow<'i, str>);
58
59/// "apply tag" directive content.
60#[derive(Debug, PartialEq, Eq, ToStatic)]
61pub struct ApplyTag<'i> {
62    pub key: Cow<'i, str>,
63    pub value: Option<MetadataValue<'i>>,
64}
65
66/// "include" directive, taking a path as an argument.
67/// Path can be a relative path or an absolute path.
68#[derive(Debug, PartialEq, Eq, ToStatic)]
69pub struct IncludeFile<'i>(pub Cow<'i, str>);
70
71/// "account" directive to declare account information.
72#[derive(Debug, PartialEq, Eq, ToStatic)]
73pub struct AccountDeclaration<'i> {
74    /// Canonical name of the account.
75    pub name: Cow<'i, str>,
76    /// sub-directives for the account.
77    pub details: Vec<AccountDetail<'i>>,
78}
79
80/// Sub directives for "account" directive.
81#[derive(Debug, PartialEq, Eq, ToStatic)]
82pub enum AccountDetail<'i> {
83    /// Comment is a pure comment without any semantics, similar to `TopLevelComment`.
84    Comment(Cow<'i, str>),
85    /// Note is a "note" sub-directive.
86    /// Usually it would be one-line.
87    Note(Cow<'i, str>),
88    /// Declare the given string is an alias for the declared account.
89    Alias(Cow<'i, str>),
90}
91
92/// "commodity" directive to declare commodity information.
93#[derive(Debug, PartialEq, Eq, ToStatic)]
94pub struct CommodityDeclaration<'i> {
95    /// Canonical name of the commodity.
96    pub name: Cow<'i, str>,
97    /// sub-directives for the commodity.
98    pub details: Vec<CommodityDetail<'i>>,
99}
100
101/// Sub directives for "commodity" directive.
102#[derive(Debug, PartialEq, Eq, ToStatic)]
103pub enum CommodityDetail<'i> {
104    /// Comment is a pure comment without any semantics, similar to `TopLevelComment`.
105    Comment(Cow<'i, str>),
106    /// Note is a "note" sub-directive to note the commodity.
107    /// Usually it would be one-line.
108    Note(Cow<'i, str>),
109    /// Declare the given string is an alias for the declared account.
110    /// Multiple declaration should work.
111    Alias(Cow<'i, str>),
112    /// Format describes how the comodity should be printed.
113    Format(expr::Amount<'i>),
114}
115
116/// Represents a transaction where the money transfered across the accounts.
117#[derive_where(Debug, PartialEq, Eq)]
118pub struct Transaction<'i, Deco: Decoration> {
119    /// Date when the transaction issued.
120    pub date: NaiveDate,
121    /// Date when the transaction got effective, optional.
122    pub effective_date: Option<NaiveDate>,
123    /// Indiacates clearing state of the entire transaction.
124    pub clear_state: ClearState,
125    /// Transaction code (not necessarily unique).
126    pub code: Option<Cow<'i, str>>,
127    /// Label of the transaction, often the opposite party of the transaction.
128    pub payee: Cow<'i, str>,
129    /// Postings of the transaction, could be empty.
130    pub posts: Vec<Deco::Decorated<Posting<'i, Deco>>>,
131    /// Transaction level metadata.
132    pub metadata: Vec<Metadata<'i>>,
133}
134
135impl<'i> Transaction<'i, plain::Ident> {
136    #[cfg(test)]
137    fn to_static(&self) -> Transaction<'static, plain::Ident> {
138        let mut posts = Vec::new();
139        for p in &self.posts {
140            posts.push(p.to_static());
141        }
142        Transaction {
143            date: self.date,
144            effective_date: self.effective_date,
145            clear_state: self.clear_state,
146            code: self.code.to_static(),
147            payee: self.payee.to_static(),
148            posts,
149            metadata: self.metadata.to_static(),
150        }
151    }
152}
153
154impl<'i, Deco: Decoration> Transaction<'i, Deco> {
155    /// Constructs minimal transaction.
156    pub fn new<T>(date: NaiveDate, payee: T) -> Self
157    where
158        T: Into<Cow<'i, str>>,
159    {
160        Transaction {
161            date,
162            effective_date: None,
163            clear_state: ClearState::Uncleared,
164            code: None,
165            payee: payee.into(),
166            metadata: Vec::new(),
167            posts: Vec::new(),
168        }
169    }
170}
171
172#[derive_where(Debug, PartialEq, Eq)]
173/// Posting in a transaction to represent a particular account amount increase / decrease.
174pub struct Posting<'i, Deco: Decoration> {
175    /// Account of the post target.
176    pub account: Cow<'i, str>,
177    /// Posting specific ClearState.
178    pub clear_state: ClearState,
179    /// Amount of the posting.
180    pub amount: Option<PostingAmount<'i, Deco>>,
181    /// Balance after the transaction of the specified account.
182    pub balance: Option<Deco::Decorated<expr::ValueExpr<'i>>>,
183    /// Metadata information such as comment or tag.
184    pub metadata: Vec<Metadata<'i>>,
185}
186
187impl<'i> Posting<'i, plain::Ident> {
188    #[cfg(test)]
189    fn to_static(&self) -> Posting<'static, plain::Ident> {
190        Posting {
191            account: self.account.to_static(),
192            clear_state: self.clear_state,
193            amount: self.amount.as_ref().map(|x| x.to_static()),
194            balance: self.balance.to_static(),
195            metadata: self.metadata.to_static(),
196        }
197    }
198}
199
200impl<'i, Deco: Decoration> Posting<'i, Deco> {
201    pub fn new<T: Into<Cow<'i, str>>>(account: T) -> Self {
202        Posting {
203            account: account.into(),
204            clear_state: ClearState::default(),
205            amount: None,
206            balance: None,
207            metadata: Vec::new(),
208        }
209    }
210}
211
212/// Represents a clearing state, often combined with the ambiguity.
213#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, ToStatic)]
214pub enum ClearState {
215    /// No specific meaning.
216    #[default]
217    Uncleared,
218    /// Useful to declare that the transaction / post is confirmed.
219    Cleared,
220    /// Useful to declare that the transaction / post is still pending.
221    Pending,
222}
223
224/// Metadata represents meta information associated with transactions / posts.
225#[derive(Debug, PartialEq, Eq, ToStatic)]
226pub enum Metadata<'i> {
227    /// Comment, which covers just one line (without the suceeding new line).
228    Comment(Cow<'i, str>),
229    /// Tags of word, in a format :tag1:tag2:tag3:, each tag can't contain white spaces.
230    WordTags(Vec<Cow<'i, str>>),
231    /// Key-value paired tag. Key can't contain white spaces.
232    KeyValueTag {
233        key: Cow<'i, str>,
234        value: MetadataValue<'i>,
235    },
236}
237
238/// MetadataValue represents the value in key-value pair used in `Metadata`.
239#[derive(Debug, PartialEq, Eq, ToStatic)]
240pub enum MetadataValue<'i> {
241    /// Regular string.
242    Text(Cow<'i, str>),
243    /// Expression parsed properly prefixed by `::` instead of `:`.
244    // TODO: Change htis type to Expr not Cow<'i, str>.
245    Expr(Cow<'i, str>),
246}
247
248/// This is an amout for each posting.
249/// Which contains
250/// - how much the asset is increased.
251/// - what was the cost in the other commodity.
252/// - lot information.
253#[derive_where(Debug, PartialEq, Eq)]
254pub struct PostingAmount<'i, Deco: Decoration> {
255    pub amount: Deco::Decorated<expr::ValueExpr<'i>>,
256    pub cost: Option<Deco::Decorated<Exchange<'i>>>,
257    pub lot: Lot<'i, Deco>,
258}
259
260impl<'i> PostingAmount<'i, plain::Ident> {
261    #[cfg(test)]
262    fn to_static(&self) -> PostingAmount<'static, plain::Ident> {
263        PostingAmount {
264            amount: self.amount.to_static(),
265            cost: self.cost.to_static(),
266            lot: self.lot.to_static(),
267        }
268    }
269}
270
271impl<'i> From<expr::ValueExpr<'i>> for PostingAmount<'i, plain::Ident> {
272    fn from(v: expr::ValueExpr<'i>) -> Self {
273        PostingAmount {
274            amount: v,
275            cost: None,
276            lot: Lot::default(),
277        }
278    }
279}
280
281/// Lot information is a set of metadata to record the original lot which the commodity is acquired with.
282#[derive_where(Debug, PartialEq, Eq)]
283pub struct Lot<'i, Deco: Decoration> {
284    pub price: Option<Deco::Decorated<Exchange<'i>>>,
285    pub date: Option<NaiveDate>,
286    pub note: Option<Cow<'i, str>>,
287}
288
289impl<'i> Lot<'i, plain::Ident> {
290    #[cfg(test)]
291    fn to_static(&self) -> Lot<'static, plain::Ident> {
292        Lot {
293            price: self.price.to_static(),
294            date: self.date.to_static(),
295            note: self.note.to_static(),
296        }
297    }
298}
299
300impl<'i, Deco: Decoration> Default for Lot<'i, Deco> {
301    fn default() -> Self {
302        Self {
303            price: None,
304            date: None,
305            note: None,
306        }
307    }
308}
309
310/// Exchange represents the amount expressed in the different commodity.
311#[derive(Debug, PartialEq, Eq, ToStatic)]
312pub enum Exchange<'i> {
313    /// Specified value equals to the total amount.
314    /// For example,
315    /// `200 JPY @@ 2 USD`
316    /// means the amount was 200 JPY, which is equal to 2 USD.
317    Total(expr::ValueExpr<'i>),
318    /// Specified value equals to the amount of one original commodity.
319    /// For example,
320    /// `200 JPY @ (1 / 100 USD)`
321    /// means the amount was 200 JPY, where 1 JPY is equal to 1/100 USD.
322    Rate(expr::ValueExpr<'i>),
323}