aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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,);