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 filelet 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 dayfor 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 Brukskontolet 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 appearsif last_seen_date == record.datum {// Store the record in write buffer} else {// A fresh new date, print all buffer contentswrite_csv(&mut wtr, &mut transaction_buffer)?;// Set the last seen datelast_seen_date = record.datum.clone();}transaction_buffer.push_back([// Daterecord.datum,// Transaction ID, let GnuCash generate"".into(),// Number, 1 by default,// if multiple in a day this gets overwritten1.to_string(),// Descriptionrecord.text,// Notes// Extra, the account balance stored in a notematch amount_balance {Some(value) => value.to_string().replace(".", ","),None => "".into(),},// Currency"CURRENCY::SEK".into(),// Void Reason"".into(),// Actionrecord.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 printedwrite_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_numberfor (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 firstwhile 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 filelet 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 dayfor 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 Brukskontolet 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 appearsif last_seen_date == record.datum {// Store the record in write buffer} else {// A fresh new date, print all buffer contentswrite_csv(&mut wtr, &mut transaction_buffer)?;// Set the last seen datelast_seen_date = record.datum.clone();}transaction_buffer.push_back([// Daterecord.datum,// Transaction ID, let GnuCash generate"".into(),// Number, 1 by default,// if multiple in a day this gets overwritten1.to_string(),// Descriptionrecord.text,// Notes// Extra, the account balance stored in a notematch amount_balance {Some(value) => value.to_string().replace('.', ","),None => "".into(),},// Currency"CURRENCY::SEK".into(),// Void Reason"".into(),// Actionrecord.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 printedwrite_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_numberfor (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 firstwhile let Some(row) = buf.pop_front() {wtr.write_record(row)?;}Ok(())}