1use crate::units::{Capacity, Dimensionless};
3use std::cmp::Ordering;
4use std::ops::{Add, Sub};
5
6#[derive(Clone, PartialEq, Copy, Debug)]
8pub enum AssetCapacity {
9 Continuous(Capacity),
11 Discrete(u32, Capacity),
14}
15
16impl AssetCapacity {
17 pub fn min(self, other: AssetCapacity) -> AssetCapacity {
25 match self.partial_cmp(&other) {
26 None => panic!("Comparing invalid AssetCapacity values ({self:?} and {other:?})"),
27 Some(Ordering::Greater) => other,
28 _ => self,
29 }
30 }
31}
32
33impl Add for AssetCapacity {
34 type Output = Self;
35
36 fn add(self, rhs: AssetCapacity) -> Self {
38 match (self, rhs) {
39 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
40 AssetCapacity::Continuous(cap1 + cap2)
41 }
42 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
43 Self::check_same_unit_size(size1, size2);
44 AssetCapacity::Discrete(units1 + units2, size1)
45 }
46 _ => panic!("Cannot add different types of AssetCapacity ({self:?} and {rhs:?})"),
47 }
48 }
49}
50
51impl Sub for AssetCapacity {
52 type Output = Self;
53
54 fn sub(self, rhs: AssetCapacity) -> Self {
56 match (self, rhs) {
57 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
58 AssetCapacity::Continuous((cap1 - cap2).max(Capacity(0.0)))
59 }
60 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
61 Self::check_same_unit_size(size1, size2);
62 AssetCapacity::Discrete(units1 - units2.min(units1), size1)
63 }
64 _ => panic!("Cannot subtract different types of AssetCapacity ({self:?} and {rhs:?})"),
65 }
66 }
67}
68
69impl PartialOrd for AssetCapacity {
70 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
71 match (self, other) {
72 (AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.partial_cmp(b),
73 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
74 (*size1 == *size2).then(|| units1.cmp(units2))
76 }
77 _ => None,
78 }
79 }
80}
81
82impl AssetCapacity {
83 fn check_same_unit_size(size1: Capacity, size2: Capacity) {
85 assert_eq!(
86 size1, size2,
87 "Can't perform operation on capacities with different unit sizes ({size1} and {size2})",
88 );
89 }
90
91 pub fn from_capacity(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
97 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
98 match unit_size {
99 Some(size) => {
100 let num_units = (capacity / size).value().ceil() as u32;
101 AssetCapacity::Discrete(num_units, size)
102 }
103 None => AssetCapacity::Continuous(capacity),
104 }
105 }
106
107 pub fn from_capacity_floor(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
113 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
114 match unit_size {
115 Some(size) => {
116 let num_units = (capacity / size).value().floor() as u32;
117 AssetCapacity::Discrete(num_units, size)
118 }
119 None => AssetCapacity::Continuous(capacity),
120 }
121 }
122
123 pub fn total_capacity(&self) -> Capacity {
125 match self {
126 AssetCapacity::Continuous(cap) => *cap,
127 AssetCapacity::Discrete(units, size) => *size * Dimensionless(*units as f64),
128 }
129 }
130
131 pub fn n_units(&self) -> Option<u32> {
133 match self {
134 AssetCapacity::Continuous(_) => None,
135 AssetCapacity::Discrete(units, _) => Some(*units),
136 }
137 }
138
139 pub fn assert_same_type(&self, other: AssetCapacity) {
141 assert!(
142 matches!(self, AssetCapacity::Continuous(_))
143 == matches!(other, AssetCapacity::Continuous(_)),
144 "Cannot change capacity type"
145 );
146 }
147
148 pub fn apply_limit_factor(self, limit_factor: Dimensionless) -> Self {
153 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
154 match self {
155 AssetCapacity::Continuous(cap) => AssetCapacity::Continuous(cap * limit_factor),
156 AssetCapacity::Discrete(units, size) => {
157 let new_units = (units as f64 * limit_factor.value()).ceil() as u32;
158 AssetCapacity::Discrete(new_units, size)
159 }
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::units::{Capacity, Dimensionless};
168 use rstest::rstest;
169
170 #[rstest]
171 #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
172 #[case::rounded_up(Capacity(11.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
173 #[case::unit_size_greater_than_capacity(
174 Capacity(3.0),
175 Some(Capacity(4.0)),
176 Some(1),
177 Capacity(4.0)
178 )]
179 #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
180 fn from_capacity(
181 #[case] capacity: Capacity,
182 #[case] unit_size: Option<Capacity>,
183 #[case] expected_n: Option<u32>,
184 #[case] expected_total: Capacity,
185 ) {
186 let got = AssetCapacity::from_capacity(capacity, unit_size);
187 assert_eq!(got.n_units(), expected_n);
188 assert_eq!(got.total_capacity(), expected_total);
189 }
190
191 #[rstest]
192 #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
193 #[case::rounded_down(Capacity(11.0), Some(Capacity(4.0)), Some(2), Capacity(8.0))]
194 #[case::unit_size_greater_than_capacity(
195 Capacity(3.0),
196 Some(Capacity(4.0)),
197 Some(0),
198 Capacity(0.0)
199 )]
200 #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
201 fn from_capacity_floor(
202 #[case] capacity: Capacity,
203 #[case] unit_size: Option<Capacity>,
204 #[case] expected_n: Option<u32>,
205 #[case] expected_total: Capacity,
206 ) {
207 let got = AssetCapacity::from_capacity_floor(capacity, unit_size);
208 assert_eq!(got.n_units(), expected_n);
209 assert_eq!(got.total_capacity(), expected_total);
210 }
211
212 #[rstest]
213 #[case::round_up(3u32, Capacity(4.0), Dimensionless(0.5), 2u32)]
214 #[case::exact(3u32, Capacity(4.0), Dimensionless(0.33), 1u32)]
215 fn apply_limit_factor(
216 #[case] start_units: u32,
217 #[case] unit_size: Capacity,
218 #[case] factor: Dimensionless,
219 #[case] expected_units: u32,
220 ) {
221 let orig = AssetCapacity::Discrete(start_units, unit_size);
222 let got = orig.apply_limit_factor(factor);
223 assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
224 }
225
226 #[rstest]
227 #[case::less(
228 AssetCapacity::Continuous(Capacity(4.0)),
229 AssetCapacity::Continuous(Capacity(6.0)),
230 Some(Ordering::Less)
231 )]
232 #[case::equal(
233 AssetCapacity::Continuous(Capacity(4.0)),
234 AssetCapacity::Continuous(Capacity(4.0)),
235 Some(Ordering::Equal)
236 )]
237 #[case::greater(
238 AssetCapacity::Continuous(Capacity(6.0)),
239 AssetCapacity::Continuous(Capacity(4.0)),
240 Some(Ordering::Greater)
241 )]
242 fn partial_cmp_continuous(
243 #[case] left: AssetCapacity,
244 #[case] right: AssetCapacity,
245 #[case] expected: Option<Ordering>,
246 ) {
247 assert_eq!(left.partial_cmp(&right), expected);
248 assert_eq!(left == right, expected == Some(Ordering::Equal));
249 }
250
251 #[rstest]
252 #[case::less(
253 AssetCapacity::Discrete(2, Capacity(3.0)),
254 AssetCapacity::Discrete(4, Capacity(3.0)),
255 Some(Ordering::Less)
256 )]
257 #[case::equal(
258 AssetCapacity::Discrete(4, Capacity(3.0)),
259 AssetCapacity::Discrete(4, Capacity(3.0)),
260 Some(Ordering::Equal)
261 )]
262 #[case::greater(
263 AssetCapacity::Discrete(5, Capacity(3.0)),
264 AssetCapacity::Discrete(4, Capacity(3.0)),
265 Some(Ordering::Greater)
266 )]
267 fn partial_cmp_discrete_with_matching_unit_size(
268 #[case] left: AssetCapacity,
269 #[case] right: AssetCapacity,
270 #[case] expected: Option<Ordering>,
271 ) {
272 assert_eq!(left.partial_cmp(&right), expected);
273 assert_eq!(left == right, expected == Some(Ordering::Equal));
274 }
275
276 #[rstest]
277 #[case::mixed_types(
278 AssetCapacity::Continuous(Capacity(4.0)),
279 AssetCapacity::Discrete(4, Capacity(1.0))
280 )]
281 #[case::different_unit_sizes(
282 AssetCapacity::Discrete(4, Capacity(1.0)),
283 AssetCapacity::Discrete(4, Capacity(2.0))
284 )]
285 #[case::nan_continuous(
286 AssetCapacity::Continuous(Capacity(f64::NAN)),
287 AssetCapacity::Continuous(Capacity(4.0))
288 )]
289 fn partial_cmp_returns_none_for_invalid_comparisons(
290 #[case] left: AssetCapacity,
291 #[case] right: AssetCapacity,
292 ) {
293 assert_eq!(left.partial_cmp(&right), None);
294 assert!(left != right);
295 }
296
297 #[rstest]
298 #[case::continuous(
299 AssetCapacity::Continuous(Capacity(4.0)),
300 AssetCapacity::Continuous(Capacity(6.0)),
301 AssetCapacity::Continuous(Capacity(4.0))
302 )]
303 #[case::discrete(
304 AssetCapacity::Discrete(2, Capacity(3.0)),
305 AssetCapacity::Discrete(4, Capacity(3.0)),
306 AssetCapacity::Discrete(2, Capacity(3.0))
307 )]
308 fn min_returns_smaller_capacity(
309 #[case] left: AssetCapacity,
310 #[case] right: AssetCapacity,
311 #[case] expected: AssetCapacity,
312 ) {
313 assert_eq!(left.min(right), expected);
314 }
315
316 #[rstest]
317 #[case::mixed_types(
318 AssetCapacity::Continuous(Capacity(4.0)),
319 AssetCapacity::Discrete(4, Capacity(1.0))
320 )]
321 #[case::different_unit_sizes(
322 AssetCapacity::Discrete(4, Capacity(1.0)),
323 AssetCapacity::Discrete(4, Capacity(2.0))
324 )]
325 #[should_panic(expected = "Comparing invalid AssetCapacity values")]
326 fn min_panics_for_invalid_comparisons(
327 #[case] left: AssetCapacity,
328 #[case] right: AssetCapacity,
329 ) {
330 let _ = left.min(right);
331 }
332}