use crate::{error::DineroError, Dinero};
fn compare(remainder: i128, is_positive: bool) -> bool {
if is_positive {
remainder > 0
} else {
remainder < 0
}
}
fn distribute(value: i128, ratios: Vec<i128>) -> Vec<i128> {
let total: i128 = ratios.iter().sum();
if total == 0 {
ratios
} else {
let mut remainder = value;
let mut shares: Vec<i128> = ratios
.iter()
.map(|ratio| -> i128 {
let share = (value * ratio) / total;
remainder -= share;
share
})
.collect();
let is_positive = value > 0;
let amount = if is_positive { 1 } else { -1 };
let mut i = 0;
while compare(remainder, is_positive) {
if ratios[i] != 0 {
shares[i] += amount;
remainder -= amount;
}
i += 1;
}
shares
}
}
pub fn allocate(item: &Dinero, ratios: Vec<i128>) -> Result<Vec<Dinero>, DineroError> {
if ratios.is_empty() {
Err(DineroError::EmptyRatiosError)?
} else {
let has_only_positive_ratios = ratios.iter().all(|ratio| *ratio >= 0);
if !has_only_positive_ratios {
Err(DineroError::NegativeRatiosError)?
} else {
let shares = distribute(item.amount, ratios);
let result: Vec<Dinero> = shares
.iter()
.map(|share| Dinero::new(*share, item.currency, Some(item.scale)))
.collect();
Ok(result)
}
}
}
#[cfg(test)]
#[cfg(not(tarpaulin_include))]
mod tests {
use super::*;
use crate::currencies::USD;
use pretty_assertions::assert_eq;
use std::{error::Error, vec};
#[test]
fn test_ratios_empty() {
let result = allocate(&Dinero::new(42, USD, None), vec![]);
assert!(result.is_err());
match result {
Err(e) => assert_eq!(
format!("{:?}", e),
format!("{:?}", DineroError::EmptyRatiosError)
),
_ => panic!("allocate should not return value for empty vector"),
}
}
#[test]
fn test_ratios_negative() {
let result = allocate(&Dinero::new(42, USD, None), vec![1, -2, 3]);
assert!(result.is_err());
match result {
Err(e) => assert_eq!(
format!("{:?}", e),
format!("{:?}", DineroError::NegativeRatiosError)
),
_ => panic!("allocate should not return value for negative vector values"),
}
}
#[test]
fn test_ratios_half() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(42, USD, None), vec![50, 50])?,
vec![
Dinero::new(21, USD, None), Dinero::new(21, USD, None)
]
);
assert_eq!(
allocate(&Dinero::new(1003, USD, None), vec![50, 50])?,
vec![
Dinero::new(502, USD, None), Dinero::new(501, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_ignore_zero() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(1003, USD, None), vec![0, 50, 50])?,
vec![
Dinero::new(0, USD, None), Dinero::new(502, USD, None),
Dinero::new(501, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_ignore_all_zeros() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(1003, USD, None), vec![0, 0, 0])?,
vec![
Dinero::new(0, USD, None), Dinero::new(0, USD, None),
Dinero::new(0, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_negative_amount() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(-1003, USD, None), vec![50, 50])?,
vec![
Dinero::new(-502, USD, None), Dinero::new(-501, USD, None),
]
);
Ok(())
}
#[test]
fn test_ratios_1_3() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(100, USD, None), vec![1, 3])?,
vec![
Dinero::new(25, USD, None), Dinero::new(75, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_20_40() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(42, USD, None), vec![20, 40])?,
vec![
Dinero::new(14, USD, None), Dinero::new(28, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_even() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(43, USD, None), vec![20, 40])?,
vec![
Dinero::new(15, USD, None), Dinero::new(28, USD, None)
]
);
Ok(())
}
#[test]
fn test_ratios_long() -> Result<(), Box<dyn Error>> {
assert_eq!(
allocate(&Dinero::new(142, USD, None), vec![20, 10, 22, 1, 40])?,
vec![
Dinero::new(31, USD, None), Dinero::new(16, USD, None),
Dinero::new(33, USD, None),
Dinero::new(1, USD, None),
Dinero::new(61, USD, None),
]
);
Ok(())
}
}