Complete move money between departments

This commit is contained in:
Piv
2023-01-28 10:51:31 +10:30
parent 232ed9dd39
commit 37a7b333ac
4 changed files with 300 additions and 70 deletions

185
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "anyhow"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
[[package]] [[package]]
name = "approx" name = "approx"
version = "0.5.1" version = "0.5.1"
@@ -17,7 +23,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.1.19",
"libc", "libc",
"winapi", "winapi",
] ]
@@ -53,25 +59,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc"
[[package]] [[package]]
name = "clap" name = "cfg-if"
version = "4.0.17" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"indexmap",
"once_cell", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap",
] ]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.0.13" version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@@ -82,9 +96,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.3.0" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
@@ -93,10 +107,57 @@ dependencies = [
name = "coster-rs" name = "coster-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"clap", "clap",
"csv", "csv",
"itertools", "itertools",
"nalgebra", "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]] [[package]]
@@ -127,6 +188,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.0" version = "0.4.0"
@@ -142,6 +209,25 @@ dependencies = [
"libc", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.3" version = "0.10.3"
@@ -184,6 +270,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "nalgebra" name = "nalgebra"
version = "0.31.0" version = "0.31.0"
@@ -211,6 +306,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "num-complex" name = "num-complex"
version = "0.4.1" version = "0.4.1"
@@ -232,11 +338,12 @@ dependencies = [
[[package]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-bigint",
"num-integer", "num-integer",
"num-traits", "num-traits",
] ]
@@ -250,6 +357,16 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.12.0"
@@ -316,6 +433,28 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.1.10" version = "0.1.10"
@@ -337,11 +476,31 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.137" version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 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]] [[package]]
name = "simba" name = "simba"
@@ -382,6 +541,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.15.0" version = "1.15.0"

View File

@@ -11,9 +11,13 @@ nalgebra = "0.31.0"
# https://docs.rs/csv/1.1.6/csv/ # https://docs.rs/csv/1.1.6/csv/
csv = "1.1" csv = "1.1"
serde = { version = "1", features = ["derive"] }
# simba = { version = "0.7.1", features = ["partial_fixed_point_support"] } # simba = { version = "0.7.1", features = ["partial_fixed_point_support"] }
# num = "0.4" # num = "0.4"
clap = { version = "4.0.17", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
anyhow = "1.0"
itertools = "0.10.3" itertools = "0.10.3"
num-rational = "0.4.1"
rayon = "1.6.1"

View File

@@ -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 // TODO: Look into serde for serialisation, can also use it to serialise/deserialise
// records from a csv file using the csv crate // records from a csv file using the csv crate
#[derive(Default)]
pub struct MovementRule { pub struct MovementRule {
// If the vectors are empty, then it means 'all' // If the vectors are empty, then it means 'all'
pub from_units: Vec<String>, pub from_units: Vec<String>,
@@ -16,16 +17,6 @@ pub struct MovementRule {
} }
impl 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 { pub fn pass_break() -> MovementRule {
MovementRule { MovementRule {
from_units: vec![], 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). // 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 // Another map is built up for each rule, and each rule is processed based on the amount in the current total
// map. // map.
// Upon a pass break (divider), the temp map will assign the values into the total map. // 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 back, and return that. Probably want to make a copy or // Once done, do a final assignment back to the total, and return that.
// 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.
// Advantage of this is the required code is tiny, and no third-party math library is required (my matrix math // Note that the movement happens on a line-by-line level. So we can stream the data from disk, and potentially apply this
// implementation probably won't be as good as one that's battle-tested) // 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: 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( pub fn move_money_2(
initial_totals: HashMap<String, f64>, initial_totals: HashMap<String, f64>,
rules: Vec<MovementRule>, rules: Vec<MovementRule>,
) -> HashMap<String, f64> { ) -> HashMap<String, f64> {
// 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 running_total = HashMap::from(initial_totals);
let mut temp_total: HashMap<String, f64> = HashMap::new(); let mut temp_total: HashMap<String, f64> = running_total.clone();
for rule in rules { for rule in rules {
if rule.is_separator { if rule.is_separator {
temp_total.into_iter().for_each(|temp| { running_total = temp_total.clone();
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
} else { } 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)] #[derive(Debug, PartialEq, Eq)]

View File

@@ -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)] #[derive(Parser)]
#[command(name = "coster-rs", author = "Pivato M. <mpivato4@gmail.com>", version = "0.0.1", about = "Simple, fast, efficient costing tool", long_about = None)] #[clap(name = "coster-rs")]
#[clap(author = "Pivato M. <mpivato4@gmail.com>")]
#[clap(version = "0.0.1")]
#[clap(about = "Simple, fast, efficient costing tool", long_about = None)]
struct Cli { struct Cli {
#[command(subcommand)] #[clap(subcommand)]
command: Commands, command: Commands,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
/// Moves money between accounts and departments, using the given rules and lines
move_money { move_money {
#[arg(short = 'r', long, value_name = "FILE")] #[clap(short = 'r', long, parse(from_os_str), value_name = "FILE")]
rules: PathBuf, rules: PathBuf,
#[arg(short, long, value_name = "FILE")] #[clap(short = 'l', long, parse(from_os_str), 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")]
lines: PathBuf, lines: PathBuf,
#[arg(short, long, value_name = "FILE", default_value = "./output.csv")] #[clap(short, long, parse(from_os_str), value_name = "FILE")]
output: PathBuf, output: Option<PathBuf>,
},
/// 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<PathBuf>,
},
/// 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<PathBuf>,
}, },
} }
// TODO: Return error (implement the required trait to allow an error to be returned) // TODO: Return error (implement the required trait to allow an error to be returned)
fn main() { fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { 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 { Commands::allocate_overheads {
rules, rules,
lines, lines,
output, output,
} => allocate_overheads(), } => allocate_overheads(rules, lines, output)?,
};
Ok(())
}
fn move_money(rules: PathBuf, lines: PathBuf, output: Option<PathBuf>) {
// 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<PathBuf>) -> anyhow::Result<()> {
Ok(())
}
fn allocate_overheads(
rules_path: PathBuf,
lines: PathBuf,
output: Option<PathBuf>,
) -> 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) { #[derive(Debug, Deserialize)]
// Read all rules/data into memory (can figure out an alternative way later) struct CsvMovementRule {
from_department: String,
to_department: String,
amount: f64,
is_percent: Option<bool>,
is_separator: Option<bool>,
} }
fn allocate_overheads() {} #[derive(Debug, Deserialize)]
struct CsvAccountCost {
account: String,
department: String,
value: f64,
}