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