From 37a7b333ac2edefea3f7013eaa8b20c1da63cbd9 Mon Sep 17 00:00:00 2001 From: Piv <18462828+Piv200@users.noreply.github.com> Date: Sat, 28 Jan 2023 10:51:31 +1030 Subject: [PATCH] Complete move money between departments --- Cargo.lock | 185 +++++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 6 +- src/lib.rs | 68 +++++++++---------- src/main.rs | 111 ++++++++++++++++++++++++------- 4 files changed, 300 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee54ba5..7689de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + [[package]] name = "approx" version = "0.5.1" @@ -17,7 +23,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -53,25 +59,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] -name = "clap" -version = "4.0.17" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06badb543e734a2d6568e19a40af66ed5364360b9226184926f89d229b4b4267" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", + "indexmap", "once_cell", "strsim", "termcolor", + "textwrap", ] [[package]] name = "clap_derive" -version = "4.0.13" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -82,9 +96,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -93,10 +107,57 @@ dependencies = [ name = "coster-rs" version = "0.1.0" dependencies = [ + "anyhow", "clap", "csv", "itertools", "nalgebra", + "num-rational", + "rayon", + "serde", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", ] [[package]] @@ -127,6 +188,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" version = "0.4.0" @@ -142,6 +209,25 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "itertools" version = "0.10.3" @@ -184,6 +270,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "nalgebra" version = "0.31.0" @@ -211,6 +306,17 @@ dependencies = [ "syn", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.1" @@ -232,11 +338,12 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -250,6 +357,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + [[package]] name = "once_cell" version = "1.12.0" @@ -316,6 +433,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "regex-automata" version = "0.1.10" @@ -337,11 +476,31 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "simba" @@ -382,6 +541,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "typenum" version = "1.15.0" diff --git a/Cargo.toml b/Cargo.toml index 12a6e24..4db726a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,13 @@ nalgebra = "0.31.0" # https://docs.rs/csv/1.1.6/csv/ csv = "1.1" +serde = { version = "1", features = ["derive"] } # simba = { version = "0.7.1", features = ["partial_fixed_point_support"] } # num = "0.4" -clap = { version = "4.0.17", features = ["derive"] } +clap = { version = "3.1.18", features = ["derive"] } +anyhow = "1.0" itertools = "0.10.3" +num-rational = "0.4.1" +rayon = "1.6.1" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 592d72a..58174bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, error::Error, ops::Mul}; // TODO: Look into serde for serialisation, can also use it to serialise/deserialise // records from a csv file using the csv crate +#[derive(Default)] pub struct MovementRule { // If the vectors are empty, then it means 'all' pub from_units: Vec, @@ -16,16 +17,6 @@ pub struct MovementRule { } impl MovementRule { - pub fn new() -> MovementRule { - MovementRule { - from_units: vec![], - to_units: vec![], - amount: 0.0, - is_percent: false, - is_separator: false, - } - } - pub fn pass_break() -> MovementRule { MovementRule { from_units: vec![], @@ -80,41 +71,50 @@ pub fn move_money_1() {} // Traditinoal/naive, total for each department is stored in an initial map (department -> total amount). // Another map is built up for each rule, and each rule is processed based on the amount in the current total // map. -// Upon a pass break (divider), the temp map will assign the values into the total map. -// Once done, do a final assignment back to the total back, and return that. Probably want to make a copy or -// borrow the total map so it isn't mutated elsewhere. -// Advantage of this is the required code is tiny, and no third-party math library is required (my matrix math -// implementation probably won't be as good as one that's battle-tested) +// Upon a pass break (separator), the temp map will assign the values into the total map. +// Once done, do a final assignment back to the total, and return that. +// Advantage of this is the required code is tiny, and no third-party math library is required. +// Note that the movement happens on a line-by-line level. So we can stream the data from disk, and potentially apply this +// to every. It's also much more memory efficient than approach 1. // TODO: Time both approaches to seee which is faster depending on the size of the input data/number of rules +// TODO: Right now this only supports movements between departments, we also need to support movements between accounts. +// This would require an expansion so that we also have from/to accounts, and the hashmap will use some struct +// that combines an account/department, which is also how the totals will be loaded. (so when loading from disk, +// we load the whole GL into memory sum the account/department totals, and move these into a map line by line) pub fn move_money_2( initial_totals: HashMap, rules: Vec, ) -> HashMap { - // TODO: Replace maps with generic objects, so we can sub in db access/load only some initially + // TODO: Should probably validate that all the rules have departments that actually exist in initial_totals. + // Note: It's potentially a bit more intensive to use cloned totals, but it's much simpler code and, and since we're only working line-by-line + // it isn't really that much memory. in practice let mut running_total = HashMap::from(initial_totals); - let mut temp_total: HashMap = HashMap::new(); + let mut temp_total: HashMap = running_total.clone(); for rule in rules { if rule.is_separator { - temp_total.into_iter().for_each(|temp| { - running_total.insert(temp.0, temp.1).unwrap(); - }); - temp_total = HashMap::new(); - } else if rule.is_percent { - let new_value: f64 = running_total - .iter() - .filter(|department| rule.from_units.contains(department.0)) - .map(|department| department.1 * rule.amount) - .sum(); - for department in rule.to_units { - let previous_temp = temp_total.entry(department).or_insert(0.0); - *previous_temp += new_value; - } - // TODO: Subtract values from the from departments + running_total = temp_total.clone(); } else { - // TODO: Simple addition to to departments/subtraction from from departments + let mut sum_from = 0.; + for department in rule.from_units { + let previous_temp = running_total.get(&department).expect( + "Failed to find department in temp totals, this should not be possible", + ); + let added_amount = if rule.is_percent { + *previous_temp * rule.amount + } else { + rule.amount + }; + sum_from += added_amount; + *temp_total.get_mut(&department).unwrap() -= added_amount; + } + + let value_per_unit = sum_from / rule.to_units.len() as f64; + for department in rule.to_units { + *temp_total.get_mut(&department).unwrap() += value_per_unit; + } } } - running_total + temp_total } #[derive(Debug, PartialEq, Eq)] diff --git a/src/main.rs b/src/main.rs index 3d66f81..e3644e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,55 +1,116 @@ -use std::path::PathBuf; +use std::{error::Error, io::Write, path::PathBuf}; -use clap::{Parser, Subcommand}; +use clap::{builder::PathBufValueParser, Parser, Subcommand}; +use serde::Deserialize; #[derive(Parser)] -#[command(name = "coster-rs", author = "Pivato M. ", version = "0.0.1", about = "Simple, fast, efficient costing tool", long_about = None)] +#[clap(name = "coster-rs")] +#[clap(author = "Pivato M. ")] +#[clap(version = "0.0.1")] +#[clap(about = "Simple, fast, efficient costing tool", long_about = None)] struct Cli { - #[command(subcommand)] + #[clap(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { + /// Moves money between accounts and departments, using the given rules and lines move_money { - #[arg(short = 'r', long, value_name = "FILE")] + #[clap(short = 'r', long, parse(from_os_str), value_name = "FILE")] rules: PathBuf, - #[arg(short, long, value_name = "FILE")] - data: PathBuf, - - #[arg(short, long, value_name = "FILE", default_value = "./output.csv")] - output: PathBuf, - }, - allocate_overheads { - #[arg(short, long, value_name = "FILE")] - rules: PathBuf, - - #[arg(short, long, value_name = "FILE")] + #[clap(short = 'l', long, parse(from_os_str), value_name = "FILE")] lines: PathBuf, - #[arg(short, long, value_name = "FILE", default_value = "./output.csv")] - output: PathBuf, + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + output: Option, + }, + /// Combines rules to the minimum set required + smush_rules { + #[clap(short = 'r', long, parse(from_os_str), value_name = "FILE")] + rules: PathBuf, + + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + output: Option, + }, + /// Allocates servicing department amounts to operating departments + allocate_overheads { + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + rules: PathBuf, + + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + lines: PathBuf, + + #[clap(short, long, parse(from_os_str), value_name = "FILE")] + output: Option, }, } // TODO: Return error (implement the required trait to allow an error to be returned) -fn main() { +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { - Commands::move_money { rules, data, output } => move_money(rules, data, output), + Commands::move_money { + rules, + lines, + output, + } => move_money(rules, lines, output), + Commands::smush_rules { rules, output } => smush_rules(rules, output)?, Commands::allocate_overheads { rules, lines, output, - } => allocate_overheads(), + } => allocate_overheads(rules, lines, output)?, + }; + Ok(()) +} + +fn move_money(rules: PathBuf, lines: PathBuf, output: Option) { + // read rules into required struct (basically map each line into an array) + + // 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) + + // Then run move_moeny, and output the result into a new file at the given output location +} + +fn smush_rules(rules_path: PathBuf, output: Option) -> anyhow::Result<()> { + Ok(()) +} + +fn allocate_overheads( + rules_path: PathBuf, + lines: PathBuf, + output: Option, +) -> anyhow::Result<()> { + let mut rdr = csv::Reader::from_path(rules_path)?; + for result in rdr.deserialize() { + let record: CsvMovementRule = result?; } + + let mut account_reader = csv::Reader::from_path(lines)?; + + for result in account_reader.deserialize() { + let record: CsvAccountCost = result?; + } + + Ok(()) } -fn move_money(rules: PathBuf, data: PathBuf, output: PathBuf) { - // Read all rules/data into memory (can figure out an alternative way later) - +#[derive(Debug, Deserialize)] +struct CsvMovementRule { + from_department: String, + to_department: String, + amount: f64, + is_percent: Option, + is_separator: Option, } -fn allocate_overheads() {} +#[derive(Debug, Deserialize)] +struct CsvAccountCost { + account: String, + department: String, + value: f64, +}