parse.rs 5.6 KB

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