aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--budget/src/lib.rs280
-rw-r--r--budget/tests/budget.rs272
-rw-r--r--rustfmt.toml2
-rw-r--r--src/main.rs471
4 files changed, 494 insertions, 531 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(())
}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..dd338c9
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,2 @@
+hard_tabs = true
+max_width = 80
diff --git a/src/main.rs b/src/main.rs
index cd401e2..5341939 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,261 +1,240 @@
+use chrono::Duration;
use clap::{
- Arg,
- App,
- ArgMatches,
- crate_version,
- crate_authors,
- crate_description
+ crate_authors, crate_description, crate_version, App, Arg, ArgMatches,
};
-use chrono::Duration;
use colored::*;
use budget::*;
fn main() {
- let matches = get_cli_matches();
-
- let no_color = matches.occurrences_of("plain") > 0;
- let force_color = matches.occurrences_of("force-color") > 0;
- let input = matches.value_of("INPUT").unwrap();
-
- let account = match budget::parse_account(input) {
- Ok(data) => data,
- Err(error) => {
- match error {
- ParseError::IOError(kind) => {
- println!("IO error while parsing: {:?}", kind);
- },
- ParseError::DeserializerError(_) => {
- println!("Can't parse the file, invalid syntax");
- },
- }
-
- ::std::process::exit(1);
- }
- };
- let maybe_calculated = budget::calculate(&account);
-
- if no_color && !force_color {
- colored::control::set_override(false);
- } else if force_color {
- colored::control::set_override(true);
- }
-
- output(account, maybe_calculated);
+ let matches = get_cli_matches();
+
+ let no_color = matches.occurrences_of("plain") > 0;
+ let force_color = matches.occurrences_of("force-color") > 0;
+ let input = matches.value_of("INPUT").unwrap();
+
+ let account = match budget::parse_account(input) {
+ Ok(data) => data,
+ Err(error) => {
+ match error {
+ ParseError::IOError(kind) => {
+ println!("IO error while parsing: {:?}", kind);
+ }
+ ParseError::DeserializerError(_) => {
+ println!("Can't parse the file, invalid syntax");
+ }
+ }
+
+ ::std::process::exit(1);
+ }
+ };
+ let maybe_calculated = budget::calculate(&account);
+
+ if no_color && !force_color {
+ colored::control::set_override(false);
+ } else if force_color {
+ colored::control::set_override(true);
+ }
+
+ output(account, maybe_calculated);
}
fn get_cli_matches() -> ArgMatches<'static> {
- App::new("finbudg")
- .version(crate_version!())
- .author(crate_authors!())
- .about(crate_description!())
- .arg(Arg::with_name("plain")
- .short("p")
- .long("plain")
- .help("Don't colorize the output. Can also be set \
- with the NO_COLOR environment variable.")
- .takes_value(false))
- .arg(Arg::with_name("force-color")
- .long("force-color")
- .help("Forces colorized output even when piping. Takes \
+ App::new("finbudg")
+ .version(crate_version!())
+ .author(crate_authors!())
+ .about(crate_description!())
+ .arg(
+ Arg::with_name("plain")
+ .short("p")
+ .long("plain")
+ .help(
+ "Don't colorize the output. Can also be set \
+ with the NO_COLOR environment variable.",
+ )
+ .takes_value(false),
+ )
+ .arg(
+ Arg::with_name("force-color")
+ .long("force-color")
+ .help(
+ "Forces colorized output even when piping. Takes \
precedence over --plain flag and NO_COLOR environment \
- variable")
- .takes_value(false))
- .arg(Arg::with_name("INPUT")
- .help("Expenses file in toml format to calculate from.")
- .required(true)
- .index(1))
- .get_matches()
+ variable",
+ )
+ .takes_value(false),
+ )
+ .arg(
+ Arg::with_name("INPUT")
+ .help("Expenses file in toml format to calculate from.")
+ .required(true)
+ .index(1),
+ )
+ .get_matches()
}
fn output(account: Account, maybe_calculated: Option<Calculated>) {
- println!(
- "{}",
- format!(
- "Your expenses for the period of {} - {}",
- account.start_date.format("%Y-%m-%d"),
- account.end_date.format("%Y-%m-%d"),
- ).cyan(),
- );
-
- let calculated = match maybe_calculated {
- Some(data) => data,
- None => {
- println!();
- println!("{}", "You have no expenses...".italic());
-
- ::std::process::exit(0);
- }
- };
-
- let days_until_end = account.end_date - calculated.last_day;
-
- println!(
- "{}",
- format!(
- "Last day on entry: {}",
- calculated.last_day.format("%Y-%m-%d"),
- ).cyan(),
- );
-
- println!(
- "{}",
- format!(
- "Days until period end: {}",
- days_until_end.num_days(),
- ).cyan(),
- );
-
- if days_until_end < Duration::zero() {
- println!();
- println!(
- "{}",
- "Your last day on entry is set after the last date of the period!"
- .yellow(),
- );
- println!();
- }
-
- println!(
- "{}",
- format!(
- "Budget: {:.2}",
- account.budget,
- ).cyan(),
- );
-
- println!();
-
- for (category, expenses) in calculated.categories_day_average.iter() {
- println!(
- "Average per day in {}: {:.2}",
- category,
- expenses,
- );
- }
-
- println!(
- "Average per day in essential expenses: {:.2}",
- calculated.essential_day_average,
- );
-
- println!(
- "Average per day: {:.2}",
- calculated.all_day_average,
- );
-
- println!();
-
- for (category, expenses) in calculated.categories_subtotal.iter() {
- println!(
- "Total in {}: {:.2}",
- category,
- expenses,
- );
- }
-
- println!(
- "Total in essential expenses: {:.2}",
- calculated.essential_subtotal,
- );
-
- println!(
- "Total: {:.2}",
- calculated.total,
- );
-
- println!();
-
- let balance_output = format!("{:.2}", calculated.balance);
- let balance_output = if calculated.balance > 0.0 {
- if account.budget / calculated.balance < 10.0 {
- balance_output.green()
- } else {
- balance_output.yellow()
- }
- } else {
- balance_output.red()
- };
-
- println!("Left on balance: {}", balance_output);
-
- println!();
-
- for (n, owed) in calculated.total_owed.iter() {
- println!(
- "{} person(s) owe you in shared expenses: {:.2}",
- n - 1,
- owed,
- );
-
- if *n > 2 {
- println!("Each owes you: {}", *owed / (*n as f64 - 1.0));
- }
-
- println!();
- }
-
- println!("Days until balance runs out:");
-
- let days_left_output = format!(
- "{:.2}",
- calculated.days_left,
- );
- let days_left_essential_output = format!(
- "{:.2}",
- calculated.days_left_essential,
- );
-
- // TODO: also show much money would be left by the end of the period
-
- let mut all_are_healthy = true;
- let mut essential_are_healthy = true;
-
- let days_left_output =
- if days_until_end.num_days() as f64 <= calculated.days_left {
- days_left_output.green()
- } else {
- all_are_healthy = false;
-
- days_left_output.red()
- };
- let days_left_essential_output =
- if days_until_end.num_days() as f64 <= calculated.days_left_essential {
- days_left_essential_output.green()
- } else {
- essential_are_healthy = false;
-
- days_left_essential_output.red()
- };
-
- println!(
- "...taking into account all expenses: {}",
- days_left_output,
- );
- println!(
- "...taking into account only essential expenses: {}",
- days_left_essential_output,
- );
- println!();
-
- if all_are_healthy {
- println!(
- "{}",
- "Your expenses are healthy, they should last you from your last \
- day on entry through your last day of the period.".green(),
- );
- } else {
- println!(
- "{}",
- "You are spending more than you can afford with your current \
- budget. Try minimizing your expenses".red(),
- );
- if essential_are_healthy {
- println!(
- "{}",
- "On the other hand, if you only spend money on essentials, \
- you should be able keep within your budget.".yellow(),
- );
- }
- }
+ println!(
+ "{}",
+ format!(
+ "Your expenses for the period of {} - {}",
+ account.start_date.format("%Y-%m-%d"),
+ account.end_date.format("%Y-%m-%d"),
+ )
+ .cyan(),
+ );
+
+ let calculated = match maybe_calculated {
+ Some(data) => data,
+ None => {
+ println!();
+ println!("{}", "You have no expenses...".italic());
+
+ ::std::process::exit(0);
+ }
+ };
+
+ let days_until_end = account.end_date - calculated.last_day;
+
+ println!(
+ "{}",
+ format!(
+ "Last day on entry: {}",
+ calculated.last_day.format("%Y-%m-%d"),
+ )
+ .cyan(),
+ );
+
+ println!(
+ "{}",
+ format!("Days until period end: {}", days_until_end.num_days(),).cyan(),
+ );
+
+ if days_until_end < Duration::zero() {
+ println!();
+ println!(
+ "{}",
+ "Your last day on entry is set after the last date of the period!"
+ .yellow(),
+ );
+ println!();
+ }
+
+ println!("{}", format!("Budget: {:.2}", account.budget,).cyan(),);
+
+ println!();
+
+ for (category, expenses) in calculated.categories_day_average.iter() {
+ println!("Average per day in {}: {:.2}", category, expenses,);
+ }
+
+ println!(
+ "Average per day in essential expenses: {:.2}",
+ calculated.essential_day_average,
+ );
+
+ println!("Average per day: {:.2}", calculated.all_day_average,);
+
+ println!();
+
+ for (category, expenses) in calculated.categories_subtotal.iter() {
+ println!("Total in {}: {:.2}", category, expenses,);
+ }
+
+ println!(
+ "Total in essential expenses: {:.2}",
+ calculated.essential_subtotal,
+ );
+
+ println!("Total: {:.2}", calculated.total,);
+
+ println!();
+
+ let balance_output = format!("{:.2}", calculated.balance);
+ let balance_output = if calculated.balance > 0.0 {
+ if account.budget / calculated.balance < 10.0 {
+ balance_output.green()
+ } else {
+ balance_output.yellow()
+ }
+ } else {
+ balance_output.red()
+ };
+
+ println!("Left on balance: {}", balance_output);
+
+ println!();
+
+ for (n, owed) in calculated.total_owed.iter() {
+ println!(
+ "{} person(s) owe you in shared expenses: {:.2}",
+ n - 1,
+ owed,
+ );
+
+ if *n > 2 {
+ println!("Each owes you: {}", *owed / (*n as f64 - 1.0));
+ }
+
+ println!();
+ }
+
+ println!("Days until balance runs out:");
+
+ let days_left_output = format!("{:.2}", calculated.days_left,);
+ let days_left_essential_output =
+ format!("{:.2}", calculated.days_left_essential,);
+
+ // TODO: also show much money would be left by the end of the period
+
+ let mut all_are_healthy = true;
+ let mut essential_are_healthy = true;
+
+ let days_left_output =
+ if days_until_end.num_days() as f64 <= calculated.days_left {
+ days_left_output.green()
+ } else {
+ all_are_healthy = false;
+
+ days_left_output.red()
+ };
+ let days_left_essential_output =
+ if days_until_end.num_days() as f64 <= calculated.days_left_essential {
+ days_left_essential_output.green()
+ } else {
+ essential_are_healthy = false;
+
+ days_left_essential_output.red()
+ };
+
+ println!("...taking into account all expenses: {}", days_left_output,);
+ println!(
+ "...taking into account only essential expenses: {}",
+ days_left_essential_output,
+ );
+ println!();
+
+ if all_are_healthy {
+ println!(
+ "{}",
+ "Your expenses are healthy, they should last you from your last \
+ day on entry through your last day of the period."
+ .green(),
+ );
+ } else {
+ println!(
+ "{}",
+ "You are spending more than you can afford with your current \
+ budget. Try minimizing your expenses"
+ .red(),
+ );
+ if essential_are_healthy {
+ println!(
+ "{}",
+ "On the other hand, if you only spend money on essentials, \
+ you should be able keep within your budget."
+ .yellow(),
+ );
+ }
+ }
}