Skip to main content

fix/
util.rs

1use paste::paste;
2#[cfg(feature = "typed-floats")]
3use typed_floats::StrictlyPositiveFinite;
4
5use crate::muldiv::MulDiv;
6use crate::num_traits::{float::FloatCore, ConstZero};
7use crate::typenum::{Integer, NInt, NonZero, Unsigned, U10};
8use crate::Fix;
9
10/// Domain specific extensions to the `Fix` type as it's used in this project.
11pub trait FixExt: Sized {
12    /// This precision's equivalent of 1.
13    const ONE: Self;
14}
15
16macro_rules! impl_fix_ext {
17    ($bits:ident) => {
18        paste! {
19            impl<U> FixExt for Fix<$bits, U10, NInt<U>>
20            where
21                U: Unsigned + NonZero,
22            {
23                const ONE: Self =
24                    Fix::constant((10 as $bits).pow(U::U32));
25            }
26        }
27    };
28}
29
30impl_fix_ext!(u8);
31impl_fix_ext!(u16);
32impl_fix_ext!(u32);
33impl_fix_ext!(u64);
34impl_fix_ext!(u128);
35impl_fix_ext!(usize);
36impl_fix_ext!(i8);
37impl_fix_ext!(i16);
38impl_fix_ext!(i32);
39impl_fix_ext!(i64);
40impl_fix_ext!(i128);
41impl_fix_ext!(isize);
42
43impl<Bits, Base, Exp> Fix<Bits, Base, Exp>
44where
45    Self: FixExt,
46{
47    /// This precision's equivalent of 1.
48    #[must_use]
49    pub const fn one() -> Self {
50        <Self as FixExt>::ONE
51    }
52}
53
54macro_rules! impl_to_f64 {
55    ($bits:ident) => {
56        impl<Exp: Integer> Fix<$bits, U10, Exp> {
57            /// Approximate `f64` value of this fixed-point number.
58            ///
59            /// Precision loss above 2^53 bits; intended for offchain
60            /// analytics, never for onchain math.
61            ///
62            /// ```
63            /// use fix::prelude::*;
64            /// let x = UFix64::<N6>::new(1_500_000u64);
65            /// assert!((x.to_f64() - 1.5).abs() < f64::EPSILON);
66            /// ```
67            #[must_use]
68            #[allow(clippy::cast_precision_loss)]
69            pub fn to_f64(self) -> f64 {
70                let exp = Exp::to_i32();
71                let scale = FloatCore::powi(10f64, exp.saturating_abs());
72                if exp.is_negative() {
73                    self.bits as f64 / scale
74                } else {
75                    self.bits as f64 * scale
76                }
77            }
78        }
79    };
80}
81
82impl_to_f64!(u64);
83impl_to_f64!(i64);
84
85#[cfg(feature = "typed-floats")]
86impl<Exp: Integer> Fix<u64, U10, Exp> {
87    /// Strictly positive finite `f64` view; `None` when zero.
88    #[must_use]
89    pub fn to_positive_f64(self) -> Option<StrictlyPositiveFinite> {
90        StrictlyPositiveFinite::try_from(self.to_f64()).ok()
91    }
92}
93
94impl<Bits, Exp> Fix<Bits, U10, Exp>
95where
96    Self: FixExt,
97    Bits: MulDiv<Output = Bits>,
98{
99    /// Converts to another _Exp_, returning `None` on overflow.
100    ///
101    /// ```
102    /// use fix::prelude::*;
103    /// let source = UFix64::<N3>::new(5u64);
104    /// let target = source.checked_convert::<N6>();
105    /// assert_eq!(target, Some(UFix64::<N6>::new(5_000u64)));
106    /// ```
107    pub fn checked_convert<ToExp>(self) -> Option<Fix<Bits, U10, ToExp>>
108    where
109        Fix<Bits, U10, ToExp>: FixExt,
110    {
111        let target_one = Fix::<Bits, U10, ToExp>::one();
112        let source_one = Self::one();
113        target_one.mul_div_floor(self, source_one)
114    }
115
116    /// Converts to another _Exp_ rounding up, returning `None` on overflow.
117    ///
118    /// ```
119    /// use fix::prelude::*;
120    /// let source = UFix64::<N6>::new(5_001u64);
121    /// let target = source.checked_convert_ceil::<N3>();
122    /// assert_eq!(target, Some(UFix64::<N3>::new(6u64)));
123    /// ```
124    pub fn checked_convert_ceil<ToExp>(self) -> Option<Fix<Bits, U10, ToExp>>
125    where
126        Fix<Bits, U10, ToExp>: FixExt,
127    {
128        let target_one = Fix::<Bits, U10, ToExp>::one();
129        let source_one = Self::one();
130        target_one.mul_div_ceil(self, source_one)
131    }
132
133    /// Divides by `rhs` at the same precision, rounding down.
134    /// `None` on overflow or division by zero.
135    ///
136    /// ```
137    /// use fix::prelude::*;
138    /// let a = UFix64::<N3>::new(10_000u64);
139    /// let b = UFix64::<N3>::new(3_000u64);
140    /// assert_eq!(a.div_floor(b), Some(UFix64::<N3>::new(3_333u64)));
141    /// ```
142    pub fn div_floor(self, rhs: Self) -> Option<Self>
143    where
144        Bits: ConstZero + PartialEq,
145    {
146        if rhs == Self::zero() {
147            None
148        } else {
149            self.mul_div_floor(Self::one(), rhs)
150        }
151    }
152
153    /// Divides by `rhs` at the same precision, rounding up.
154    /// `None` on overflow or division by zero.
155    ///
156    /// ```
157    /// use fix::prelude::*;
158    /// let a = UFix64::<N3>::new(10_000u64);
159    /// let b = UFix64::<N3>::new(3_000u64);
160    /// assert_eq!(a.div_ceil(b), Some(UFix64::<N3>::new(3_334u64)));
161    /// ```
162    pub fn div_ceil(self, rhs: Self) -> Option<Self>
163    where
164        Bits: ConstZero + PartialEq,
165    {
166        if rhs == Self::zero() {
167            None
168        } else {
169            self.mul_div_ceil(Self::one(), rhs)
170        }
171    }
172
173    /// Multiplies by `rhs` at the same precision, rounding down.
174    /// `None` on overflow.
175    ///
176    /// ```
177    /// use fix::prelude::*;
178    /// let a = UFix64::<N3>::new(1_001u64);
179    /// assert_eq!(a.mul_floor(a), Some(UFix64::<N3>::new(1_002u64)));
180    /// ```
181    pub fn mul_floor(self, rhs: Self) -> Option<Self> {
182        self.mul_div_floor(rhs, Self::one())
183    }
184
185    /// Multiplies by `rhs` at the same precision, rounding up.
186    /// `None` on overflow.
187    ///
188    /// ```
189    /// use fix::prelude::*;
190    /// let a = UFix64::<N3>::new(1_001u64);
191    /// assert_eq!(a.mul_ceil(a), Some(UFix64::<N3>::new(1_003u64)));
192    /// ```
193    pub fn mul_ceil(self, rhs: Self) -> Option<Self> {
194        self.mul_div_ceil(rhs, Self::one())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::aliases::decimal::{IFix64, UFix64};
201    #[cfg(feature = "typed-floats")]
202    use crate::typenum::N6;
203    use crate::typenum::{N3, N9};
204
205    #[test]
206    fn to_f64_small_bits_exact() {
207        let x = UFix64::<N3>::new(1_500u64);
208        assert!((x.to_f64() - 1.5).abs() < f64::EPSILON);
209    }
210
211    #[test]
212    fn to_f64_negative_bits_and_exp() {
213        let x = IFix64::<N9>::new(-975i64);
214        assert!((x.to_f64() - -9.75e-7).abs() < 1e-21);
215    }
216
217    #[test]
218    #[allow(clippy::excessive_precision)]
219    fn to_f64_max_bits_relative_error() {
220        let got = UFix64::<N9>::new(u64::MAX).to_f64();
221        let expected = 18_446_744_073.709_551_615_f64;
222        assert!(((got - expected) / expected).abs() < 1e-15);
223    }
224
225    #[cfg(feature = "typed-floats")]
226    #[test]
227    fn to_positive_f64_zero_is_none() {
228        assert!(UFix64::<N6>::zero().to_positive_f64().is_none());
229    }
230
231    #[cfg(feature = "typed-floats")]
232    #[test]
233    fn to_positive_f64_nonzero_is_some() {
234        let x = UFix64::<N6>::new(2_500_000u64);
235        let positive = x.to_positive_f64().map(f64::from);
236        assert_eq!(positive, Some(x.to_f64()));
237    }
238
239    #[test]
240    fn div_floor_rounds_down() {
241        let a = UFix64::<N3>::new(10_000u64);
242        let b = UFix64::<N3>::new(3_000u64);
243        assert_eq!(a.div_floor(b), Some(UFix64::<N3>::new(3_333u64)));
244    }
245
246    #[test]
247    fn div_ceil_rounds_up() {
248        let a = UFix64::<N3>::new(10_000u64);
249        let b = UFix64::<N3>::new(3_000u64);
250        assert_eq!(a.div_ceil(b), Some(UFix64::<N3>::new(3_334u64)));
251    }
252
253    #[test]
254    fn div_exact_floor_eq_ceil() {
255        let a = UFix64::<N3>::new(9_000u64);
256        let b = UFix64::<N3>::new(3_000u64);
257        let exact = Some(UFix64::<N3>::new(3_000u64));
258        assert_eq!(a.div_floor(b), exact);
259        assert_eq!(a.div_ceil(b), exact);
260    }
261
262    #[test]
263    fn div_by_zero_is_none() {
264        let a = UFix64::<N3>::new(10_000u64);
265        assert_eq!(a.div_floor(UFix64::<N3>::zero()), None);
266        assert_eq!(a.div_ceil(UFix64::<N3>::zero()), None);
267    }
268
269    #[test]
270    fn div_negative_rounds_toward_neg_infinity() {
271        let a = IFix64::<N3>::new(-10_000i64);
272        let b = IFix64::<N3>::new(3_000i64);
273        assert_eq!(a.div_floor(b), Some(IFix64::<N3>::new(-3_334i64)));
274        assert_eq!(a.div_ceil(b), Some(IFix64::<N3>::new(-3_333i64)));
275    }
276
277    #[test]
278    fn mul_floor_rounds_down() {
279        let a = UFix64::<N3>::new(1_001u64);
280        assert_eq!(a.mul_floor(a), Some(UFix64::<N3>::new(1_002u64)));
281    }
282
283    #[test]
284    fn mul_floor_exact() {
285        let a = UFix64::<N3>::new(2_000u64);
286        let b = UFix64::<N3>::new(1_500u64);
287        assert_eq!(a.mul_floor(b), Some(UFix64::<N3>::new(3_000u64)));
288    }
289
290    #[test]
291    fn mul_floor_overflow_is_none() {
292        let a = UFix64::<N3>::new(u64::MAX);
293        assert_eq!(a.mul_floor(a), None);
294    }
295
296    #[test]
297    fn mul_ceil_rounds_up() {
298        let a = UFix64::<N3>::new(1_001u64);
299        assert_eq!(a.mul_ceil(a), Some(UFix64::<N3>::new(1_003u64)));
300    }
301
302    #[test]
303    fn mul_ceil_exact() {
304        let a = UFix64::<N3>::new(2_000u64);
305        let b = UFix64::<N3>::new(1_500u64);
306        assert_eq!(a.mul_ceil(b), Some(UFix64::<N3>::new(3_000u64)));
307    }
308
309    #[test]
310    fn mul_ceil_overflow_is_none() {
311        let a = UFix64::<N3>::new(u64::MAX);
312        assert_eq!(a.mul_ceil(a), None);
313    }
314}