From ba279c8c9b0cebd7380b9d4ea57074e3c3cde6e4 Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Sat, 28 Jan 2023 23:13:49 +1030 Subject: [PATCH] Finish implementing move money --- .gitignore | 1 + src/lib.rs | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 98 ++++++----------------------- 3 files changed, 194 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..212de44 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.DS_Store \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4de7b74..43c1ff5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ extern crate nalgebra as na; use itertools::Itertools; use na::{DMatrix, Dynamic, LU}; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, error::Error, ops::Mul}; #[derive(Hash, Clone, Default, PartialEq, Eq)] @@ -10,6 +11,30 @@ pub struct Unit { pub account: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct CsvCost { + account: String, + department: String, + value: f64, +} + +#[derive(Debug, Deserialize)] +struct CsvMovementRule { + #[serde(rename = "FromCC")] + // Need strings to further split later + from_departments: String, + to_departments: String, + all_from_departments: bool, + all_to_departments: bool, + from_accounts: String, + to_accounts: String, + all_from_accounts: bool, + all_to_accounts: bool, + amount: f64, + is_percent: Option, + is_separator: Option, +} + #[derive(Default)] pub struct MovementRule { // If the vectors are empty, then it means 'all' @@ -65,6 +90,155 @@ pub fn smush_rules(rules: Vec) -> Vec { vec![] } +pub fn move_money( + rules_reader: csv::Reader, + lines_reader: csv::Reader, + output: csv::Writer, + use_numeric_accounts: bool, +) -> anyhow::Result<()> +where + R: std::io::Read, + L: std::io::Read, + O: std::io::Write, +{ + let mut lines_reader = lines_reader; + let headers = lines_reader.headers()?; + let mut account_index = 0; + let mut department_index = 0; + for (index, field) in headers.iter().enumerate() { + if field.eq_ignore_ascii_case("account") { + account_index = index; + } else if field.eq_ignore_ascii_case("department") { + department_index = index; + } + } + + let lines: HashMap = lines_reader + .records() + .map(|record| { + let record = record.unwrap(); + let account = record.get(account_index).unwrap(); + let department = record.get(department_index).unwrap(); + let sum = record + .iter() + .enumerate() + .filter(|(i, _)| *i != account_index && *i != department_index) + .map(|(_, f)| f.parse::().unwrap()) + .sum(); + ( + Unit { + account: account.into(), + department: department.into(), + }, + sum, + ) + }) + .collect(); + + let all_accounts_sorted = if use_numeric_accounts { + lines + .keys() + .map(|key| key.account.clone().parse::().unwrap()) + .sorted() + .map(|account| account.to_string()) + .collect() + } else { + lines + .keys() + .map(|key| key.account.clone()) + .sorted() + .collect() + }; + let all_departments_sorted = lines + .keys() + .map(|key| key.department.clone()) + .sorted() + .collect(); + let mut rules_reader = rules_reader; + let mut rules: Vec = vec![]; + for result in rules_reader.deserialize() { + let movement_rule: CsvMovementRule = result?; + let from_accounts = extract_range( + movement_rule.from_accounts, + movement_rule.all_from_accounts, + &all_accounts_sorted, + ); + let to_accounts = extract_range( + movement_rule.to_accounts, + movement_rule.all_to_accounts, + &all_accounts_sorted, + ); + let from_departments = extract_range( + movement_rule.from_departments, + movement_rule.all_from_departments, + &all_departments_sorted, + ); + let to_departments = extract_range( + movement_rule.to_departments, + movement_rule.all_to_departments, + &all_departments_sorted, + ); + rules.push(MovementRule { + from_departments, + to_departments, + all_from_departments: movement_rule.all_from_departments, + all_to_departments: movement_rule.all_to_departments, + from_accounts, + to_accounts, + all_from_accounts: movement_rule.all_from_accounts, + all_to_accounts: movement_rule.all_to_accounts, + amount: movement_rule.amount, + is_percent: movement_rule.is_percent.unwrap_or(false), + is_separator: movement_rule.is_separator.unwrap_or(false), + }) + } + + // Then run move_money + let moved = move_money_2(lines, &rules); + let mut output = output; + + // Ouput the list moved moneys + for money in moved { + output.serialize(CsvCost { + account: money.0.account, + department: money.0.department, + value: money.1, + })?; + } + + Ok(()) +} + +fn extract_range(range: String, all: bool, options: &Vec) -> Vec { + if all { + return vec![]; + } + let split_range: Vec<&str> = range.split("-").collect(); + if split_range.len() == 1 { + return vec![range]; + } + let start_index = options + .iter() + .enumerate() + .find(|option| option.1 == split_range[0]) + .map(|start| start.0); + let end_index = options + .iter() + .enumerate() + .find(|option| option.1 == split_range[1]) + .map(|end| end.0); + if let Some(start) = start_index { + if let Some(end) = end_index { + return Vec::from(&options[start..end + 1]); + } else { + return vec![options[start].clone()]; + } + } else if let Some(end) = end_index { + return vec![options[end].clone()]; + } + return vec![]; +} + // Approach 1: // Use math (linear algebra) to move between departments. Memory/computationally it's equivalent // to the worst case of approach one, however can take advantage of auto parallelisation/simd diff --git a/src/main.rs b/src/main.rs index f01996b..36434b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, error::Error, io::Write, path::PathBuf}; -use clap::{builder::PathBufValueParser, Parser, Subcommand}; -use coster_rs::Unit; +use clap::{Parser, Subcommand}; +use coster_rs::{CsvCost, Unit}; use serde::Deserialize; #[derive(Parser)] @@ -26,6 +26,9 @@ enum Commands { #[clap(short, long, parse(from_os_str), value_name = "FILE")] output: Option, + + #[clap(short, long, default_value_t = false)] + use_numeric_accounts: bool, }, /// Combines rules to the minimum set required smush_rules { @@ -56,7 +59,8 @@ fn main() -> anyhow::Result<()> { rules, lines, output, - } => move_money(rules, lines, output), + use_numeric_accounts, + } => move_money(rules, lines, output, use_numeric_accounts), Commands::smush_rules { rules, output } => smush_rules(rules, output), Commands::allocate_overheads { rules, @@ -66,58 +70,18 @@ fn main() -> anyhow::Result<()> { } } -fn move_money(rules: PathBuf, lines: PathBuf, output: Option) -> anyhow::Result<()> { - let mut rdr = csv::Reader::from_path(lines)?; - let headers = rdr.headers()?; - let mut account_index = 0; - let mut department_index = 0; - for (index, field) in headers.iter().enumerate() { - if field == "Account" { - account_index = index; - } else if field == "Department" { - department_index = index; - } - } - - let lines: HashMap = rdr - .records() - .map(|record| { - let record = record.unwrap(); - let account = record.get(account_index).unwrap(); - let department = record.get(department_index).unwrap(); - let sum = record - .iter() - .enumerate() - .filter(|(i, _)| *i != account_index && *i != department_index) - .map(|(_, f)| f.parse::().unwrap()) - .sum(); - ( - Unit { - account: account.into(), - department: department.into(), - }, - sum, - ) - }) - .collect(); - - // read rules into required struct (basically map each line into an array). Need to figure out the input format... - let mut rdr = csv::Reader::from_path(rules)?; - for result in rdr.deserialize() { - let record: CsvMovementRule = result?; - // TODO: Problem is the from/to are ranges, that rely on the gl data, so need to get a list of - // - } - - // Read gl lines data (all of it). For each line, sum the periods, and insert the summed - // line into a hashmap (need to read the whole gl in as we can move between every line) - // We use a record, everything that's not an account/department is assumed to be a period - - // Then run move_moeny - - // Ouput the list moved moneys - - Ok(()) +fn move_money( + rules: PathBuf, + lines: PathBuf, + output: Option, + use_numeric_accounts: bool, +) -> anyhow::Result<()> { + coster_rs::move_money( + csv::Reader::from_path(rules)?, + csv::Reader::from_path(lines)?, + csv::Writer::from_path(output.unwrap_or(PathBuf::from("output.csv")))?, + use_numeric_accounts, + ) } fn smush_rules(rules_path: PathBuf, output: Option) -> anyhow::Result<()> { @@ -144,23 +108,6 @@ fn allocate_overheads( Ok(()) } -#[derive(Debug, Deserialize)] -struct CsvMovementRule { - #[serde(rename = "FromCC")] - // Need strings to further split later - from_departments: String, - to_departments: String, - all_from_departments: bool, - all_to_departments: bool, - from_accounts: String, - to_accounts: String, - all_from_accounts: bool, - all_to_accounts: bool, - amount: f64, - is_percent: Option, - is_separator: Option, -} - #[derive(Debug, Deserialize)] struct CsvOverheadAllocationRule { from_overhead_department: String, @@ -168,10 +115,3 @@ struct CsvOverheadAllocationRule { percent: f64, to_department_type: String, } - -#[derive(Debug, Deserialize)] -struct CsvCost { - account: String, - department: String, - value: f64, -}