lumi/
ledger.rs

1use crate::parse::Parser;
2pub(crate) use chrono::NaiveDate;
3use getset::{CopyGetters, Getters};
4use rust_decimal::Decimal;
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8use std::convert::From;
9use std::fmt;
10use std::ops::{Div, Mul};
11use std::sync::Arc;
12
13/// Representing a location, line number and column number, in a source file.
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
16pub struct Location {
17    pub line: usize,
18    pub col: usize,
19}
20
21impl Location {
22    pub fn advance(&self, width: usize) -> Self {
23        Location {
24            col: self.col + width,
25            line: self.line,
26        }
27    }
28}
29
30impl From<(usize, usize)> for Location {
31    fn from(tuple: (usize, usize)) -> Self {
32        Location {
33            line: tuple.0,
34            col: tuple.1,
35        }
36    }
37}
38
39/// A string wrapped in [`Arc`](std::sync::Arc)
40/// representing the source file path.
41pub type SrcFile = Arc<String>;
42
43/// Represents a range in a source file. This struct is used to track the origins
44/// of any information in the generated [`Ledger`], as well as for locating errors.
45#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct Source {
48    pub file: SrcFile,
49    pub start: Location,
50    pub end: Location,
51}
52
53impl fmt::Display for Source {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}:{}:{}", self.file, self.start.line, self.start.col)
56    }
57}
58
59/// Kinds of errors that `lumi` encountered during generating [`Ledger`] from
60/// files input text.
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum ErrorType {
64    /// IO error, e.g., the context of an input file cannot be read.
65    Io,
66    /// Syntax error in the source file.
67    Syntax,
68    /// Indicates a transactions is not balanced.
69    NotBalanced,
70    /// A transaction missing too much information such that `lumi` cannot infer
71    /// for the context.
72    Incomplete,
73    /// An unopened or already closed account is referred.
74    Account,
75    /// `lumi` cannot find a position in the running balance sheet that matching
76    /// the cost basis provided in the posting.
77    NoMatch,
78    /// Multiple Positions are founded in the running balance sheet that matching
79    /// the cost basis provided in the posting.
80    Ambiguous,
81    /// Duplicate information, such as two identical tags in a single transaction.
82    Duplicate,
83}
84
85/// The level of an error. Any information in the source file resulting an
86/// [`ErrorLevel::Error`] are dropped.
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum ErrorLevel {
90    Info,
91    Warning,
92    Error,
93}
94/// Contains the full information of an error.
95#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub struct Error {
98    pub msg: String,
99    pub src: Source,
100    pub r#type: ErrorType,
101    pub level: ErrorLevel,
102}
103
104impl fmt::Display for Error {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(
107            f,
108            "{:?}: {}\n  {}:{}:{}",
109            self.level, self.msg, self.src.file, self.src.start.line, self.src.start.col
110        )
111    }
112}
113
114pub type Currency = String;
115
116/// A [`Decimal`] number plus the currency.
117#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
118#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub struct Amount {
120    pub number: Decimal,
121    pub currency: Currency,
122}
123
124impl fmt::Display for Amount {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "{} {}", self.number, self.currency)
127    }
128}
129
130impl<'a> Div<Decimal> for &'a Amount {
131    type Output = Amount;
132
133    fn div(self, rhs: Decimal) -> Self::Output {
134        Amount {
135            number: self.number / rhs,
136            currency: self.currency.clone(),
137        }
138    }
139}
140
141impl Div<Decimal> for Amount {
142    type Output = Amount;
143
144    fn div(self, rhs: Decimal) -> Self::Output {
145        Amount {
146            number: self.number / rhs,
147            currency: self.currency,
148        }
149    }
150}
151
152impl<'a> Mul<Decimal> for &'a Amount {
153    type Output = Amount;
154
155    fn mul(self, rhs: Decimal) -> Self::Output {
156        Amount {
157            number: self.number * rhs,
158            currency: self.currency.clone(),
159        }
160    }
161}
162
163/// The unit price.
164pub type Price = Amount;
165
166/// The cost basis information (unit cost and transaction date) used to identify
167/// a position in the running balances.
168#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
169#[derive(Debug, Clone, PartialEq, Eq, Hash)]
170pub struct UnitCost {
171    /// The unit cost basis.
172    pub amount: Amount,
173    /// The transaction date.
174    pub date: NaiveDate,
175}
176
177impl fmt::Display for UnitCost {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        write!(f, "{{ {}, {} }}", self.amount, self.date)
180    }
181}
182
183/// The flag of a [`Transaction`].
184#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
185#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
186pub enum TxnFlag {
187    /// transactions flagged by `?`.
188    Pending,
189    /// transactions flagged by `txn` or `*`.
190    Posted,
191    /// `pad` directives.
192    Pad,
193    /// `balance` directives.
194    Balance,
195}
196
197impl fmt::Display for TxnFlag {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            TxnFlag::Pending => write!(f, "!"),
201            TxnFlag::Posted | TxnFlag::Pad => write!(f, "*"),
202            TxnFlag::Balance => write!(f, "balance"),
203        }
204    }
205}
206
207/// A string wrapped in [`Arc`](std::sync::Arc)
208/// representing the account name.
209pub type Account = Arc<String>;
210
211/// A posting like `Assets::Bank -100 JPY` inside a [`Transaction`].
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct Posting {
215    pub account: Account,
216    pub amount: Amount,
217    pub cost: Option<UnitCost>,
218    pub price: Option<Price>,
219    pub meta: Meta,
220    pub src: Source,
221}
222
223impl fmt::Display for Posting {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        let num_str = self.amount.to_string();
226        let index = num_str.find(|c| c == ' ' || c == '.').unwrap();
227        let width = f.width().unwrap_or(46) - 1;
228        let account_width = std::cmp::max(self.account.len() + 1, width - index);
229        write!(
230            f,
231            "{:width$}{}",
232            self.account,
233            num_str,
234            width = account_width
235        )?;
236        if let Some(cost) = &self.cost {
237            write!(f, " {}", cost)?;
238        }
239        if let Some(ref price) = self.price {
240            write!(f, " {}", price)?;
241        }
242        Ok(())
243    }
244}
245
246pub type Payee = String;
247pub type Narration = String;
248pub type Link = String;
249pub type Tag = String;
250
251/// Represents a transaction, or a `pad` directives, or a `balance` directive in
252/// the source file.
253#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
254#[derive(Debug, Clone, PartialEq, Eq, Getters, CopyGetters)]
255pub struct Transaction {
256    /// Returns the transaction date.
257    #[getset(get_copy = "pub")]
258    pub(crate) date: NaiveDate,
259
260    /// Returns the transaction flag.
261    #[getset(get_copy = "pub")]
262    pub(crate) flag: TxnFlag,
263
264    /// Returns the payee.
265    #[getset(get = "pub")]
266    pub(crate) payee: Payee,
267
268    /// Returns the narration.
269    #[getset(get = "pub")]
270    pub(crate) narration: Narration,
271
272    /// Returns the links.
273    #[getset(get = "pub")]
274    pub(crate) links: Vec<Link>,
275
276    /// Returns the tags.
277    #[getset(get = "pub")]
278    pub(crate) tags: Vec<Tag>,
279
280    /// Returns the meta data associated with this transaction.
281    #[getset(get = "pub")]
282    pub(crate) meta: Meta,
283
284    /// Returns the postings of this transaction.
285    #[getset(get = "pub")]
286    pub(crate) postings: Vec<Posting>,
287
288    /// Returns the source of this transaction.
289    #[getset(get = "pub")]
290    pub(crate) src: Source,
291}
292
293/// Represents a `note` directive
294#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
295#[derive(Debug, Clone, PartialEq, Eq, Hash)]
296pub struct AccountNote {
297    pub date: NaiveDate,
298    pub val: String,
299    pub src: Source,
300}
301
302/// Represents a `document` directive
303pub type AccountDoc = AccountNote;
304
305/// Represents the meta data attached to a commodity, a transaction, or a posting.
306pub type Meta = HashMap<String, (String, Source)>;
307
308/// Contains the open/close date of an account, as well as the notes and documents.
309#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
310#[derive(Debug, Clone, PartialEq, Eq, Getters)]
311pub struct AccountInfo {
312    /// Returns the account open date and the source of the `open` directive.
313    #[getset(get = "pub")]
314    pub(crate) open: (NaiveDate, Source),
315
316    /// Returns the account close date and the source of the `close` directive.
317    #[getset(get = "pub")]
318    pub(crate) close: Option<(NaiveDate, Source)>,
319
320    /// Returns the allowed currencies of this account. If there are no limitations,
321    /// an empty set is returned.
322    #[getset(get = "pub")]
323    pub(crate) currencies: HashSet<Currency>,
324
325    /// Returns the account notes in `note` directives.
326    #[getset(get = "pub")]
327    pub(crate) notes: Vec<AccountNote>,
328
329    /// Returns the account documents in `document` directives.
330    #[getset(get = "pub")]
331    pub(crate) docs: Vec<AccountDoc>,
332
333    /// Returns the account meta data associated with the `open` directive.
334    #[getset(get = "pub")]
335    pub(crate) meta: Meta,
336}
337
338/// Represents an `event` directive.
339#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
340#[derive(Debug, Clone, PartialEq, Eq, Hash)]
341pub struct EventInfo {
342    pub date: NaiveDate,
343    pub desc: String,
344    pub src: Source,
345}
346
347impl From<(NaiveDate, String, Source)> for EventInfo {
348    fn from(tuple: (NaiveDate, String, Source)) -> Self {
349        EventInfo {
350            date: tuple.0,
351            desc: tuple.1,
352            src: tuple.2,
353        }
354    }
355}
356
357/// Represents the final balances of all accounts.
358pub type BalanceSheet = HashMap<Account, HashMap<Currency, HashMap<Option<UnitCost>, Decimal>>>;
359
360/// Represents a valid ledger containing all valid accounts and balanced
361/// transactions.
362#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
363#[derive(Debug, Clone, PartialEq, Eq, Getters)]
364pub struct Ledger {
365    /// Returns the information of accounts.
366    #[getset(get = "pub")]
367    pub(crate) accounts: HashMap<Account, AccountInfo>,
368    /// Returns all the currencies defined by `commodity` directives.
369    #[getset(get = "pub")]
370    pub(crate) commodities: HashMap<Currency, (Meta, Source)>,
371    /// Returns transactions, `pad` directives, and `balance` directives, sorted
372    /// by date.
373    #[getset(get = "pub")]
374    pub(crate) txns: Vec<Transaction>,
375    /// Returns the options as a hash map.
376    #[getset(get = "pub")]
377    pub(crate) options: HashMap<String, (String, Source)>,
378    /// Returns the events.
379    #[getset(get = "pub")]
380    pub(crate) events: HashMap<String, Vec<EventInfo>>,
381    /// Returns a list of source files.
382    #[getset(get = "pub")]
383    pub(crate) files: Vec<SrcFile>,
384    /// Returns the final balances.
385    #[getset(get = "pub")]
386    pub(crate) balance_sheet: BalanceSheet,
387}
388
389impl Ledger {
390    pub fn from_file(path: &str) -> (Self, Vec<Error>) {
391        let (draft, mut errors) = Parser::parse(path);
392        let (ledger, more_errors) = draft.into_ledger();
393        errors.extend(more_errors);
394        (ledger, errors)
395    }
396}
397
398impl fmt::Display for Transaction {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        match self.flag {
401            TxnFlag::Balance => write!(f, "{} {}", self.date, self.flag)?,
402            _ => write!(
403                f,
404                "{} {} \"{}\" \"{}\"",
405                self.date, self.flag, self.payee, self.narration
406            )?,
407        };
408        for tag in &self.tags {
409            write!(f, " {}", tag)?;
410        }
411        for link in &self.links {
412            write!(f, " {}", link)?;
413        }
414        for (key, val) in self.meta.iter() {
415            write!(f, "\n  {}: {}", key, val.0)?;
416        }
417        let width = f.width().unwrap_or(50);
418        match self.flag {
419            TxnFlag::Balance => {
420                if self.postings.len() == 1 {
421                    write!(f, " {:width$}", self.postings[0], width = width - 19)?;
422                } else {
423                    for posting in self.postings.iter() {
424                        write!(f, "\n    {:width$}", posting, width = width - 4)?;
425                    }
426                }
427            }
428            _ => {
429                for posting in self.postings.iter() {
430                    write!(f, "\n    {:width$}", posting, width = width - 4)?;
431                }
432            }
433        }
434        Ok(())
435    }
436}