Split out CSV into a module
Dependencies
- [2]
4DJWIQSIImplement cargo clippy suggestions - [3]
J2PVPX3PPrint rows in original order Change from a Vec to VecDeque - [4]
3HZYHDXTTrim whitespace - [5]
V6WMVNO6GnuCash import quotes values, expecting them to use comma (,) with Swedish SEK Achieve this by once again replacing period to comma for final output - [6]
ZQNDNT3KHandle argument errors, provide help - [7]
YFZX4FSZGenerate transaction numbers - [8]
ZEUBLA35Do not have subaccounts for Brukskonto - [9]
Q4VYTFJ7Add saldo/balance in the Note field - [10]
LDUI5PR2Change to using STDIO - [11]
QDZOD3MNAble to parse CSV - [12]
ZKSXZMQFProvide feedback when done - [13]
4BWPI66VAdd first mapping attempt - [14]
R5EWGEJKAdd account actions, default to Brukskonto
Change contents
- replacement in src/main.rs at line 1[6.48]→[6.0:24](∅→∅),[6.24]→[3.0:32](∅→∅),[3.32]→[6.24:38](∅→∅),[6.24]→[6.24:38](∅→∅),[6.38]→[6.49:72](∅→∅),[6.48]→[6.49:72](∅→∅),[6.13]→[6.85:103](∅→∅),[6.85]→[6.85:103](∅→∅)
use serde::Deserialize;use std::collections::VecDeque;use std::env;use std::error::Error;use std::process;use std::{env, path::Path, process}; - replacement in src/main.rs at line 3
// 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; - edit in src/main.rs at line 6[6.509]→[6.509:1095](∅→∅),[6.1095]→[6.0:29](∅→∅),[6.29]→[6.1127:1229](∅→∅),[6.1127]→[6.1127:1229](∅→∅),[6.1229]→[6.39:278](∅→∅),[6.278]→[4.0:30](∅→∅),[4.30]→[6.278:327](∅→∅),[6.278]→[6.278:327](∅→∅),[6.327]→[6.1274:1370](∅→∅),[6.1274]→[6.1274:1370](∅→∅),[6.32]→[6.1463:1464](∅→∅),[6.85]→[6.1463:1464](∅→∅),[6.1463]→[6.1463:1464](∅→∅),[6.1464]→[6.328:350](∅→∅),[6.350]→[3.33:72](∅→∅),[3.72]→[6.350:421](∅→∅),[6.350]→[6.350:421](∅→∅),[6.421]→[6.145:203](∅→∅),[6.145]→[6.145:203](∅→∅),[6.87]→[6.1528:1952](∅→∅),[6.203]→[6.1528:1952](∅→∅),[6.1528]→[6.1528:1952](∅→∅),[6.1952]→[6.204:205](∅→∅),[6.205]→[6.422:467](∅→∅),[6.60]→[6.230:231](∅→∅),[6.467]→[6.230:231](∅→∅),[6.230]→[6.230:231](∅→∅),[6.231]→[3.73:147](∅→∅),[3.147]→[6.532:822](∅→∅),[6.532]→[6.532:822](∅→∅),[6.231]→[6.1952:2132](∅→∅),[6.822]→[6.1952:2132](∅→∅),[6.1952]→[6.1952:2132](∅→∅),[6.85]→[6.85:86](∅→∅),[6.86]→[6.124:171](∅→∅),[6.254]→[6.124:171](∅→∅),[6.124]→[6.124:171](∅→∅),[6.171]→[6.0:39](∅→∅),[6.39]→[6.171:226](∅→∅),[6.171]→[6.171:226](∅→∅),[6.226]→[6.87:493](∅→∅),[6.493]→[6.40:174](∅→∅),[6.174]→[6.493:633](∅→∅),[6.493]→[6.493:633](∅→∅),[6.633]→[6.226:295](∅→∅),[6.891]→[6.226:295](∅→∅),[6.226]→[6.226:295](∅→∅),[6.295]→[6.175:232](∅→∅),[6.232]→[6.523:534](∅→∅),[6.523]→[6.523:534](∅→∅),[6.534]→[6.0:115](∅→∅),[6.147]→[6.147:335](∅→∅),[6.335]→[2.0:367](∅→∅),[2.367]→[6.534:535](∅→∅),[6.806]→[6.534:535](∅→∅),[6.1400]→[6.534:535](∅→∅),[6.534]→[6.534:535](∅→∅),[6.535]→[6.823:1203](∅→∅),[6.1203]→[6.1203:1204](∅→∅),[6.1204]→[3.148:187](∅→∅),[3.187]→[6.563:661](∅→∅),[6.1238]→[6.563:661](∅→∅),[6.563]→[6.563:661](∅→∅),[6.728]→[6.728:751](∅→∅),[6.751]→[6.1239:1361](∅→∅),[6.1361]→[6.751:824](∅→∅),[6.751]→[6.751:824](∅→∅),[6.824]→[6.807:901](∅→∅),[6.901]→[5.0:68](∅→∅),[5.68]→[6.951:1001](∅→∅),[6.951]→[6.951:1001](∅→∅),[6.1001]→[6.847:1252](∅→∅),[6.847]→[6.847:1252](∅→∅),[6.1252]→[5.69:123](∅→∅),[5.123]→[6.1606:1764](∅→∅),[6.1437]→[6.1606:1764](∅→∅),[6.1606]→[6.1606:1764](∅→∅),[6.1764]→[6.1362:1374](∅→∅),[6.1374]→[6.2166:2172](∅→∅),[6.1777]→[6.2166:2172](∅→∅),[6.2166]→[6.2166:2172](∅→∅),[6.2172]→[6.1375:1511](∅→∅),[6.1511]→[6.2172:2173](∅→∅),[6.2172]→[6.2172:2173](∅→∅),[6.2173]→[6.1512:1568](∅→∅),[6.1568]→[3.188:226](∅→∅),[3.226]→[6.1601:1635](∅→∅),[6.1601]→[6.1601:1635](∅→∅),[6.1635]→[3.227:556](∅→∅),[3.556]→[6.1762:1800](∅→∅),[6.1762]→[6.1762:1800](∅→∅),[6.1800]→[6.2173:2184](∅→∅),[6.2173]→[6.2173:2184](∅→∅),[6.2184]→[6.0:3](∅→∅)
// 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(())} - replacement in src/main.rs at line 24
if args.len() < 2 {if args.is_empty() { - replacement in src/main.rs at line 31[6.1937]→[6.1937:2027](∅→∅),[6.2027]→[6.2285:2311](∅→∅),[6.2285]→[6.2285:2311](∅→∅),[6.2311]→[6.0:40](∅→∅)
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);}} - edit in src/main.rs at line 49
.trim(csv::Trim::All).from_reader(io::stdin()); - resolve order conflict in src/main.rs at line 49[6.2319]
- file addition: ica_csv.rs[6.15]
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(())}