diff options
author | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2020-12-08 00:39:16 +0300 |
---|---|---|
committer | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2020-12-08 00:39:16 +0300 |
commit | cc00db276c203bdcff9ca32d857d23e5dc61f400 (patch) | |
tree | 42d6940af40002aef9d630cdcb2aa7d738405dfc /budget | |
parent | d79b2661c8af24c508f90419f01009175cd704d4 (diff) | |
download | finbudg-cc00db276c203bdcff9ca32d857d23e5dc61f400.tar.gz finbudg-cc00db276c203bdcff9ca32d857d23e5dc61f400.zip |
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.
Diffstat (limited to 'budget')
-rw-r--r-- | budget/src/lib.rs | 81 | ||||
-rw-r--r-- | budget/tests/budget.rs | 121 | ||||
-rw-r--r-- | budget/tests/test.toml | 19 |
3 files changed, 138 insertions, 83 deletions
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<String>, + #[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<String>, } @@ -49,7 +51,8 @@ pub struct Calculated { pub categories_subtotal: HashMap<String, f64>, pub total: f64, pub balance: f64, - pub total_owed: HashMap<u32, f64>, + pub owed: HashMap<String, f64>, + 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<NaiveDate, D::Error> where @@ -97,7 +92,7 @@ pub fn parse_account(path: &str) -> Result<Account, ParseError> { } } -pub fn calculate(account: &Account) -> Option<Calculated> { +pub fn calculate(account: &Account, consider_owed: bool) -> Option<Calculated> { if account.days.is_empty() { return None; } @@ -110,7 +105,8 @@ pub fn calculate(account: &Account) -> Option<Calculated> { categories_subtotal: HashMap::<String, f64>::new(), total: 0.0, balance: 0.0, - total_owed: HashMap::<u32, f64>::new(), + owed: HashMap::<String, f64>::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<Calculated> { } 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::<String, f64>::new(), - essential_subtotal: 18.9, + essential_subtotal: 14.3, categories_subtotal: HashMap::<String, f64>::new(), - total: 22.71, - balance: 397.29, - total_owed: HashMap::<u32, f64>::new(), - days_left: 69.9762219286658, - days_left_essential: 84.08253968253969, + total: 18.11, + balance: 401.89, + owed: HashMap::<String, f64>::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::<String, f64>::new(), + essential_subtotal: 18.9, + categories_subtotal: HashMap::<String, f64>::new(), + total: 22.71, + balance: 397.29, + owed: HashMap::<String, f64>::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" |