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
42pub 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), 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), 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), 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), 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), 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), 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), 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), 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), 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}