use serde::Deserialize;
use std::collections::VecDeque;
use std::env;
use std::error::Error;
use std::process;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IcaTransaction {
datum: String,
text: String,
typ: String,
budgetgrupp: String,
belopp: String,
saldo: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct GCTransaction {
date: String,
transaction_id: String,
number: String,
description: Option<String>,
notes: Option<String>,
commodity_currency: Option<String>,
void_reason: Option<String>,
action: Option<String>,
memo: Option<String>,
full_account_name: Option<String>,
account_name: Option<String>,
amount_with_sym: Option<String>,
amount_num: Option<f32>,
reconcile: Option<String>,
reconcile_date: Option<String>,
rate_price: Option<String>,
}
fn parse_csv(args: Vec<String>) -> Result<(), Box<dyn Error>> {
#[cfg(not(pipe))]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_path(&args[1])?;
#[cfg(pipe)]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_reader(io::stdin());
#[cfg(not(pipe))]
let mut wtr = csv::Writer::from_path(&args[2])?;
#[cfg(pipe)]
let mut wtr = csv::Writer::from_writer(io::stdout());
wtr.write_record(&[
"Date",
"Transaction ID",
"Number",
"Description",
"Notes",
"Commodity/Currency",
"Void Reason",
"Action",
"Memo",
"Full Account Name",
"Account Name",
"Amount With Sym.",
"Amount Num",
"Reconcile",
"Reconcile Date",
"Rate/Price",
])?;
let mut last_seen_date = "".to_string();
let mut transaction_buffer: VecDeque<[String; 16]> = VecDeque::new();
for result in rdr.deserialize() {
let record: IcaTransaction = result?;
let account_name = match record.typ.as_str() {
"E-faktura" => "Tillgångar:Likvida Medel:Brukskonto",
"Pg-bg" => "Tillgångar:Likvida Medel:Brukskonto",
"Autogiro" => "Tillgångar:Likvida Medel:Brukskonto",
"Korttransaktion" => "Tillgångar:Likvida Medel:Brukskonto",
"Uttag" => "Tillgångar:Likvida Medel:Brukskonto",
"Utlandsbetalning" => "Tillgångar:Likvida Medel:Brukskonto",
"Försäkring" => "Tillgångar:Likvida Medel:Brukskonto",
"Fritid" => "Tillgångar:Likvida Medel:Brukskonto",
"Övrigt" => "Tillgångar:Likvida Medel:Brukskonto",
"Reserverat Belopp" => "Tillgångar:Likvida Medel:Brukskonto",
"Insättning" => "Tillgångar:Likvida Medel:Brukskonto",
_ => "Tillgångar:Likvida Medel:Brukskonto",
};
let amount_num = record
.belopp
.replace(" kr", "")
.replace(",", ".")
.chars()
.filter(|c| c.is_ascii())
.filter(|c| !c.is_whitespace())
.collect::<String>()
.parse::<f32>()
.unwrap();
let amount_balance = record.saldo.map(|saldo| {
saldo
.replace(" kr", "")
.replace(",", ".")
.chars()
.filter(|c| c.is_ascii())
.filter(|c| !c.is_whitespace())
.collect::<String>()
.parse::<f32>()
.unwrap()
});
if last_seen_date == record.datum {
} else {
write_csv(&mut wtr, &mut transaction_buffer)?;
last_seen_date = record.datum.clone();
}
transaction_buffer.push_back([
record.datum,
"".into(),
1.to_string(),
record.text,
match amount_balance {
Some(value) => value.to_string().replace(".", ","),
None => "".into(),
},
"CURRENCY::SEK".into(),
"".into(),
record.typ,
"".into(),
account_name.into(),
"".into(),
"".into(),
amount_num.to_string().replace(".", ","),
"n".into(),
"".into(),
"1".into(),
]);
}
write_csv(&mut wtr, &mut transaction_buffer)?;
Ok(())
}
fn write_csv(
wtr: &mut csv::Writer<std::fs::File>,
buf: &mut VecDeque<[String; 16]>,
) -> Result<(), Box<dyn Error>> {
for (counter, row) in buf.iter_mut().rev().enumerate() {
let transaction_number = counter + 1;
row[2] = transaction_number.to_string();
}
while let Some(row) = buf.pop_front() {
wtr.write_record(row)?;
}
Ok(())
}
fn print_help() {
println!("Error: Missing arguments!");
println!();
println!("Usage:");
println!();
println!("This program requires two arguments:");
println!("Argument 1: Input CSV file from ICA Banken");
println!("Argument 2: Output CSV file formatted for GnuCash import");
println!();
println!("Example:");
println!("icabanken2gnucash ica.csv ica-gnucash.csv");
println!();
println!("If compiled with `pipe` feature, pipe in and out instead");
println!("icabanken2gnucash < ica.csv > ica-gnucash.csv");
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_help();
process::exit(1);
}
println!(" Input file: {}", args[1]);
println!("Output file: {}", args[2]);
if let Err(err) = parse_csv(args) {
println!("Unable to parse CSV: {}", err);
process::exit(1);
} else {
println!("Done!");
}
}