check.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. use std::collections::BTreeMap;
  2. use ariadne::Span as ASpan;
  3. use chumsky::span::Span as CSpan;
  4. // use crate::data::{DataError, Decimal, Hoard, Span, Spanned, UnitName};
  5. use crate::prelude::*;
  6. #[derive(Default, PartialEq, PartialOrd)]
  7. pub enum CheckLevel {
  8. /// Check individual transactions
  9. WellFormed,
  10. /// Check relations between transactions
  11. #[default]
  12. Consistent,
  13. }
  14. fn check_equal_sum(root: &data::Hoard) -> Result<(), DataError> {
  15. log::trace!("Checking for equal sums in monounit ledger entries...");
  16. for le in root.all_raw_ledger_data() {
  17. let Some(tx) = le.as_transaction() else {
  18. continue;
  19. };
  20. let Some(mono) = tx.mono_unit() else { continue };
  21. let net = tx
  22. .changes
  23. .iter()
  24. .try_fold(data::Decimal::ZERO, |acc, b| acc.checked_add(*b.amount));
  25. if net != Some(data::Decimal::ZERO) {
  26. let report =
  27. ariadne::Report::build(ariadne::ReportKind::Error, tx.source.unwrap_or_default())
  28. .with_labels(tx.changes.iter().map(|v| {
  29. let span = v.amount.span().union(v.unit.span());
  30. ariadne::Label::new(span).with_message("change here")
  31. }));
  32. let report = if let Some(net) = net {
  33. report
  34. .with_message("imbalanced transaction")
  35. .with_note(format!("net value: {net} {mono}"))
  36. } else {
  37. report.with_note("numeric overflow")
  38. };
  39. return Err(report.finish().into());
  40. }
  41. }
  42. Ok(())
  43. }
  44. fn check_precision(root: &data::Hoard) -> Result<(), DataError> {
  45. log::trace!("Checking for precision errors in ledger entries...");
  46. for le in root.all_raw_ledger_data() {
  47. let Some(tx) = le.as_transaction() else {
  48. continue;
  49. };
  50. for change in &tx.changes {
  51. let unit_spec = root.unit_spec(*change.unit).unwrap();
  52. let Some(precision) = unit_spec.precision else {
  53. continue;
  54. };
  55. let amount_scale = change.amount.scale();
  56. if amount_scale > precision {
  57. return Err(ariadne::Report::build(
  58. ariadne::ReportKind::Error,
  59. change.amount.span(),
  60. )
  61. .with_label(
  62. ariadne::Label::new(change.amount.span()).with_message(format!(
  63. "{} has {} digits of precision, not {}",
  64. change.unit,
  65. precision,
  66. change.amount.0.scale()
  67. )),
  68. )
  69. .finish()
  70. .into());
  71. }
  72. let Some(balance) = change.balance.as_ref() else {
  73. continue;
  74. };
  75. let balance_scale = balance.scale();
  76. if balance_scale > precision {
  77. return Err(
  78. ariadne::Report::build(ariadne::ReportKind::Error, balance.span())
  79. .with_label(ariadne::Label::new(balance.span()).with_message(format!(
  80. "{} has {} digits of precision, not {}",
  81. change.unit,
  82. precision,
  83. change.amount.0.scale()
  84. )))
  85. .finish()
  86. .into(),
  87. );
  88. }
  89. }
  90. }
  91. Ok(())
  92. }
  93. fn check_balances(root: &data::Hoard) -> Result<(), DataError> {
  94. log::trace!("Checking balance consistency...");
  95. for account in root.account_names() {
  96. let Some(ledger) = root.ledger_data_for(account) else {
  97. continue;
  98. };
  99. let mut running_balance = BTreeMap::<data::UnitName, Spanned<data::Decimal>>::new();
  100. for txn_ref in ledger {
  101. let txn = txn_ref.borrow();
  102. let change = txn.change_for(account).unwrap();
  103. let bal = running_balance
  104. .entry(*change.unit)
  105. .or_insert_with(|| Spanned::new(data::Decimal::default(), io::Span::default()));
  106. let last_span = bal.span();
  107. bal.0 = bal.checked_add(*change.amount).unwrap();
  108. bal.1 = change.source.unwrap();
  109. if let Some(sbal) = change.balance.as_ref() {
  110. if **sbal != bal.0 {
  111. let report = ariadne::Report::build(
  112. ariadne::ReportKind::Error,
  113. txn.source.unwrap_or_default(),
  114. )
  115. .with_label(ariadne::Label::new(sbal.span()).with_message(format!(
  116. "Calculated balance is {} {}, specified balance is {} {}",
  117. bal, change.unit, sbal, change.unit
  118. )));
  119. let report = if !last_span.is_empty() {
  120. report.with_label(
  121. ariadne::Label::new(last_span)
  122. .with_message("Last balance is from here"),
  123. )
  124. } else {
  125. report
  126. };
  127. return Err(report.finish().into());
  128. }
  129. }
  130. }
  131. }
  132. Ok(())
  133. }
  134. pub fn run_checks(root: &mut data::Hoard, level: CheckLevel) -> Result<(), DataError> {
  135. if level >= CheckLevel::WellFormed {
  136. log::debug!("Running transaction well-formedness checks...");
  137. check_precision(root)?;
  138. check_equal_sum(root)?;
  139. }
  140. if level >= CheckLevel::Consistent {
  141. log::debug!("Running ledger consistency checks...");
  142. check_balances(root)?;
  143. }
  144. Ok(())
  145. }