diff options
| author | Joe Carstairs <me@joeac.net> | 2024-11-17 09:23:28 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2024-11-17 09:23:28 +0000 |
| commit | 1ae19eb3315c40e3f9062592c27fa76c11cf626c (patch) | |
| tree | 09d9ce380795971f4b50e1d4b0ca3c57c9e1db1a /rust/core/src/utils | |
| parent | 1a494937021561595d0fb29e26fb707aef9e55de (diff) | |
Renames workspace folder
Diffstat (limited to 'rust/core/src/utils')
| -rw-r--r-- | rust/core/src/utils/calculate_budgets_accrual.rs | 155 | ||||
| -rw-r--r-- | rust/core/src/utils/calculate_category_balance.rs | 153 | ||||
| -rw-r--r-- | rust/core/src/utils/find_by_id_or.rs | 41 | ||||
| -rw-r--r-- | rust/core/src/utils/mod.rs | 7 |
4 files changed, 356 insertions, 0 deletions
diff --git a/rust/core/src/utils/calculate_budgets_accrual.rs b/rust/core/src/utils/calculate_budgets_accrual.rs new file mode 100644 index 0000000..56171ca --- /dev/null +++ b/rust/core/src/utils/calculate_budgets_accrual.rs @@ -0,0 +1,155 @@ +use anyhow::Result; +use crate::{models::{ + budget_update::BudgetUpdate, category::Category, date_utc::DateUtc +}, traits::nowlike::Nowlike}; + +pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> Result<i64> { + let mut budget_updates = budget_updates + .iter() + .filter(|bu| bu.category_id == category.id) + .collect::<Vec<&BudgetUpdate>>(); + budget_updates.sort_by_key(|bu| bu.date.clone()); + budget_updates.reverse(); + + let mut budgets_accrual = 0.0_f64; + let mut calculated_back_to_date = DateUtc::now(); + + for budget_update in budget_updates { + let days_on_this_budget = calculated_back_to_date.day_diff(&budget_update.date); + if days_on_this_budget == 0_u32 { + return Err(anyhow::Error::msg(format!( + "failed to calculate budgets accrual since two budget updates had the same date ({})", + budget_update.date, + ))); + } + let budget_per_day = budget_update.new_budget as f64 / budget_update.new_period as f64; + budgets_accrual += days_on_this_budget as f64 * budget_per_day; + calculated_back_to_date = budget_update.date.clone(); + } + + Ok(budgets_accrual.floor() as i64) +} + +#[cfg(test)] +mod test { + use chrono::{Days, Utc}; + + use crate::{ + models::{budget_update::BudgetUpdate, category::Category, date_utc::DateUtc}, traits::nowlike::Nowlike, utils::calculate_budgets_accrual + }; + + #[test] + fn when_no_budget_updates_then_return_zero() { + let category = Category { + id: 0, + name: String::from("Groceries"), + }; + let budget_updates = []; + + let result = calculate_budgets_accrual(&category, &budget_updates); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_one_budget_update_for_another_category_then_return_zero() { + let category = Category { + id: 0, + name: String::from("Groceries"), + }; + let budget_updates = [BudgetUpdate { + id: 0, + category_id: 1, + date: DateUtc::now(), + new_budget: 100, + new_period: 1, + }]; + + let result = calculate_budgets_accrual(&category, &budget_updates); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_one_budget_update_then_return_quantity_times_days_in_past() { + let category = Category { + id: 0, + name: String::from("Groceries"), + }; + let budget_updates = [BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from(Utc::now().checked_sub_days(Days::new(120)).unwrap()), + new_budget: 100, + new_period: 1, + }]; + + let result = calculate_budgets_accrual(&category, &budget_updates); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 12_000); + } + + #[test] + fn when_two_budget_updates_then_calculates_as_expected() { + let category = Category { + id: 0, + name: String::from("Groceries"), + }; + let budget_updates = [ + BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(60)).unwrap()), + new_budget: 200, + new_period: 1, + }, + BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(120)).unwrap()), + new_budget: 100, + new_period: 1, + }, + ]; + + let result = calculate_budgets_accrual(&category, &budget_updates); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 18_000); + } + + #[test] + fn when_two_budget_updates_on_same_day_then_returns_err() { + let category = Category { + id: 0, + name: String::from("Groceries"), + }; + let budget_updates = [ + BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from_ymd(2020, 12, 25).unwrap(), + new_budget: 200, + new_period: 1, + }, + BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from_ymd(2020, 12, 25).unwrap(), + new_budget: 100, + new_period: 1, + }, + ]; + let mut budget_updates_reversed = budget_updates.clone(); + budget_updates_reversed.reverse(); + + let result_leftways = calculate_budgets_accrual(&category, &budget_updates); + let result_rightways = calculate_budgets_accrual(&category, &budget_updates_reversed); + + assert!(result_leftways.is_err()); + assert!(result_rightways.is_err()); + } +} diff --git a/rust/core/src/utils/calculate_category_balance.rs b/rust/core/src/utils/calculate_category_balance.rs new file mode 100644 index 0000000..61b94a8 --- /dev/null +++ b/rust/core/src/utils/calculate_category_balance.rs @@ -0,0 +1,153 @@ +use anyhow::Result; +use crate::{ + models::{budget_update::BudgetUpdate, category::Category}, + utils::{calculate_budgets_accrual, find_by_id_or}, +}; + +pub fn calculate_category_balance( + category: &Category, + transaction_sums: &[(i32, i64)], + category_transfer_from_sums: &[(i32, i64)], + category_transfer_to_sums: &[(i32, i64)], + budget_updates: &[BudgetUpdate], +) -> Result<i64> { + let transaction_sum = find_by_id_or(transaction_sums, category.id, 0); + let category_transfer_from_sum = find_by_id_or(category_transfer_from_sums, category.id, 0); + let category_transfer_to_sum = find_by_id_or(category_transfer_to_sums, category.id, 0); + let budget_accruals_sum = calculate_budgets_accrual(category, budget_updates)?; + + Ok( + transaction_sum + + budget_accruals_sum + + category_transfer_to_sum + - category_transfer_from_sum + ) +} + +#[cfg(test)] +mod test { + use chrono::{Days, Utc}; + + use crate::{ + models::{budget_update::BudgetUpdate, category::Category, date_utc::DateUtc}, + utils::calculate_category_balance + }; + + #[test] + fn when_no_data_then_returns_zero() { + let category = Category { + id: 0, + name: String::new(), + }; + + let result = calculate_category_balance(&category, &[], &[], &[], &[]); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_data_only_for_another_category_then_returns_zero() { + let category = Category { + id: 0, + name: String::new(), + }; + let transaction_sums = [(1, 100)]; + let category_transfer_from_sums = [(1, 91)]; + let category_transfer_to_sums = [(2, 79)]; + let budget_updates = [BudgetUpdate { + id: 0, + category_id: 1, + date: DateUtc::from(Utc::now().checked_sub_days(Days::new(120)).unwrap()), + new_budget: 77, + new_period: 1, + }]; + + let result = calculate_category_balance( + &category, + &transaction_sums, + &category_transfer_from_sums, + &category_transfer_to_sums, + &budget_updates, + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_transaction_sum_is_zero_then_returns_zero() { + let category = Category { + id: 0, + name: String::new(), + }; + let transaction_sums = [(0, 0)]; + let category_transfer_from_sums = []; + let category_transfer_to_sums = []; + let budget_updates = []; + + let result = calculate_category_balance( + &category, + &transaction_sums, + &category_transfer_from_sums, + &category_transfer_to_sums, + &budget_updates, + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_category_transfers_are_zero_then_returns_zero() { + let category = Category { + id: 0, + name: String::new(), + }; + let transaction_sums = []; + let category_transfer_from_sums = [(0, 0)]; + let category_transfer_to_sums = [(0, 0)]; + let budget_updates = []; + + let result = calculate_category_balance( + &category, + &transaction_sums, + &category_transfer_from_sums, + &category_transfer_to_sums, + &budget_updates, + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0_i64); + } + + #[test] + fn when_some_data_then_calculates_as_expected() { + let category = Category { + id: 0, + name: String::new(), + }; + let transaction_sums = [(0, 100)]; + let category_transfer_from_sums = [(0, 91)]; + let category_transfer_to_sums = [(0, 79)]; + let budget_updates = [BudgetUpdate { + id: 0, + category_id: 0, + date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(120)).unwrap()), + new_budget: 77, + new_period: 1, + }]; + + let result = calculate_category_balance( + &category, + &transaction_sums, + &category_transfer_from_sums, + &category_transfer_to_sums, + &budget_updates, + ); + + let expected_result = 100 + (-91) + 79 + 77 * 120; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_result); + } +} diff --git a/rust/core/src/utils/find_by_id_or.rs b/rust/core/src/utils/find_by_id_or.rs new file mode 100644 index 0000000..8335e5c --- /dev/null +++ b/rust/core/src/utils/find_by_id_or.rs @@ -0,0 +1,41 @@ +pub fn find_by_id_or<Id: PartialEq, Val: Copy>( + arr: &[(Id, Val)], + id: Id, + default: Val, +) -> Val { + arr.iter() + .find(|(elem_id, _)| *elem_id == id) + .map_or(default, |row| row.1) +} + +#[cfg(test)] +mod test { + use super::find_by_id_or; + + #[test] + fn when_no_elements_then_return_default() { + let arr = []; + + let result = find_by_id_or(&arr, 1, 42); + + assert_eq!(result, 42); + } + + #[test] + fn when_id_not_in_elements_then_return_default() { + let arr = [(0, 100), (2, 200)]; + + let result = find_by_id_or(&arr, 1, 42); + + assert_eq!(result, 42); + } + + #[test] + fn when_id_in_elements_then_return_value() { + let arr = [(1, 67)]; + + let result = find_by_id_or(&arr, 1, 42); + + assert_eq!(result, 67); + } +} diff --git a/rust/core/src/utils/mod.rs b/rust/core/src/utils/mod.rs new file mode 100644 index 0000000..84a27af --- /dev/null +++ b/rust/core/src/utils/mod.rs @@ -0,0 +1,7 @@ +mod calculate_budgets_accrual; +mod calculate_category_balance; +mod find_by_id_or; + +pub use calculate_budgets_accrual::calculate_budgets_accrual; +pub use calculate_category_balance::calculate_category_balance; +pub use find_by_id_or::find_by_id_or; |
