check.rs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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_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 = ariadne::Report::build(ariadne::ReportKind::Error, tx.span()).with_labels(
  27. tx.changes.iter().map(|v| {
  28. let span = v.amount.span().union(v.unit.span());
  29. ariadne::Label::new(span).with_message("change here")
  30. }),
  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_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 in ledger {
  101. let change = txn.change_for(account).unwrap();
  102. let bal = running_balance
  103. .entry(*change.unit)
  104. .or_insert_with(|| Spanned::new(data::Decimal::default(), io::Span::default()));
  105. let last_span = bal.span();
  106. bal.0 = bal.checked_add(*change.amount).unwrap();
  107. bal.1 = change.source.unwrap();
  108. if let Some(sbal) = change.balance.as_ref() {
  109. if **sbal != bal.0 {
  110. let report = ariadne::Report::build(ariadne::ReportKind::Error, txn.span())
  111. .with_label(ariadne::Label::new(sbal.span()).with_message(format!(
  112. "Calculated balance is {} {}, specified balance is {} {}",
  113. bal, change.unit, sbal, change.unit
  114. )));
  115. let report = if !last_span.is_empty() {
  116. report.with_label(
  117. ariadne::Label::new(last_span)
  118. .with_message("Last balance is from here"),
  119. )
  120. } else {
  121. report
  122. };
  123. return Err(report.finish().into());
  124. }
  125. }
  126. }
  127. }
  128. Ok(())
  129. }
  130. pub fn run_checks(root: &mut data::Hoard, level: CheckLevel) -> Result<(), DataError> {
  131. if level >= CheckLevel::WellFormed {
  132. log::debug!("Running transaction well-formedness checks...");
  133. check_precision(root)?;
  134. check_equal_sum(root)?;
  135. }
  136. if level >= CheckLevel::Consistent {
  137. log::debug!("Running ledger consistency checks...");
  138. check_balances(root)?;
  139. }
  140. Ok(())
  141. }