diff --git a/Cargo.lock b/Cargo.lock index 7e72e1a..b8ad0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" + [[package]] name = "split" version = "0.1.0" +dependencies = [ + "uuid", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml index 3a743d9..d2e0a54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ edition = "2021" #build = "build.rs" [dependencies] +uuid = { version = "1.4.1", features = ["v4", "serde"] } diff --git a/src/lib.rs b/src/lib.rs index 9935b49..4a585bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,94 +17,119 @@ use std::collections::HashMap; use std::rc::Rc; +use uuid::Uuid; + pub trait Purchasable { fn cost(&self) -> f64; fn name(&self) -> &str; - fn paid_by(&self) -> &dyn Actor; + fn paid_by(&self) -> Rc; } +pub fn money_owned(splits: &[Split]) { + for split in splits.iter() {} +} + +#[derive(Clone)] pub struct Split { purchase: Rc, split_strategy: SplitStrategy, + parties: Vec>, } -#[derive(Default)] -pub struct SplitHashMap { - shares: HashMap, f64)>, -} -impl SplitHashMap { - pub fn iter(&self) -> SplitHashMapIter { - let iter = self.shares.iter(); - SplitHashMapIter { iter } +//#[derive(Clone)] +//pub struct Split { +// shares: ShareHashMap, +// paid_by: Rc, +//} + +impl Split { + pub fn new(purchase: Rc, split_strategy: SplitStrategy, parties: Vec>) -> Self { + Self { + split_strategy, parties, purchase + } } - pub fn insert(&mut self, actor: Rc, amount: f64) { - let name = actor.name().to_owned(); - self.shares.insert(name, (actor, amount)); - } +// pub fn iter(&self) -> SplitIter { +// let iter = self.shares.iter(); +// let paid_by = self.paid_by; +// SplitIter { iter, paid_by} +// } +// +// pub fn insert(&mut self, actor: Rc, amount: f64) { +// let name = actor.name().to_owned(); +// self.shares.insert(name, (actor, amount)); +// } pub fn get(&mut self, actor: Rc) -> Option { - if let Some((_actor, cost)) = self.shares.get(actor.name()) { - Some(*cost) - } else { - None - } - } + unimplemented!(); +// if let Some((_actor, cost)) = self.shares.get(actor.name()) { +// Some(*cost) +// } else { +// None +// } + } } -pub struct SplitHashMapIter<'a> { - iter: std::collections::hash_map::Iter<'a, String, (Rc, f64)>, -} - -impl<'a> Iterator for SplitHashMapIter<'a> { - type Item = (Rc, f64); - - fn next(&mut self) -> Option { - if let Some((_, (actor, cost))) = self.iter.next() { - Some((actor.clone(), *cost)) - } else { - None - } - } -} +//pub struct SplitIter<'a> { +// iter: std::collections::hash_map::Iter<'a, String, (Rc, f64)>, +// paid_by: Rc, +//} +// +//impl<'a> Iterator for SplitIter<'a> { +// type Item = (Rc, f64); +// +// fn next(&mut self) -> Option { +// if let Some((_, (actor, cost))) = self.iter.next() { +// Some((actor.clone(), *cost)) +// } else { +// None +// } +// } +//} impl Split { pub fn validate(&self) -> bool { - self.split_strategy.validate(self.purchase.as_ref()) + self.split_strategy.validate(&self) } } + +// key is a string for getting Eq and PartialEq w object safety +pub type ShareHashMap = HashMap, f64)>; + + /* * 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(SplitHashMap), - Shares(SplitHashMap), - Equal(Vec>), + Unequal(ShareHashMap), + Shares(ShareHashMap), + Equal, } impl SplitStrategy { - pub fn validate(&self, item: &dyn Purchasable) -> bool { + pub fn validate(&self, split: &Split) -> bool { match self { - SplitStrategy::Unequal(config) => { + SplitStrategy::Unequal(shares) => { let mut total = 0.00; - for (_actor, share) in config.iter() { + for (_actor_str, (_actor_obj, share)) in shares.iter() { total += share; } - let cost = item.cost(); + let cost = split.purchase.cost(); println!("Unequal sharing: expected {cost} got {total}"); total == cost } - SplitStrategy::Shares(config) => { + SplitStrategy::Shares(shares) => { let mut total = 0.00; - let cost = item.cost(); + let cost = split.purchase.cost(); let mut total_shares = 0.00; - for (_actor, share) in config.iter() { + for (_actor_str, (_actor_obj, share)) in shares.iter() { total_shares += share; total += cost * share; } @@ -113,7 +138,47 @@ impl SplitStrategy { println!("Unequal sharing: expected {cost} got {total}"); total_shares == 1.00 && total == cost } - SplitStrategy::Equal(c) => !c.is_empty(), + SplitStrategy::Equal => true, + } + } + + pub fn money_owed(&self, split: &Split) -> ShareHashMap { + match self { + SplitStrategy::Unequal(shares) => { + let mut s = shares.clone(); + let payer = split.purchase.paid_by(); + if let Some((_,money)) = s.get_mut(payer.name()) { + *money = 0.0; + } + s + + } + SplitStrategy::Equal => { + let mut shares = ShareHashMap::default(); + let cost = split.purchase.cost(); + for actor in split.parties.iter() { + if *actor == split.purchase.paid_by() { + shares.insert(actor.name().to_owned(), (actor.clone(), 0.00)); + } else { + shares.insert(actor.name().to_owned(), (actor.clone(), cost / split.parties.len() as f64)); + } + } + + shares + } + SplitStrategy::Shares(c) => { + let mut shares = ShareHashMap::default(); + let cost = split.purchase.cost(); + for (actor_name,(actor, share)) in c.iter() { + //if actor.name() == split.purchase.paid_by().name() { + if *actor == split.purchase.paid_by() { + shares.insert(actor_name.clone(), (actor.clone(), 0.00)); + } else { + shares.insert(actor_name.clone(), (actor.clone(), cost * share)); + } + } + shares + } } } } @@ -123,20 +188,28 @@ pub trait Actor { 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) -> Self { + pub fn new(name: String, id: Uuid) -> Self { let owes = Vec::default(); let spent = Vec::default(); - Self { name, spent, owes } + Self { name, spent, owes, id} } } @@ -158,6 +231,10 @@ impl Actor for Person { fn name(&self) -> &str { &self.name } + fn id(&self) -> &Uuid { + &self.id + } + } pub struct Item { @@ -180,8 +257,8 @@ impl Purchasable for Item { &self.name } - fn paid_by(&self) -> &dyn Actor { - self.payer.as_ref() + fn paid_by(&self) -> Rc { + self.payer.clone() } } @@ -196,10 +273,10 @@ mod tests { #[test] fn split_works() { - let person1: Rc = Rc::new(Person::new("person1".into())); - let person2: Rc = Rc::new(Person::new("person2".into())); - let person3: Rc = Rc::new(Person::new("person3".into())); - let person4: Rc = Rc::new(Person::new("person4".into())); + 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(), 240.00, person1.clone()); let actors = vec![ @@ -208,27 +285,55 @@ mod tests { person3.clone(), person4.clone(), ]; + // equal split - let strategy = SplitStrategy::Equal(actors.clone()); - assert!(strategy.validate(&item)); + let strategy = SplitStrategy::Equal; + + let mut split = Split::new(Rc::new(item), strategy, actors); + assert!(split.split_strategy.validate(&split)); + let dues = split.split_strategy.money_owed(&split); + for (_actor_name,(actor, due)) in dues.iter() { + if split.purchase.paid_by().name() == actor.name() { + assert_eq!(*due, 0.00); + } else { + assert_eq!(*due, split.purchase.cost() / split.parties.len() as f64); + } + } // share split - let mut split = SplitHashMap::default(); - split.insert(person1.clone(), 0.3); - split.insert(person2.clone(), 0.4); - split.insert(person3.clone(), 0.2); - split.insert(person4.clone(), 0.1); - let strategy = SplitStrategy::Shares(split); - assert!(strategy.validate(&item)); + let mut shares = ShareHashMap::default(); + shares.insert(person1.name().to_owned(), (person1.clone(), 0.3)); + shares.insert(person2.name().to_owned(), (person2.clone(), 0.4)); + shares.insert(person3.name().to_owned(), (person3.clone(), 0.2)); + shares.insert(person4.name().to_owned(), (person4.clone(), 0.1)); + let strategy = SplitStrategy::Shares(shares.clone()); + + split.split_strategy = strategy; + assert!(split.split_strategy.validate(&split)); + + let dues = split.split_strategy.money_owed(&split); + 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, 0.00); + } else { + let (_, share) = shares.get(actor_name).unwrap(); + assert_eq!(*due, split.purchase.cost() * *share); + } + } // Unequal split - let mut split = SplitHashMap::default(); - split.insert(person1.clone(), 0.3 * item.cost()); - split.insert(person2.clone(), 0.4 * item.cost()); - split.insert(person3.clone(), 0.2 * item.cost()); - split.insert(person4.clone(), 0.1 * item.cost()); - let strategy = SplitStrategy::Unequal(split); - assert!(strategy.validate(&item)); + let mut shares = ShareHashMap::default(); + shares.insert(person1.name().to_owned(), (person1.clone(), 0.3 * split.purchase.cost())); + shares.insert(person2.name().to_owned(), (person2.clone(), 0.4 * split.purchase.cost())); + shares.insert(person3.name().to_owned(), (person3.clone(), 0.2 * split.purchase.cost())); + shares.insert(person4.name().to_owned(), (person4.clone(), 0.1 * split.purchase.cost())); + let strategy = SplitStrategy::Unequal(shares); + split.split_strategy = strategy; + assert!(split.split_strategy.validate(&split)); + + } + }