use crate::data::{AccountName, UnitName, Span, Spanned, spec::SpecRoot, DataError, SourceFile}; use super::{LedgerEntry, Balance}; use chumsky::{prelude::*, text::inline_whitespace}; type InputWithContext<'a> = chumsky::input::WithContext; /* #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] struct InputWrap<'a>(SourceFile, &'a str); impl<'a> chumsky::input::Input<'a> for InputWrap<'a> { type Token = char; type Span = Span; type Cache = (); type Cursor = usize; type MaybeToken = char; fn begin(self) -> (Self::Cursor, Self::Cache) { (0, ()) } fn cursor_location(cursor: &Self::Cursor) -> usize { *cursor } unsafe fn span(_cache: &mut Self::Cache, range: std::ops::Range<&Self::Cursor>) -> Self::Span { let e : &[u8] = &[]; Span::new(stringstore::StoredOsString::from(e), (*range.start..*range.end)) } unsafe fn next_maybe( cache: &mut Self::Cache, cursor: &mut Self::Cursor, ) -> Option { None } } */ fn ledger_parser<'a>() -> impl Parser< 'a, InputWithContext<'a>, Vec>, chumsky::extra::Full< chumsky::error::Rich<'a, char, Span>, chumsky::extra::SimpleState<&'a SpecRoot>, (), >, > { let int = chumsky::text::digits(10) .to_slice() .map(|v: &str| v.parse::().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| { Ok(Spanned::new( rust_decimal::Decimal::from_str_exact(s.trim()).map_err(|e| { Rich::custom(span, format!("Failed to parse '{s}' as a decimal number")) })?, span )) }); 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(|(_, acc, _, amount, unit, ), e| { let span = e.span(); let spec: &mut chumsky::extra::SimpleState<&SpecRoot> = e.state(); let Some(acc_spec) = spec.accounts.get(acc.as_ref()) else { return Err(chumsky::error::Rich::custom(acc.span(), "no such account")); }; let unit = match unit { Some(sunit) => sunit, None => acc_spec.default_unit.map(|u| Spanned(u, span)).ok_or_else(|| chumsky::error::Rich::custom(span, format!("No unit specified and no default unit specified for account '{}'", acc.as_ref())))? }; if !spec.units.contains_key(&unit) { return Err(chumsky::error::Rich::custom(unit.span(), format!("no such unit '{unit}' found"))) } Ok(Spanned::new(Balance { account: acc, amount, unit, }, span)) }); let entry = group(( chumsky::text::whitespace(), datestamp, mark(':'), inline_whitespace(), chumsky::primitive::none_of("\n") .repeated() .collect::(), chumsky::text::newline(), balance.repeated().at_least(1).collect(), chumsky::text::whitespace(), )) .map_with(|(_, datestamp, _, _, title, _, balances, _), e| Spanned::new(LedgerEntry { datestamp, title: (!title.is_empty()).then_some(title), balances, }, e.span())); entry.repeated().collect() } pub fn parse_ledger( source: SourceFile, spec: &SpecRoot, data: &str, ) -> Result>, DataError> { let parser = ledger_parser(); let (presult, errors) = parser .parse_with_state(data.with_context(source), &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()) } }