Skip to main content

datavalue_rs/
number.rs

1//! `NumberValue` — numeric type for [`crate::ArenaValue`].
2//!
3//! Distinguishes `Integer(i64)` from `Float(f64)` natively (vs. the opaque
4//! internal string of `serde_json::Number`) so integer arithmetic stays in
5//! i64 with overflow checks instead of round-tripping through f64.
6
7use std::cmp::Ordering;
8use std::fmt;
9
10/// Specialised numeric representation. Integers stay in i64 unless they
11/// overflow during arithmetic, in which case the result falls back to f64.
12#[derive(Debug, Clone, Copy)]
13pub enum NumberValue {
14    Integer(i64),
15    Float(f64),
16}
17
18impl NumberValue {
19    #[inline]
20    pub fn from_i64(value: i64) -> Self {
21        NumberValue::Integer(value)
22    }
23
24    /// Construct from an f64. Whole-valued floats within i64 range collapse
25    /// to `Integer` so subsequent arithmetic uses the integer fast path.
26    #[inline]
27    pub fn from_f64(value: f64) -> Self {
28        if value.fract() == 0.0
29            && !value.is_nan()
30            && !value.is_infinite()
31            && value >= i64::MIN as f64
32            && value <= i64::MAX as f64
33        {
34            NumberValue::Integer(value as i64)
35        } else {
36            NumberValue::Float(value)
37        }
38    }
39
40    #[inline]
41    pub fn is_integer(&self) -> bool {
42        matches!(self, NumberValue::Integer(_))
43    }
44
45    #[inline]
46    pub fn as_i64(&self) -> Option<i64> {
47        match *self {
48            NumberValue::Integer(i) => Some(i),
49            NumberValue::Float(f) => {
50                if f.fract() == 0.0
51                    && !f.is_nan()
52                    && !f.is_infinite()
53                    && f >= i64::MIN as f64
54                    && f <= i64::MAX as f64
55                {
56                    Some(f as i64)
57                } else {
58                    None
59                }
60            }
61        }
62    }
63
64    #[inline]
65    pub fn as_f64(&self) -> f64 {
66        match *self {
67            NumberValue::Integer(i) => i as f64,
68            NumberValue::Float(f) => f,
69        }
70    }
71
72    #[inline]
73    pub fn is_zero(&self) -> bool {
74        match *self {
75            NumberValue::Integer(i) => i == 0,
76            NumberValue::Float(f) => f == 0.0,
77        }
78    }
79
80    #[inline]
81    pub fn is_nan(&self) -> bool {
82        matches!(*self, NumberValue::Float(f) if f.is_nan())
83    }
84
85    /// Add. Integer-integer uses checked_add; on overflow falls back to f64.
86    pub fn add(&self, other: &NumberValue) -> NumberValue {
87        match (*self, *other) {
88            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_add(b) {
89                Some(r) => NumberValue::Integer(r),
90                None => NumberValue::Float(a as f64 + b as f64),
91            },
92            _ => NumberValue::from_f64(self.as_f64() + other.as_f64()),
93        }
94    }
95
96    pub fn sub(&self, other: &NumberValue) -> NumberValue {
97        match (*self, *other) {
98            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_sub(b) {
99                Some(r) => NumberValue::Integer(r),
100                None => NumberValue::Float(a as f64 - b as f64),
101            },
102            _ => NumberValue::from_f64(self.as_f64() - other.as_f64()),
103        }
104    }
105
106    pub fn mul(&self, other: &NumberValue) -> NumberValue {
107        match (*self, *other) {
108            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_mul(b) {
109                Some(r) => NumberValue::Integer(r),
110                None => NumberValue::Float(a as f64 * b as f64),
111            },
112            _ => NumberValue::from_f64(self.as_f64() * other.as_f64()),
113        }
114    }
115
116    /// Divide. Returns `None` for division by zero — callers handle.
117    pub fn div(&self, other: &NumberValue) -> Option<NumberValue> {
118        if other.is_zero() {
119            return None;
120        }
121        match (*self, *other) {
122            (NumberValue::Integer(a), NumberValue::Integer(b)) => {
123                // i64::MIN / -1 overflows; fall through to float.
124                if a == i64::MIN && b == -1 {
125                    return Some(NumberValue::Float(-(i64::MIN as f64)));
126                }
127                if a % b == 0 {
128                    Some(NumberValue::Integer(a / b))
129                } else {
130                    Some(NumberValue::Float(a as f64 / b as f64))
131                }
132            }
133            _ => Some(NumberValue::from_f64(self.as_f64() / other.as_f64())),
134        }
135    }
136
137    /// Modulo. Returns `None` for division by zero — caller handles.
138    pub fn rem(&self, other: &NumberValue) -> Option<NumberValue> {
139        if other.is_zero() {
140            return None;
141        }
142        match (*self, *other) {
143            (NumberValue::Integer(a), NumberValue::Integer(b)) => {
144                // i64::MIN % -1 overflows; the mathematical result is 0.
145                if a == i64::MIN && b == -1 {
146                    return Some(NumberValue::Integer(0));
147                }
148                Some(NumberValue::Integer(a % b))
149            }
150            _ => Some(NumberValue::from_f64(self.as_f64() % other.as_f64())),
151        }
152    }
153
154    pub fn neg(&self) -> NumberValue {
155        match *self {
156            NumberValue::Integer(i) => match i.checked_neg() {
157                Some(r) => NumberValue::Integer(r),
158                None => NumberValue::Float(-(i as f64)),
159            },
160            NumberValue::Float(f) => NumberValue::Float(-f),
161        }
162    }
163
164    pub fn abs(&self) -> NumberValue {
165        match *self {
166            NumberValue::Integer(i) => match i.checked_abs() {
167                Some(r) => NumberValue::Integer(r),
168                None => NumberValue::Float((i as f64).abs()),
169            },
170            NumberValue::Float(f) => NumberValue::Float(f.abs()),
171        }
172    }
173}
174
175impl PartialEq for NumberValue {
176    #[inline]
177    fn eq(&self, other: &Self) -> bool {
178        match (*self, *other) {
179            (NumberValue::Integer(a), NumberValue::Integer(b)) => a == b,
180            (NumberValue::Float(a), NumberValue::Float(b)) => a == b,
181            (NumberValue::Integer(a), NumberValue::Float(b)) => (a as f64) == b,
182            (NumberValue::Float(a), NumberValue::Integer(b)) => a == (b as f64),
183        }
184    }
185}
186
187impl PartialOrd for NumberValue {
188    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189        match (*self, *other) {
190            (NumberValue::Integer(a), NumberValue::Integer(b)) => Some(a.cmp(&b)),
191            (NumberValue::Float(a), NumberValue::Float(b)) => a.partial_cmp(&b),
192            (NumberValue::Integer(a), NumberValue::Float(b)) => (a as f64).partial_cmp(&b),
193            (NumberValue::Float(a), NumberValue::Integer(b)) => a.partial_cmp(&(b as f64)),
194        }
195    }
196}
197
198impl fmt::Display for NumberValue {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match *self {
201            NumberValue::Integer(i) => write!(f, "{}", i),
202            NumberValue::Float(fl) => {
203                // Match serde_json::Number's f64 formatting: "1.5" not "1.5e0".
204                if fl.is_nan() || fl.is_infinite() {
205                    write!(f, "null")
206                } else if fl.fract() == 0.0 {
207                    write!(f, "{}.0", fl as i64)
208                } else {
209                    write!(f, "{}", fl)
210                }
211            }
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn from_f64_collapses_whole() {
222        assert!(matches!(
223            NumberValue::from_f64(42.0),
224            NumberValue::Integer(42)
225        ));
226        assert!(matches!(
227            NumberValue::from_f64(-3.0),
228            NumberValue::Integer(-3)
229        ));
230        assert!(matches!(NumberValue::from_f64(1.5), NumberValue::Float(_)));
231    }
232
233    #[test]
234    fn from_f64_rejects_nan_inf_for_int_path() {
235        assert!(matches!(
236            NumberValue::from_f64(f64::NAN),
237            NumberValue::Float(_)
238        ));
239        assert!(matches!(
240            NumberValue::from_f64(f64::INFINITY),
241            NumberValue::Float(_)
242        ));
243    }
244
245    #[test]
246    fn add_overflow_falls_to_float() {
247        let a = NumberValue::Integer(i64::MAX);
248        let b = NumberValue::Integer(1);
249        assert!(matches!(a.add(&b), NumberValue::Float(_)));
250    }
251
252    #[test]
253    fn add_no_overflow_stays_int() {
254        let a = NumberValue::Integer(2);
255        let b = NumberValue::Integer(3);
256        assert!(matches!(a.add(&b), NumberValue::Integer(5)));
257    }
258
259    #[test]
260    fn div_zero_returns_none() {
261        let a = NumberValue::Integer(1);
262        let z = NumberValue::Integer(0);
263        assert!(a.div(&z).is_none());
264        let zf = NumberValue::Float(0.0);
265        assert!(a.div(&zf).is_none());
266    }
267
268    #[test]
269    fn div_int_int_exact_stays_int() {
270        let a = NumberValue::Integer(10);
271        let b = NumberValue::Integer(2);
272        assert!(matches!(a.div(&b).unwrap(), NumberValue::Integer(5)));
273    }
274
275    #[test]
276    fn div_int_int_inexact_promotes_float() {
277        let a = NumberValue::Integer(7);
278        let b = NumberValue::Integer(2);
279        assert!(matches!(a.div(&b).unwrap(), NumberValue::Float(_)));
280    }
281
282    #[test]
283    fn cross_type_eq_and_ord() {
284        let i = NumberValue::Integer(5);
285        let f = NumberValue::Float(5.0);
286        assert_eq!(i, f);
287        assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));
288
289        let f2 = NumberValue::Float(5.5);
290        assert_eq!(i.partial_cmp(&f2), Some(Ordering::Less));
291    }
292
293    #[test]
294    fn neg_overflow_falls_to_float() {
295        let a = NumberValue::Integer(i64::MIN);
296        assert!(matches!(a.neg(), NumberValue::Float(_)));
297    }
298}