diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7e72e1a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "split" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3a743d9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "split" +version = "0.1.0" +description = "split - libre clone of splitwise for sharing expenses amongs a group" +homepage = "https://git.batsense.net/realaravinth/split" +repository = "https://git.batsense.net/realaravinth/split" +documentation = "https://git.batsense.net/realaravinth/split" +license = "AGPLv3 or later version" +authors = ["realaravinth "] +edition = "2021" +#default-run = "split" +#build = "build.rs" + +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9935b49 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,234 @@ +/* + * 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; + +pub trait Purchasable { + fn cost(&self) -> f64; + fn name(&self) -> &str; + fn paid_by(&self) -> &dyn Actor; +} + +pub struct Split { + purchase: Rc, + split_strategy: SplitStrategy, +} + +#[derive(Default)] +pub struct SplitHashMap { + shares: HashMap, f64)>, +} + +impl SplitHashMap { + pub fn iter(&self) -> SplitHashMapIter { + let iter = self.shares.iter(); + SplitHashMapIter { iter } + } + + 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 + } + } +} + +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 + } + } +} + +impl Split { + pub fn validate(&self) -> bool { + self.split_strategy.validate(self.purchase.as_ref()) + } +} + +/* + * 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 + */ +pub enum SplitStrategy { + Unequal(SplitHashMap), + Shares(SplitHashMap), + Equal(Vec>), +} + +impl SplitStrategy { + pub fn validate(&self, item: &dyn Purchasable) -> bool { + match self { + SplitStrategy::Unequal(config) => { + let mut total = 0.00; + for (_actor, share) in config.iter() { + total += share; + } + + let cost = item.cost(); + println!("Unequal sharing: expected {cost} got {total}"); + total == cost + } + SplitStrategy::Shares(config) => { + let mut total = 0.00; + let cost = item.cost(); + let mut total_shares = 0.00; + for (_actor, share) in config.iter() { + total_shares += share; + total += cost * share; + } + + println!("Unequal sharing: expected shares 1 got {total_shares}"); + println!("Unequal sharing: expected {cost} got {total}"); + total_shares == 1.00 && total == cost + } + SplitStrategy::Equal(c) => !c.is_empty(), + } + } +} + +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; +} + +pub struct Person { + name: String, + spent: Vec, + owes: Vec, +} + +impl Person { + pub fn new(name: String) -> Self { + let owes = Vec::default(); + let spent = Vec::default(); + + Self { name, spent, owes } + } +} + +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 + } +} + +pub struct Item { + cost: f64, + name: String, + payer: Rc, +} + +impl Item { + pub fn new(name: String, cost: f64, payer: Rc) -> Self { + Self { cost, name, payer } + } +} + +impl Purchasable for Item { + fn cost(&self) -> f64 { + self.cost + } + fn name(&self) -> &str { + &self.name + } + + fn paid_by(&self) -> &dyn Actor { + self.payer.as_ref() + } +} + +//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())); + 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 item = Item::new("test item".into(), 240.00, person1.clone()); + let actors = vec![ + person1.clone(), + person2.clone(), + person3.clone(), + person4.clone(), + ]; + + // equal split + let strategy = SplitStrategy::Equal(actors.clone()); + assert!(strategy.validate(&item)); + + // 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)); + + // 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)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}