feat: def actor, item and sharing strategies and related tests

This commit is contained in:
Aravinth Manivannan 2022-07-19 22:55:00 +05:30
parent 6b3a0f8eeb
commit 3a2708091c
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
4 changed files with 258 additions and 0 deletions

7
Cargo.lock generated Normal file
View file

@ -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"

14
Cargo.toml Normal file
View file

@ -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 <realaravinth@batsense.net>"]
edition = "2021"
#default-run = "split"
#build = "build.rs"
[dependencies]

234
src/lib.rs Normal file
View file

@ -0,0 +1,234 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<dyn Purchasable>,
split_strategy: SplitStrategy,
}
#[derive(Default)]
pub struct SplitHashMap {
shares: HashMap<String, (Rc<dyn Actor>, f64)>,
}
impl SplitHashMap {
pub fn iter(&self) -> SplitHashMapIter {
let iter = self.shares.iter();
SplitHashMapIter { iter }
}
pub fn insert(&mut self, actor: Rc<dyn Actor>, amount: f64) {
let name = actor.name().to_owned();
self.shares.insert(name, (actor, amount));
}
pub fn get(&mut self, actor: Rc<dyn Actor>) -> Option<f64> {
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<dyn Actor>, f64)>,
}
impl<'a> Iterator for SplitHashMapIter<'a> {
type Item = (Rc<dyn Actor>, f64);
fn next(&mut self) -> Option<Self::Item> {
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<Rc<dyn Actor>>),
}
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<Split>,
owes: Vec<Split>,
}
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<dyn Actor>,
}
impl Item {
pub fn new(name: String, cost: f64, payer: Rc<dyn Actor>) -> 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<P: Purchasable>(&self, i: &I);
// fn owes<T: Actor>(&self, amount: usize);
//}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_works() {
let person1: Rc<dyn Actor> = Rc::new(Person::new("person1".into()));
let person2: Rc<dyn Actor> = Rc::new(Person::new("person2".into()));
let person3: Rc<dyn Actor> = Rc::new(Person::new("person3".into()));
let person4: Rc<dyn Actor> = 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));
}
}

3
src/main.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}