// Copyright (c) 2017-2019, scoobybejesus // Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/blob/master/LEGAL.txt use std::error::Error; use std::process; use std::fs::File; use std::cell::{RefCell}; use std::collections::{HashMap}; use chrono::NaiveDate; use decimal::d128; use crate::transaction::{Transaction, ActionRecord}; use crate::account::{Account, RawAccount}; use crate::decimal_utils::{round_d128_1e8}; pub fn import_accounts( rdr: &mut csv::Reader, raw_acct_map: &mut HashMap, acct_map: &mut HashMap, ) -> Result<(), Box> { let header1: csv::StringRecord; header1 = rdr.headers()?.clone(); // account_num // Declare remaining headers, and initialize with None let mut header2: Option = None; // name let mut header3: Option = None; // ticker let header4: csv::StringRecord; // is_margin // Declare string vector (array) to which [all but the first three] Strings of the header row will be appended let mut headerstrings: Vec = [].to_vec(); // Basically, append three empty strings, and then the list of account numbers one by one for element in header1.into_iter() { match element { // Previously had `let element_str: String = element.to_string();` and then `match &*element_str` "txDate" => { headerstrings.push("".to_string()) }, "proceeds" => { headerstrings.push("".to_string()) }, "memo" => { headerstrings.push("".to_string()) }, _ => headerstrings.push(element.to_string()) }; } // End result is headerstrings = ["","","",1,2,3...n] // Account Creation loop. Iterate through 'data' records. We set hasheaders() to true above, so the first record here is the second row of the CSV for result in rdr.records() { // This initial iteration through records will break after the 4th row, after accounts have been created let record = result?; if header2 == None { header2 = Some(record.clone()); continue // After header2 is set, continue to next record } else if header3 == None { header3 = Some(record.clone()); continue // After header3 is set, continue to next record } else { header4 = record.clone(); // println!("Assigned last header, record: {:?}", record); let warn = "FATAL: Transactions will not import correctly if account numbers in the CSV import file aren't ordered chronologically (i.e., beginning in column 4 - the 1st account column - the values should be 1. The next column should be 2, then 3, etc, until the final account)."; // We've got all our header rows. It's now that we set up the accounts. println!("Attempting to create accounts..."); let mut no_dup_acct_nums = HashMap::new(); let length = &headerstrings.len(); for num in headerstrings[3..*length].iter().enumerate() { let counter = no_dup_acct_nums.entry(num).or_insert(0); *counter += 1; } for acct_num in no_dup_acct_nums.keys() { assert_eq!(no_dup_acct_nums[acct_num], 1, "Found accounts with duplicate numbers during import."); } for (idx, item) in headerstrings[3..*length].iter().enumerate() { // println!("Headerstrings value: {:?}", item); let ind = idx+3; // Add three because the idx skips the first three 'key' columns let account_num = item.parse::()?; assert_eq!((idx + 1) as u16, account_num, "Found improper Account Number usage: {}", warn); let name:String = header2.clone().unwrap()[ind].trim().to_string(); let ticker:String = header3.clone().unwrap()[ind].trim().to_string(); let margin_string = &header4.clone()[ind]; let is_margin:bool = match margin_string.trim().to_lowercase().as_str() { "no" | "non" | "false" => false, "yes" | "margin" | "true" => true, _ => { println!("\n Couldn't parse margin value for acct {} {} \n",account_num, name); process::exit(1) } }; let just_account: RawAccount = RawAccount { account_num: account_num, name: name, ticker: ticker, is_margin: is_margin, }; raw_acct_map.insert(account_num, just_account); let account: Account = Account { raw_key: account_num, list_of_lots: RefCell::new([].to_vec()) }; acct_map.insert(account_num, account); } break // This `break` exits this scope so `accounts` can be accessed in `import_transactions`. The rdr stays put. } }; Ok(()) } pub fn import_transactions( rdr: &mut csv::Reader, txns_map: &mut HashMap, action_records: &mut HashMap, raw_acct_map: &mut HashMap, acct_map: &mut HashMap, ) -> Result<(), Box> { let mut this_tx_number = 0; let mut this_ar_number = 0; let mut changed_action_records = 0; let mut changed_txn_num = Vec::new(); println!("Attempting to create transactions..."); for result in rdr.records() { // rdr's cursor is at row 5, which is the first transaction row let record = result?; this_tx_number += 1; // First, initialize metadata fields. let mut this_tx_date: &str = ""; let mut this_proceeds: &str = ""; let mut this_memo: &str = ""; let mut this: String = "".to_string(); // Next, create action_records. let mut action_records_map_keys_vec: Vec = [].to_vec(); let mut outgoing_ar: Option = None; let mut incoming_ar: Option = None; let mut outgoing_ar_num: Option = None; let mut incoming_ar_num: Option = None; for (idx, field) in record.iter().enumerate() { // Set metadata fields on first three fields. if idx == 0 { this_tx_date = field; } else if idx == 1 { this = field.replace(",", ""); this_proceeds = this.as_str(); } else if idx == 2 { this_memo = field; } // Check for empty strings. If not empty, it's a value for an action_record. else if field != "" { this_ar_number += 1; let ind = idx; // starts at 3, which is the fourth field let acct_idx = ind - 2; // acct_num and acct_key would be idx + 1, so subtract 2 from ind to get 1 let account_key = acct_idx as u16; let amount_str = field.replace(",", ""); let amount = amount_str.parse::().unwrap(); let amount_rounded = round_d128_1e8(&amount); if amount != amount_rounded { changed_action_records += 1; changed_txn_num.push(this_tx_number); } let action_record = ActionRecord { account_key: account_key, amount: amount_rounded, tx_key: this_tx_number, self_ar_key: this_ar_number, movements: RefCell::new([].to_vec()), }; if amount > d128!(0.0) { incoming_ar = Some(action_record); incoming_ar_num = Some(this_ar_number); action_records_map_keys_vec.push(incoming_ar_num.unwrap()) } else { outgoing_ar = Some(action_record); outgoing_ar_num = Some(this_ar_number); action_records_map_keys_vec.insert(0, outgoing_ar_num.unwrap()) }; } } match incoming_ar { Some(incoming_ar) => { let x = incoming_ar_num.unwrap(); action_records.insert(x, incoming_ar); }, None => {} } match outgoing_ar { Some(outgoing_ar) => { let y = outgoing_ar_num.unwrap(); action_records.insert(y, outgoing_ar); }, None => {} } let tx_date = NaiveDate::parse_from_str(this_tx_date, "%m/%d/%y") .unwrap_or(NaiveDate::parse_from_str(this_tx_date, "%m/%d/%Y") .expect("%m/%d/%y (or %Y) format required for ledger import") ); let transaction = Transaction { tx_number: this_tx_number, date_as_string: this_tx_date.to_string(), date: tx_date, memo: this_memo.to_string(), proceeds: this_proceeds.parse::()?, action_record_idx_vec: action_records_map_keys_vec, }; txns_map.insert(this_tx_number, transaction); }; if changed_action_records > 0 { println!(" Changed actionrecord amounts: {}. Changed txn numbers: {:?}.", changed_action_records, changed_txn_num); } Ok(()) }