|
@@ -1,7 +1,7 @@
|
|
|
use crate::data::{
|
|
|
- AccountName, Datestamp, Decimal, Span, Spanned, UnitName,
|
|
|
+ AccountName, Datestamp, Decimal, UnitName,
|
|
|
ledger::{Change, Transaction},
|
|
|
- spec::{AccountSpec, CSVColumnSpec, CSVImportSpec, ImportFileFormat},
|
|
|
+ spec::{AccountSpec, CsvColumnSpec, CsvImportSpec, ImportFileFormat},
|
|
|
};
|
|
|
|
|
|
#[derive(Debug)]
|
|
@@ -9,7 +9,7 @@ pub enum ImportError {
|
|
|
IOError(std::io::Error),
|
|
|
ConfigError(String),
|
|
|
InputError(String),
|
|
|
- CSVError(csv::Error),
|
|
|
+ CsvError(csv::Error),
|
|
|
}
|
|
|
|
|
|
impl From<std::io::Error> for ImportError {
|
|
@@ -20,7 +20,7 @@ impl From<std::io::Error> for ImportError {
|
|
|
|
|
|
impl From<csv::Error> for ImportError {
|
|
|
fn from(value: csv::Error) -> Self {
|
|
|
- Self::CSVError(value)
|
|
|
+ Self::CsvError(value)
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -47,29 +47,27 @@ fn try_parse_decimal(from: &str) -> Result<Decimal, ImportError> {
|
|
|
}
|
|
|
|
|
|
fn import_from_csv(
|
|
|
- csv_spec: &CSVImportSpec,
|
|
|
+ csv_spec: &CsvImportSpec,
|
|
|
aspec: &AccountSpec,
|
|
|
target: AccountName,
|
|
|
- mut reader: impl std::io::Read,
|
|
|
+ reader: impl std::io::Read,
|
|
|
) -> Result<Vec<Transaction>, ImportError> {
|
|
|
let mut csv_reader = csv::Reader::from_reader(reader);
|
|
|
|
|
|
// 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(),
|
|
|
));
|
|
|
}
|
|
|
|
|
|
- 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::Change)
|
|
|
+ && (!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(),
|
|
|
+ ));
|
|
|
}
|
|
|
|
|
|
// strptime is silly and wants a &'static format string
|
|
@@ -91,8 +89,8 @@ fn import_from_csv(
|
|
|
|
|
|
for (record, spec) in record.iter().zip(csv_spec.cols.iter()) {
|
|
|
match spec {
|
|
|
- CSVColumnSpec::Ignore => (),
|
|
|
- CSVColumnSpec::Datestamp => {
|
|
|
+ CsvColumnSpec::Ignore => (),
|
|
|
+ CsvColumnSpec::Datestamp => {
|
|
|
let date = date_parser.parse(record)?.date()?;
|
|
|
txn_datestamp = Some(Datestamp {
|
|
|
year: date.year() as u16,
|
|
@@ -100,17 +98,17 @@ fn import_from_csv(
|
|
|
day: date.day(),
|
|
|
});
|
|
|
}
|
|
|
- CSVColumnSpec::Title => {
|
|
|
+ CsvColumnSpec::Title => {
|
|
|
txn_title = Some(record.into());
|
|
|
}
|
|
|
- CSVColumnSpec::Deposit => {
|
|
|
+ CsvColumnSpec::Deposit => {
|
|
|
if record.trim().is_empty() {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
|
}
|
|
|
- CSVColumnSpec::Withdraw => {
|
|
|
+ CsvColumnSpec::Withdraw => {
|
|
|
if record.trim().is_empty() {
|
|
|
continue;
|
|
|
}
|
|
@@ -118,21 +116,21 @@ fn import_from_csv(
|
|
|
dec.set_sign_negative(true);
|
|
|
txn_change = Some(dec);
|
|
|
}
|
|
|
- CSVColumnSpec::Change => {
|
|
|
+ CsvColumnSpec::Change => {
|
|
|
if record.trim().is_empty() {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
txn_change = Some(try_parse_decimal(record)?);
|
|
|
}
|
|
|
- CSVColumnSpec::Balance => {
|
|
|
+ CsvColumnSpec::Balance => {
|
|
|
if record.trim().is_empty() {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
txn_balance = Some(try_parse_decimal(record)?);
|
|
|
}
|
|
|
- CSVColumnSpec::Unit => {
|
|
|
+ CsvColumnSpec::Unit => {
|
|
|
txn_unit = Some(UnitName::new(record));
|
|
|
}
|
|
|
}
|
|
@@ -152,16 +150,16 @@ fn import_from_csv(
|
|
|
.into(),
|
|
|
Change {
|
|
|
account: unbalanced.into(),
|
|
|
- amount: txn_change.unwrap().into(),
|
|
|
- balance: txn_balance.map(Into::into),
|
|
|
+ amount: Decimal::ZERO
|
|
|
+ .checked_sub(txn_change.unwrap())
|
|
|
+ .unwrap()
|
|
|
+ .into(),
|
|
|
+ balance: None,
|
|
|
unit: txn_unit.or(aspec.unit).map(Into::into).unwrap(),
|
|
|
}
|
|
|
.into(),
|
|
|
],
|
|
|
});
|
|
|
-
|
|
|
- // println!("{txn_datestamp:?}: {txn_title:?}");
|
|
|
- // println!("- account: {txn_change:?} = {txn_balance:?} {txn_unit:?}");
|
|
|
}
|
|
|
|
|
|
Ok(txns)
|
|
@@ -175,7 +173,7 @@ pub fn import_from(
|
|
|
let reader = std::fs::File::open(path)?;
|
|
|
|
|
|
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}"
|
|
|
))),
|