Finish implementing move money

This commit is contained in:
Piv
2023-01-28 23:13:49 +10:30
parent a2091c13b4
commit ba279c8c9b
3 changed files with 194 additions and 79 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
.DS_Store

View File

@@ -2,6 +2,7 @@ extern crate nalgebra as na;
use itertools::Itertools; use itertools::Itertools;
use na::{DMatrix, Dynamic, LU}; use na::{DMatrix, Dynamic, LU};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, error::Error, ops::Mul}; use std::{collections::HashMap, error::Error, ops::Mul};
#[derive(Hash, Clone, Default, PartialEq, Eq)] #[derive(Hash, Clone, Default, PartialEq, Eq)]
@@ -10,6 +11,30 @@ pub struct Unit {
pub account: String, 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<bool>,
is_separator: Option<bool>,
}
#[derive(Default)] #[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'
@@ -65,6 +90,155 @@ pub fn smush_rules(rules: Vec<MovementRule>) -> Vec<MovementRule> {
vec![] vec![]
} }
pub fn move_money<R, L, O>(
rules_reader: csv::Reader<R>,
lines_reader: csv::Reader<L>,
output: csv::Writer<O>,
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<Unit, f64> = 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::<f64>().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::<i32>().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<MovementRule> = 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<String>) -> Vec<String> {
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: // Approach 1:
// Use math (linear algebra) to move between departments. Memory/computationally it's equivalent // 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 // to the worst case of approach one, however can take advantage of auto parallelisation/simd

View File

@@ -1,7 +1,7 @@
use std::{collections::HashMap, error::Error, io::Write, path::PathBuf}; use std::{collections::HashMap, error::Error, io::Write, path::PathBuf};
use clap::{builder::PathBufValueParser, Parser, Subcommand}; use clap::{Parser, Subcommand};
use coster_rs::Unit; use coster_rs::{CsvCost, Unit};
use serde::Deserialize; use serde::Deserialize;
#[derive(Parser)] #[derive(Parser)]
@@ -26,6 +26,9 @@ enum Commands {
#[clap(short, long, parse(from_os_str), value_name = "FILE")] #[clap(short, long, parse(from_os_str), value_name = "FILE")]
output: Option<PathBuf>, output: Option<PathBuf>,
#[clap(short, long, default_value_t = false)]
use_numeric_accounts: bool,
}, },
/// Combines rules to the minimum set required /// Combines rules to the minimum set required
smush_rules { smush_rules {
@@ -56,7 +59,8 @@ fn main() -> anyhow::Result<()> {
rules, rules,
lines, lines,
output, 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::smush_rules { rules, output } => smush_rules(rules, output),
Commands::allocate_overheads { Commands::allocate_overheads {
rules, rules,
@@ -66,58 +70,18 @@ fn main() -> anyhow::Result<()> {
} }
} }
fn move_money(rules: PathBuf, lines: PathBuf, output: Option<PathBuf>) -> anyhow::Result<()> { fn move_money(
let mut rdr = csv::Reader::from_path(lines)?; rules: PathBuf,
let headers = rdr.headers()?; lines: PathBuf,
let mut account_index = 0; output: Option<PathBuf>,
let mut department_index = 0; use_numeric_accounts: bool,
for (index, field) in headers.iter().enumerate() { ) -> anyhow::Result<()> {
if field == "Account" { coster_rs::move_money(
account_index = index; csv::Reader::from_path(rules)?,
} else if field == "Department" { csv::Reader::from_path(lines)?,
department_index = index; csv::Writer::from_path(output.unwrap_or(PathBuf::from("output.csv")))?,
} use_numeric_accounts,
} )
let lines: HashMap<Unit, f64> = 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::<f64>().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 smush_rules(rules_path: PathBuf, output: Option<PathBuf>) -> anyhow::Result<()> { fn smush_rules(rules_path: PathBuf, output: Option<PathBuf>) -> anyhow::Result<()> {
@@ -144,23 +108,6 @@ fn allocate_overheads(
Ok(()) 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<bool>,
is_separator: Option<bool>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct CsvOverheadAllocationRule { struct CsvOverheadAllocationRule {
from_overhead_department: String, from_overhead_department: String,
@@ -168,10 +115,3 @@ struct CsvOverheadAllocationRule {
percent: f64, percent: f64,
to_department_type: String, to_department_type: String,
} }
#[derive(Debug, Deserialize)]
struct CsvCost {
account: String,
department: String,
value: f64,
}