feat: def actor, item and sharing strategies and related tests
This commit is contained in:
parent
6b3a0f8eeb
commit
3a2708091c
4 changed files with 258 additions and 0 deletions
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal 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
14
Cargo.toml
Normal 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
234
src/lib.rs
Normal 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
3
src/main.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
Loading…
Reference in a new issue