From e6b225226d4f19577ac7c46e3bf43d97ae77c670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaroslav=20de=20la=20Pe=C3=B1a=20Smirnov?= Date: Sat, 10 Oct 2020 16:04:07 +0300 Subject: 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. --- budget/src/lib.rs | 280 +++++++++++++++-------------- budget/tests/budget.rs | 272 ++++++++++++++-------------- rustfmt.toml | 2 + src/main.rs | 471 +++++++++++++++++++++++-------------------------- 4 files changed, 494 insertions(+), 531 deletions(-) create mode 100644 rustfmt.toml 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, - pub days: Vec, + #[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, + pub days: Vec, } #[derive(Deserialize, PartialEq, Debug)] pub struct Day { - #[serde(deserialize_with = "deserialize_date")] - pub date: NaiveDate, - #[serde(default)] - pub expenses: Vec, + #[serde(deserialize_with = "deserialize_date")] + pub date: NaiveDate, + #[serde(default)] + pub expenses: Vec, } #[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, + 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, } #[derive(PartialEq, Debug)] pub struct Calculated { - pub all_day_average: f64, - pub essential_day_average: f64, - pub categories_day_average: HashMap, - pub essential_subtotal: f64, - pub categories_subtotal: HashMap, - pub total: f64, - pub balance: f64, - pub total_owed: HashMap, - 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, + pub essential_subtotal: f64, + pub categories_subtotal: HashMap, + pub total: f64, + pub balance: f64, + pub total_owed: HashMap, + 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 -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 { - let contents = match fs::read_to_string(path) { - Ok(data) => data, - Err(error) => { - return Err(ParseError::IOError(error.kind())); - }, - }; - - match toml::from_str::(&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::(&contents) { + Ok(budget) => Ok(budget), + Err(error) => Err(ParseError::DeserializerError(error)), + } } pub fn calculate(account: &Account) -> Option { - 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::::new(), - essential_subtotal: 0.0, - categories_subtotal: HashMap::::new(), - total: 0.0, - balance: 0.0, - total_owed: HashMap::::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::::new(), + essential_subtotal: 0.0, + categories_subtotal: HashMap::::new(), + total: 0.0, + balance: 0.0, + total_owed: HashMap::::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::::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::::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::::new(), - essential_subtotal: 18.9, - categories_subtotal: HashMap::::new(), - total: 22.71, - balance: 397.29, - total_owed: HashMap::::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::::new(), + essential_subtotal: 18.9, + categories_subtotal: HashMap::::new(), + total: 22.71, + balance: 397.29, + total_owed: HashMap::::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) { - 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(), + ); + } + } } -- cgit v1.2.3