|
@@ -1,4 +1,8 @@
|
|
-use crate::data::{AccountName, Datestamp, Decimal, spec::{AccountSpec, ImportFileFormat, CSVImportSpec, CSVColumnSpec}, ledger::{Change, Transaction}, Span, Spanned, UnitName};
|
|
|
|
|
|
+use crate::data::{
|
|
|
|
+ AccountName, Datestamp, Decimal, Span, Spanned, UnitName,
|
|
|
|
+ ledger::{Change, Transaction},
|
|
|
|
+ spec::{AccountSpec, CSVColumnSpec, CSVImportSpec, ImportFileFormat},
|
|
|
|
+};
|
|
|
|
|
|
#[derive(Debug)]
|
|
#[derive(Debug)]
|
|
pub enum ImportError {
|
|
pub enum ImportError {
|
|
@@ -34,22 +38,37 @@ impl From<rust_decimal::Error> for ImportError {
|
|
|
|
|
|
fn try_parse_decimal(from: &str) -> Result<Decimal, ImportError> {
|
|
fn try_parse_decimal(from: &str) -> Result<Decimal, ImportError> {
|
|
// remove any '$' or units from the string
|
|
// remove any '$' or units from the string
|
|
- let filtered = from.chars().filter(|f| char::is_digit(*f, 10) || *f == '.' || *f == '-').collect::<String>();
|
|
|
|
|
|
+ let filtered = from
|
|
|
|
+ .chars()
|
|
|
|
+ .filter(|f| char::is_digit(*f, 10) || *f == '.' || *f == '-')
|
|
|
|
+ .collect::<String>();
|
|
|
|
|
|
Decimal::from_str_radix(filtered.as_str(), 10).map_err(Into::into)
|
|
Decimal::from_str_radix(filtered.as_str(), 10).map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
|
|
-fn import_from_csv(csv_spec: &CSVImportSpec, aspec: &AccountSpec, target: AccountName, mut reader: impl std::io::Read) -> Result<Vec<Transaction>, ImportError> {
|
|
|
|
|
|
+fn import_from_csv(
|
|
|
|
+ csv_spec: &CSVImportSpec,
|
|
|
|
+ aspec: &AccountSpec,
|
|
|
|
+ target: AccountName,
|
|
|
|
+ mut reader: impl std::io::Read,
|
|
|
|
+) -> Result<Vec<Transaction>, ImportError> {
|
|
let mut csv_reader = csv::Reader::from_reader(reader);
|
|
let mut csv_reader = csv::Reader::from_reader(reader);
|
|
|
|
|
|
// validate CSV spec
|
|
// validate CSV spec
|
|
if !csv_spec.cols.contains(&CSVColumnSpec::Datestamp) {
|
|
if !csv_spec.cols.contains(&CSVColumnSpec::Datestamp) {
|
|
- return Err(ImportError::ConfigError("CSV config does not have a datestamp column".into()))
|
|
|
|
|
|
+ return Err(ImportError::ConfigError(
|
|
|
|
+ "CSV config does not have a datestamp column".into(),
|
|
|
|
+ ));
|
|
}
|
|
}
|
|
|
|
|
|
if !csv_spec.cols.contains(&CSVColumnSpec::Change) {
|
|
if !csv_spec.cols.contains(&CSVColumnSpec::Change) {
|
|
- if !csv_spec.cols.contains(&CSVColumnSpec::Withdraw) || !csv_spec.cols.contains(&CSVColumnSpec::Deposit) {
|
|
|
|
- return Err(ImportError::ConfigError("CSV config needs either a change column or both withdraw and deposit columns!".into()))
|
|
|
|
|
|
+ if !csv_spec.cols.contains(&CSVColumnSpec::Withdraw)
|
|
|
|
+ || !csv_spec.cols.contains(&CSVColumnSpec::Deposit)
|
|
|
|
+ {
|
|
|
|
+ return Err(ImportError::ConfigError(
|
|
|
|
+ "CSV config needs either a change column or both withdraw and deposit columns!"
|
|
|
|
+ .into(),
|
|
|
|
+ ));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -64,54 +83,58 @@ fn import_from_csv(csv_spec: &CSVImportSpec, aspec: &AccountSpec, target: Accoun
|
|
for record in csv_reader.records() {
|
|
for record in csv_reader.records() {
|
|
let record = record?;
|
|
let record = record?;
|
|
|
|
|
|
- let mut txn_datestamp : Option<Datestamp> = None;
|
|
|
|
- let mut txn_title : Option<String> = None;
|
|
|
|
- let mut txn_change : Option<Decimal> = None;
|
|
|
|
- let mut txn_balance : Option<Decimal> = None;
|
|
|
|
- let mut txn_unit : Option<UnitName> = None;
|
|
|
|
|
|
+ let mut txn_datestamp: Option<Datestamp> = None;
|
|
|
|
+ let mut txn_title: Option<String> = None;
|
|
|
|
+ let mut txn_change: Option<Decimal> = None;
|
|
|
|
+ let mut txn_balance: Option<Decimal> = None;
|
|
|
|
+ let mut txn_unit: Option<UnitName> = None;
|
|
|
|
|
|
for (record, spec) in record.iter().zip(csv_spec.cols.iter()) {
|
|
for (record, spec) in record.iter().zip(csv_spec.cols.iter()) {
|
|
match spec {
|
|
match spec {
|
|
CSVColumnSpec::Ignore => (),
|
|
CSVColumnSpec::Ignore => (),
|
|
CSVColumnSpec::Datestamp => {
|
|
CSVColumnSpec::Datestamp => {
|
|
let date = date_parser.parse(record)?.date()?;
|
|
let date = date_parser.parse(record)?.date()?;
|
|
- txn_datestamp = Some(Datestamp { year: date.year() as u16, month: date.month(), day: date.day() });
|
|
|
|
- },
|
|
|
|
|
|
+ txn_datestamp = Some(Datestamp {
|
|
|
|
+ year: date.year() as u16,
|
|
|
|
+ month: date.month(),
|
|
|
|
+ day: date.day(),
|
|
|
|
+ });
|
|
|
|
+ }
|
|
CSVColumnSpec::Title => {
|
|
CSVColumnSpec::Title => {
|
|
txn_title = Some(record.into());
|
|
txn_title = Some(record.into());
|
|
- },
|
|
|
|
|
|
+ }
|
|
CSVColumnSpec::Deposit => {
|
|
CSVColumnSpec::Deposit => {
|
|
if record.trim().is_empty() {
|
|
if record.trim().is_empty() {
|
|
- continue
|
|
|
|
|
|
+ continue;
|
|
}
|
|
}
|
|
|
|
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
- },
|
|
|
|
|
|
+ }
|
|
CSVColumnSpec::Withdraw => {
|
|
CSVColumnSpec::Withdraw => {
|
|
if record.trim().is_empty() {
|
|
if record.trim().is_empty() {
|
|
- continue
|
|
|
|
|
|
+ continue;
|
|
}
|
|
}
|
|
let mut dec = try_parse_decimal(record)?;
|
|
let mut dec = try_parse_decimal(record)?;
|
|
dec.set_sign_negative(true);
|
|
dec.set_sign_negative(true);
|
|
txn_change = Some(dec);
|
|
txn_change = Some(dec);
|
|
- },
|
|
|
|
|
|
+ }
|
|
CSVColumnSpec::Change => {
|
|
CSVColumnSpec::Change => {
|
|
if record.trim().is_empty() {
|
|
if record.trim().is_empty() {
|
|
- continue
|
|
|
|
|
|
+ continue;
|
|
}
|
|
}
|
|
|
|
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
- },
|
|
|
|
|
|
+ }
|
|
CSVColumnSpec::Balance => {
|
|
CSVColumnSpec::Balance => {
|
|
if record.trim().is_empty() {
|
|
if record.trim().is_empty() {
|
|
- continue
|
|
|
|
|
|
+ continue;
|
|
}
|
|
}
|
|
|
|
|
|
txn_balance = Some(try_parse_decimal(record)?);
|
|
txn_balance = Some(try_parse_decimal(record)?);
|
|
- },
|
|
|
|
|
|
+ }
|
|
CSVColumnSpec::Unit => {
|
|
CSVColumnSpec::Unit => {
|
|
txn_unit = Some(UnitName::new(record));
|
|
txn_unit = Some(UnitName::new(record));
|
|
- },
|
|
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -125,15 +148,16 @@ fn import_from_csv(csv_spec: &CSVImportSpec, aspec: &AccountSpec, target: Accoun
|
|
amount: txn_change.unwrap().into(),
|
|
amount: txn_change.unwrap().into(),
|
|
balance: txn_balance.map(Into::into),
|
|
balance: txn_balance.map(Into::into),
|
|
unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
|
|
unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
|
|
- }.into(),
|
|
|
|
-
|
|
|
|
|
|
+ }
|
|
|
|
+ .into(),
|
|
Change {
|
|
Change {
|
|
account: unbalanced.into(),
|
|
account: unbalanced.into(),
|
|
amount: txn_change.unwrap().into(),
|
|
amount: txn_change.unwrap().into(),
|
|
balance: txn_balance.map(Into::into),
|
|
balance: txn_balance.map(Into::into),
|
|
unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
|
|
unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
|
|
- }.into(),
|
|
|
|
- ]
|
|
|
|
|
|
+ }
|
|
|
|
+ .into(),
|
|
|
|
+ ],
|
|
});
|
|
});
|
|
|
|
|
|
// println!("{txn_datestamp:?}: {txn_title:?}");
|
|
// println!("{txn_datestamp:?}: {txn_title:?}");
|
|
@@ -143,11 +167,17 @@ fn import_from_csv(csv_spec: &CSVImportSpec, aspec: &AccountSpec, target: Accoun
|
|
Ok(txns)
|
|
Ok(txns)
|
|
}
|
|
}
|
|
|
|
|
|
-pub fn import_from(aspec: &AccountSpec, target: AccountName, path: &std::path::Path) -> Result<Vec<Transaction>, ImportError> {
|
|
|
|
|
|
+pub fn import_from(
|
|
|
|
+ aspec: &AccountSpec,
|
|
|
|
+ target: AccountName,
|
|
|
|
+ path: &std::path::Path,
|
|
|
|
+) -> Result<Vec<Transaction>, ImportError> {
|
|
let reader = std::fs::File::open(path)?;
|
|
let reader = std::fs::File::open(path)?;
|
|
|
|
|
|
match &aspec.import {
|
|
match &aspec.import {
|
|
Some(ImportFileFormat::CSV(csv)) => import_from_csv(csv, aspec, target, reader),
|
|
Some(ImportFileFormat::CSV(csv)) => import_from_csv(csv, aspec, target, reader),
|
|
- None => Err(ImportError::ConfigError(format!("no import configuration for {target}"))),
|
|
|
|
|
|
+ None => Err(ImportError::ConfigError(format!(
|
|
|
|
+ "no import configuration for {target}"
|
|
|
|
+ ))),
|
|
}
|
|
}
|
|
}
|
|
}
|