5USC3DZ3KIU47LIPFHWDFJB7IXH4A7FNGTCD2LXPSIK4S4GEX6KAC
4DJWIQSI4HHSSWX5JSGZXW7L77NCUWFC23TWHMBPWYRNYIO44EWAC
J2PVPX3PMJTSF6WGVBVOGF6ZAKRII5OXQ7XZPPV44NL4EUWJGG3QC
3HZYHDXTGH2EQJSCU7TDAYDDSJSIFSVBE5WMISSZPV4W7ZCNL4TQC
V6WMVNO6HSBPTGGC4OL2JFRVDEKJI47VRMTQ7V3GMJ7OE3RRQBWQC
QDZOD3MNMZUH4NDQLFTR537OCTJ2ATTHV4MJGBAQGJRW342PYSTQC
YFZX4FSZVP55LLENXKOD4JXAPAEGHWLB5JAYWWD6FKQSU6XEQHTAC
LDUI5PR2REYBDTO27SSVTFICM2WY5FB3BBLFA24RIQWFWOYQLXIAC
4BWPI66VMCGJSBGGIZMAFEKJGFMBS7HSV5H2GUC6QUF32XR6ZSCAC
ZEUBLA35LPTI4NLIQVKRX433VNT6TRIRX5TVIG6QZNQJIITEPQJQC
R5EWGEJKF3T22HDNWMHKLNDOTCG2PL6Y27ICW2AAYJZRTB3IZACAC
Q4VYTFJ74VGREANMVE4ETUQMDLX7VV3HXQXVUY2YZI5LWUDDKYNAC
ZQNDNT3KX3XTPQI7OQ7EXOTHZ5XVT3F5CAH7Q4MHIKMV27JSPASQC
ZKSXZMQFFORKJBXBOYXWFZ7UDLXKTZTK36BYHRKXCOCWJZFJD7EAC
use serde::Deserialize;
use std::collections::VecDeque;
use std::env;
use std::error::Error;
use std::process;
use std::{env, path::Path, process};
// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IcaTransaction {
datum: String,
text: String,
typ: String,
budgetgrupp: String,
belopp: String,
saldo: Option<String>,
}
mod ica_csv;
use crate::ica_csv::parse_csv;
// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[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>> {
// Read the CSV file
// assuming it has a header
#[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';')
#[cfg(not(pipe))]
// Truncates any pre-existing file
let mut wtr = csv::Writer::from_path(&args[2])?;
#[cfg(pipe)]
let mut wtr = csv::Writer::from_writer(io::stdout());
// Write headers manually.
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();
// The file is read from top to bottom, ICA exorts
// the most recent transactions at the top
// thus the deserialization goes "backwards" in time
//
// This is of special importance when parsing transaction_number
// with multiple transactions during the same day
for result in rdr.deserialize() {
// Notice that we need to provide a type hint for automatic
// deserialization.
let record: IcaTransaction = result?;
// Match based on action which account
// Default match on Brukskonto
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 this is the second time a date appears
if last_seen_date == record.datum {
// Store the record in write buffer
} else {
// A fresh new date, print all buffer contents
write_csv(&mut wtr, &mut transaction_buffer)?;
// Set the last seen date
last_seen_date = record.datum.clone();
}
transaction_buffer.push_back([
// Date
record.datum,
// Transaction ID, let GnuCash generate
"".into(),
// Number, 1 by default,
// if multiple in a day this gets overwritten
1.to_string(),
// Description
record.text,
// Notes
// Extra, the account balance stored in a note
match amount_balance {
Some(value) => value.to_string().replace(".", ","),
None => "".into(),
},
// Currency
"CURRENCY::SEK".into(),
// Void Reason
"".into(),
// Action
record.typ,
// Memo
"".into(),
// Full Account Name",
account_name.into(),
// Account Name",
"".into(),
// Amount With Sym.",
"".into(),
// Amount Num",
amount_num.to_string().replace(".", ","),
// Reconcile",
"n".into(),
// Reconcile Date",
"".into(),
// Rate/Price",
"1".into(),
]);
}
// Make sure any unprinted lines still in the buffer gets printed
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>> {
// Iterate in reverse order to assign correct transaction_number
for (counter, row) in buf.iter_mut().rev().enumerate() {
let transaction_number = counter + 1;
row[2] = transaction_number.to_string();
}
// Write CSV in original order, most recent first
while let Some(row) = buf.pop_front() {
wtr.write_record(row)?;
}
Ok(())
}
if let Err(err) = parse_csv(args) {
println!("Unable to parse CSV: {}", err);
process::exit(1);
} else {
println!("Done!");
let input_path = Path::new(&args[1]);
if let Some(extension_type) = input_path.extension() {
match extension_type.to_str() {
Some("csv") => {
if let Err(err) = parse_csv(args) {
println!("Unable to parse CSV: {}", err);
process::exit(1);
} else {
println!("Done! CSV Parsed");
}
}
_ => {
println!("Unsupported file format, CSV supported. Exiting");
process::exit(1);
}
}
use serde::Deserialize;
use std::{collections::VecDeque, error::Error};
// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct IcaTransaction {
datum: String,
text: String,
typ: String,
budgetgrupp: String,
belopp: String,
saldo: Option<String>,
}
// By default, struct field names are deserialized based on the position of
// a corresponding field in the CSV data's header record.
#[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>,
value_with_sym: Option<String>,
value_num: Option<f32>,
reconcile: Option<String>,
reconcile_date: Option<String>,
rate_price: Option<String>,
}
pub fn parse_csv(args: Vec<String>) -> Result<(), Box<dyn Error>> {
let input_file = &args[1];
let output_file = &args[2];
// Read the CSV file
// assuming it has a header
#[cfg(not(pipe))]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_path(input_file)?;
#[cfg(pipe)]
let mut rdr = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.trim(csv::Trim::All)
.from_reader(io::stdin());
#[cfg(not(pipe))]
// Truncates any pre-existing file
let mut wtr = csv::Writer::from_path(output_file)?;
#[cfg(pipe)]
let mut wtr = csv::Writer::from_writer(io::stdout());
// Write headers manually.
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",
"Value With Sym.",
"Value Num",
"Reconcile",
"Reconcile Date",
"Rate/Price",
])?;
let mut last_seen_date = "".to_string();
let mut transaction_buffer: VecDeque<[String; 18]> = VecDeque::new();
// The file is read from top to bottom, ICA exorts
// the most recent transactions at the top
// thus the deserialization goes "backwards" in time
//
// This is of special importance when parsing transaction_number
// with multiple transactions during the same day
for result in rdr.deserialize() {
// Notice that we need to provide a type hint for automatic
// deserialization.
let record: IcaTransaction = result?;
// Match based on action which account
// Default match on Brukskonto
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 this is the second time a date appears
if last_seen_date == record.datum {
// Store the record in write buffer
} else {
// A fresh new date, print all buffer contents
write_csv(&mut wtr, &mut transaction_buffer)?;
// Set the last seen date
last_seen_date = record.datum.clone();
}
transaction_buffer.push_back([
// Date
record.datum,
// Transaction ID, let GnuCash generate
"".into(),
// Number, 1 by default,
// if multiple in a day this gets overwritten
1.to_string(),
// Description
record.text,
// Notes
// Extra, the account balance stored in a note
match amount_balance {
Some(value) => value.to_string().replace('.', ","),
None => "".into(),
},
// Currency
"CURRENCY::SEK".into(),
// Void Reason
"".into(),
// Action
record.typ,
// Memo
"".into(),
// Full Account Name,
account_name.into(),
// Account Name,
"".into(),
// Amount With Sym.,
"".into(),
// Amount Num,
amount_num.to_string().replace('.', ","),
// Value With Sym.,
"".into(),
// Value Num,
amount_num.to_string().replace('.', ","),
// Reconcile,
"n".into(),
// Reconcile Date,
"".into(),
// Rate/Price,
"1".into(),
]);
}
// Make sure any unprinted lines still in the buffer gets printed
write_csv(&mut wtr, &mut transaction_buffer)?;
Ok(())
}
fn write_csv(
wtr: &mut csv::Writer<std::fs::File>,
buf: &mut VecDeque<[String; 18]>,
) -> Result<(), Box<dyn Error>> {
// Iterate in reverse order to assign correct transaction_number
for (counter, row) in buf.iter_mut().rev().enumerate() {
let transaction_number = counter + 1;
row[2] = transaction_number.to_string();
}
// Write CSV in original order, most recent first
while let Some(row) = buf.pop_front() {
wtr.write_record(row)?;
}
Ok(())
}