1use bytemuck::{Pod, Zeroable};
11#[cfg(feature = "decimal")]
12use rust_decimal::Decimal;
13
14use crate::error::BasisPointsError;
15pub mod error;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Zeroable, Pod)]
18#[cfg_attr(
19 feature = "anchor",
20 derive(
21 anchor_lang::InitSpace,
22 anchor_lang::prelude::AnchorSerialize,
23 anchor_lang::prelude::AnchorDeserialize
24 )
25)]
26#[repr(C)]
27pub struct BasisPoints(u16);
29
30impl BasisPoints {
31 pub const MAX: u16 = 10000;
33
34 pub fn new(bps: u16) -> Result<Self, BasisPointsError> {
38 if bps > Self::MAX {
39 return Err(BasisPointsError::InvalidBasisPoints);
40 }
41
42 Ok(Self(bps))
43 }
44
45 pub fn checked_add(self, rhs: Self) -> Option<Self> {
47 let lhs = u16::from(self);
48 let rhs = u16::from(rhs);
49 let result = lhs.checked_add(rhs)?;
50 Self::new(result).ok()
51 }
52
53 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
55 let lhs = u16::from(self);
56 let rhs = u16::from(rhs);
57 let result = lhs.checked_sub(rhs)?;
58 Self::new(result).ok()
59 }
60
61 pub fn checked_mul(self, rhs: Self) -> Option<Self> {
63 let lhs = u16::from(self);
64 let rhs = u16::from(rhs);
65 let result = lhs.checked_mul(rhs)?;
66 Self::new(result).ok()
67 }
68
69 pub fn checked_div(self, rhs: Self) -> Option<Self> {
71 let lhs = u16::from(self);
72 let rhs = u16::from(rhs);
73 let result = lhs.checked_div(rhs)?;
74 Self::new(result).ok()
75 }
76}
77
78impl From<BasisPoints> for u16 {
79 fn from(bps: BasisPoints) -> Self {
80 bps.0
81 }
82}
83
84impl From<BasisPoints> for u32 {
85 fn from(bps: BasisPoints) -> Self {
86 u32::from(bps.0)
87 }
88}
89
90impl From<BasisPoints> for u64 {
91 fn from(bps: BasisPoints) -> Self {
92 u64::from(bps.0)
93 }
94}
95
96impl From<BasisPoints> for u128 {
97 fn from(bps: BasisPoints) -> Self {
98 u128::from(bps.0)
99 }
100}
101
102#[cfg(feature = "decimal")]
103impl From<BasisPoints> for Decimal {
104 fn from(bps: BasisPoints) -> Self {
106 Decimal::from(bps.0) / Decimal::from(BasisPoints::MAX)
107 }
108}
109
110#[cfg(feature = "decimal")]
111impl TryFrom<Decimal> for BasisPoints {
112 type Error = BasisPointsError;
113
114 fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
118 let bps = (decimal * Decimal::from(BasisPoints::MAX))
119 .try_into()
120 .map_err(|_| BasisPointsError::ConversionFailed)?;
121 BasisPoints::new(bps)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_new() {
131 assert_eq!(BasisPoints::new(10000), Ok(BasisPoints(10000)));
132 }
133
134 #[test]
135 fn test_new_error() {
136 assert_eq!(
137 BasisPoints::new(10001),
138 Err(BasisPointsError::InvalidBasisPoints)
139 );
140 }
141
142 #[test]
143 fn test_checked_add() {
144 let lhs = BasisPoints::new(2500).unwrap();
145 let rhs = BasisPoints::new(2500).unwrap();
146
147 assert_eq!(lhs.checked_add(rhs), BasisPoints::new(5000).ok());
148 }
149
150 #[test]
151 fn test_checked_add_overflow() {
152 let lhs = BasisPoints::new(9000).unwrap();
153 let rhs = BasisPoints::new(2000).unwrap();
154
155 assert_eq!(lhs.checked_add(rhs), None);
156 }
157
158 #[test]
159 fn test_checked_sub() {
160 let lhs = BasisPoints::new(2500).unwrap();
161 let rhs = BasisPoints::new(500).unwrap();
162
163 assert_eq!(lhs.checked_sub(rhs), BasisPoints::new(2000).ok());
164 }
165
166 #[test]
167 fn test_checked_sub_underflow() {
168 let lhs = BasisPoints::new(500).unwrap();
169 let rhs = BasisPoints::new(2500).unwrap();
170
171 assert_eq!(lhs.checked_sub(rhs), None);
172 }
173
174 #[test]
175 fn test_checked_mul() {
176 let lhs = BasisPoints::new(100).unwrap();
177 let rhs = BasisPoints::new(50).unwrap();
178
179 assert_eq!(lhs.checked_mul(rhs), BasisPoints::new(5000).ok());
180 }
181
182 #[test]
183 fn test_checked_mul_overflow() {
184 let lhs = BasisPoints::new(200).unwrap();
185 let rhs = BasisPoints::new(100).unwrap();
186
187 assert_eq!(lhs.checked_mul(rhs), None);
188 }
189
190 #[test]
191 fn test_checked_div() {
192 let lhs = BasisPoints::new(5000).unwrap();
193 let rhs = BasisPoints::new(100).unwrap();
194
195 assert_eq!(lhs.checked_div(rhs), BasisPoints::new(50).ok());
196 }
197
198 #[test]
199 fn test_checked_div_by_zero() {
200 let lhs = BasisPoints::new(5000).unwrap();
201 let rhs = BasisPoints::new(0).unwrap();
202
203 assert_eq!(lhs.checked_div(rhs), None);
204 }
205
206 #[cfg(feature = "decimal")]
207 #[test]
208 fn test_from_basis_points_to_decimal() {
209 let bps = BasisPoints::new(1234).unwrap();
210 let decimal: Decimal = bps.into();
211
212 assert_eq!(decimal, Decimal::new(1234, 4));
213 }
214
215 #[cfg(feature = "decimal")]
216 #[test]
217 fn test_from_basis_points_max_to_decimal_one() {
218 let bps = BasisPoints::new(BasisPoints::MAX).unwrap();
219 let decimal: Decimal = bps.into();
220
221 assert_eq!(decimal, Decimal::ONE);
222 }
223
224 #[cfg(feature = "decimal")]
225 #[test]
226 fn test_try_from_decimal_to_basis_points() {
227 let decimal = Decimal::new(4321, 4);
228 let bps = BasisPoints::try_from(decimal);
229
230 assert_eq!(bps, Ok(BasisPoints(4321)));
231 }
232
233 #[cfg(feature = "decimal")]
234 #[test]
235 fn test_round_trip_basis_points_decimal_basis_points() {
236 let original = BasisPoints::new(9999).unwrap();
237 let decimal: Decimal = original.into();
238 let round_trip = BasisPoints::try_from(decimal);
239
240 assert_eq!(round_trip, Ok(original));
241 }
242
243 #[cfg(feature = "decimal")]
244 #[test]
245 fn test_try_from_decimal_fractional_truncates() {
246 let decimal = Decimal::new(12345, 5); let bps = BasisPoints::try_from(decimal);
248
249 assert_eq!(bps, Ok(BasisPoints(1234)));
250 }
251
252 #[cfg(feature = "decimal")]
253 #[test]
254 fn test_try_from_decimal_above_basis_points_max_error() {
255 let decimal = Decimal::new(10001, 4); let bps = BasisPoints::try_from(decimal);
257
258 assert_eq!(bps, Err(BasisPointsError::InvalidBasisPoints));
259 }
260
261 #[cfg(feature = "decimal")]
262 #[test]
263 fn test_try_from_decimal_u16_overflow_error() {
264 let decimal = Decimal::new(7, 0); let bps = BasisPoints::try_from(decimal);
266
267 assert_eq!(bps, Err(BasisPointsError::ConversionFailed));
268 }
269}