Skip to main content

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