diff options
Diffstat (limited to 'budget/src/lib.rs')
-rw-r--r-- | budget/src/lib.rs | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/budget/src/lib.rs b/budget/src/lib.rs new file mode 100644 index 0000000..4dab46a --- /dev/null +++ b/budget/src/lib.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; +use std::io::ErrorKind; +use std::fs; + +use toml::de::Error as DeserializerError; +use serde::{Deserialize, Deserializer}; +use chrono::NaiveDate; + +#[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>, +} + +#[derive(Deserialize, PartialEq, Debug)] +pub struct Day { + #[serde(deserialize_with = "deserialize_date")] + pub date: NaiveDate, + 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>, +} + +#[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 days_left: f64, + pub days_left_essential: f64, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum ParseError { + IOError(ErrorKind), + DeserializerError(DeserializerError), +} + +fn shared_qty_default() -> u32 { + 1 +} + +fn recurring_default() -> bool { + false +} + +// Parse the dates from toml's Datetime to Chrono's NaiveDate +// Probably unnecessary for now, but since I am planning on using the dates in +// the future to more easily count the days, it would be better to have them in +// a proper format +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) +} + +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)), + } +} + +pub fn calculate(account: &Account) -> Calculated { + 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, + days_left: 0.0, + days_left_essential: 0.0, + }; + + let mut days_calculated = 0; + + for day in account.days.iter() { + days_calculated += 1; + + 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; + } + } + } + } + + calculated.all_day_average = calculated.total / days_calculated as f64; + calculated.essential_day_average = + calculated.essential_subtotal / days_calculated as f64; + + for (category, subtotal) in calculated.categories_subtotal.iter() { + calculated.categories_day_average + .insert( + category.clone(), + subtotal / days_calculated 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; + + calculated +} + +pub fn get_calculated(path: &str) -> Result<Calculated, ParseError> { + let account = parse_account(path)?; + + Ok(calculate(&account)) +} |