diff options
| author | Yaroslav <contact@yaroslavps.com> | 2020-10-05 21:20:59 +0300 | 
|---|---|---|
| committer | Yaroslav <contact@yaroslavps.com> | 2020-10-05 21:20:59 +0300 | 
| commit | 408b0ac993496b108ec1e479151d549e9535051a (patch) | |
| tree | 6f19aa190a7f5707cc037f3f9590a89187582254 /budget/src | |
| download | finbudg-408b0ac993496b108ec1e479151d549e9535051a.tar.gz finbudg-408b0ac993496b108ec1e479151d549e9535051a.zip | |
initial commitv0.1.0
Diffstat (limited to 'budget/src')
| -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)) +} | 
