aboutsummaryrefslogtreecommitdiff
path: root/budget
diff options
context:
space:
mode:
authorYaroslav de la Peña Smirnov <yps@yaroslavps.com>2020-10-10 16:04:07 +0300
committerYaroslav de la Peña Smirnov <yps@yaroslavps.com>2020-10-10 16:04:07 +0300
commite6b225226d4f19577ac7c46e3bf43d97ae77c670 (patch)
treeeef2eeb7b51b483258d021f0e03875a3623ae58e /budget
parentc1d4b0a43046b8aa4bad6ebf6ca3f74aba1bb54f (diff)
downloadfinbudg-e6b225226d4f19577ac7c46e3bf43d97ae77c670.tar.gz
finbudg-e6b225226d4f19577ac7c46e3bf43d97ae77c670.zip
Switch to superior indentation method
Also set max width of lines to 80, because I like being able to open my code in vertical splits and fitting each window.
Diffstat (limited to 'budget')
-rw-r--r--budget/src/lib.rs280
-rw-r--r--budget/tests/budget.rs272
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(())
}