Complete move money between departments
This commit is contained in:
68
src/lib.rs
68
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<String>,
|
||||
@@ -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<String, f64>,
|
||||
rules: Vec<MovementRule>,
|
||||
) -> 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 temp_total: HashMap<String, f64> = HashMap::new();
|
||||
let mut temp_total: HashMap<String, f64> = 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)]
|
||||
|
||||
111
src/main.rs
111
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. <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 {
|
||||
#[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<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)
|
||||
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<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) {
|
||||
// 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<bool>,
|
||||
is_separator: Option<bool>,
|
||||
}
|
||||
|
||||
fn allocate_overheads() {}
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CsvAccountCost {
|
||||
account: String,
|
||||
department: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user