diff options
| author | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2020-10-10 16:04:07 +0300 | 
|---|---|---|
| committer | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2020-10-10 16:04:07 +0300 | 
| commit | e6b225226d4f19577ac7c46e3bf43d97ae77c670 (patch) | |
| tree | eef2eeb7b51b483258d021f0e03875a3623ae58e | |
| parent | c1d4b0a43046b8aa4bad6ebf6ca3f74aba1bb54f (diff) | |
| download | finbudg-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.
| -rw-r--r-- | budget/src/lib.rs | 280 | ||||
| -rw-r--r-- | budget/tests/budget.rs | 272 | ||||
| -rw-r--r-- | rustfmt.toml | 2 | ||||
| -rw-r--r-- | src/main.rs | 471 | 
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(), +			); +		} +	}  } | 
