parse.rs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. use crate::data::{
  2. AccountName, DataError, Datestamp, Decimal, SourceFile, Span, Spanned, UnitName, spec::SpecRoot,
  3. };
  4. use super::{Change, Transaction, LedgerEntry};
  5. use chumsky::{prelude::*, text::inline_whitespace};
  6. type InputWithContext<'a> = chumsky::input::WithContext<Span, &'a str>;
  7. fn ledger_parser<'a>() -> impl Parser<
  8. 'a,
  9. InputWithContext<'a>,
  10. Vec<LedgerEntry>,
  11. chumsky::extra::Full<
  12. chumsky::error::Rich<'a, char, Span>,
  13. chumsky::extra::SimpleState<&'a SpecRoot>,
  14. (),
  15. >,
  16. > {
  17. let int = chumsky::text::digits(10)
  18. .to_slice()
  19. .map(|v: &str| v.parse::<usize>().unwrap());
  20. let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
  21. .map(|(y, _, m, _, d)| Datestamp { year: y as u16, month: m as u8, day: d as u8 });
  22. let mark = |m| just(m).padded_by(inline_whitespace());
  23. let decimal_digit = one_of("0123456789.,");
  24. let decimal_digits = decimal_digit
  25. .or(just(' ').repeated().ignore_then(decimal_digit))
  26. .repeated();
  27. let decimal = choice((just('-').ignored(), just('+').ignored(), empty()))
  28. .then(decimal_digits)
  29. .to_slice()
  30. .try_map(|s: &str, span| {
  31. Ok(Spanned::new(
  32. Decimal::from_str_exact(s.trim()).map_err(|e| {
  33. Rich::custom(
  34. span,
  35. format!("Failed to parse '{s}' as a decimal number: {e}"),
  36. )
  37. })?,
  38. span,
  39. ))
  40. });
  41. let change = group((
  42. mark('-'),
  43. none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(AccountName::new(v), e.span())),
  44. mark(':'),
  45. decimal,
  46. choice((
  47. mark('=').ignore_then(decimal).map(Some),
  48. empty().map(|_| None)
  49. )),
  50. choice((
  51. inline_whitespace()
  52. .at_least(1)
  53. .ignore_then(chumsky::text::ident())
  54. .then_ignore(inline_whitespace())
  55. .map_with(|u, e| Some(Spanned(UnitName::new(u), e.span()))),
  56. inline_whitespace().map(|_| None),
  57. ))
  58. .then_ignore(chumsky::text::newline()),
  59. ))
  60. .try_map_with(|(_, acc, _, amount, balance, unit, ), e| {
  61. let span = e.span();
  62. let spec: &mut chumsky::extra::SimpleState<&SpecRoot> = e.state();
  63. let Some(acc_spec) = spec.accounts.get(acc.as_ref()) else {
  64. return Err(chumsky::error::Rich::custom(acc.span(), "no such account"));
  65. };
  66. let unit = match (unit, acc_spec.unit) {
  67. (None, None) => {
  68. return Err(chumsky::error::Rich::custom(span, "account does not have a unit specified, so all transactions must specify units"))
  69. }
  70. (Some(unit), None) => unit,
  71. (None, Some(unit)) => Spanned(unit, amount.span().to_end()),
  72. (Some(unit1), Some(unit2)) => {
  73. if *unit1 != unit2 {
  74. return Err(chumsky::error::Rich::custom(span, "unit mismatch between account and transaction"))
  75. } else {
  76. unit1
  77. }
  78. }
  79. };
  80. if !spec.units.contains_key(&unit) {
  81. return Err(chumsky::error::Rich::custom(unit.span(), format!("no such unit '{unit}' found")))
  82. }
  83. Ok(Spanned::new(Change {
  84. account: acc,
  85. amount,
  86. balance,
  87. unit,
  88. }, span))
  89. });
  90. let annotation = mark('[')
  91. .ignore_then(none_of("]\n\t").repeated().to_slice())
  92. .then_ignore(mark(']'))
  93. .map(|v: &str| String::from(v.trim()));
  94. let transaction = group((
  95. chumsky::text::whitespace(),
  96. datestamp,
  97. mark(':'),
  98. inline_whitespace(),
  99. chumsky::primitive::none_of("\n")
  100. .repeated()
  101. .collect::<String>(),
  102. chumsky::text::newline(),
  103. choice((
  104. annotation
  105. .repeated()
  106. .collect()
  107. .then_ignore(chumsky::text::newline()),
  108. empty().map(|_| vec![]),
  109. )),
  110. change.repeated().at_least(1).collect(),
  111. chumsky::text::whitespace(),
  112. ))
  113. .map_with(
  114. |(_, datestamp, _, _, title, _, annotations, changes, _), e| {
  115. LedgerEntry::Transaction(Spanned::new(
  116. Transaction {
  117. datestamp,
  118. title: (!title.is_empty()).then_some(title),
  119. annotations,
  120. changes,
  121. },
  122. e.span(),
  123. ))
  124. },
  125. );
  126. 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())));
  127. (transaction.or(comment)).repeated().collect()
  128. }
  129. pub fn parse_ledger(
  130. source: SourceFile,
  131. spec: &SpecRoot,
  132. data: &str,
  133. ) -> Result<Vec<LedgerEntry>, DataError> {
  134. let parser = ledger_parser();
  135. let (presult, errors) = parser
  136. .parse_with_state(
  137. data.with_context(source),
  138. &mut chumsky::extra::SimpleState(spec),
  139. )
  140. .into_output_errors();
  141. if let Some(e) = errors.first() {
  142. // let span = e.span().start()..e.span().end();
  143. let span = *e.span();
  144. let report = ariadne::Report::build(ariadne::ReportKind::Error, span)
  145. .with_label(ariadne::Label::new(span).with_message(e.reason()))
  146. .finish();
  147. Err(report.into())
  148. } else {
  149. Ok(presult.unwrap())
  150. }
  151. }