dinero/api/
allocate.rs

1use crate::{error::DineroError, Dinero};
2
3fn compare(remainder: i128, is_positive: bool) -> bool {
4    if is_positive {
5        remainder > 0
6    } else {
7        remainder < 0
8    }
9}
10
11fn distribute(value: i128, ratios: Vec<i128>) -> Vec<i128> {
12    let total: i128 = ratios.iter().sum();
13    if total == 0 {
14        ratios
15    } else {
16        let mut remainder = value;
17
18        let mut shares: Vec<i128> = ratios
19            .iter()
20            .map(|ratio| -> i128 {
21                let share = (value * ratio) / total;
22                remainder -= share;
23                share
24            })
25            .collect();
26
27        let is_positive = value > 0;
28        let amount = if is_positive { 1 } else { -1 };
29
30        let mut i = 0;
31        while compare(remainder, is_positive) {
32            if ratios[i] != 0 {
33                shares[i] += amount;
34                remainder -= amount;
35            }
36            i += 1;
37        }
38        shares
39    }
40}
41
42/// Distribute the amount of a Dinero object across a list of ratios.
43///
44/// Monetary values have indivisible units, meaning you can't always exactly split them. With allocate, you can split a monetary amount then distribute the remainder as evenly as possible.
45pub fn allocate(item: &Dinero, ratios: Vec<i128>) -> Result<Vec<Dinero>, DineroError> {
46    if ratios.is_empty() {
47        Err(DineroError::EmptyRatiosError)?
48    } else {
49        let has_only_positive_ratios = ratios.iter().all(|ratio| *ratio >= 0);
50
51        if !has_only_positive_ratios {
52            Err(DineroError::NegativeRatiosError)?
53        } else {
54            let shares = distribute(item.amount, ratios);
55            let result: Vec<Dinero> = shares
56                .iter()
57                .map(|share| Dinero::new(*share, item.currency, Some(item.scale)))
58                .collect();
59
60            Ok(result)
61        }
62    }
63}
64
65#[cfg(test)]
66#[cfg(not(tarpaulin_include))]
67mod tests {
68    use super::*;
69    use crate::currencies::USD;
70    use pretty_assertions::assert_eq;
71    use std::{error::Error, vec};
72
73    #[test]
74    fn test_ratios_empty() {
75        let result = allocate(&Dinero::new(42, USD, None), vec![]);
76        assert!(result.is_err());
77        match result {
78            Err(e) => assert_eq!(
79                format!("{:?}", e),
80                format!("{:?}", DineroError::EmptyRatiosError)
81            ),
82            _ => panic!("allocate should not return value for empty vector"),
83        }
84    }
85
86    #[test]
87    fn test_ratios_negative() {
88        let result = allocate(&Dinero::new(42, USD, None), vec![1, -2, 3]);
89        assert!(result.is_err());
90        match result {
91            Err(e) => assert_eq!(
92                format!("{:?}", e),
93                format!("{:?}", DineroError::NegativeRatiosError)
94            ),
95            _ => panic!("allocate should not return value for negative vector values"),
96        }
97    }
98
99    #[test]
100    fn test_ratios_half() -> Result<(), Box<dyn Error>> {
101        assert_eq!(
102            allocate(&Dinero::new(42, USD, None), vec![50, 50])?,
103            vec![
104                Dinero::new(21, USD, None), //
105                Dinero::new(21, USD, None)
106            ]
107        );
108
109        assert_eq!(
110            allocate(&Dinero::new(1003, USD, None), vec![50, 50])?,
111            vec![
112                Dinero::new(502, USD, None), //
113                Dinero::new(501, USD, None)
114            ]
115        );
116        Ok(())
117    }
118
119    #[test]
120    fn test_ratios_ignore_zero() -> Result<(), Box<dyn Error>> {
121        assert_eq!(
122            allocate(&Dinero::new(1003, USD, None), vec![0, 50, 50])?,
123            vec![
124                Dinero::new(0, USD, None), //
125                Dinero::new(502, USD, None),
126                Dinero::new(501, USD, None)
127            ]
128        );
129        Ok(())
130    }
131
132    #[test]
133    fn test_ratios_ignore_all_zeros() -> Result<(), Box<dyn Error>> {
134        assert_eq!(
135            allocate(&Dinero::new(1003, USD, None), vec![0, 0, 0])?,
136            vec![
137                Dinero::new(0, USD, None), //
138                Dinero::new(0, USD, None),
139                Dinero::new(0, USD, None)
140            ]
141        );
142        Ok(())
143    }
144
145    #[test]
146    fn test_ratios_negative_amount() -> Result<(), Box<dyn Error>> {
147        assert_eq!(
148            allocate(&Dinero::new(-1003, USD, None), vec![50, 50])?,
149            vec![
150                Dinero::new(-502, USD, None), //
151                Dinero::new(-501, USD, None),
152            ]
153        );
154        Ok(())
155    }
156
157    #[test]
158    fn test_ratios_1_3() -> Result<(), Box<dyn Error>> {
159        assert_eq!(
160            allocate(&Dinero::new(100, USD, None), vec![1, 3])?,
161            vec![
162                Dinero::new(25, USD, None), //
163                Dinero::new(75, USD, None)
164            ]
165        );
166        Ok(())
167    }
168
169    #[test]
170    fn test_ratios_20_40() -> Result<(), Box<dyn Error>> {
171        assert_eq!(
172            allocate(&Dinero::new(42, USD, None), vec![20, 40])?,
173            vec![
174                Dinero::new(14, USD, None), //
175                Dinero::new(28, USD, None)
176            ]
177        );
178        Ok(())
179    }
180
181    #[test]
182    fn test_ratios_even() -> Result<(), Box<dyn Error>> {
183        assert_eq!(
184            allocate(&Dinero::new(43, USD, None), vec![20, 40])?,
185            vec![
186                Dinero::new(15, USD, None), //
187                Dinero::new(28, USD, None)
188            ]
189        );
190        Ok(())
191    }
192
193    #[test]
194    fn test_ratios_long() -> Result<(), Box<dyn Error>> {
195        assert_eq!(
196            allocate(&Dinero::new(142, USD, None), vec![20, 10, 22, 1, 40])?,
197            vec![
198                Dinero::new(31, USD, None), //
199                Dinero::new(16, USD, None),
200                Dinero::new(33, USD, None),
201                Dinero::new(1, USD, None),
202                Dinero::new(61, USD, None),
203            ]
204        );
205        Ok(())
206    }
207}