From a2cb5f2e6dcf52fbcc530ab09a5414ea82a14626 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Thu, 17 Aug 2023 02:30:57 +0530 Subject: [PATCH] chore: refactor --- src/actor.rs | 61 ++++++++++ src/expense.rs | 173 +++++++++++++++++++++++++++ src/lib.rs | 311 +----------------------------------------------- src/purchase.rs | 36 ++++++ src/strategy.rs | 56 +++++++++ 5 files changed, 330 insertions(+), 307 deletions(-) create mode 100644 src/actor.rs create mode 100644 src/expense.rs create mode 100644 src/purchase.rs create mode 100644 src/strategy.rs diff --git a/src/actor.rs b/src/actor.rs new file mode 100644 index 0000000..62015ac --- /dev/null +++ b/src/actor.rs @@ -0,0 +1,61 @@ +use uuid::Uuid; + +use crate::expense::Split; + +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 + } +} diff --git a/src/expense.rs b/src/expense.rs new file mode 100644 index 0000000..8887e8c --- /dev/null +++ b/src/expense.rs @@ -0,0 +1,173 @@ +use std::rc::Rc; + +use rust_decimal::prelude::*; +use rust_decimal_macros::dec; + +use crate::actor::*; +use crate::purchase::*; +use crate::strategy::*; + +#[derive(Clone)] +pub struct Split { + pub(crate) purchase: Rc, + pub(crate) split_strategy: SplitStrategy, + pub(crate) 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()) + } +} + +//impl Actor for Person { +// fn spends(&self, i: &I); +// fn owes(&self, amount: usize); +//} + +#[cfg(test)] +mod tests { + use super::*; + use uuid::Uuid; + + #[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()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1eaf0ad..f3b5a8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,310 +14,7 @@ * 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()); - } -} +pub mod actor; +pub mod expense; +pub mod purchase; +pub mod strategy; diff --git a/src/purchase.rs b/src/purchase.rs new file mode 100644 index 0000000..a76f953 --- /dev/null +++ b/src/purchase.rs @@ -0,0 +1,36 @@ +use std::rc::Rc; + +use rust_decimal::prelude::*; + +use crate::actor::*; + +pub trait Purchasable { + fn cost(&self) -> Decimal; + fn name(&self) -> &str; + fn paid_by(&self) -> Rc; +} + +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() + } +} diff --git a/src/strategy.rs b/src/strategy.rs new file mode 100644 index 0000000..850390c --- /dev/null +++ b/src/strategy.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use rust_decimal::prelude::*; +use rust_decimal_macros::dec; + +use crate::actor::*; +use crate::purchase::*; + +// 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 { + pub(crate) 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, + } + } +}