summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-11-25 13:05:35 +0000
committerJoe Carstairs <me@joeac.net>2024-11-25 13:05:35 +0000
commite79fa9875d2122e607fb1150dafa99d703ed11b8 (patch)
tree2417a8b6cfa9b0ea284859b9d9b70f082520375f
parent5bd223ee9c6853502d509627678f696c6ea5fcdc (diff)
Transform categories
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs190
-rw-r--r--rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs25
-rw-r--r--rust/schist_models/src/date_utc.rs16
3 files changed, 204 insertions, 27 deletions
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
index e0fc687..b78fd2b 100644
--- a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
@@ -1,38 +1,184 @@
-use actualbudget_models::ActualbudgetCategory;
-use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category};
+use actualbudget_models::{ActualbudgetCategory, ActualbudgetZeroBudget};
+use schist_models::{budget_period_unit::BudgetPeriodUnit, category::Category, date_utc::DateUtc};
use super::transform_id;
pub fn transform_categories(
actualbudget_categories: &[ActualbudgetCategory],
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+ before_first_transaction: &DateUtc,
) -> Vec<Category> {
- let mut categories = Vec::<Category>::new();
- for actualbudget_category in actualbudget_categories {
- categories.push(Category {
- id: transform_id(&actualbudget_category.id.clone()),
- name: actualbudget_category.name.clone(),
- balance: 0,
- balance_date: get_balance_date(),
- budget_period: 1,
- budget_period_unit: BudgetPeriodUnit::Month,
- budget_quantity: get_budget_quantity(),
- })
- }
- categories
+ actualbudget_categories.iter().map(|actualbudget_category| Category {
+ id: transform_id(&actualbudget_category.id.clone()),
+ name: actualbudget_category.name.clone(),
+ balance: 0,
+ balance_date: before_first_transaction.clone(),
+ budget_period: 1,
+ budget_period_unit: BudgetPeriodUnit::Month,
+ budget_quantity: get_budget_quantity(actualbudget_category, actualbudget_zero_budgets),
+ }).collect()
}
-fn get_balance_date() -> schist_models::date_utc::DateUtc {
- todo!()
-}
+fn get_budget_quantity(
+ actualbudget_category: &ActualbudgetCategory,
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget]
+) -> i32 {
+ let latest_zero_budget_for_category = actualbudget_zero_budgets
+ .iter()
+ .filter(|zb| zb.category_id == actualbudget_category.id)
+ .max_by_key(|zb| zb.month.clone());
-fn get_budget_quantity() -> i32 {
- todo!()
+ match latest_zero_budget_for_category {
+ Some(latest_zero_budget_for_category) => latest_zero_budget_for_category.amount,
+ None => 0,
+ }
}
#[cfg(test)]
mod test {
+ use actualbudget_fakes::{actualbudget_category::make_fake_actualbudget_category_builder, actualbudget_zero_budget::make_fake_actualbudget_zero_budget_builder};
+ use actualbudget_models::ActualbudgetDate;
+ use schist_models::{budget_period_unit::BudgetPeriodUnit, date_utc::DateUtc};
+ use schist_traits::nowlike::Nowlike;
+
+ use crate::transform_state::transform_id::transform_id;
+
+ use super::transform_categories;
+
+ #[test]
+ fn when_one_category_one_zero_budget_then_transforms_category() {
+ let actualbudget_categories = [
+ make_fake_actualbudget_category_builder("0").name(String::from("Food")).build().unwrap(),
+ ];
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0").amount(100).build().unwrap(),
+ ];
+ let before_first_transaction = DateUtc::now();
+
+ let categories = transform_categories(
+ &actualbudget_categories,
+ &actualbudget_zero_budgets,
+ &before_first_transaction,
+ );
+
+ assert_eq!(1, categories.len());
+ let category = categories.get(0).unwrap();
+ assert_eq!("Food", category.name);
+ assert_eq!(0, category.balance);
+ assert_eq!(before_first_transaction, category.balance_date);
+ assert_eq!(1, category.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit);
+ assert_eq!(100, category.budget_quantity);
+ }
+
+ #[test]
+ fn when_one_category_no_zero_budget_then_transforms_category() {
+ let actualbudget_categories = [
+ make_fake_actualbudget_category_builder("0").name(String::from("Food")).build().unwrap(),
+ ];
+ let actualbudget_zero_budgets = [];
+ let before_first_transaction = DateUtc::now();
+
+ let categories = transform_categories(
+ &actualbudget_categories,
+ &actualbudget_zero_budgets,
+ &before_first_transaction,
+ );
+
+ assert_eq!(1, categories.len());
+ let category = categories.get(0).unwrap();
+ assert_eq!("Food", category.name);
+ assert_eq!(0, category.balance);
+ assert_eq!(before_first_transaction, category.balance_date);
+ assert_eq!(1, category.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit);
+ assert_eq!(0, category.budget_quantity);
+ }
+
#[test]
- fn todo() {
- todo!()
+ fn when_one_category_multiple_zero_budgets_then_transforms_category() {
+ let actualbudget_categories = [
+ make_fake_actualbudget_category_builder("0").name(String::from("Food")).build().unwrap(),
+ ];
+ let jan = ActualbudgetDate::from_i32(202401);
+ let feb = ActualbudgetDate::from_i32(202402);
+ let mar = ActualbudgetDate::from_i32(202403);
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0").amount(100).month(jan).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("1").amount(200).month(feb).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("2").amount(300).month(mar).build().unwrap(),
+ ];
+ let before_first_transaction = DateUtc::now();
+
+ let categories = transform_categories(
+ &actualbudget_categories,
+ &actualbudget_zero_budgets,
+ &before_first_transaction,
+ );
+
+ assert_eq!(1, categories.len());
+ let category = categories.get(0).unwrap();
+ assert_eq!("Food", category.name);
+ assert_eq!(0, category.balance);
+ assert_eq!(before_first_transaction, category.balance_date);
+ assert_eq!(1, category.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category.budget_period_unit);
+ assert_eq!(300, category.budget_quantity);
+ }
+
+ #[test]
+ fn when_multiple_categories_multiple_zero_budgets_then_transforms_category() {
+ let actualbudget_categories = [
+ make_fake_actualbudget_category_builder("0").name(String::from("Food")).build().unwrap(),
+ make_fake_actualbudget_category_builder("1").name(String::from("Clothes")).build().unwrap(),
+ make_fake_actualbudget_category_builder("2").name(String::from("Rent")).build().unwrap(),
+ ];
+ let jan = ActualbudgetDate::from_i32(202401);
+ let feb = ActualbudgetDate::from_i32(202402);
+ let mar = ActualbudgetDate::from_i32(202403);
+ let actualbudget_zero_budgets = [
+ make_fake_actualbudget_zero_budget_builder("0").amount(100).month(jan.clone()).category_id(String::from("0")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("1").amount(200).month(feb.clone()).category_id(String::from("0")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("2").amount(300).month(mar.clone()).category_id(String::from("0")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("3").amount(110).month(jan.clone()).category_id(String::from("1")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("4").amount(210).month(feb.clone()).category_id(String::from("1")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("5").amount(310).month(mar.clone()).category_id(String::from("1")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("6").amount(120).month(jan.clone()).category_id(String::from("2")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("7").amount(220).month(feb.clone()).category_id(String::from("2")).build().unwrap(),
+ make_fake_actualbudget_zero_budget_builder("8").amount(320).month(mar.clone()).category_id(String::from("2")).build().unwrap(),
+ ];
+ let before_first_transaction = DateUtc::now();
+
+ let categories = transform_categories(
+ &actualbudget_categories,
+ &actualbudget_zero_budgets,
+ &before_first_transaction,
+ );
+
+ assert_eq!(3, categories.len());
+
+ let category_0 = categories.iter().filter(|c| c.id == transform_id("0")).last().unwrap();
+ assert_eq!("Food", category_0.name);
+ assert_eq!(0, category_0.balance);
+ assert_eq!(before_first_transaction, category_0.balance_date);
+ assert_eq!(1, category_0.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category_0.budget_period_unit);
+ assert_eq!(300, category_0.budget_quantity);
+
+ let category_1 = categories.iter().filter(|c| c.id == transform_id("1")).last().unwrap();
+ assert_eq!("Clothes", category_1.name);
+ assert_eq!(0, category_1.balance);
+ assert_eq!(before_first_transaction, category_1.balance_date);
+ assert_eq!(1, category_1.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category_1.budget_period_unit);
+ assert_eq!(310, category_1.budget_quantity);
+
+ let category_2 = categories.iter().filter(|c| c.id == transform_id("2")).last().unwrap();
+ assert_eq!("Rent", category_2.name);
+ assert_eq!(0, category_2.balance);
+ assert_eq!(before_first_transaction, category_2.balance_date);
+ assert_eq!(1, category_2.budget_period);
+ assert_eq!(BudgetPeriodUnit::Month, category_2.budget_period_unit);
+ assert_eq!(320, category_2.budget_quantity);
}
}
diff --git a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
index 2285276..cc5dfe1 100644
--- a/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
+++ b/rust/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -1,5 +1,5 @@
-use anyhow::Result;
-use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
+use anyhow::{bail, Result};
+use schist_models::{account::Account, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, datetime_utc::DatetimeUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation};
use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState};
@@ -8,11 +8,15 @@ use super::{transform_accounts, transform_budget_drips::transform_budget_drips,
pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
let first_transaction_date =
transform_date(&actualbudget_state.actualbudget_first_transaction_date)?;
+ let before_first_transaction_date = find_date_before(&first_transaction_date)?;
let accounts: Vec<Account> =
transform_accounts(&actualbudget_state.actualbudget_accounts, &first_transaction_date);
let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?;
- let categories: Vec<Category> =
- transform_categories(&actualbudget_state.actualbudget_categories);
+ let categories: Vec<Category> = transform_categories(
+ &actualbudget_state.actualbudget_categories,
+ &actualbudget_state.actualbudget_zero_budgets,
+ &before_first_transaction_date,
+ );
let (transactions, transaction_categorisations): (Vec<Transaction>, Vec<TransactionCategorisation>) =
transform_transactions(
&actualbudget_state.actualbudget_transactions,
@@ -33,3 +37,16 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt
category_transfers,
})
}
+
+fn find_date_before(datetime: &DatetimeUtc) -> Result<DateUtc> {
+ let date: DateUtc = datetime.into();
+
+ for days_before in 0..31 {
+ let result = date.sub_days(days_before);
+ if result.is_ok() {
+ return result;
+ }
+ }
+
+ bail!("failed to find a date before {}", date)
+}
diff --git a/rust/schist_models/src/date_utc.rs b/rust/schist_models/src/date_utc.rs
index c801585..d3a1d2e 100644
--- a/rust/schist_models/src/date_utc.rs
+++ b/rust/schist_models/src/date_utc.rs
@@ -69,6 +69,15 @@ impl DateUtc {
Ok(Self { chrono_datetime_utc })
}
+ pub fn sub_days(&self, days: u64) -> Result<Self> {
+ let chrono_datetime_utc = self
+ .chrono_datetime_utc
+ .checked_sub_days(chrono::Days::new(days))
+ .with_context(|| format!("failed to subtract {} days from {}", days, self))
+ .unwrap();
+ Ok(Self { chrono_datetime_utc })
+ }
+
fn is_day_str(str: &str) -> bool {
if str.len() != 10 {
return false;
@@ -151,9 +160,14 @@ impl Dateable for DateUtc {
impl From<DatetimeUtc> for DateUtc {
fn from(datetime_utc: DatetimeUtc) -> Self {
+ Self::from(&datetime_utc)
+ }
+}
+
+impl From<&DatetimeUtc> for DateUtc {
+ fn from(datetime_utc: &DatetimeUtc) -> Self {
Self {
chrono_datetime_utc: datetime_utc
- .borrow()
.to_string()
.parse::<DateTime<Utc>>().unwrap()
.with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()).unwrap(),