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 Add for AssetCapacity {
17 type Output = Self;
18
19 fn add(self, rhs: AssetCapacity) -> Self {
21 match (self, rhs) {
22 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
23 AssetCapacity::Continuous(cap1 + cap2)
24 }
25 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
26 Self::check_same_unit_size(size1, size2);
27 AssetCapacity::Discrete(units1 + units2, size1)
28 }
29 _ => panic!("Cannot add different types of AssetCapacity ({self:?} and {rhs:?})"),
30 }
31 }
32}
33
34impl Sub for AssetCapacity {
35 type Output = Self;
36
37 fn sub(self, rhs: AssetCapacity) -> Self {
39 match (self, rhs) {
40 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
41 AssetCapacity::Continuous((cap1 - cap2).max(Capacity(0.0)))
42 }
43 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
44 Self::check_same_unit_size(size1, size2);
45 AssetCapacity::Discrete(units1 - units2.min(units1), size1)
46 }
47 _ => panic!("Cannot subtract different types of AssetCapacity ({self:?} and {rhs:?})"),
48 }
49 }
50}
51
52impl Eq for AssetCapacity {}
53
54impl PartialOrd for AssetCapacity {
55 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
56 Some(self.cmp(other))
57 }
58}
59
60impl Ord for AssetCapacity {
61 fn cmp(&self, other: &Self) -> Ordering {
62 match (self, other) {
63 (AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.total_cmp(b),
64 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
65 Self::check_same_unit_size(*size1, *size2);
66 units1.cmp(units2)
67 }
68 _ => panic!("Cannot compare different types of AssetCapacity ({self:?} and {other:?})"),
69 }
70 }
71}
72
73impl AssetCapacity {
74 fn check_same_unit_size(size1: Capacity, size2: Capacity) {
76 assert_eq!(
77 size1, size2,
78 "Can't perform operation on capacities with different unit sizes ({size1} and {size2})",
79 );
80 }
81
82 pub fn from_capacity(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
88 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
89 match unit_size {
90 Some(size) => {
91 let num_units = (capacity / size).value().ceil() as u32;
92 AssetCapacity::Discrete(num_units, size)
93 }
94 None => AssetCapacity::Continuous(capacity),
95 }
96 }
97
98 pub fn from_capacity_floor(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
104 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
105 match unit_size {
106 Some(size) => {
107 let num_units = (capacity / size).value().floor() as u32;
108 AssetCapacity::Discrete(num_units, size)
109 }
110 None => AssetCapacity::Continuous(capacity),
111 }
112 }
113
114 pub fn total_capacity(&self) -> Capacity {
116 match self {
117 AssetCapacity::Continuous(cap) => *cap,
118 AssetCapacity::Discrete(units, size) => *size * Dimensionless(*units as f64),
119 }
120 }
121
122 pub fn n_units(&self) -> Option<u32> {
124 match self {
125 AssetCapacity::Continuous(_) => None,
126 AssetCapacity::Discrete(units, _) => Some(*units),
127 }
128 }
129
130 pub fn assert_same_type(&self, other: AssetCapacity) {
132 assert!(
133 matches!(self, AssetCapacity::Continuous(_))
134 == matches!(other, AssetCapacity::Continuous(_)),
135 "Cannot change capacity type"
136 );
137 }
138
139 pub fn apply_limit_factor(self, limit_factor: Dimensionless) -> Self {
144 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
145 match self {
146 AssetCapacity::Continuous(cap) => AssetCapacity::Continuous(cap * limit_factor),
147 AssetCapacity::Discrete(units, size) => {
148 let new_units = (units as f64 * limit_factor.value()).ceil() as u32;
149 AssetCapacity::Discrete(new_units, size)
150 }
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::units::{Capacity, Dimensionless};
159 use rstest::rstest;
160
161 #[rstest]
162 #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
163 #[case::rounded_up(Capacity(11.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
164 #[case::unit_size_greater_than_capacity(
165 Capacity(3.0),
166 Some(Capacity(4.0)),
167 Some(1),
168 Capacity(4.0)
169 )]
170 #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
171 fn from_capacity(
172 #[case] capacity: Capacity,
173 #[case] unit_size: Option<Capacity>,
174 #[case] expected_n: Option<u32>,
175 #[case] expected_total: Capacity,
176 ) {
177 let got = AssetCapacity::from_capacity(capacity, unit_size);
178 assert_eq!(got.n_units(), expected_n);
179 assert_eq!(got.total_capacity(), expected_total);
180 }
181
182 #[rstest]
183 #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
184 #[case::rounded_down(Capacity(11.0), Some(Capacity(4.0)), Some(2), Capacity(8.0))]
185 #[case::unit_size_greater_than_capacity(
186 Capacity(3.0),
187 Some(Capacity(4.0)),
188 Some(0),
189 Capacity(0.0)
190 )]
191 #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
192 fn from_capacity_floor(
193 #[case] capacity: Capacity,
194 #[case] unit_size: Option<Capacity>,
195 #[case] expected_n: Option<u32>,
196 #[case] expected_total: Capacity,
197 ) {
198 let got = AssetCapacity::from_capacity_floor(capacity, unit_size);
199 assert_eq!(got.n_units(), expected_n);
200 assert_eq!(got.total_capacity(), expected_total);
201 }
202
203 #[rstest]
204 #[case::round_up(3u32, Capacity(4.0), Dimensionless(0.5), 2u32)]
205 #[case::exact(3u32, Capacity(4.0), Dimensionless(0.33), 1u32)]
206 fn apply_limit_factor(
207 #[case] start_units: u32,
208 #[case] unit_size: Capacity,
209 #[case] factor: Dimensionless,
210 #[case] expected_units: u32,
211 ) {
212 let orig = AssetCapacity::Discrete(start_units, unit_size);
213 let got = orig.apply_limit_factor(factor);
214 assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
215 }
216}