parse.rs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. use crate::data::{AccountName, UnitName, Span, Spanned, spec::SpecRoot, DataError, SourceFile};
  2. use super::{LedgerEntry, Balance};
  3. use chumsky::{prelude::*, text::inline_whitespace};
  4. type InputWithContext<'a> = chumsky::input::WithContext<Span, &'a str>;
  5. /*
  6. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
  7. struct InputWrap<'a>(SourceFile, &'a str);
  8. impl<'a> chumsky::input::Input<'a> for InputWrap<'a> {
  9. type Token = char;
  10. type Span = Span;
  11. type Cache = ();
  12. type Cursor = usize;
  13. type MaybeToken = char;
  14. fn begin(self) -> (Self::Cursor, Self::Cache) {
  15. (0, ())
  16. }
  17. fn cursor_location(cursor: &Self::Cursor) -> usize {
  18. *cursor
  19. }
  20. unsafe fn span(_cache: &mut Self::Cache, range: std::ops::Range<&Self::Cursor>) -> Self::Span {
  21. let e : &[u8] = &[];
  22. Span::new(stringstore::StoredOsString::from(e), (*range.start..*range.end))
  23. }
  24. unsafe fn next_maybe(
  25. cache: &mut Self::Cache,
  26. cursor: &mut Self::Cursor,
  27. ) -> Option<Self::MaybeToken> {
  28. None
  29. }
  30. }
  31. */
  32. fn ledger_parser<'a>() -> impl Parser<
  33. 'a,
  34. InputWithContext<'a>,
  35. Vec<Spanned<LedgerEntry>>,
  36. chumsky::extra::Full<
  37. chumsky::error::Rich<'a, char, Span>,
  38. chumsky::extra::SimpleState<&'a SpecRoot>,
  39. (),
  40. >,
  41. > {
  42. let int = chumsky::text::digits(10)
  43. .to_slice()
  44. .map(|v: &str| v.parse::<usize>().unwrap());
  45. let datestamp = group((int, just('-').ignored(), int, just('-').ignored(), int))
  46. .map(|(y, _, m, _, d)| (y as u16, m as u8, d as u8));
  47. let mark = |m| just(m).padded_by(inline_whitespace());
  48. let decimal_digit = one_of("0123456789.,");
  49. let decimal_digits = decimal_digit
  50. .or(just(' ').repeated().ignore_then(decimal_digit))
  51. .repeated();
  52. let decimal = choice((just('-').ignored(), just('+').ignored(), empty()))
  53. .then(decimal_digits)
  54. .to_slice()
  55. .try_map(|s: &str, span| {
  56. Ok(Spanned::new(
  57. rust_decimal::Decimal::from_str_exact(s.trim()).map_err(|e| {
  58. Rich::custom(span, format!("Failed to parse '{s}' as a decimal number"))
  59. })?,
  60. span
  61. ))
  62. });
  63. let balance = group((
  64. mark('-'),
  65. none_of(": \n\t").repeated().to_slice().map_with(|v, e| Spanned(stringstore::StoredString::new(v), e.span())),
  66. mark(':'),
  67. decimal,
  68. choice((
  69. inline_whitespace()
  70. .at_least(1)
  71. .ignore_then(chumsky::text::ident())
  72. .then_ignore(inline_whitespace())
  73. .map_with(|u, e| Some(Spanned(UnitName::new(u), e.span()))),
  74. inline_whitespace().map(|_| None),
  75. ))
  76. .then_ignore(chumsky::text::newline()),
  77. ))
  78. .try_map_with(|(_, acc, _, amount, unit, ), e| {
  79. let span = e.span();
  80. let spec: &mut chumsky::extra::SimpleState<&SpecRoot> = e.state();
  81. let Some(acc_spec) = spec.accounts.get(acc.as_ref()) else {
  82. return Err(chumsky::error::Rich::custom(acc.span(), "no such account"));
  83. };
  84. let unit = match unit {
  85. Some(sunit) => sunit,
  86. None => acc_spec.default_unit.map(|u| Spanned(u, span)).ok_or_else(||
  87. chumsky::error::Rich::custom(span, format!("No unit specified and no default unit specified for account '{}'", acc.as_ref())))?
  88. };
  89. if !spec.units.contains_key(&unit) {
  90. return Err(chumsky::error::Rich::custom(unit.span(), format!("no such unit '{unit}' found")))
  91. }
  92. Ok(Spanned::new(Balance {
  93. account: acc,
  94. amount,
  95. unit,
  96. }, span))
  97. });
  98. let entry = group((
  99. chumsky::text::whitespace(),
  100. datestamp,
  101. mark(':'),
  102. inline_whitespace(),
  103. chumsky::primitive::none_of("\n")
  104. .repeated()
  105. .collect::<String>(),
  106. chumsky::text::newline(),
  107. balance.repeated().at_least(1).collect(),
  108. chumsky::text::whitespace(),
  109. ))
  110. .map_with(|(_, datestamp, _, _, title, _, balances, _), e| Spanned::new(LedgerEntry {
  111. datestamp,
  112. title: (!title.is_empty()).then_some(title),
  113. balances,
  114. }, e.span()));
  115. entry.repeated().collect()
  116. }
  117. pub fn parse_ledger(
  118. source: SourceFile,
  119. spec: &SpecRoot,
  120. data: &str,
  121. ) -> Result<Vec<Spanned<LedgerEntry>>, DataError> {
  122. let parser = ledger_parser();
  123. let (presult, errors) = parser
  124. .parse_with_state(data.with_context(source), &mut chumsky::extra::SimpleState(spec))
  125. .into_output_errors();
  126. if let Some(e) = errors.first() {
  127. let span = e.span().start()..e.span().end();
  128. let report = ariadne::Report::build(ariadne::ReportKind::Error, (source, span.clone()))
  129. .with_label(ariadne::Label::new((source, span)).with_message(e.reason()))
  130. .finish();
  131. Err(report.into())
  132. } else {
  133. Ok(presult.unwrap())
  134. }
  135. }