muse2/asset/
capacity.rs

1//! Represents the capacity of an asset
2use crate::units::{Capacity, Dimensionless};
3use std::cmp::Ordering;
4use std::ops::{Add, Sub};
5
6/// Capacity of an asset, which may be continuous or a discrete number of indivisible units
7#[derive(Clone, PartialEq, Copy, Debug)]
8pub enum AssetCapacity {
9    /// Continuous capacity
10    Continuous(Capacity),
11    /// Discrete capacity represented by a number of indivisible units
12    /// Stores: (number of units, unit size)
13    Discrete(u32, Capacity),
14}
15
16impl Add for AssetCapacity {
17    type Output = Self;
18
19    // Add two AssetCapacity values together
20    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    // Subtract rhs from self, ensuring that the result is non-negative
38    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    /// Validates that two discrete capacities have the same unit size.
75    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    /// Create an `AssetCapacity` from a total capacity and optional unit size
83    ///
84    /// If a unit size is provided, the capacity is represented as a discrete number of units,
85    /// calculated as the ceiling of (capacity / `unit_size`). If no unit size is provided, the
86    /// capacity is represented as continuous.
87    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    /// Create an `AssetCapacity` from a total capacity and optional unit size
99    ///
100    /// If a unit size is provided, the capacity is represented as a discrete number of units,
101    /// calculated as the floor of (capacity / `unit_size`). If no unit size is provided, the
102    /// capacity is represented as continuous.
103    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    /// Returns the total capacity represented by this `AssetCapacity`.
115    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    /// Returns the number of units if this is a discrete capacity, or `None` if continuous.
123    pub fn n_units(&self) -> Option<u32> {
124        match self {
125            AssetCapacity::Continuous(_) => None,
126            AssetCapacity::Discrete(units, _) => Some(*units),
127        }
128    }
129
130    /// Asserts that both capacities are the same type (both continuous or both discrete).
131    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    /// Applies a limit factor to the capacity, scaling it accordingly.
140    ///
141    /// For discrete capacities, the number of units is scaled by the limit factor and rounded up to
142    /// the nearest integer.
143    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}