123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- use crate::data::{
- AccountName, DataError, Datestamp, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
- };
- use super::{Change, Transaction, LedgerEntry};
- use chumsky::{prelude::*, text::inline_whitespace};
- type InputWithContext<'a> = chumsky::input::WithContext<Span, &'a str>;
- fn ledger_parser<'a>() -> impl Parser<
- 'a,
- InputWithContext<'a>,
- Vec<LedgerEntry>,
- 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::<usize>().unwrap());
- let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
- .map(|(y, _, m, _, d)| Datestamp { year: y as u16, month: m as u8, day: 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(
- Decimal::from_str_exact(s.trim()).map_err(|e| {
- Rich::custom(
- span,
- format!("Failed to parse '{s}' as a decimal number: {e}"),
- )
- })?,
- span,
- ))
- });
- let change = group((
- mark('-'),
- none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(AccountName::new(v), e.span())),
- mark(':'),
- decimal,
- choice((
- mark('=').ignore_then(decimal).map(Some),
- empty().map(|_| None)
- )),
- 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, balance, 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, acc_spec.unit) {
- (None, None) => {
- return Err(chumsky::error::Rich::custom(span, "account does not have a unit specified, so all transactions must specify units"))
- }
- (Some(unit), None) => unit,
- (None, Some(unit)) => Spanned(unit, amount.span().to_end()),
- (Some(unit1), Some(unit2)) => {
- if *unit1 != unit2 {
- return Err(chumsky::error::Rich::custom(span, "unit mismatch between account and transaction"))
- } else {
- unit1
- }
- }
- };
- if !spec.units.contains_key(&unit) {
- return Err(chumsky::error::Rich::custom(unit.span(), format!("no such unit '{unit}' found")))
- }
- Ok(Spanned::new(Change {
- account: acc,
- amount,
- balance,
- unit,
- }, span))
- });
- let annotation = mark('[')
- .ignore_then(none_of("]\n\t").repeated().to_slice())
- .then_ignore(mark(']'))
- .map(|v: &str| String::from(v.trim()));
- let transaction = group((
- chumsky::text::whitespace(),
- datestamp,
- mark(':'),
- inline_whitespace(),
- chumsky::primitive::none_of("\n")
- .repeated()
- .collect::<String>(),
- chumsky::text::newline(),
- choice((
- annotation
- .repeated()
- .collect()
- .then_ignore(chumsky::text::newline()),
- empty().map(|_| vec![]),
- )),
- change.repeated().at_least(1).collect(),
- chumsky::text::whitespace(),
- ))
- .map_with(
- |(_, datestamp, _, _, title, _, annotations, changes, _), e| {
- LedgerEntry::Transaction(Spanned::new(
- Transaction {
- datestamp,
- title: (!title.is_empty()).then_some(title),
- annotations,
- changes,
- },
- e.span(),
- ))
- },
- );
- let comment = mark('#').ignore_then(none_of("\n").repeated()).padded().to_slice().map_with(|s: &str, e| LedgerEntry::Comment(Spanned::new(s.into(), e.span())));
- (transaction.or(comment)).repeated().collect()
- }
- pub fn parse_ledger(
- source: SourceFile,
- spec: &SpecRoot,
- data: &str,
- ) -> Result<Vec<LedgerEntry>, 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 span = *e.span();
- let report = ariadne::Report::build(ariadne::ReportKind::Error, span)
- .with_label(ariadne::Label::new(span).with_message(e.reason()))
- .finish();
- Err(report.into())
- } else {
- Ok(presult.unwrap())
- }
- }
|