|
@@ -1,6 +1,6 @@
|
|
|
-use super::{AccountName, UnitName};
|
|
|
+use super::{AccountName, UnitName, spec::RootSpec};
|
|
|
|
|
|
-use chumsky::prelude::*;
|
|
|
+use chumsky::{prelude::*, text::inline_whitespace};
|
|
|
|
|
|
#[derive(Clone, Copy, Hash, PartialEq, PartialOrd, Debug, Ord, Eq)]
|
|
|
pub enum Direction {
|
|
@@ -11,8 +11,7 @@ pub enum Direction {
|
|
|
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
|
|
pub struct Balance {
|
|
|
pub account: AccountName,
|
|
|
- pub amount: usize,
|
|
|
- pub dir: Direction,
|
|
|
+ pub amount: rust_decimal::Decimal,
|
|
|
pub unit: UnitName,
|
|
|
}
|
|
|
|
|
@@ -30,52 +29,105 @@ impl LedgerEntry {
|
|
|
.find(|b| b.account == account)
|
|
|
.is_some()
|
|
|
}
|
|
|
+
|
|
|
+ pub fn balance_for(&self, account: AccountName) -> Option<&Balance> {
|
|
|
+ self.balances.iter().find(|b| b.account == account)
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn split_balances(
|
|
|
+ &self,
|
|
|
+ account: AccountName,
|
|
|
+ ) -> Option<(&Balance, impl Iterator<Item = &Balance>)> {
|
|
|
+ let index = self.balances.iter().position(|b| b.account == account)?;
|
|
|
+ Some((
|
|
|
+ &self.balances[index],
|
|
|
+ self.balances[0..index]
|
|
|
+ .iter()
|
|
|
+ .chain(self.balances[index + 1..].iter()),
|
|
|
+ ))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-fn ledger_parser<'a>()
|
|
|
--> impl Parser<'a, &'a str, Vec<LedgerEntry>, chumsky::extra::Err<chumsky::error::Rich<'a, char>>> {
|
|
|
+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)
|
|
|
- .collect()
|
|
|
- .map(|v: String| v.parse::<usize>().unwrap());
|
|
|
+ .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| {
|
|
|
- chumsky::text::inline_whitespace()
|
|
|
- .ignore_then(just(m))
|
|
|
- .then_ignore(chumsky::text::inline_whitespace())
|
|
|
- };
|
|
|
+ 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('-'),
|
|
|
- chumsky::text::ident().map(|v| stringstore::StoredString::new(v)),
|
|
|
+ none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(stringstore::StoredString::new(v), e.span())),
|
|
|
mark(':'),
|
|
|
+ decimal,
|
|
|
choice((
|
|
|
- mark('-').map(|_| Direction::Withdrawal),
|
|
|
- mark('+').map(|_| Direction::Deposit),
|
|
|
- )),
|
|
|
- int,
|
|
|
- just('.'),
|
|
|
- int,
|
|
|
- chumsky::text::inline_whitespace(),
|
|
|
- chumsky::primitive::none_of("\n")
|
|
|
- .repeated()
|
|
|
- .collect::<String>(),
|
|
|
- chumsky::text::newline(),
|
|
|
+ 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()),
|
|
|
))
|
|
|
- .map(|(_, acc, _, dir, w, _, f, _, unit, _)| Balance {
|
|
|
- account: acc,
|
|
|
- dir,
|
|
|
- amount: w,
|
|
|
- unit: "UNIT".into(),
|
|
|
+ .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(':'),
|
|
|
- chumsky::text::inline_whitespace(),
|
|
|
+ inline_whitespace(),
|
|
|
chumsky::primitive::none_of("\n")
|
|
|
.repeated()
|
|
|
.collect::<String>(),
|
|
@@ -94,11 +146,14 @@ fn ledger_parser<'a>()
|
|
|
|
|
|
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(data).into_output_errors();
|
|
|
+ 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();
|