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