datavalue-rs 0.2.0

Bump-allocated JSON value type with a built-in zero-copy parser and serde_json-style access API.
Documentation
//! `NumberValue` — numeric type for [`crate::ArenaValue`].
//!
//! Distinguishes `Integer(i64)` from `Float(f64)` natively (vs. the opaque
//! internal string of `serde_json::Number`) so integer arithmetic stays in
//! i64 with overflow checks instead of round-tripping through f64.

use std::cmp::Ordering;
use std::fmt;

/// Specialised numeric representation. Integers stay in i64 unless they
/// overflow during arithmetic, in which case the result falls back to f64.
#[derive(Debug, Clone, Copy)]
pub enum NumberValue {
    Integer(i64),
    Float(f64),
}

impl NumberValue {
    #[inline]
    pub fn from_i64(value: i64) -> Self {
        NumberValue::Integer(value)
    }

    /// Construct from an f64. Whole-valued floats within i64 range collapse
    /// to `Integer` so subsequent arithmetic uses the integer fast path.
    #[inline]
    pub fn from_f64(value: f64) -> Self {
        if value.fract() == 0.0
            && !value.is_nan()
            && !value.is_infinite()
            && value >= i64::MIN as f64
            && value <= i64::MAX as f64
        {
            NumberValue::Integer(value as i64)
        } else {
            NumberValue::Float(value)
        }
    }

    #[inline]
    pub fn is_integer(&self) -> bool {
        matches!(self, NumberValue::Integer(_))
    }

    #[inline]
    pub fn as_i64(&self) -> Option<i64> {
        match *self {
            NumberValue::Integer(i) => Some(i),
            NumberValue::Float(f) => {
                if f.fract() == 0.0
                    && !f.is_nan()
                    && !f.is_infinite()
                    && f >= i64::MIN as f64
                    && f <= i64::MAX as f64
                {
                    Some(f as i64)
                } else {
                    None
                }
            }
        }
    }

    #[inline]
    pub fn as_f64(&self) -> f64 {
        match *self {
            NumberValue::Integer(i) => i as f64,
            NumberValue::Float(f) => f,
        }
    }

    #[inline]
    pub fn is_zero(&self) -> bool {
        match *self {
            NumberValue::Integer(i) => i == 0,
            NumberValue::Float(f) => f == 0.0,
        }
    }

    #[inline]
    pub fn is_nan(&self) -> bool {
        matches!(*self, NumberValue::Float(f) if f.is_nan())
    }

    /// Add. Integer-integer uses checked_add; on overflow falls back to f64.
    pub fn add(&self, other: &NumberValue) -> NumberValue {
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_add(b) {
                Some(r) => NumberValue::Integer(r),
                None => NumberValue::Float(a as f64 + b as f64),
            },
            _ => NumberValue::from_f64(self.as_f64() + other.as_f64()),
        }
    }

    pub fn sub(&self, other: &NumberValue) -> NumberValue {
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_sub(b) {
                Some(r) => NumberValue::Integer(r),
                None => NumberValue::Float(a as f64 - b as f64),
            },
            _ => NumberValue::from_f64(self.as_f64() - other.as_f64()),
        }
    }

    pub fn mul(&self, other: &NumberValue) -> NumberValue {
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => match a.checked_mul(b) {
                Some(r) => NumberValue::Integer(r),
                None => NumberValue::Float(a as f64 * b as f64),
            },
            _ => NumberValue::from_f64(self.as_f64() * other.as_f64()),
        }
    }

    /// Divide. Returns `None` for division by zero — callers handle.
    pub fn div(&self, other: &NumberValue) -> Option<NumberValue> {
        if other.is_zero() {
            return None;
        }
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => {
                // i64::MIN / -1 overflows; fall through to float.
                if a == i64::MIN && b == -1 {
                    return Some(NumberValue::Float(-(i64::MIN as f64)));
                }
                if a % b == 0 {
                    Some(NumberValue::Integer(a / b))
                } else {
                    Some(NumberValue::Float(a as f64 / b as f64))
                }
            }
            _ => Some(NumberValue::from_f64(self.as_f64() / other.as_f64())),
        }
    }

    /// Modulo. Returns `None` for division by zero — caller handles.
    pub fn rem(&self, other: &NumberValue) -> Option<NumberValue> {
        if other.is_zero() {
            return None;
        }
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => {
                // i64::MIN % -1 overflows; the mathematical result is 0.
                if a == i64::MIN && b == -1 {
                    return Some(NumberValue::Integer(0));
                }
                Some(NumberValue::Integer(a % b))
            }
            _ => Some(NumberValue::from_f64(self.as_f64() % other.as_f64())),
        }
    }

    pub fn neg(&self) -> NumberValue {
        match *self {
            NumberValue::Integer(i) => match i.checked_neg() {
                Some(r) => NumberValue::Integer(r),
                None => NumberValue::Float(-(i as f64)),
            },
            NumberValue::Float(f) => NumberValue::Float(-f),
        }
    }

    pub fn abs(&self) -> NumberValue {
        match *self {
            NumberValue::Integer(i) => match i.checked_abs() {
                Some(r) => NumberValue::Integer(r),
                None => NumberValue::Float((i as f64).abs()),
            },
            NumberValue::Float(f) => NumberValue::Float(f.abs()),
        }
    }
}

impl PartialEq for NumberValue {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => a == b,
            (NumberValue::Float(a), NumberValue::Float(b)) => a == b,
            (NumberValue::Integer(a), NumberValue::Float(b)) => (a as f64) == b,
            (NumberValue::Float(a), NumberValue::Integer(b)) => a == (b as f64),
        }
    }
}

impl PartialOrd for NumberValue {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match (*self, *other) {
            (NumberValue::Integer(a), NumberValue::Integer(b)) => Some(a.cmp(&b)),
            (NumberValue::Float(a), NumberValue::Float(b)) => a.partial_cmp(&b),
            (NumberValue::Integer(a), NumberValue::Float(b)) => (a as f64).partial_cmp(&b),
            (NumberValue::Float(a), NumberValue::Integer(b)) => a.partial_cmp(&(b as f64)),
        }
    }
}

impl fmt::Display for NumberValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            NumberValue::Integer(i) => write!(f, "{}", i),
            NumberValue::Float(fl) => {
                // Match serde_json::Number's f64 formatting: "1.5" not "1.5e0".
                if fl.is_nan() || fl.is_infinite() {
                    write!(f, "null")
                } else if fl.fract() == 0.0 {
                    write!(f, "{}.0", fl as i64)
                } else {
                    write!(f, "{}", fl)
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_f64_collapses_whole() {
        assert!(matches!(
            NumberValue::from_f64(42.0),
            NumberValue::Integer(42)
        ));
        assert!(matches!(
            NumberValue::from_f64(-3.0),
            NumberValue::Integer(-3)
        ));
        assert!(matches!(NumberValue::from_f64(1.5), NumberValue::Float(_)));
    }

    #[test]
    fn from_f64_rejects_nan_inf_for_int_path() {
        assert!(matches!(
            NumberValue::from_f64(f64::NAN),
            NumberValue::Float(_)
        ));
        assert!(matches!(
            NumberValue::from_f64(f64::INFINITY),
            NumberValue::Float(_)
        ));
    }

    #[test]
    fn add_overflow_falls_to_float() {
        let a = NumberValue::Integer(i64::MAX);
        let b = NumberValue::Integer(1);
        assert!(matches!(a.add(&b), NumberValue::Float(_)));
    }

    #[test]
    fn add_no_overflow_stays_int() {
        let a = NumberValue::Integer(2);
        let b = NumberValue::Integer(3);
        assert!(matches!(a.add(&b), NumberValue::Integer(5)));
    }

    #[test]
    fn div_zero_returns_none() {
        let a = NumberValue::Integer(1);
        let z = NumberValue::Integer(0);
        assert!(a.div(&z).is_none());
        let zf = NumberValue::Float(0.0);
        assert!(a.div(&zf).is_none());
    }

    #[test]
    fn div_int_int_exact_stays_int() {
        let a = NumberValue::Integer(10);
        let b = NumberValue::Integer(2);
        assert!(matches!(a.div(&b).unwrap(), NumberValue::Integer(5)));
    }

    #[test]
    fn div_int_int_inexact_promotes_float() {
        let a = NumberValue::Integer(7);
        let b = NumberValue::Integer(2);
        assert!(matches!(a.div(&b).unwrap(), NumberValue::Float(_)));
    }

    #[test]
    fn cross_type_eq_and_ord() {
        let i = NumberValue::Integer(5);
        let f = NumberValue::Float(5.0);
        assert_eq!(i, f);
        assert_eq!(i.partial_cmp(&f), Some(Ordering::Equal));

        let f2 = NumberValue::Float(5.5);
        assert_eq!(i.partial_cmp(&f2), Some(Ordering::Less));
    }

    #[test]
    fn neg_overflow_falls_to_float() {
        let a = NumberValue::Integer(i64::MIN);
        assert!(matches!(a.neg(), NumberValue::Float(_)));
    }
}