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, NaiveDateTime};
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 LedgerEntry<'_, 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 Transaction<'_, 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: Deco::Decorated<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 Posting<'_, 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> Posting<'i, plain::Ident> {
201    pub fn new_untracked<T>(account: T) -> Self
202    where
203        T: Into<Cow<'i, str>>,
204    {
205        Posting {
206            account: account.into(),
207            clear_state: ClearState::default(),
208            amount: None,
209            balance: None,
210            metadata: Vec::new(),
211        }
212    }
213}
214
215impl<'i, Deco: Decoration> Posting<'i, Deco> {
216    pub fn new(account: Deco::Decorated<Cow<'i, str>>) -> Self {
217        Posting {
218            account,
219            clear_state: ClearState::default(),
220            amount: None,
221            balance: None,
222            metadata: Vec::new(),
223        }
224    }
225}
226
227/// Represents a clearing state, often combined with the ambiguity.
228#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, ToStatic)]
229pub enum ClearState {
230    /// No specific meaning.
231    #[default]
232    Uncleared,
233    /// Useful to declare that the transaction / post is confirmed.
234    Cleared,
235    /// Useful to declare that the transaction / post is still pending.
236    Pending,
237}
238
239/// Metadata represents meta information associated with transactions / posts.
240#[derive(Debug, PartialEq, Eq, ToStatic)]
241pub enum Metadata<'i> {
242    /// Comment, which covers just one line (without the suceeding new line).
243    Comment(Cow<'i, str>),
244    /// Tags of word, in a format :tag1:tag2:tag3:, each tag can't contain white spaces.
245    WordTags(Vec<Cow<'i, str>>),
246    /// Key-value paired tag. Key can't contain white spaces.
247    KeyValueTag {
248        key: Cow<'i, str>,
249        value: MetadataValue<'i>,
250    },
251}
252
253/// MetadataValue represents the value in key-value pair used in `Metadata`.
254#[derive(Debug, PartialEq, Eq, ToStatic)]
255pub enum MetadataValue<'i> {
256    /// Regular string.
257    Text(Cow<'i, str>),
258    /// Expression parsed properly prefixed by `::` instead of `:`.
259    // TODO: Change htis type to Expr not Cow<'i, str>.
260    Expr(Cow<'i, str>),
261}
262
263/// This is an amout for each posting.
264/// Which contains
265/// - how much the asset is increased.
266/// - what was the cost in the other commodity.
267/// - lot information.
268#[derive_where(Debug, PartialEq, Eq)]
269pub struct PostingAmount<'i, Deco: Decoration> {
270    pub amount: Deco::Decorated<expr::ValueExpr<'i>>,
271    pub cost: Option<Deco::Decorated<Exchange<'i>>>,
272    pub lot: Lot<'i, Deco>,
273}
274
275impl PostingAmount<'_, plain::Ident> {
276    #[cfg(test)]
277    fn to_static(&self) -> PostingAmount<'static, plain::Ident> {
278        PostingAmount {
279            amount: self.amount.to_static(),
280            cost: self.cost.to_static(),
281            lot: self.lot.to_static(),
282        }
283    }
284}
285
286impl<'i> From<expr::ValueExpr<'i>> for PostingAmount<'i, plain::Ident> {
287    fn from(v: expr::ValueExpr<'i>) -> Self {
288        PostingAmount {
289            amount: v,
290            cost: None,
291            lot: Lot::default(),
292        }
293    }
294}
295
296/// Lot information is a set of metadata to record the original lot which the commodity is acquired with.
297#[derive_where(Debug, PartialEq, Eq)]
298pub struct Lot<'i, Deco: Decoration> {
299    pub price: Option<Deco::Decorated<Exchange<'i>>>,
300    pub date: Option<NaiveDate>,
301    pub note: Option<Cow<'i, str>>,
302}
303
304impl Lot<'_, plain::Ident> {
305    #[cfg(test)]
306    fn to_static(&self) -> Lot<'static, plain::Ident> {
307        Lot {
308            price: self.price.to_static(),
309            date: self.date.to_static(),
310            note: self.note.to_static(),
311        }
312    }
313}
314
315impl<Deco: Decoration> Default for Lot<'_, Deco> {
316    fn default() -> Self {
317        Self {
318            price: None,
319            date: None,
320            note: None,
321        }
322    }
323}
324
325/// Exchange represents the amount expressed in the different commodity.
326#[derive(Debug, PartialEq, Eq, ToStatic)]
327pub enum Exchange<'i> {
328    /// Specified value equals to the total amount.
329    /// For example,
330    /// `200 JPY @@ 2 USD`
331    /// means the amount was 200 JPY, which is equal to 2 USD.
332    Total(expr::ValueExpr<'i>),
333    /// Specified value equals to the amount of one original commodity.
334    /// For example,
335    /// `200 JPY @ (1 / 100 USD)`
336    /// means the amount was 200 JPY, where 1 JPY is equal to 1/100 USD.
337    Rate(expr::ValueExpr<'i>),
338}
339
340/// Price DB entry, which contains one commodity price
341/// in another commodity on a particular date time.
342#[derive(Debug, PartialEq, Eq, ToStatic)]
343pub struct PriceDBEntry<'i> {
344    pub datetime: NaiveDateTime,
345    /// Target commodity of the price.
346    pub target: Cow<'i, str>,
347    /// The rate of the target commodity.
348    /// 1 target == rate.
349    pub rate: expr::Amount<'i>,
350}