Skip to main content

basis_points/
lib.rs

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