From cc00db276c203bdcff9ca32d857d23e5dc61f400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaroslav=20de=20la=20Pe=C3=B1a=20Smirnov?= Date: Tue, 8 Dec 2020 00:39:16 +0300 Subject: Change way that shared expenses are calculated The way that shared expenses are recorded and calculated has changed. Now instead of specifying how among many people the expense was divided, the names of the persons are specified. You can also specified expenses that are "owed", or in other words, things that you paid for other people, or maybe loans you made. Don't know why I didn't think of this system before. Sometimes good ideas just come at the least expected times. Also removed some unused fields. --- budget/src/lib.rs | 81 +++++++++++++++++++-------------- budget/tests/budget.rs | 121 +++++++++++++++++++++++++++++++++---------------- budget/tests/test.toml | 19 ++++---- 3 files changed, 138 insertions(+), 83 deletions(-) (limited to 'budget') diff --git a/budget/src/lib.rs b/budget/src/lib.rs index 5013a9f..e848035 100644 --- a/budget/src/lib.rs +++ b/budget/src/lib.rs @@ -30,12 +30,14 @@ pub struct Day { pub struct Expense { pub name: String, pub price: f64, - #[serde(default = "shared_qty_default")] - pub qty: u32, // unused for now, might use it the future or remove it - #[serde(default = "shared_qty_default")] - pub shared: u32, - #[serde(default = "recurring_default")] - pub recurring: bool, + #[serde(default)] + /// Whom this expense is shared with (if anybody). + pub shared: Vec, + #[serde(default)] + /// Whether this was something we paid for somebody else, and thus is owed + /// to us. If true, then shared is the list of person(s) that owe us this + /// expense, and should therefore contain at least one name. + pub owed: bool, #[serde(default)] pub category: Option, } @@ -49,7 +51,8 @@ pub struct Calculated { pub categories_subtotal: HashMap, pub total: f64, pub balance: f64, - pub total_owed: HashMap, + pub owed: HashMap, + pub total_owed: f64, pub days_left: f64, pub days_left_essential: f64, pub last_day: NaiveDate, @@ -61,14 +64,6 @@ pub enum ParseError { DeserializerError(DeserializerError), } -fn shared_qty_default() -> u32 { - 1 -} - -fn recurring_default() -> bool { - false -} - // Parse the dates from toml's Datetime to Chrono's NaiveDate fn deserialize_date<'de, D>(deserializer: D) -> Result where @@ -97,7 +92,7 @@ pub fn parse_account(path: &str) -> Result { } } -pub fn calculate(account: &Account) -> Option { +pub fn calculate(account: &Account, consider_owed: bool) -> Option { if account.days.is_empty() { return None; } @@ -110,7 +105,8 @@ pub fn calculate(account: &Account) -> Option { categories_subtotal: HashMap::::new(), total: 0.0, balance: 0.0, - total_owed: HashMap::::new(), + owed: HashMap::::new(), + total_owed: 0.0, days_left: 0.0, days_left_essential: 0.0, last_day: account.days.last().unwrap().date, @@ -122,34 +118,51 @@ pub fn calculate(account: &Account) -> Option { } for expense in day.expenses.iter() { - calculated.total += expense.price; + let mut actual_expense: f64 = 0.0; + + if expense.shared.len() > 0 { + let owed_share = if expense.owed { + expense.price / expense.shared.len() as f64 + } else { + actual_expense = + expense.price / (expense.shared.len() as f64 + 1.0); + actual_expense + }; + + for person in expense.shared.iter() { + calculated.total_owed += owed_share; + + if let Some(owed_by_person) = + calculated.owed.get_mut(person) + { + *owed_by_person += owed_share; + } else { + calculated.owed.insert(person.clone(), owed_share); + } + } + } + + if expense.shared.len() == 0 || consider_owed { + actual_expense = expense.price; + } else if expense.owed { + continue; + } + + calculated.total += actual_expense; if let Some(category) = &expense.category { if let Some(category_subtotal) = calculated.categories_subtotal.get_mut(category) { - *category_subtotal += expense.price; + *category_subtotal += actual_expense; } else { calculated .categories_subtotal - .insert(category.to_string(), expense.price); + .insert(category.to_string(), actual_expense); } if account.essential_categories.contains(category) { - calculated.essential_subtotal += expense.price; - } - - if expense.shared > 1 { - let owed = expense.price * (expense.shared as f64 - 1.0) - / expense.shared as f64; - - if let Some(total_owed_by) = - calculated.total_owed.get_mut(&expense.shared) - { - *total_owed_by += owed; - } else { - calculated.total_owed.insert(expense.shared, owed); - } + calculated.essential_subtotal += actual_expense; } } } diff --git a/budget/tests/budget.rs b/budget/tests/budget.rs index 4ed549c..836a715 100644 --- a/budget/tests/budget.rs +++ b/budget/tests/budget.rs @@ -11,7 +11,7 @@ fn can_parse_account() -> Result<(), ParseError> { end_date: NaiveDate::from_ymd(2020, 10, 31), budget: 420.0, essential_categories: vec![ - String::from("products"), + String::from("produce"), String::from("transport"), String::from("utilities"), ], @@ -22,42 +22,40 @@ fn can_parse_account() -> Result<(), ParseError> { Expense { name: String::from("Potato masher"), price: 3.81, - qty: 1, - shared: 1, - recurring: false, + shared: vec![], + owed: false, category: Some(String::from("supplies")), }, Expense { name: String::from("Bacon"), price: 3.33, - qty: 1, - shared: 3, - recurring: false, - category: Some(String::from("products")), + shared: vec![ + String::from("Fox"), + String::from("Falco"), + ], + owed: false, + category: Some(String::from("produce")), }, Expense { name: String::from("Yoghurt"), price: 1.24, - qty: 2, - shared: 2, - recurring: false, - category: Some(String::from("products")), + shared: vec![String::from("Falco")], + owed: true, + category: Some(String::from("produce")), }, Expense { name: String::from("Onion"), price: 0.15, - qty: 1, - shared: 1, - recurring: false, - category: Some(String::from("products")), + shared: vec![], + owed: false, + category: Some(String::from("produce")), }, Expense { name: String::from("Chicken"), price: 2.28, - qty: 1, - shared: 2, - recurring: false, - category: Some(String::from("products")), + shared: vec![String::from("Fox")], + owed: false, + category: Some(String::from("produce")), }, ], }, @@ -71,17 +69,15 @@ fn can_parse_account() -> Result<(), ParseError> { Expense { name: String::from("VPS"), price: 5.0, - qty: 1, - shared: 1, - recurring: true, + shared: vec![], + owed: false, category: Some(String::from("utilities")), }, Expense { name: String::from("Transport card"), price: 6.9, - qty: 1, - shared: 1, - recurring: false, + shared: vec![], + owed: false, category: Some(String::from("transport")), }, ], @@ -99,16 +95,17 @@ fn can_parse_account() -> Result<(), ParseError> { #[test] fn can_calculate() -> Result<(), ParseError> { let mut should_be = Calculated { - all_day_average: 5.6775, - essential_day_average: 4.725, + all_day_average: 4.5275, + essential_day_average: 3.575, categories_day_average: HashMap::::new(), - essential_subtotal: 18.9, + essential_subtotal: 14.3, categories_subtotal: HashMap::::new(), - total: 22.71, - balance: 397.29, - total_owed: HashMap::::new(), - days_left: 69.9762219286658, - days_left_essential: 84.08253968253969, + total: 18.11, + balance: 401.89, + owed: HashMap::::new(), + total_owed: 4.6, + days_left: 88.76642738818333, + days_left_essential: 112.4167832167832, last_day: NaiveDate::from_ymd(2020, 10, 04), }; @@ -117,7 +114,7 @@ fn can_calculate() -> Result<(), ParseError> { .insert("supplies".to_string(), 0.9525); should_be .categories_day_average - .insert("products".to_string(), 1.75); + .insert("produce".to_string(), 0.6); should_be .categories_day_average .insert("transport".to_string(), 1.725); @@ -130,7 +127,7 @@ fn can_calculate() -> Result<(), ParseError> { .insert("supplies".to_string(), 3.81); should_be .categories_subtotal - .insert("products".to_string(), 7.0); + .insert("produce".to_string(), 2.4); should_be .categories_subtotal .insert("transport".to_string(), 6.9); @@ -138,13 +135,59 @@ fn can_calculate() -> Result<(), ParseError> { .categories_subtotal .insert("utilities".to_string(), 5.0); - should_be.total_owed.insert(2, 1.7599999999999998); - should_be.total_owed.insert(3, 2.22); + should_be.owed.insert(String::from("Fox"), 2.25); + should_be.owed.insert(String::from("Falco"), 2.35); + + let mut should_be_with_owed = Calculated { + all_day_average: 5.6775, + essential_day_average: 4.725, + categories_day_average: HashMap::::new(), + essential_subtotal: 18.9, + categories_subtotal: HashMap::::new(), + total: 22.71, + balance: 397.29, + owed: HashMap::::new(), + total_owed: 4.6, + days_left: 69.9762219286658, + days_left_essential: 84.08253968253969, + last_day: NaiveDate::from_ymd(2020, 10, 04), + }; + + should_be_with_owed + .categories_day_average + .insert("supplies".to_string(), 0.9525); + should_be_with_owed + .categories_day_average + .insert("produce".to_string(), 1.75); + should_be_with_owed + .categories_day_average + .insert("transport".to_string(), 1.725); + should_be_with_owed + .categories_day_average + .insert("utilities".to_string(), 1.25); + + should_be_with_owed + .categories_subtotal + .insert("supplies".to_string(), 3.81); + should_be_with_owed + .categories_subtotal + .insert("produce".to_string(), 7.0); + should_be_with_owed + .categories_subtotal + .insert("transport".to_string(), 6.9); + should_be_with_owed + .categories_subtotal + .insert("utilities".to_string(), 5.0); + + should_be_with_owed.owed.insert(String::from("Fox"), 2.25); + should_be_with_owed.owed.insert(String::from("Falco"), 2.35); let account = budget::parse_account("tests/test.toml")?; - let actually_is = budget::calculate(&account).unwrap(); + let actually_is = budget::calculate(&account, false).unwrap(); + let actually_is_with_owed = budget::calculate(&account, true).unwrap(); assert_eq!(actually_is, should_be); + assert_eq!(actually_is_with_owed, should_be_with_owed); Ok(()) } diff --git a/budget/tests/test.toml b/budget/tests/test.toml index ca2c1e5..103094e 100644 --- a/budget/tests/test.toml +++ b/budget/tests/test.toml @@ -2,7 +2,7 @@ start_date = 2020-10-01 end_date = 2020-10-31 budget = 420.0 essential_categories = [ - "products", + "produce", "transport", "utilities", ] @@ -18,26 +18,26 @@ date = 2020-10-01 [[days.expenses]] name = "Bacon" price = 3.33 - category = "products" - shared = 3 + category = "produce" + shared = ["Fox", "Falco"] [[days.expenses]] name = "Yoghurt" price = 1.24 - category = "products" - qty = 2 - shared = 2 + category = "produce" + owed = true + shared = ["Falco"] [[days.expenses]] name = "Onion" price = 0.15 - category = "products" + category = "produce" [[days.expenses]] name = "Chicken" price = 2.28 - category = "products" - shared = 2 + category = "produce" + shared = ["Fox"] [[days]] date = 2020-10-04 @@ -49,7 +49,6 @@ date = 2020-10-02 name = "VPS" price = 5.0 category = "utilities" - recurring = true [[days.expenses]] name = "Transport card" -- cgit v1.2.3