diff options
Diffstat (limited to 'budget')
-rw-r--r-- | budget/src/lib.rs | 280 | ||||
-rw-r--r-- | budget/tests/budget.rs | 272 |
2 files changed, 267 insertions, 285 deletions
diff --git a/budget/src/lib.rs b/budget/src/lib.rs index 7d194c0..5013a9f 100644 --- a/budget/src/lib.rs +++ b/budget/src/lib.rs @@ -1,182 +1,178 @@ use std::collections::HashMap; -use std::io::ErrorKind; use std::fs; +use std::io::ErrorKind; -use toml::de::Error as DeserializerError; -use serde::{Deserialize, Deserializer}; use chrono::NaiveDate; +use serde::{Deserialize, Deserializer}; +use toml::de::Error as DeserializerError; #[derive(Deserialize, PartialEq, Debug)] pub struct Account { - #[serde(deserialize_with = "deserialize_date")] - pub start_date: NaiveDate, - #[serde(deserialize_with = "deserialize_date")] - pub end_date: NaiveDate, - pub budget: f64, - #[serde(default)] - pub essential_categories: Vec<String>, - pub days: Vec<Day>, + #[serde(deserialize_with = "deserialize_date")] + pub start_date: NaiveDate, + #[serde(deserialize_with = "deserialize_date")] + pub end_date: NaiveDate, + pub budget: f64, + #[serde(default)] + pub essential_categories: Vec<String>, + pub days: Vec<Day>, } #[derive(Deserialize, PartialEq, Debug)] pub struct Day { - #[serde(deserialize_with = "deserialize_date")] - pub date: NaiveDate, - #[serde(default)] - pub expenses: Vec<Expense>, + #[serde(deserialize_with = "deserialize_date")] + pub date: NaiveDate, + #[serde(default)] + pub expenses: Vec<Expense>, } #[derive(Deserialize, PartialEq, Debug)] 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)] - pub category: Option<String>, + 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)] + pub category: Option<String>, } #[derive(PartialEq, Debug)] pub struct Calculated { - pub all_day_average: f64, - pub essential_day_average: f64, - pub categories_day_average: HashMap<String, f64>, - pub essential_subtotal: f64, - pub categories_subtotal: HashMap<String, f64>, - pub total: f64, - pub balance: f64, - pub total_owed: HashMap<u32, f64>, - pub days_left: f64, - pub days_left_essential: f64, - pub last_day: NaiveDate, + pub all_day_average: f64, + pub essential_day_average: f64, + pub categories_day_average: HashMap<String, f64>, + pub essential_subtotal: f64, + pub categories_subtotal: HashMap<String, f64>, + pub total: f64, + pub balance: f64, + pub total_owed: HashMap<u32, f64>, + pub days_left: f64, + pub days_left_essential: f64, + pub last_day: NaiveDate, } #[derive(PartialEq, Eq, Debug)] pub enum ParseError { - IOError(ErrorKind), - DeserializerError(DeserializerError), + IOError(ErrorKind), + DeserializerError(DeserializerError), } fn shared_qty_default() -> u32 { - 1 + 1 } fn recurring_default() -> bool { - false + false } // Parse the dates from toml's Datetime to Chrono's NaiveDate fn deserialize_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error> -where D: Deserializer<'de> { - toml::value::Datetime::deserialize(deserializer) - .map(|v| { - let s = v.to_string(); - - NaiveDate::parse_from_str(&s, "%Y-%m-%d") - })? - .map_err(serde::de::Error::custom) +where + D: Deserializer<'de>, +{ + toml::value::Datetime::deserialize(deserializer) + .map(|v| { + let s = v.to_string(); + + NaiveDate::parse_from_str(&s, "%Y-%m-%d") + })? + .map_err(serde::de::Error::custom) } pub fn parse_account(path: &str) -> Result<Account, ParseError> { - let contents = match fs::read_to_string(path) { - Ok(data) => data, - Err(error) => { - return Err(ParseError::IOError(error.kind())); - }, - }; - - match toml::from_str::<Account>(&contents) { - Ok(budget) => Ok(budget), - Err(error) => Err(ParseError::DeserializerError(error)), - } + let contents = match fs::read_to_string(path) { + Ok(data) => data, + Err(error) => { + return Err(ParseError::IOError(error.kind())); + } + }; + + match toml::from_str::<Account>(&contents) { + Ok(budget) => Ok(budget), + Err(error) => Err(ParseError::DeserializerError(error)), + } } pub fn calculate(account: &Account) -> Option<Calculated> { - if account.days.is_empty() { - return None; - } - - let mut calculated = Calculated { - all_day_average: 0.0, - essential_day_average: 0.0, - categories_day_average: HashMap::<String, f64>::new(), - essential_subtotal: 0.0, - categories_subtotal: HashMap::<String, f64>::new(), - total: 0.0, - balance: 0.0, - total_owed: HashMap::<u32, f64>::new(), - days_left: 0.0, - days_left_essential: 0.0, - last_day: account.days.last().unwrap().date, - }; - - for day in account.days.iter() { - if day.date > calculated.last_day { - calculated.last_day = day.date; - } - - for expense in day.expenses.iter() { - calculated.total += expense.price; - - if let Some(category) = &expense.category { - if let Some(category_subtotal) = - calculated.categories_subtotal.get_mut(category) { - *category_subtotal += expense.price; - } else { - calculated.categories_subtotal.insert( - category.to_string(), - expense.price, - ); - } - - 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, - ); - } - } - } - } - } - - let days_elapsed = - (calculated.last_day - account.start_date).num_days() + 1; - - calculated.all_day_average = calculated.total / days_elapsed as f64; - calculated.essential_day_average = - calculated.essential_subtotal / days_elapsed as f64; - - for (category, subtotal) in calculated.categories_subtotal.iter() { - calculated.categories_day_average - .insert( - category.clone(), - subtotal / days_elapsed as f64, - ); - } - - calculated.balance = account.budget - calculated.total; - - calculated.days_left = calculated.balance / calculated.all_day_average; - calculated.days_left_essential = - calculated.balance / calculated.essential_day_average; - - Some(calculated) + if account.days.is_empty() { + return None; + } + + let mut calculated = Calculated { + all_day_average: 0.0, + essential_day_average: 0.0, + categories_day_average: HashMap::<String, f64>::new(), + essential_subtotal: 0.0, + categories_subtotal: HashMap::<String, f64>::new(), + total: 0.0, + balance: 0.0, + total_owed: HashMap::<u32, f64>::new(), + days_left: 0.0, + days_left_essential: 0.0, + last_day: account.days.last().unwrap().date, + }; + + for day in account.days.iter() { + if day.date > calculated.last_day { + calculated.last_day = day.date; + } + + for expense in day.expenses.iter() { + calculated.total += expense.price; + + if let Some(category) = &expense.category { + if let Some(category_subtotal) = + calculated.categories_subtotal.get_mut(category) + { + *category_subtotal += expense.price; + } else { + calculated + .categories_subtotal + .insert(category.to_string(), expense.price); + } + + 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); + } + } + } + } + } + + let days_elapsed = + (calculated.last_day - account.start_date).num_days() + 1; + + calculated.all_day_average = calculated.total / days_elapsed as f64; + calculated.essential_day_average = + calculated.essential_subtotal / days_elapsed as f64; + + for (category, subtotal) in calculated.categories_subtotal.iter() { + calculated + .categories_day_average + .insert(category.clone(), subtotal / days_elapsed as f64); + } + + calculated.balance = account.budget - calculated.total; + + calculated.days_left = calculated.balance / calculated.all_day_average; + calculated.days_left_essential = + calculated.balance / calculated.essential_day_average; + + Some(calculated) } diff --git a/budget/tests/budget.rs b/budget/tests/budget.rs index 6a9214d..4ed549c 100644 --- a/budget/tests/budget.rs +++ b/budget/tests/budget.rs @@ -5,160 +5,146 @@ use chrono::NaiveDate; use budget::*; #[test] -fn can_parse_account() -> Result<(), ParseError>{ - let should_be = Account { - start_date: NaiveDate::from_ymd(2020, 10, 1), - end_date: NaiveDate::from_ymd(2020, 10, 31), - budget: 420.0, - essential_categories: vec![ - String::from("products"), - String::from("transport"), - String::from("utilities"), - ], - days: vec![ - Day { - date: NaiveDate::from_ymd(2020, 10, 1), - expenses: vec![ - Expense { - name: String::from("Potato masher"), - price: 3.81, - qty: 1, - shared: 1, - recurring: 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")), - }, - Expense { - name: String::from("Yoghurt"), - price: 1.24, - qty: 2, - shared: 2, - recurring: false, - category: Some(String::from("products")), - }, - Expense { - name: String::from("Onion"), - price: 0.15, - qty: 1, - shared: 1, - recurring: false, - category: Some(String::from("products")), - }, - Expense { - name: String::from("Chicken"), - price: 2.28, - qty: 1, - shared: 2, - recurring: false, - category: Some(String::from("products")), - }, - ], - }, - Day { - date: NaiveDate::from_ymd(2020, 10, 4), - expenses: Vec::<Expense>::new(), - }, - Day { - date: NaiveDate::from_ymd(2020, 10, 2), - expenses: vec![ - Expense { - name: String::from("VPS"), - price: 5.0, - qty: 1, - shared: 1, - recurring: true, - category: Some(String::from("utilities")), - }, - Expense { - name: String::from("Transport card"), - price: 6.9, - qty: 1, - shared: 1, - recurring: false, - category: Some(String::from("transport")), - }, - ], - }, - ], - }; +fn can_parse_account() -> Result<(), ParseError> { + let should_be = Account { + start_date: NaiveDate::from_ymd(2020, 10, 1), + end_date: NaiveDate::from_ymd(2020, 10, 31), + budget: 420.0, + essential_categories: vec![ + String::from("products"), + String::from("transport"), + String::from("utilities"), + ], + days: vec![ + Day { + date: NaiveDate::from_ymd(2020, 10, 1), + expenses: vec![ + Expense { + name: String::from("Potato masher"), + price: 3.81, + qty: 1, + shared: 1, + recurring: 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")), + }, + Expense { + name: String::from("Yoghurt"), + price: 1.24, + qty: 2, + shared: 2, + recurring: false, + category: Some(String::from("products")), + }, + Expense { + name: String::from("Onion"), + price: 0.15, + qty: 1, + shared: 1, + recurring: false, + category: Some(String::from("products")), + }, + Expense { + name: String::from("Chicken"), + price: 2.28, + qty: 1, + shared: 2, + recurring: false, + category: Some(String::from("products")), + }, + ], + }, + Day { + date: NaiveDate::from_ymd(2020, 10, 4), + expenses: Vec::<Expense>::new(), + }, + Day { + date: NaiveDate::from_ymd(2020, 10, 2), + expenses: vec![ + Expense { + name: String::from("VPS"), + price: 5.0, + qty: 1, + shared: 1, + recurring: true, + category: Some(String::from("utilities")), + }, + Expense { + name: String::from("Transport card"), + price: 6.9, + qty: 1, + shared: 1, + recurring: false, + category: Some(String::from("transport")), + }, + ], + }, + ], + }; - let actually_is = budget::parse_account("tests/test.toml")?; + let actually_is = budget::parse_account("tests/test.toml")?; - assert_eq!(actually_is, should_be); + assert_eq!(actually_is, should_be); - Ok(()) + Ok(()) } #[test] fn can_calculate() -> Result<(), ParseError> { - let mut should_be = 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, - total_owed: HashMap::<u32, f64>::new(), - days_left: 69.9762219286658, - days_left_essential: 84.08253968253969, - last_day: NaiveDate::from_ymd(2020, 10, 04), - }; + let mut should_be = 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, + total_owed: HashMap::<u32, f64>::new(), + days_left: 69.9762219286658, + days_left_essential: 84.08253968253969, + last_day: NaiveDate::from_ymd(2020, 10, 04), + }; - should_be.categories_day_average.insert( - "supplies".to_string(), - 0.9525, - ); - should_be.categories_day_average.insert( - "products".to_string(), - 1.75, - ); - should_be.categories_day_average.insert( - "transport".to_string(), - 1.725, - ); - should_be.categories_day_average.insert( - "utilities".to_string(), - 1.25, - ); + should_be + .categories_day_average + .insert("supplies".to_string(), 0.9525); + should_be + .categories_day_average + .insert("products".to_string(), 1.75); + should_be + .categories_day_average + .insert("transport".to_string(), 1.725); + should_be + .categories_day_average + .insert("utilities".to_string(), 1.25); - should_be.categories_subtotal.insert( - "supplies".to_string(), - 3.81, - ); - should_be.categories_subtotal.insert( - "products".to_string(), - 7.0, - ); - should_be.categories_subtotal.insert( - "transport".to_string(), - 6.9, - ); - should_be.categories_subtotal.insert( - "utilities".to_string(), - 5.0, - ); + should_be + .categories_subtotal + .insert("supplies".to_string(), 3.81); + should_be + .categories_subtotal + .insert("products".to_string(), 7.0); + should_be + .categories_subtotal + .insert("transport".to_string(), 6.9); + should_be + .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.total_owed.insert(2, 1.7599999999999998); + should_be.total_owed.insert(3, 2.22); - let account = budget::parse_account("tests/test.toml")?; - let actually_is = budget::calculate(&account).unwrap(); + let account = budget::parse_account("tests/test.toml")?; + let actually_is = budget::calculate(&account).unwrap(); - assert_eq!(actually_is, should_be); + assert_eq!(actually_is, should_be); - Ok(()) + Ok(()) } |