diff options
Diffstat (limited to 'budget')
| -rw-r--r-- | budget/src/lib.rs | 81 | ||||
| -rw-r--r-- | budget/tests/budget.rs | 121 | ||||
| -rw-r--r-- | budget/tests/test.toml | 19 | 
3 files changed, 138 insertions, 83 deletions
| 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" | 
