summaryrefslogtreecommitdiff
path: root/backend/core/src/utils
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-10-22 07:46:43 +0100
committerJoe Carstairs <me@joeac.net>2024-10-22 07:46:43 +0100
commit5824c353f978a5b9ab9696451ee3014c6a614ffc (patch)
treebc39754ca417e84eb00b0aa9a1ae2330da0bf282 /backend/core/src/utils
parent7adaace61a4fe983eba5ded3aaaf624ce6fb9014 (diff)
Refactors importer to use core stuff
Diffstat (limited to 'backend/core/src/utils')
-rw-r--r--backend/core/src/utils/calculate_budgets_accrual.rs162
-rw-r--r--backend/core/src/utils/calculate_category_balance.rs142
-rw-r--r--backend/core/src/utils/find_by_id_or.rs53
-rw-r--r--backend/core/src/utils/mod.rs7
4 files changed, 364 insertions, 0 deletions
diff --git a/backend/core/src/utils/calculate_budgets_accrual.rs b/backend/core/src/utils/calculate_budgets_accrual.rs
new file mode 100644
index 0000000..bbc197c
--- /dev/null
+++ b/backend/core/src/utils/calculate_budgets_accrual.rs
@@ -0,0 +1,162 @@
+use crate::models::{
+ budget_update::BudgetUpdate,
+ category::Category,
+ date_time_utc::DateTimeUtc
+};
+
+pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> 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);
+ budget_updates.reverse();
+
+ let mut budgets_accrual = 0.0_f64;
+ let mut calculated_back_to_date = DateTimeUtc::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 {
+ continue;
+ }
+ 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;
+ }
+
+ budgets_accrual.floor() as i64
+}
+
+#[cfg(test)]
+mod test {
+ use chrono::{Days, Timelike, Utc};
+
+ use crate::{
+ models::{budget_update::BudgetUpdate, category::Category, date_time_utc::DateTimeUtc},
+ 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_eq!(result, 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: DateTimeUtc::now(),
+ new_budget: 100,
+ new_period: 1,
+ }];
+
+ let result = calculate_budgets_accrual(&category, &budget_updates);
+
+ assert_eq!(result, 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: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ new_budget: 100,
+ new_period: 1,
+ }];
+
+ let result = calculate_budgets_accrual(&category, &budget_updates);
+
+ assert_eq!(result, 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: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(60)).unwrap()),
+ new_budget: 200,
+ new_period: 1,
+ },
+ BudgetUpdate {
+ id: 0,
+ category_id: 0,
+ date: DateTimeUtc::new(Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ new_budget: 100,
+ new_period: 1,
+ },
+ ];
+
+ let result = calculate_budgets_accrual(&category, &budget_updates);
+
+ assert_eq!(result, 18_000);
+ }
+
+ #[test]
+ fn when_two_budget_updates_on_same_day_then_uses_latest_only() {
+ let category = Category {
+ id: 0,
+ name: String::from("Groceries"),
+ };
+ let budget_updates = [
+ BudgetUpdate {
+ id: 0,
+ category_id: 0,
+ date: DateTimeUtc::new(
+ Utc::now()
+ .checked_sub_days(Days::new(120))
+ .unwrap()
+ .with_hour(13)
+ .unwrap(),
+ ),
+ new_budget: 200,
+ new_period: 1,
+ },
+ BudgetUpdate {
+ id: 0,
+ category_id: 0,
+ date: DateTimeUtc::new(
+ Utc::now()
+ .checked_sub_days(Days::new(120))
+ .unwrap()
+ .with_hour(12)
+ .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_eq!(result_leftways, 24_000);
+ assert_eq!(result_rightways, 24_000);
+ }
+}
diff --git a/backend/core/src/utils/calculate_category_balance.rs b/backend/core/src/utils/calculate_category_balance.rs
new file mode 100644
index 0000000..5eca05b
--- /dev/null
+++ b/backend/core/src/utils/calculate_category_balance.rs
@@ -0,0 +1,142 @@
+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, Option<i64>)],
+ category_transfer_from_sums: &[(i32, Option<i64>)],
+ category_transfer_to_sums: &[(i32, Option<i64>)],
+ budget_updates: &[BudgetUpdate],
+) -> 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);
+
+ 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_time_utc::DateTimeUtc},
+ 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_eq!(result, 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, Some(100))];
+ let category_transfer_from_sums = [(1, Some(91))];
+ let category_transfer_to_sums = [(2, Some(79))];
+ let budget_updates = [BudgetUpdate {
+ id: 0,
+ category_id: 1,
+ date: DateTimeUtc::new(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_eq!(result, 0_i64);
+ }
+
+ #[test]
+ fn when_transaction_sum_is_none_then_returns_zero() {
+ let category = Category {
+ id: 0,
+ name: String::new(),
+ };
+ let transaction_sums = [(0, None)];
+ 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_eq!(result, 0_i64);
+ }
+
+ #[test]
+ fn when_category_transfers_are_none_then_returns_zero() {
+ let category = Category {
+ id: 0,
+ name: String::new(),
+ };
+ let transaction_sums = [];
+ let category_transfer_from_sums = [(0, None)];
+ let category_transfer_to_sums = [(0, None)];
+ let budget_updates = [];
+
+ let result = calculate_category_balance(
+ &category,
+ &transaction_sums,
+ &category_transfer_from_sums,
+ &category_transfer_to_sums,
+ &budget_updates,
+ );
+
+ assert_eq!(result, 0_i64);
+ }
+
+ #[test]
+ fn when_some_data_then_calculates_as_expected() {
+ let category = Category {
+ id: 0,
+ name: String::new(),
+ };
+ let transaction_sums = [(0, Some(100))];
+ let category_transfer_from_sums = [(0, Some(91))];
+ let category_transfer_to_sums = [(0, Some(79))];
+ let budget_updates = [BudgetUpdate {
+ id: 0,
+ category_id: 0,
+ date: DateTimeUtc::new(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_eq!(result, expected_result);
+ }
+}
diff --git a/backend/core/src/utils/find_by_id_or.rs b/backend/core/src/utils/find_by_id_or.rs
new file mode 100644
index 0000000..f5add30
--- /dev/null
+++ b/backend/core/src/utils/find_by_id_or.rs
@@ -0,0 +1,53 @@
+pub fn find_by_id_or<Id: PartialEq, Val: Copy>(
+ arr: &[(Id, Option<Val>)],
+ id: Id,
+ default: Val,
+) -> Val {
+ arr.iter()
+ .find(|(elem_id, _)| *elem_id == id)
+ .map_or(default, |row| match row {
+ (_, Some(quantity)) => *quantity,
+ (_, _) => default,
+ })
+}
+
+#[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, Some(100)), (2, Some(200))];
+
+ let result = find_by_id_or(&arr, 1, 42);
+
+ assert_eq!(result, 42);
+ }
+
+ #[test]
+ fn when_value_is_none_then_return_default() {
+ let arr = [(1, None)];
+
+ let result = find_by_id_or(&arr, 1, 42);
+
+ assert_eq!(result, 42);
+ }
+
+ #[test]
+ fn when_value_is_some_then_return_value() {
+ let arr = [(1, Some(67))];
+
+ let result = find_by_id_or(&arr, 1, 42);
+
+ assert_eq!(result, 67);
+ }
+}
diff --git a/backend/core/src/utils/mod.rs b/backend/core/src/utils/mod.rs
new file mode 100644
index 0000000..84a27af
--- /dev/null
+++ b/backend/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;