Finish implementing move money
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
.DS_Store
|
||||||
174
src/lib.rs
174
src/lib.rs
@@ -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
|
||||||
|
|||||||
98
src/main.rs
98
src/main.rs
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user