aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYaroslav de la Peña Smirnov <yps@yaroslavps.com>2020-12-08 00:39:16 +0300
committerYaroslav de la Peña Smirnov <yps@yaroslavps.com>2020-12-08 00:39:16 +0300
commitcc00db276c203bdcff9ca32d857d23e5dc61f400 (patch)
tree42d6940af40002aef9d630cdcb2aa7d738405dfc
parentd79b2661c8af24c508f90419f01009175cd704d4 (diff)
downloadfinbudg-cc00db276c203bdcff9ca32d857d23e5dc61f400.tar.gz
finbudg-cc00db276c203bdcff9ca32d857d23e5dc61f400.zip
Change way that shared expenses are calculated
The way that shared expenses are recorded and calculated has changed. Now instead of specifying how among many people the expense was divided, the names of the persons are specified. You can also specified expenses that are "owed", or in other words, things that you paid for other people, or maybe loans you made. Don't know why I didn't think of this system before. Sometimes good ideas just come at the least expected times. Also removed some unused fields.
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml4
-rw-r--r--budget/src/lib.rs81
-rw-r--r--budget/tests/budget.rs121
-rw-r--r--budget/tests/test.toml19
-rw-r--r--src/main.rs50
6 files changed, 179 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
index eb3b2d7..21cb7f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,7 +82,7 @@ dependencies = [
[[package]]
name = "finbudg"
-version = "0.2.0"
+version = "0.3.0"
dependencies = [
"budget",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 612070a..b05f320 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,11 +1,11 @@
[package]
name = "finbudg"
-version = "0.2.0"
+version = "0.3.0"
edition = "2018"
description = "Quick cli tool to calculate your expenses and balance for a set period of time."
license = "MIT"
readme = "README.md"
-authors = ["Yaroslav de la Peña Smirnov <contact@yaroslavps.com>"]
+authors = ["Yaroslav de la Peña Smirnov <yps@yaroslavps.com>"]
homepage = "https://www.yaroslavps.com/"
repository = "https://github.com/Yaroslav-95/finbudg"
diff --git a/budget/src/lib.rs b/budget/src/lib.rs
index 5013a9f..e848035 100644
--- a/budget/src/lib.rs
+++ b/budget/src/lib.rs
@@ -30,12 +30,14 @@ pub struct Day {
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)]
+ /// Whom this expense is shared with (if anybody).
+ pub shared: Vec<String>,
+ #[serde(default)]
+ /// Whether this was something we paid for somebody else, and thus is owed
+ /// to us. If true, then shared is the list of person(s) that owe us this
+ /// expense, and should therefore contain at least one name.
+ pub owed: bool,
#[serde(default)]
pub category: Option<String>,
}
@@ -49,7 +51,8 @@ pub struct Calculated {
pub categories_subtotal: HashMap<String, f64>,
pub total: f64,
pub balance: f64,
- pub total_owed: HashMap<u32, f64>,
+ pub owed: HashMap<String, f64>,
+ pub total_owed: f64,
pub days_left: f64,
pub days_left_essential: f64,
pub last_day: NaiveDate,
@@ -61,14 +64,6 @@ pub enum ParseError {
DeserializerError(DeserializerError),
}
-fn shared_qty_default() -> u32 {
- 1
-}
-
-fn recurring_default() -> bool {
- false
-}
-
// Parse the dates from toml's Datetime to Chrono's NaiveDate
fn deserialize_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
@@ -97,7 +92,7 @@ pub fn parse_account(path: &str) -> Result<Account, ParseError> {
}
}
-pub fn calculate(account: &Account) -> Option<Calculated> {
+pub fn calculate(account: &Account, consider_owed: bool) -> Option<Calculated> {
if account.days.is_empty() {
return None;
}
@@ -110,7 +105,8 @@ pub fn calculate(account: &Account) -> Option<Calculated> {
categories_subtotal: HashMap::<String, f64>::new(),
total: 0.0,
balance: 0.0,
- total_owed: HashMap::<u32, f64>::new(),
+ owed: HashMap::<String, f64>::new(),
+ total_owed: 0.0,
days_left: 0.0,
days_left_essential: 0.0,
last_day: account.days.last().unwrap().date,
@@ -122,34 +118,51 @@ pub fn calculate(account: &Account) -> Option<Calculated> {
}
for expense in day.expenses.iter() {
- calculated.total += expense.price;
+ let mut actual_expense: f64 = 0.0;
+
+ if expense.shared.len() > 0 {
+ let owed_share = if expense.owed {
+ expense.price / expense.shared.len() as f64
+ } else {
+ actual_expense =
+ expense.price / (expense.shared.len() as f64 + 1.0);
+ actual_expense
+ };
+
+ for person in expense.shared.iter() {
+ calculated.total_owed += owed_share;
+
+ if let Some(owed_by_person) =
+ calculated.owed.get_mut(person)
+ {
+ *owed_by_person += owed_share;
+ } else {
+ calculated.owed.insert(person.clone(), owed_share);
+ }
+ }
+ }
+
+ if expense.shared.len() == 0 || consider_owed {
+ actual_expense = expense.price;
+ } else if expense.owed {
+ continue;
+ }
+
+ calculated.total += actual_expense;
if let Some(category) = &expense.category {
if let Some(category_subtotal) =
calculated.categories_subtotal.get_mut(category)
{
- *category_subtotal += expense.price;
+ *category_subtotal += actual_expense;
} else {
calculated
.categories_subtotal
- .insert(category.to_string(), expense.price);
+ .insert(category.to_string(), actual_expense);
}
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);
- }
+ calculated.essential_subtotal += actual_expense;
}
}
}
diff --git a/budget/tests/budget.rs b/budget/tests/budget.rs
index 4ed549c..836a715 100644
--- a/budget/tests/budget.rs
+++ b/budget/tests/budget.rs
@@ -11,7 +11,7 @@ fn can_parse_account() -> Result<(), ParseError> {
end_date: NaiveDate::from_ymd(2020, 10, 31),
budget: 420.0,
essential_categories: vec![
- String::from("products"),
+ String::from("produce"),
String::from("transport"),
String::from("utilities"),
],
@@ -22,42 +22,40 @@ fn can_parse_account() -> Result<(), ParseError> {
Expense {
name: String::from("Potato masher"),
price: 3.81,
- qty: 1,
- shared: 1,
- recurring: false,
+ shared: vec![],
+ owed: 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")),
+ shared: vec![
+ String::from("Fox"),
+ String::from("Falco"),
+ ],
+ owed: false,
+ category: Some(String::from("produce")),
},
Expense {
name: String::from("Yoghurt"),
price: 1.24,
- qty: 2,
- shared: 2,
- recurring: false,
- category: Some(String::from("products")),
+ shared: vec![String::from("Falco")],
+ owed: true,
+ category: Some(String::from("produce")),
},
Expense {
name: String::from("Onion"),
price: 0.15,
- qty: 1,
- shared: 1,
- recurring: false,
- category: Some(String::from("products")),
+ shared: vec![],
+ owed: false,
+ category: Some(String::from("produce")),
},
Expense {
name: String::from("Chicken"),
price: 2.28,
- qty: 1,
- shared: 2,
- recurring: false,
- category: Some(String::from("products")),
+ shared: vec![String::from("Fox")],
+ owed: false,
+ category: Some(String::from("produce")),
},
],
},
@@ -71,17 +69,15 @@ fn can_parse_account() -> Result<(), ParseError> {
Expense {
name: String::from("VPS"),
price: 5.0,
- qty: 1,
- shared: 1,
- recurring: true,
+ shared: vec![],
+ owed: false,
category: Some(String::from("utilities")),
},
Expense {
name: String::from("Transport card"),
price: 6.9,
- qty: 1,
- shared: 1,
- recurring: false,
+ shared: vec![],
+ owed: false,
category: Some(String::from("transport")),
},
],
@@ -99,16 +95,17 @@ fn can_parse_account() -> Result<(), ParseError> {
#[test]
fn can_calculate() -> Result<(), ParseError> {
let mut should_be = Calculated {
- all_day_average: 5.6775,
- essential_day_average: 4.725,
+ all_day_average: 4.5275,
+ essential_day_average: 3.575,
categories_day_average: HashMap::<String, f64>::new(),
- essential_subtotal: 18.9,
+ essential_subtotal: 14.3,
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,
+ total: 18.11,
+ balance: 401.89,
+ owed: HashMap::<String, f64>::new(),
+ total_owed: 4.6,
+ days_left: 88.76642738818333,
+ days_left_essential: 112.4167832167832,
last_day: NaiveDate::from_ymd(2020, 10, 04),
};
@@ -117,7 +114,7 @@ fn can_calculate() -> Result<(), ParseError> {
.insert("supplies".to_string(), 0.9525);
should_be
.categories_day_average
- .insert("products".to_string(), 1.75);
+ .insert("produce".to_string(), 0.6);
should_be
.categories_day_average
.insert("transport".to_string(), 1.725);
@@ -130,7 +127,7 @@ fn can_calculate() -> Result<(), ParseError> {
.insert("supplies".to_string(), 3.81);
should_be
.categories_subtotal
- .insert("products".to_string(), 7.0);
+ .insert("produce".to_string(), 2.4);
should_be
.categories_subtotal
.insert("transport".to_string(), 6.9);
@@ -138,13 +135,59 @@ fn can_calculate() -> Result<(), ParseError> {
.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.owed.insert(String::from("Fox"), 2.25);
+ should_be.owed.insert(String::from("Falco"), 2.35);
+
+ let mut should_be_with_owed = 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,
+ owed: HashMap::<String, f64>::new(),
+ total_owed: 4.6,
+ days_left: 69.9762219286658,
+ days_left_essential: 84.08253968253969,
+ last_day: NaiveDate::from_ymd(2020, 10, 04),
+ };
+
+ should_be_with_owed
+ .categories_day_average
+ .insert("supplies".to_string(), 0.9525);
+ should_be_with_owed
+ .categories_day_average
+ .insert("produce".to_string(), 1.75);
+ should_be_with_owed
+ .categories_day_average
+ .insert("transport".to_string(), 1.725);
+ should_be_with_owed
+ .categories_day_average
+ .insert("utilities".to_string(), 1.25);
+
+ should_be_with_owed
+ .categories_subtotal
+ .insert("supplies".to_string(), 3.81);
+ should_be_with_owed
+ .categories_subtotal
+ .insert("produce".to_string(), 7.0);
+ should_be_with_owed
+ .categories_subtotal
+ .insert("transport".to_string(), 6.9);
+ should_be_with_owed
+ .categories_subtotal
+ .insert("utilities".to_string(), 5.0);
+
+ should_be_with_owed.owed.insert(String::from("Fox"), 2.25);
+ should_be_with_owed.owed.insert(String::from("Falco"), 2.35);
let account = budget::parse_account("tests/test.toml")?;
- let actually_is = budget::calculate(&account).unwrap();
+ let actually_is = budget::calculate(&account, false).unwrap();
+ let actually_is_with_owed = budget::calculate(&account, true).unwrap();
assert_eq!(actually_is, should_be);
+ assert_eq!(actually_is_with_owed, should_be_with_owed);
Ok(())
}
diff --git a/budget/tests/test.toml b/budget/tests/test.toml
index ca2c1e5..103094e 100644
--- a/budget/tests/test.toml
+++ b/budget/tests/test.toml
@@ -2,7 +2,7 @@ start_date = 2020-10-01
end_date = 2020-10-31
budget = 420.0
essential_categories = [
- "products",
+ "produce",
"transport",
"utilities",
]
@@ -18,26 +18,26 @@ date = 2020-10-01
[[days.expenses]]
name = "Bacon"
price = 3.33
- category = "products"
- shared = 3
+ category = "produce"
+ shared = ["Fox", "Falco"]
[[days.expenses]]
name = "Yoghurt"
price = 1.24
- category = "products"
- qty = 2
- shared = 2
+ category = "produce"
+ owed = true
+ shared = ["Falco"]
[[days.expenses]]
name = "Onion"
price = 0.15
- category = "products"
+ category = "produce"
[[days.expenses]]
name = "Chicken"
price = 2.28
- category = "products"
- shared = 2
+ category = "produce"
+ shared = ["Fox"]
[[days]]
date = 2020-10-04
@@ -49,7 +49,6 @@ date = 2020-10-02
name = "VPS"
price = 5.0
category = "utilities"
- recurring = true
[[days.expenses]]
name = "Transport card"
diff --git a/src/main.rs b/src/main.rs
index 1dd437b..dec53ff 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,7 @@ fn main() {
let no_color = matches.occurrences_of("plain") > 0;
let force_color = matches.occurrences_of("force-color") > 0;
+ let consider_owed = matches.occurrences_of("consider-owed") > 0;
let input = matches.value_of("INPUT").unwrap();
let account = match budget::parse_account(input) {
@@ -28,7 +29,7 @@ fn main() {
::std::process::exit(1);
}
};
- let maybe_calculated = budget::calculate(&account);
+ let maybe_calculated = budget::calculate(&account, consider_owed);
if no_color && !force_color {
colored::control::set_override(false);
@@ -36,7 +37,7 @@ fn main() {
colored::control::set_override(true);
}
- output(account, maybe_calculated);
+ output(account, maybe_calculated, consider_owed);
}
fn get_cli_matches() -> ArgMatches<'static> {
@@ -45,6 +46,16 @@ fn get_cli_matches() -> ArgMatches<'static> {
.author(crate_authors!())
.about(crate_description!())
.arg(
+ Arg::with_name("consider-owed")
+ .short("w")
+ .long("consider-owed")
+ .help(
+ "Take into account what's owed when calculating the total \
+ and subtotals."
+ )
+ .takes_value(false)
+ )
+ .arg(
Arg::with_name("plain")
.short("p")
.long("plain")
@@ -60,21 +71,25 @@ fn get_cli_matches() -> ArgMatches<'static> {
.help(
"Forces colorized output even when piping. Takes \
precedence over --plain flag and NO_COLOR environment \
- variable",
+ variable.",
)
.takes_value(false),
)
.arg(
Arg::with_name("INPUT")
.help("Expenses file to calculate from. For more information \
- on the format of this file see 'man 5 finbudg'")
+ on the format of this file see 'man 5 finbudg'.")
.required(true)
.index(1),
)
.get_matches()
}
-fn output(account: Account, maybe_calculated: Option<Calculated>) {
+fn output(
+ account: Account,
+ maybe_calculated: Option<Calculated>,
+ consider_owed: bool,
+) {
println!(
"{}",
format!(
@@ -166,20 +181,31 @@ fn output(account: Account, maybe_calculated: Option<Calculated>) {
println!();
- for (n, owed) in calculated.total_owed.iter() {
+ for (person, owed) in calculated.owed.iter() {
println!(
- "{} person(s) owe you in shared expenses: {:.2}",
- n - 1,
+ "{} owes you in shared expenses: {:.2}",
+ person,
owed,
);
+ }
- if *n > 2 {
- println!("Each owes you: {}", *owed / (*n as f64 - 1.0));
+ if calculated.owed.len() > 0 {
+ println!("In total you're owed: {:.2}", calculated.total_owed);
+ if consider_owed {
+ println!(
+ "Supposing you've been repaid, you should be left with: {:.2}",
+ calculated.balance + calculated.total_owed,
+ );
+ } else {
+ println!(
+ "Assuming you haven't been repaid, you're left with: {:.2}",
+ calculated.balance - calculated.total_owed,
+ );
}
-
- println!();
}
+ println!();
+
println!("Days until balance runs out:");
let days_left_output = format!("{:.2}", calculated.days_left,);