diff --git a/src/overhead_allocation.rs b/src/overhead_allocation.rs index 14765db..058d7b9 100644 --- a/src/overhead_allocation.rs +++ b/src/overhead_allocation.rs @@ -50,11 +50,13 @@ pub struct OverheadAllocationRule { to_department_type: DepartmentType, } +#[derive(Debug, PartialEq)] pub struct TotalDepartmentCost { department: String, value: f64, } +#[derive(Debug, PartialEq)] pub struct AccountCost { account: String, summed_department_costs: Vec, @@ -77,25 +79,6 @@ impl ReciprocalAllocationSolver for DMatrix { } } -fn get_rules_indexes( - allocations: &Vec, - department_type: DepartmentType, -) -> HashMap { - allocations - .iter() - .filter(|allocation| allocation.to_department_type == department_type) - .flat_map(|department| { - [ - department.from_overhead_department.clone(), - department.to_department.clone(), - ] - }) - .unique() - .enumerate() - .map(|(index, department)| (department, index)) - .collect() -} - pub fn reciprocal_allocation( lines: csv::Reader, accounts: csv::Reader, @@ -103,7 +86,9 @@ pub fn reciprocal_allocation, cost_centres: csv::Reader, output: csv::Writer, -) where + use_numeric_accounts: bool, +) -> anyhow::Result<()> +where Lines: Read, Account: Read, AllocationStatistic: Read, @@ -111,11 +96,28 @@ pub fn reciprocal_allocation, csv::Error> = + accounts_reader.deserialize::().collect(); + let mut accounts_sorted = all_accounts_sorted?; + // Sort the accounts, as allocation statistics use account ranges + if use_numeric_accounts { + accounts_sorted.sort_by(|a, b| { + a.code + .parse::() + .unwrap() + .cmp(&b.code.parse::().unwrap()) + }) + } else { + accounts_sorted.sort_by(|a, b| a.code.cmp(&b.code)) + }; + // Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers') // do reciprocal allocation (only for variable portion of accounts), for each account // Copy across fixed stuff (if necessary, not sure it is) + Ok(()) } // Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs @@ -126,10 +128,7 @@ fn reciprocal_allocation_impl( account_costs: Vec, // TODO: Throw an appropriate error ) -> anyhow::Result> { - let overhead_department_mappings: HashMap = - get_rules_indexes(&allocations, DepartmentType::Overhead); - let operating_department_mappings: HashMap = - get_rules_indexes(&allocations, DepartmentType::Operating); + let overhead_department_mappings = get_rules_indexes(&allocations, DepartmentType::Overhead); let mut slice_allocations = vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()]; @@ -138,25 +137,23 @@ fn reciprocal_allocation_impl( .iter() .filter(|allocation| allocation.to_department_type == DepartmentType::Overhead) { - // TODO: Check if we need to flp this around let from_index = overhead_department_mappings .get(&allocation.from_overhead_department) .unwrap(); - let to_index = operating_department_mappings + let to_index = overhead_department_mappings .get(&allocation.to_department) .unwrap(); - let elem = &mut slice_allocations - [(*from_index) + (overhead_department_mappings.len() * (*to_index))]; - *elem = allocation.percent; + // TODO: Check if dmatrix is row or column major order, would need to flip if column major + slice_allocations[from_index * overhead_department_mappings.len() + to_index] = + allocation.percent * -1.; } - // TODO: Also need ones along the diagonal, and negatives in some places... - - let mat: DMatrix = DMatrix::from_row_slice( + let mut mat: DMatrix = DMatrix::from_vec( overhead_department_mappings.len(), overhead_department_mappings.len(), - &slice_allocations, + slice_allocations, ); + mat.fill_diagonal(1.); if mat.determinant() == 0. { let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001); @@ -176,31 +173,193 @@ fn reciprocal_allocation_impl( } } +fn get_rules_indexes( + allocations: &Vec, + department_type: DepartmentType, +) -> HashMap { + allocations + .iter() + .filter(|allocation| allocation.to_department_type == department_type) + .flat_map(|department| { + if department.to_department_type == DepartmentType::Operating { + vec![department.to_department.clone()] + } else { + vec![ + department.from_overhead_department.clone(), + department.to_department.clone(), + ] + } + }) + .unique() + .enumerate() + .map(|(index, department)| (department, index)) + .collect() +} + fn do_solve_reciprocal( solver: T, account_costs: Vec, - department_mappings: HashMap, + overhead_department_mappings: HashMap, allocations: Vec, ) -> anyhow::Result> { - // TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out - for total_costs in account_costs { - let mut slice_costs = vec![0.; department_mappings.len()]; - - for cost in total_costs.summed_department_costs { - let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()]; - *elem = cost.value; + let operating_department_mappings = get_rules_indexes(&allocations, DepartmentType::Operating); + let mut operating_overhead_mappings = + vec![0.; overhead_department_mappings.len() * operating_department_mappings.len()]; + for rule in allocations { + if rule.to_department_type == DepartmentType::Operating { + let from_index = *overhead_department_mappings + .get(&rule.from_overhead_department) + .unwrap(); + let to_index = *operating_department_mappings + .get(&rule.to_department) + .unwrap(); + operating_overhead_mappings + [from_index * overhead_department_mappings.len() + to_index] = rule.percent; } + } + let operating_overhead_mappings_mat: DMatrix = DMatrix::from_vec( + operating_department_mappings.len(), + overhead_department_mappings.len(), + operating_overhead_mappings, + ); + let mut final_account_costs: Vec = Vec::with_capacity(account_costs.len()); + for total_costs in account_costs { + // TODO: There has to be a cleaner way to do this, perhaps by presorting things? + let mut overhead_slice_costs = vec![0.; overhead_department_mappings.len()]; + for cost in total_costs.summed_department_costs.iter() { + if overhead_department_mappings.contains_key(&cost.department) { + overhead_slice_costs[*overhead_department_mappings.get(&cost.department).unwrap()] = + cost.value + } + } + let overhead_costs_vec: DMatrix = + DMatrix::from_row_slice(overhead_department_mappings.len(), 1, &overhead_slice_costs); + let calculated_overheads = solver.solve(&overhead_costs_vec); - let costs_vec: DMatrix = - DMatrix::from_row_slice(department_mappings.len(), 1, &slice_costs); + let mut operating_slice_costs = vec![0.; operating_department_mappings.len()]; + for cost in total_costs.summed_department_costs { + if operating_department_mappings.contains_key(&cost.department) { + let elem = &mut operating_slice_costs + [*operating_department_mappings.get(&cost.department).unwrap()]; + *elem = cost.value; + } + } + let operating_costs_vec: DMatrix = DMatrix::from_row_slice( + operating_department_mappings.len(), + 1, + &operating_slice_costs, + ); - let calculated_overheads = solver.solve(&costs_vec); + // Borrow so we don't move between loops + let operating_overhead_mappings = &operating_overhead_mappings_mat; + let calculated_overheads = &calculated_overheads; // Calculation: operating_overhead_usage . calculated_overheads + initial_totals // Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the // solved overheads usages after taking into account usage between departments, and initial_totals is the initial values // for the operating departments. + let calculated = operating_overhead_mappings * calculated_overheads + operating_costs_vec; + + let converted_result: Vec = operating_department_mappings + .iter() + .map(|(department, index)| TotalDepartmentCost { + department: department.clone(), + value: *calculated.get(*index).unwrap(), + }) + .collect(); + final_account_costs.push(AccountCost { + account: total_costs.account, + summed_department_costs: converted_result, + }); + } + Ok(final_account_costs) +} + +#[cfg(test)] +mod tests { + use crate::AccountCost; + use crate::DepartmentType; + use crate::OverheadAllocationRule; + use crate::TotalDepartmentCost; + + use super::reciprocal_allocation_impl; + + #[test] + fn test_basic() { + let allocation_rules = vec![ + OverheadAllocationRule { + from_overhead_department: "Y".to_owned(), + to_department: "Z".to_owned(), + percent: 0.2, + to_department_type: DepartmentType::Overhead, + }, + OverheadAllocationRule { + from_overhead_department: "Z".to_owned(), + to_department: "Y".to_owned(), + percent: 0.3, + to_department_type: DepartmentType::Overhead, + }, + OverheadAllocationRule { + from_overhead_department: "Y".to_owned(), + to_department: "A".to_owned(), + percent: 0.4, + to_department_type: DepartmentType::Operating, + }, + OverheadAllocationRule { + from_overhead_department: "Y".to_owned(), + to_department: "B".to_owned(), + percent: 0.4, + to_department_type: DepartmentType::Operating, + }, + OverheadAllocationRule { + from_overhead_department: "Z".to_owned(), + to_department: "A".to_owned(), + percent: 0.2, + to_department_type: DepartmentType::Operating, + }, + OverheadAllocationRule { + from_overhead_department: "Z".to_owned(), + to_department: "B".to_owned(), + percent: 0.5, + to_department_type: DepartmentType::Operating, + }, + ]; + let initial_totals = vec![AccountCost { + account: "Default".to_owned(), + summed_department_costs: vec![ + TotalDepartmentCost { + department: "Y".to_owned(), + value: 7260., + }, + TotalDepartmentCost { + department: "Z".to_owned(), + value: 4000., + }, + TotalDepartmentCost { + department: "A".to_owned(), + value: 12000., + }, + TotalDepartmentCost { + department: "B".to_owned(), + value: 16000., + }, + ], + }]; + let expected_final_allocations = vec![AccountCost { + account: "Default".to_owned(), + summed_department_costs: vec![ + TotalDepartmentCost { + department: "A".to_owned(), + value: 16760., + }, + TotalDepartmentCost { + department: "B".to_owned(), + value: 22500., + }, + ], + }]; + + let result = reciprocal_allocation_impl(allocation_rules, initial_totals).unwrap(); + assert_eq!(expected_final_allocations, result); } - // TODO: return something appropriate - Ok(vec![]) }