import.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. use std::collections::BTreeSet;
  2. use crate::prelude::*;
  3. use crate::data::Decimal;
  4. #[derive(Debug)]
  5. pub enum ImportError {
  6. IOError(std::io::Error),
  7. ConfigError(String),
  8. InputError(String),
  9. CsvError(csv::Error),
  10. }
  11. impl From<std::io::Error> for ImportError {
  12. fn from(value: std::io::Error) -> Self {
  13. Self::IOError(value)
  14. }
  15. }
  16. impl From<csv::Error> for ImportError {
  17. fn from(value: csv::Error) -> Self {
  18. Self::CsvError(value)
  19. }
  20. }
  21. impl From<rust_decimal::Error> for ImportError {
  22. fn from(value: rust_decimal::Error) -> Self {
  23. Self::InputError(value.to_string())
  24. }
  25. }
  26. fn try_parse_decimal(from: &str) -> Result<data::Decimal, ImportError> {
  27. // remove any '$' or units from the string
  28. let filtered = from
  29. .chars()
  30. .filter(|f| char::is_digit(*f, 10) || *f == '.' || *f == '-')
  31. .collect::<String>();
  32. data::Decimal::from_str_radix(filtered.as_str(), 10).map_err(Into::into)
  33. }
  34. fn import_from_csv(
  35. csv_spec: &data::spec::CsvImportSpec,
  36. aspec: &data::spec::AccountSpec,
  37. account: data::AccountName,
  38. transaction_target: data::AccountName,
  39. reader: impl std::io::Read,
  40. ) -> Result<Vec<data::Transaction>, ImportError> {
  41. let mut csv_reader = csv::ReaderBuilder::new()
  42. .has_headers(false)
  43. .trim(csv::Trim::All)
  44. .from_reader(reader);
  45. // validate CSV spec
  46. if !csv_spec
  47. .cols
  48. .contains(&data::spec::CsvColumnSpec::Datestamp)
  49. {
  50. return Err(ImportError::ConfigError(
  51. "CSV config does not have a datestamp column".into(),
  52. ));
  53. }
  54. if !csv_spec.cols.contains(&data::spec::CsvColumnSpec::Change)
  55. && (!csv_spec.cols.contains(&data::spec::CsvColumnSpec::Withdraw)
  56. || !csv_spec.cols.contains(&data::spec::CsvColumnSpec::Deposit))
  57. {
  58. return Err(ImportError::ConfigError(
  59. "CSV config needs either a change column or both withdraw and deposit columns!".into(),
  60. ));
  61. }
  62. let mut txns = vec![];
  63. for record in csv_reader.records().skip(csv_spec.skip_start.unwrap_or(0)) {
  64. let record = record?;
  65. let mut txn_datestamp: Option<data::Datestamp> = None;
  66. let mut txn_title: Option<String> = None;
  67. let mut txn_change: Option<data::Decimal> = None;
  68. let mut txn_balance: Option<data::Decimal> = None;
  69. let mut txn_unit: Option<data::UnitName> = None;
  70. for (record, spec) in record.iter().zip(csv_spec.cols.iter()) {
  71. match spec {
  72. data::spec::CsvColumnSpec::Ignore => (),
  73. data::spec::CsvColumnSpec::Datestamp => {
  74. txn_datestamp =
  75. data::Datestamp::parse_from_str(record, &csv_spec.date_format).ok();
  76. }
  77. data::spec::CsvColumnSpec::Title => {
  78. txn_title = Some(record.into());
  79. }
  80. data::spec::CsvColumnSpec::Deposit => {
  81. if record.trim().is_empty() {
  82. continue;
  83. }
  84. txn_change = Some(try_parse_decimal(record)?);
  85. }
  86. data::spec::CsvColumnSpec::Withdraw => {
  87. if record.trim().is_empty() {
  88. continue;
  89. }
  90. let mut dec = try_parse_decimal(record)?;
  91. dec.set_sign_negative(true);
  92. txn_change = Some(dec);
  93. }
  94. data::spec::CsvColumnSpec::Change => {
  95. if record.trim().is_empty() {
  96. continue;
  97. }
  98. txn_change = Some(try_parse_decimal(record)?);
  99. }
  100. data::spec::CsvColumnSpec::Balance => {
  101. if record.trim().is_empty() {
  102. continue;
  103. }
  104. txn_balance = Some(try_parse_decimal(record)?);
  105. }
  106. data::spec::CsvColumnSpec::Unit => {
  107. txn_unit = Some(data::UnitName::new(record));
  108. }
  109. }
  110. }
  111. if csv_spec.reverse {
  112. if let Some(bal) = txn_balance.as_mut() {
  113. *bal = Decimal::ZERO - *bal;
  114. }
  115. if let Some(amount) = txn_change.as_mut() {
  116. *amount = Decimal::ZERO - *amount;
  117. }
  118. }
  119. txns.push(data::Transaction {
  120. datestamp: txn_datestamp.unwrap(),
  121. title: txn_title,
  122. annotations: Default::default(),
  123. changes: vec![
  124. data::Change {
  125. account: account.into(),
  126. amount: txn_change.unwrap().into(),
  127. balance: txn_balance.map(Into::into),
  128. unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
  129. source: None,
  130. },
  131. data::Change {
  132. account: transaction_target.into(),
  133. amount: data::Decimal::ZERO
  134. .checked_sub(txn_change.unwrap())
  135. .unwrap()
  136. .into(),
  137. balance: None,
  138. unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
  139. source: None,
  140. },
  141. ],
  142. source: None,
  143. });
  144. }
  145. Ok(txns)
  146. }
  147. fn recursive_order_search(
  148. txns: &[data::Transaction],
  149. account: data::AccountName,
  150. order: &mut Vec<usize>,
  151. remaining: &mut BTreeSet<usize>,
  152. ) -> bool {
  153. if remaining.is_empty() {
  154. return true;
  155. }
  156. let possibles = remaining.iter().cloned().collect::<Vec<_>>();
  157. if let Some(last) = order.last() {
  158. let Some(last_balance) = txns[*last].change_for(account).unwrap().balance.as_deref() else {
  159. return false;
  160. };
  161. for possible in possibles.iter() {
  162. // check if balances line up
  163. let change = txns[*possible].change_for(account).unwrap();
  164. let Some(nbal) = change.balance else { continue };
  165. if last_balance.checked_add(*change.amount) != Some(*nbal) {
  166. continue;
  167. }
  168. remaining.remove(possible);
  169. order.push(*possible);
  170. if recursive_order_search(txns, account, order, remaining) {
  171. return true;
  172. }
  173. order.pop();
  174. remaining.insert(*possible);
  175. }
  176. } else {
  177. for possible in possibles.into_iter() {
  178. remaining.remove(&possible);
  179. order.push(possible);
  180. if recursive_order_search(txns, account, order, remaining) {
  181. return true;
  182. }
  183. order.pop();
  184. remaining.insert(possible);
  185. }
  186. }
  187. false
  188. }
  189. fn postprocess(account: data::AccountName, transactions: &mut Vec<data::Transaction>) {
  190. // check if we're sorted by datestamp already
  191. if transactions.is_sorted_by_key(|tx| tx.datestamp) {
  192. // already vaguely in the right order
  193. } else if transactions
  194. .iter()
  195. .rev()
  196. .is_sorted_by_key(|tx| tx.datestamp)
  197. {
  198. // reverse everything
  199. transactions.reverse();
  200. } else {
  201. // otherwise try to get things vaguely sorted
  202. transactions.sort_by_key(|tx| tx.datestamp);
  203. }
  204. let mut to_assign = BTreeSet::from_iter(0..transactions.len());
  205. let mut order = vec![];
  206. if !recursive_order_search(transactions, account, &mut order, &mut to_assign) {
  207. log::warn!("Unable to determine transaction ordering!");
  208. return;
  209. }
  210. let mut ntransact = order
  211. .iter()
  212. .map(|v| transactions[*v].clone())
  213. .collect::<Vec<_>>();
  214. std::mem::swap(&mut ntransact, transactions);
  215. }
  216. pub fn import_from(
  217. aspec: &data::spec::AccountSpec,
  218. account: data::AccountName,
  219. transaction_target: data::AccountName,
  220. path: &std::path::Path,
  221. ) -> Result<Vec<data::Transaction>, ImportError> {
  222. let reader = std::fs::File::open(path)?;
  223. let mut output = match &aspec.import {
  224. Some(data::spec::ImportFileFormat::Csv(csv)) => {
  225. import_from_csv(csv, aspec, account, transaction_target, reader)
  226. }
  227. None => Err(ImportError::ConfigError(format!(
  228. "no import configuration for {account}"
  229. ))),
  230. }?;
  231. postprocess(account, &mut output);
  232. Ok(output)
  233. }