/* * Copyright (C) 2022 Aravinth Manivannan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::collections::HashMap; use std::rc::Rc; use rust_decimal::prelude::*; use rust_decimal_macros::dec; use uuid::Uuid; pub trait Purchasable { fn cost(&self) -> Decimal; fn name(&self) -> &str; fn paid_by(&self) -> Rc; } #[derive(Clone)] pub struct Split { purchase: Rc, split_strategy: SplitStrategy, parties: Vec>, } impl Split { pub fn new( purchase: Rc, split_strategy: SplitStrategy, parties: Vec>, ) -> Self { Self { split_strategy, parties, purchase, } } pub fn money_owed(&self) -> ShareHashMap { match &self.split_strategy { SplitStrategy::Unequal(shares) => { let mut s = shares.clone(); let payer = self.purchase.paid_by(); if let Some((_, money)) = s.get_mut(payer.name()) { *money = dec!(0.0); } s } SplitStrategy::Equal => { let mut shares = ShareHashMap::default(); let cost = self.purchase.cost(); let total_parties = Decimal::new(self.parties.len() as i64, 0); for actor in self.parties.iter() { if *actor == self.purchase.paid_by() { shares.insert(actor.name().to_owned(), (actor.clone(), dec!(0.00))); } else { shares.insert( actor.name().to_owned(), (actor.clone(), cost / total_parties), ); } } shares } SplitStrategy::Shares(c) => { let mut shares = ShareHashMap::default(); let cost = self.purchase.cost(); for (actor_name, (actor, share)) in c.iter() { //if actor.name() == split.purchase.paid_by().name() { if *actor == self.purchase.paid_by() { shares.insert(actor_name.clone(), (actor.clone(), dec!(0.00))); } else { shares.insert(actor_name.clone(), (actor.clone(), cost * share)); } } shares } } } pub fn get(&mut self, actor: Rc) -> Option { unimplemented!(); // if let Some((_actor, cost)) = self.shares.get(actor.name()) { // Some(*cost) // } else { // None // } } pub fn validate(&self) -> bool { self.split_strategy.validate(self.purchase.as_ref()) } } // key is a string for getting Eq and PartialEq w object safety pub type ShareHashMap = HashMap, Decimal)>; /* * SPLITTING STRATEGIES: * Unequal: Each actor's split is specified in specific amounts * Shares: Each actor's split is specified in specific percentage * Equal: Each actor's split is computed by splitting total expense among all participating actors */ #[derive(Clone)] pub enum SplitStrategy { Unequal(ShareHashMap), Shares(ShareHashMap), Equal, } impl SplitStrategy { fn validate(&self, purchase: &dyn Purchasable) -> bool { match self { SplitStrategy::Unequal(shares) => { let mut total = dec!(0.00); for (_actor_str, (_actor_obj, share)) in shares.iter() { total += share; } let cost = purchase.cost(); println!("Unequal sharing: expected {cost} got {total}"); total == cost } SplitStrategy::Shares(shares) => { let mut total = dec!(0.00); let cost = purchase.cost(); let mut total_shares = dec!(0.00); for (_actor_str, (_actor_obj, share)) in shares.iter() { total_shares += share; total += cost * share; println!("Shares: {:?}, total: {total_shares}", share); } println!("Unequal sharing: expected shares 1 got {total_shares}"); println!("Unequal sharing: expected {cost} got {total}"); total_shares == dec!(1.00) && total == cost } SplitStrategy::Equal => true, } } } pub trait Actor { fn spends(&mut self, i: Split); fn owes(&mut self, i: Split); // name must be unique in a namespace fn name(&self) -> &str; fn id(&self) -> &Uuid; } impl PartialEq for dyn Actor { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } pub struct Person { name: String, spent: Vec, owes: Vec, id: Uuid, } impl Person { pub fn new(name: String, id: Uuid) -> Self { let owes = Vec::default(); let spent = Vec::default(); Self { name, spent, owes, id, } } } impl Actor for Person { fn spends(&mut self, split: Split) { if split.purchase.paid_by().name() == self.name() { self.spent.push(split); } else { unimplemented!("Raise error: Actor didn't pay for purchase") } } fn owes(&mut self, split: Split) { if split.purchase.paid_by().name() == self.name() { unimplemented!("Raise error: Actor didn't paid for purchase; can't own money") } else { self.owes.push(split); } } fn name(&self) -> &str { &self.name } fn id(&self) -> &Uuid { &self.id } } pub struct Item { cost: Decimal, name: String, payer: Rc, } impl Item { pub fn new(name: String, cost: Decimal, payer: Rc) -> Self { Self { cost, name, payer } } } impl Purchasable for Item { fn cost(&self) -> Decimal { self.cost } fn name(&self) -> &str { &self.name } fn paid_by(&self) -> Rc { self.payer.clone() } } //impl Actor for Person { // fn spends(&self, i: &I); // fn owes(&self, amount: usize); //} #[cfg(test)] mod tests { use super::*; #[test] fn split_works() { let person1: Rc = Rc::new(Person::new("person1".into(), Uuid::new_v4())); let person2: Rc = Rc::new(Person::new("person2".into(), Uuid::new_v4())); let person3: Rc = Rc::new(Person::new("person3".into(), Uuid::new_v4())); let person4: Rc = Rc::new(Person::new("person4".into(), Uuid::new_v4())); let item = Item::new("test item".into(), dec!(240.00), person1.clone()); let actors = vec![ person1.clone(), person2.clone(), person3.clone(), person4.clone(), ]; // equal split let strategy = SplitStrategy::Equal; let mut split = Split::new(Rc::new(item), strategy, actors); assert!(split.validate()); let dues = split.money_owed(); for (_actor_name, (actor, due)) in dues.iter() { if split.purchase.paid_by().name() == actor.name() { assert_eq!(*due, dec!(0.00)); } else { assert_eq!( *due, split.purchase.cost() / Decimal::new(split.parties.len() as i64, 0) ); } } // share split let mut shares = ShareHashMap::default(); shares.insert(person1.name().to_owned(), (person1.clone(), dec!(0.3))); shares.insert(person2.name().to_owned(), (person2.clone(), dec!(0.4))); shares.insert(person3.name().to_owned(), (person3.clone(), dec!(0.2))); shares.insert(person4.name().to_owned(), (person4.clone(), dec!(0.1))); let strategy = SplitStrategy::Shares(shares.clone()); split.split_strategy = strategy; assert!(split.validate()); let dues = split.money_owed(); for (actor_name, (actor, due)) in dues.iter() { println!("Testing for actor: {actor_name} with due: {due}"); if split.purchase.paid_by().name() == actor.name() { assert_eq!(*due, dec!(0.00)); } else { let (_, share) = shares.get(actor_name).unwrap(); assert_eq!(*due, split.purchase.cost() * *share); } } // Unequal split let mut shares = ShareHashMap::default(); shares.insert( person1.name().to_owned(), (person1.clone(), dec!(0.3) * split.purchase.cost()), ); shares.insert( person2.name().to_owned(), (person2.clone(), dec!(0.4) * split.purchase.cost()), ); shares.insert( person3.name().to_owned(), (person3.clone(), dec!(0.2) * split.purchase.cost()), ); shares.insert( person4.name().to_owned(), (person4.clone(), dec!(0.1) * split.purchase.cost()), ); let strategy = SplitStrategy::Unequal(shares); split.split_strategy = strategy; assert!(split.validate()); } }