Skip to main content

basis_points/
lib.rs

1use bytemuck::{Pod, Zeroable};
2#[cfg(feature = "decimal")]
3use rust_decimal::Decimal;
4
5use crate::error::BasisPointsError;
6pub mod error;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Zeroable, Pod)]
9#[cfg_attr(
10    feature = "anchor",
11    derive(
12        anchor_lang::InitSpace,
13        anchor_lang::prelude::AnchorSerialize,
14        anchor_lang::prelude::AnchorDeserialize
15    )
16)]
17#[repr(C)]
18/// A validated basis-points value in the inclusive range `0..=10_000`.
19pub struct BasisPoints(u16);
20
21impl BasisPoints {
22    /// The maximum valid basis-points value (`10000`, equal to 100%).
23    pub const MAX: u16 = 10000;
24
25    /// Creates a new [`BasisPoints`] after validating the input range.
26    ///
27    /// Returns [`BasisPointsError::InvalidBasisPoints`] if `bps > 10_000`.
28    pub fn new(bps: u16) -> Result<Self, BasisPointsError> {
29        if bps > Self::MAX {
30            return Err(BasisPointsError::InvalidBasisPoints);
31        }
32
33        Ok(Self(bps))
34    }
35}
36
37impl From<BasisPoints> for u16 {
38    fn from(bps: BasisPoints) -> Self {
39        bps.0
40    }
41}
42
43impl From<BasisPoints> for u32 {
44    fn from(bps: BasisPoints) -> Self {
45        u32::from(bps.0)
46    }
47}
48
49impl From<BasisPoints> for u64 {
50    fn from(bps: BasisPoints) -> Self {
51        u64::from(bps.0)
52    }
53}
54
55impl From<BasisPoints> for u128 {
56    fn from(bps: BasisPoints) -> Self {
57        u128::from(bps.0)
58    }
59}
60
61#[cfg(feature = "decimal")]
62impl From<BasisPoints> for Decimal {
63    fn from(bps: BasisPoints) -> Self {
64        Decimal::from(bps.0)
65    }
66}
67
68#[cfg(feature = "decimal")]
69impl TryFrom<Decimal> for BasisPoints {
70    type Error = BasisPointsError;
71
72    fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
73        let bps = decimal
74            .try_into()
75            .map_err(|_| BasisPointsError::ConversionFailed)?;
76        BasisPoints::new(bps)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_new() {
86        assert_eq!(BasisPoints::new(10000), Ok(BasisPoints(10000)));
87    }
88
89    #[test]
90    fn test_new_error() {
91        assert_eq!(
92            BasisPoints::new(10001),
93            Err(BasisPointsError::InvalidBasisPoints)
94        );
95    }
96
97    #[cfg(feature = "decimal")]
98    #[test]
99    fn test_from_basis_points_to_decimal() {
100        let bps = BasisPoints::new(1234).unwrap();
101        let decimal: Decimal = bps.into();
102
103        assert_eq!(decimal, Decimal::new(1234, 0));
104    }
105
106    #[cfg(feature = "decimal")]
107    #[test]
108    fn test_try_from_decimal_to_basis_points() {
109        let decimal = Decimal::new(4321, 0);
110        let bps = BasisPoints::try_from(decimal);
111
112        assert_eq!(bps, Ok(BasisPoints(4321)));
113    }
114
115    #[cfg(feature = "decimal")]
116    #[test]
117    fn test_round_trip_basis_points_decimal_basis_points() {
118        let original = BasisPoints::new(9999).unwrap();
119        let decimal: Decimal = original.into();
120        let round_trip = BasisPoints::try_from(decimal);
121
122        assert_eq!(round_trip, Ok(original));
123    }
124
125    #[cfg(feature = "decimal")]
126    #[test]
127    fn test_try_from_decimal_fractional_truncates() {
128        let decimal = Decimal::new(12345, 1); // 1234.5
129        let bps = BasisPoints::try_from(decimal);
130
131        assert_eq!(bps, Ok(BasisPoints(1234)));
132    }
133
134    #[cfg(feature = "decimal")]
135    #[test]
136    fn test_try_from_decimal_above_basis_points_max_error() {
137        let decimal = Decimal::new(10001, 0);
138        let bps = BasisPoints::try_from(decimal);
139
140        assert_eq!(bps, Err(BasisPointsError::InvalidBasisPoints));
141    }
142
143    #[cfg(feature = "decimal")]
144    #[test]
145    fn test_try_from_decimal_u16_overflow_error() {
146        let decimal = Decimal::new(70000, 0);
147        let bps = BasisPoints::try_from(decimal);
148
149        assert_eq!(bps, Err(BasisPointsError::ConversionFailed));
150    }
151}