1use crate::units::{Capacity, Dimensionless};
3use std::cmp::Ordering;
4use std::ops::{Add, Sub};
5
6#[derive(Clone, PartialEq, Copy, Debug)]
8pub struct AssetCapacity {
9 num_tranches: u32,
11 tranche_size: Capacity,
13}
14
15impl AssetCapacity {
16 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 pub fn single(tranche_size: Capacity) -> Self {
30 Self::new(1, tranche_size)
31 }
32
33 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 pub fn num_tranches(&self) -> u32 {
48 self.num_tranches
49 }
50
51 pub fn tranche_size(&self) -> Capacity {
53 self.tranche_size
54 }
55
56 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 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 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 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}