Skip to main content

betfair_types/
numeric.rs

1//! Numeric primitive abstraction
2//!
3//! This module provides a unified interface for numeric operations using `f64`.
4
5/// Wrapper around f64 that implements Eq, Ord, and Hash using total_cmp
6/// This allows f64 to be used in contexts that require these traits
7#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
8#[serde(transparent)]
9pub struct F64Ord(pub f64);
10
11impl F64Ord {
12    pub const fn new(value: f64) -> Self {
13        Self(value)
14    }
15
16    pub const fn zero() -> Self {
17        Self(0.0)
18    }
19
20    pub const fn as_f64(&self) -> f64 {
21        self.0
22    }
23}
24
25impl From<f64> for F64Ord {
26    fn from(value: f64) -> Self {
27        Self(value)
28    }
29}
30
31impl From<F64Ord> for f64 {
32    fn from(value: F64Ord) -> Self {
33        value.0
34    }
35}
36
37impl PartialEq for F64Ord {
38    fn eq(&self, other: &Self) -> bool {
39        self.0.to_bits() == other.0.to_bits()
40    }
41}
42
43impl Eq for F64Ord {}
44
45impl PartialOrd for F64Ord {
46    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
47        Some(self.cmp(other))
48    }
49}
50
51impl Ord for F64Ord {
52    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
53        self.0.total_cmp(&other.0)
54    }
55}
56
57impl core::hash::Hash for F64Ord {
58    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
59        self.0.to_bits().hash(state);
60    }
61}
62
63impl core::fmt::Display for F64Ord {
64    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        self.0.fmt(f)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    use std::cmp::Ordering;
74    use std::collections::hash_map::DefaultHasher;
75    use std::hash::{Hash, Hasher};
76
77    #[test]
78    fn f64ord_should_use_bitwise_equality() {
79        let nan = F64Ord::new(f64::NAN);
80        assert_eq!(nan, F64Ord::new(f64::NAN)); // This would fail for a normal f64.
81
82        assert_eq!(F64Ord::new(-0.0).cmp(&F64Ord::new(0.0)), Ordering::Less);
83        assert_ne!(F64Ord::new(-0.0), F64Ord::new(0.0));
84
85        let mut h1 = DefaultHasher::new();
86        let mut h2 = DefaultHasher::new();
87        nan.hash(&mut h1);
88        nan.hash(&mut h2);
89        assert_eq!(h1.finish(), h2.finish());
90    }
91}
92
93// Re-export for convenience
94pub use core::ops::{Add, Div, Mul, Sub};
95
96/// Trait for creating numeric literals in a type-agnostic way
97pub trait NumericLiteral {
98    fn literal_from_f64(value: f64) -> Self;
99    fn literal_from_str(value: &str) -> Result<Self, String>
100    where
101        Self: Sized;
102}
103
104impl NumericLiteral for f64 {
105    fn literal_from_f64(value: f64) -> Self {
106        value
107    }
108
109    fn literal_from_str(value: &str) -> Result<Self, String> {
110        value
111            .parse()
112            .map_err(|e| format!("Failed to parse f64: {}", e))
113    }
114}
115
116/// Helper trait for numeric operations that work across both Decimal and f64
117pub trait NumericOps: Copy + Clone + PartialOrd + PartialEq {
118    fn checked_add(&self, other: Self) -> Option<Self>;
119    fn checked_sub(&self, other: Self) -> Option<Self>;
120    fn checked_mul(&self, other: Self) -> Option<Self>;
121    fn checked_div(&self, other: Self) -> Option<Self>;
122    fn checked_rem(&self, other: Self) -> Option<Self>;
123    fn saturating_add(&self, other: Self) -> Self;
124    fn saturating_sub(&self, other: Self) -> Self;
125    fn saturating_mul(&self, other: Self) -> Self;
126    fn round_2dp(self) -> Self;
127    fn zero() -> Self;
128    fn is_sign_positive(&self) -> bool;
129    fn is_sign_negative(&self) -> bool;
130}
131
132impl NumericOps for f64 {
133    fn checked_add(&self, other: Self) -> Option<Self> {
134        let result = self + other;
135        if result.is_finite() {
136            Some(result)
137        } else {
138            None
139        }
140    }
141
142    fn checked_sub(&self, other: Self) -> Option<Self> {
143        let result = self - other;
144        if result.is_finite() {
145            Some(result)
146        } else {
147            None
148        }
149    }
150
151    fn checked_mul(&self, other: Self) -> Option<Self> {
152        let result = self * other;
153        if result.is_finite() {
154            Some(result)
155        } else {
156            None
157        }
158    }
159
160    fn checked_div(&self, other: Self) -> Option<Self> {
161        if other == 0.0 {
162            None
163        } else {
164            let result = self / other;
165            if result.is_finite() {
166                Some(result)
167            } else {
168                None
169            }
170        }
171    }
172
173    fn checked_rem(&self, other: Self) -> Option<Self> {
174        if other == 0.0 {
175            None
176        } else {
177            let result = self % other;
178            if result.is_finite() {
179                Some(result)
180            } else {
181                None
182            }
183        }
184    }
185
186    fn saturating_add(&self, other: Self) -> Self {
187        let result = self + other;
188        if result.is_finite() {
189            result
190        } else if result.is_infinite() && result.is_sign_positive() {
191            f64::MAX
192        } else {
193            f64::MIN
194        }
195    }
196
197    fn saturating_sub(&self, other: Self) -> Self {
198        let result = self - other;
199        if result.is_finite() {
200            result
201        } else if result.is_infinite() && result.is_sign_positive() {
202            f64::MAX
203        } else {
204            f64::MIN
205        }
206    }
207
208    fn saturating_mul(&self, other: Self) -> Self {
209        let result = self * other;
210        if result.is_finite() {
211            result
212        } else if result.is_infinite() && result.is_sign_positive() {
213            f64::MAX
214        } else {
215            f64::MIN
216        }
217    }
218
219    #[inline(always)]
220    fn round_2dp(self) -> Self {
221        (self * 100.0).round() / 100.0
222    }
223
224    fn zero() -> Self {
225        0.0
226    }
227
228    fn is_sign_positive(&self) -> bool {
229        f64::is_sign_positive(*self)
230    }
231
232    fn is_sign_negative(&self) -> bool {
233        f64::is_sign_negative(*self)
234    }
235}
236
237/// Create a numeric constant (f64)
238#[macro_export]
239macro_rules! num {
240    ($lit:literal) => {{ $lit as f64 }};
241}
242
243/// Create a numeric constant (F64Ord)
244#[macro_export]
245macro_rules! num_ord {
246    ($lit:literal) => {{ $crate::numeric::F64Ord::from($lit as f64) }};
247}
248
249/// Create a numeric constant (u8)
250#[macro_export]
251macro_rules! num_u8 {
252    ($lit:literal) => {{ $lit as u8 }};
253}