|
@@ -1,41 +1,39 @@
|
|
-use super::{AccountName, UnitName, spec::RootSpec};
|
|
|
|
|
|
+use super::{AccountName, UnitName, Spanned};
|
|
|
|
|
|
-use chumsky::{prelude::*, text::inline_whitespace};
|
|
|
|
|
|
+mod parse;
|
|
|
|
+pub use parse::parse_ledger;
|
|
|
|
|
|
-#[derive(Clone, Copy, Hash, PartialEq, PartialOrd, Debug, Ord, Eq)]
|
|
|
|
-pub enum Direction {
|
|
|
|
- Deposit,
|
|
|
|
- Withdrawal,
|
|
|
|
-}
|
|
|
|
|
|
+mod print;
|
|
|
|
+pub use print::print_ledger;
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
|
pub struct Balance {
|
|
pub struct Balance {
|
|
- pub account: AccountName,
|
|
|
|
- pub amount: rust_decimal::Decimal,
|
|
|
|
- pub unit: UnitName,
|
|
|
|
|
|
+ pub account: Spanned<AccountName>,
|
|
|
|
+ pub amount: Spanned<rust_decimal::Decimal>,
|
|
|
|
+ pub unit: Spanned<UnitName>,
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
|
pub struct LedgerEntry {
|
|
pub struct LedgerEntry {
|
|
pub datestamp: (u16, u8, u8),
|
|
pub datestamp: (u16, u8, u8),
|
|
pub title: Option<String>,
|
|
pub title: Option<String>,
|
|
- pub balances: Vec<Balance>,
|
|
|
|
|
|
+ pub balances: Vec<Spanned<Balance>>,
|
|
}
|
|
}
|
|
|
|
|
|
impl LedgerEntry {
|
|
impl LedgerEntry {
|
|
pub fn modifies(&self, account: AccountName) -> bool {
|
|
pub fn modifies(&self, account: AccountName) -> bool {
|
|
- self.balances.iter().any(|b| b.account == account)
|
|
|
|
|
|
+ self.balances.iter().any(|b| b.account.as_ref() == &account)
|
|
}
|
|
}
|
|
|
|
|
|
- pub fn balance_for(&self, account: AccountName) -> Option<&Balance> {
|
|
|
|
- self.balances.iter().find(|b| b.account == account)
|
|
|
|
|
|
+ pub fn balance_for(&self, account: AccountName) -> Option<&Spanned<Balance>> {
|
|
|
|
+ self.balances.iter().find(|b| b.account.as_ref() == &account)
|
|
}
|
|
}
|
|
|
|
|
|
pub fn split_balances(
|
|
pub fn split_balances(
|
|
&self,
|
|
&self,
|
|
account: AccountName,
|
|
account: AccountName,
|
|
- ) -> Option<(&Balance, impl Iterator<Item = &Balance>)> {
|
|
|
|
- let index = self.balances.iter().position(|b| b.account == account)?;
|
|
|
|
|
|
+ ) -> Option<(&Spanned<Balance>, impl Iterator<Item = &Spanned<Balance>>)> {
|
|
|
|
+ let index = self.balances.iter().position(|b| b.account.as_ref() == &account)?;
|
|
Some((
|
|
Some((
|
|
&self.balances[index],
|
|
&self.balances[index],
|
|
self.balances[0..index]
|
|
self.balances[0..index]
|
|
@@ -44,123 +42,3 @@ impl LedgerEntry {
|
|
))
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
-struct Spanned<T>(T, SimpleSpan);
|
|
|
|
-
|
|
|
|
-fn ledger_parser<'a>() -> impl Parser<
|
|
|
|
- 'a,
|
|
|
|
- &'a str,
|
|
|
|
- Vec<LedgerEntry>,
|
|
|
|
- chumsky::extra::Full<
|
|
|
|
- chumsky::error::Rich<'a, char>,
|
|
|
|
- chumsky::extra::SimpleState<&'a RootSpec>,
|
|
|
|
- (),
|
|
|
|
- >,
|
|
|
|
-> {
|
|
|
|
- let int = chumsky::text::digits(10)
|
|
|
|
- .to_slice()
|
|
|
|
- .map(|v: &str| v.parse::<usize>().unwrap());
|
|
|
|
-
|
|
|
|
- let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
|
|
|
|
- .map(|(y, _, m, _, d)| (y as u16, m as u8, d as u8));
|
|
|
|
-
|
|
|
|
- let mark = |m| just(m).padded_by(inline_whitespace());
|
|
|
|
-
|
|
|
|
- let decimal_digit = one_of("0123456789.,");
|
|
|
|
- let decimal_digits = decimal_digit
|
|
|
|
- .or(just(' ').repeated().ignore_then(decimal_digit))
|
|
|
|
- .repeated();
|
|
|
|
-
|
|
|
|
- let decimal = choice((just('-').ignored(), just('+').ignored(), empty()))
|
|
|
|
- .then(decimal_digits)
|
|
|
|
- .to_slice()
|
|
|
|
- .try_map(|s: &str, span| {
|
|
|
|
- rust_decimal::Decimal::from_str_exact(s.trim()).map_err(|e| {
|
|
|
|
- Rich::custom(span, format!("Failed to parse '{s}' as a decimal number"))
|
|
|
|
- })
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- let balance = group((
|
|
|
|
- mark('-'),
|
|
|
|
- none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(stringstore::StoredString::new(v), e.span())),
|
|
|
|
- mark(':'),
|
|
|
|
- decimal,
|
|
|
|
- choice((
|
|
|
|
- inline_whitespace()
|
|
|
|
- .at_least(1)
|
|
|
|
- .ignore_then(chumsky::text::ident())
|
|
|
|
- .then_ignore(inline_whitespace())
|
|
|
|
- .map_with(|u, e| Some(Spanned(UnitName::new(u), e.span()))),
|
|
|
|
- inline_whitespace().map(|_| None),
|
|
|
|
- ))
|
|
|
|
- .then_ignore(chumsky::text::newline()),
|
|
|
|
- ))
|
|
|
|
- .try_map_with(|(_, Spanned(acc_name, acc_span), _, amount, unit_info, ), e| {
|
|
|
|
- let span = e.span();
|
|
|
|
- let spec: &mut chumsky::extra::SimpleState<&RootSpec> = e.state();
|
|
|
|
-
|
|
|
|
- let Some(acc_spec) = spec.accounts.get(&acc_name) else {
|
|
|
|
- return Err(chumsky::error::Rich::custom(acc_span, "no such account"));
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- let (unit, unit_span) = match unit_info {
|
|
|
|
- Some(Spanned(unit, unit_span)) => (unit, unit_span),
|
|
|
|
- None => acc_spec.default_unit.map(|u| (u, span)).ok_or_else(||
|
|
|
|
- chumsky::error::Rich::custom(span, format!("No unit specified and no default unit specified for account '{acc_name}'")))?
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- if !spec.units.contains_key(&unit) {
|
|
|
|
- return Err(chumsky::error::Rich::custom(unit_span, format!("no such unit '{unit}' found")))
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- Ok(Balance {
|
|
|
|
- account: acc_name,
|
|
|
|
- amount,
|
|
|
|
- unit,
|
|
|
|
- })
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- let entry = group((
|
|
|
|
- chumsky::text::whitespace(),
|
|
|
|
- datestamp,
|
|
|
|
- mark(':'),
|
|
|
|
- inline_whitespace(),
|
|
|
|
- chumsky::primitive::none_of("\n")
|
|
|
|
- .repeated()
|
|
|
|
- .collect::<String>(),
|
|
|
|
- chumsky::text::newline(),
|
|
|
|
- balance.repeated().at_least(1).collect(),
|
|
|
|
- chumsky::text::whitespace(),
|
|
|
|
- ))
|
|
|
|
- .map(|(_, datestamp, _, _, title, _, balances, _)| LedgerEntry {
|
|
|
|
- datestamp,
|
|
|
|
- title: (!title.is_empty()).then_some(title),
|
|
|
|
- balances,
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- entry.repeated().collect()
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-pub fn parse_ledger(
|
|
|
|
- source: super::SourceFile,
|
|
|
|
- spec: &super::spec::RootSpec,
|
|
|
|
- data: &str,
|
|
|
|
-) -> Result<Vec<LedgerEntry>, super::DataError> {
|
|
|
|
- let parser = ledger_parser();
|
|
|
|
-
|
|
|
|
- let (presult, errors) = parser
|
|
|
|
- .parse_with_state(data, &mut chumsky::extra::SimpleState(spec))
|
|
|
|
- .into_output_errors();
|
|
|
|
-
|
|
|
|
- if let Some(e) = errors.first() {
|
|
|
|
- let span = e.span().start()..e.span().end();
|
|
|
|
-
|
|
|
|
- let report = ariadne::Report::build(ariadne::ReportKind::Error, (source, span.clone()))
|
|
|
|
- .with_label(ariadne::Label::new((source, span)).with_message(e.reason()))
|
|
|
|
- .finish();
|
|
|
|
-
|
|
|
|
- Err(report.into())
|
|
|
|
- } else {
|
|
|
|
- Ok(presult.unwrap())
|
|
|
|
- }
|
|
|
|
-}
|
|
|