Skip to main content

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, expressed in terms of a number of discrete tranches of a given size.
7#[derive(Clone, PartialEq, Copy, Debug)]
8pub struct AssetCapacity {
9    /// Number of tranches
10    num_tranches: u32,
11    /// Size of each tranche
12    tranche_size: Capacity,
13}
14
15impl AssetCapacity {
16    /// Create a new `AssetCapacity` with the given number of tranches and tranche size
17    pub fn new(num_tranches: u32, tranche_size: Capacity) -> Self {
18        assert!(
19            tranche_size.is_finite() && tranche_size >= Capacity(0.0),
20            "Tranche size must be a finite non-negative number"
21        );
22        AssetCapacity {
23            num_tranches,
24            tranche_size,
25        }
26    }
27
28    /// Create a new `AssetCapacity` with a single tranche of the given size
29    pub fn single(tranche_size: Capacity) -> Self {
30        Self::new(1, tranche_size)
31    }
32
33    /// Return the smaller of `self` or `other`.
34    ///
35    /// # Panics
36    ///
37    /// Panics if the tranche size differs.
38    pub fn min(self, other: AssetCapacity) -> AssetCapacity {
39        match self.partial_cmp(&other) {
40            None => panic!("Comparing invalid AssetCapacity values ({self:?} and {other:?})"),
41            Some(Ordering::Greater) => other,
42            _ => self,
43        }
44    }
45
46    /// Returns the number of tranches in this `AssetCapacity`.
47    pub fn num_tranches(&self) -> u32 {
48        self.num_tranches
49    }
50
51    /// Returns the tranche size of this `AssetCapacity`.
52    pub fn tranche_size(&self) -> Capacity {
53        self.tranche_size
54    }
55
56    /// Validates that two capacities have the same tranche size.
57    fn check_same_tranche_size(&self, other: AssetCapacity) {
58        assert_eq!(
59            self.tranche_size, other.tranche_size,
60            "Can't perform operation on capacities with different tranche sizes ({} and {})",
61            self.tranche_size, other.tranche_size,
62        );
63    }
64
65    /// Returns the total capacity represented by this `AssetCapacity`.
66    pub fn total_capacity(&self) -> Capacity {
67        self.tranche_size * Dimensionless(self.num_tranches as f64)
68    }
69}
70
71impl Add for AssetCapacity {
72    type Output = Self;
73
74    // Add two AssetCapacity values together
75    fn add(self, rhs: AssetCapacity) -> Self {
76        self.check_same_tranche_size(rhs);
77        AssetCapacity {
78            num_tranches: self.num_tranches + rhs.num_tranches,
79            tranche_size: self.tranche_size,
80        }
81    }
82}
83
84impl Sub for AssetCapacity {
85    type Output = Self;
86
87    // Subtract rhs from self, ensuring that the result is non-negative
88    fn sub(self, rhs: AssetCapacity) -> Self {
89        self.check_same_tranche_size(rhs);
90        assert!(
91            self.num_tranches >= rhs.num_tranches,
92            "Cannot subtract a larger AssetCapacity ({rhs:?}) from a smaller one ({self:?})"
93        );
94        AssetCapacity {
95            num_tranches: self.num_tranches - rhs.num_tranches,
96            tranche_size: self.tranche_size,
97        }
98    }
99}
100
101impl PartialOrd for AssetCapacity {
102    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
103        let size1 = self.tranche_size;
104        let size2 = other.tranche_size;
105        (size1 == size2).then(|| self.num_tranches.cmp(&other.num_tranches))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::units::Capacity;
113    use rstest::rstest;
114
115    #[rstest]
116    #[case(1, Capacity(1.0), Capacity(1.0))]
117    #[case(2, Capacity(3.5), Capacity(7.0))]
118    #[case(u32::MAX, Capacity(0.0), Capacity(0.0))]
119    fn new_works(
120        #[case] num_tranches: u32,
121        #[case] tranche_size: Capacity,
122        #[case] expected_total_capacity: Capacity,
123    ) {
124        let capacity = AssetCapacity::new(num_tranches, tranche_size);
125
126        assert_eq!(capacity.num_tranches(), num_tranches);
127        assert_eq!(capacity.tranche_size(), tranche_size);
128        assert_eq!(capacity.total_capacity(), expected_total_capacity);
129    }
130
131    #[rstest]
132    #[case(Capacity(-1.0))]
133    #[case(Capacity(f64::INFINITY))]
134    #[case(Capacity(f64::NEG_INFINITY))]
135    #[case(Capacity(f64::NAN))]
136    #[should_panic(expected = "Tranche size must be a finite non-negative number")]
137    fn new_rejects_non_finite_tranche_size(#[case] tranche_size: Capacity) {
138        let num_tranches = 1;
139        let _ = AssetCapacity::new(num_tranches, tranche_size);
140    }
141
142    #[rstest]
143    #[case::less(
144        AssetCapacity::new(2, Capacity(3.0)),
145        AssetCapacity::new(4, Capacity(3.0)),
146        Some(Ordering::Less)
147    )]
148    #[case::equal(
149        AssetCapacity::new(4, Capacity(3.0)),
150        AssetCapacity::new(4, Capacity(3.0)),
151        Some(Ordering::Equal)
152    )]
153    #[case::greater(
154        AssetCapacity::new(5, Capacity(3.0)),
155        AssetCapacity::new(4, Capacity(3.0)),
156        Some(Ordering::Greater)
157    )]
158    fn partial_cmp_with_matching_tranche_size(
159        #[case] left: AssetCapacity,
160        #[case] right: AssetCapacity,
161        #[case] expected: Option<Ordering>,
162    ) {
163        assert_eq!(left.partial_cmp(&right), expected);
164        assert_eq!(left == right, expected == Some(Ordering::Equal));
165    }
166
167    #[rstest]
168    #[case::different_tranche_sizes(
169        AssetCapacity::new(4, Capacity(1.0)),
170        AssetCapacity::new(4, Capacity(2.0))
171    )]
172    fn partial_cmp_returns_none_for_invalid_comparisons(
173        #[case] left: AssetCapacity,
174        #[case] right: AssetCapacity,
175    ) {
176        assert_eq!(left.partial_cmp(&right), None);
177        assert_ne!(left, right);
178    }
179
180    #[rstest]
181    #[case::discrete(
182        AssetCapacity::new(2, Capacity(3.0)),
183        AssetCapacity::new(4, Capacity(3.0)),
184        AssetCapacity::new(2, Capacity(3.0))
185    )]
186    fn min_returns_smaller_capacity(
187        #[case] left: AssetCapacity,
188        #[case] right: AssetCapacity,
189        #[case] expected: AssetCapacity,
190    ) {
191        assert_eq!(left.min(right), expected);
192    }
193
194    #[rstest]
195    #[case::different_tranche_sizes(
196        AssetCapacity::new(4, Capacity(1.0)),
197        AssetCapacity::new(4, Capacity(2.0))
198    )]
199    #[should_panic(expected = "Comparing invalid AssetCapacity values")]
200    fn min_panics_for_invalid_comparisons(
201        #[case] left: AssetCapacity,
202        #[case] right: AssetCapacity,
203    ) {
204        let _ = left.min(right);
205    }
206
207    #[test]
208    fn subtracting_equal_capacities_returns_zero_tranches() {
209        let capacity = AssetCapacity::new(2, Capacity(3.0));
210
211        assert_eq!(
212            capacity - capacity,
213            AssetCapacity {
214                num_tranches: 0,
215                tranche_size: Capacity(3.0),
216            }
217        );
218    }
219}