1use std::cmp::Ordering;
8use std::fmt;
9
10#[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 #[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 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 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 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 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 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 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}