Skip to main content

reliakit_json/
number.rs

1//! Precision-preserving JSON numbers.
2
3use alloc::boxed::Box;
4use alloc::string::{String, ToString};
5
6use crate::error::JsonNumberError;
7
8/// A JSON number that preserves its exact, validated source representation.
9///
10/// Parsing never silently rounds or truncates: the original token is kept
11/// verbatim and conversions to `i64`/`u64`/`f64` are explicit and fallible.
12/// Equality is **structural** over the representation — `1.0`, `1`, and `1e0`
13/// are distinct `JsonNumber`s. Compare numerically by converting first.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct JsonNumber {
16    repr: Box<str>,
17}
18
19impl JsonNumber {
20    /// Creates a number from a string, validating it against the strict JSON
21    /// number grammar (no leading `+`, no leading zeros, no `NaN`/`Infinity`).
22    pub fn new(input: &str) -> Result<Self, JsonNumberError> {
23        if is_valid_json_number(input) {
24            Ok(Self { repr: input.into() })
25        } else {
26            Err(JsonNumberError::InvalidNumber)
27        }
28    }
29
30    /// Creates a number from a token already validated by the parser.
31    pub(crate) fn from_validated(repr: String) -> Self {
32        Self {
33            repr: repr.into_boxed_str(),
34        }
35    }
36
37    /// Returns the exact source representation.
38    pub fn as_str(&self) -> &str {
39        &self.repr
40    }
41
42    /// Returns `true` if the representation has no fraction or exponent.
43    pub fn is_integer(&self) -> bool {
44        !self
45            .repr
46            .bytes()
47            .any(|b| b == b'.' || b == b'e' || b == b'E')
48    }
49
50    /// Converts to `i64`, or fails if the value is not an integer or is out of
51    /// range.
52    pub fn to_i64(&self) -> Result<i64, JsonNumberError> {
53        if !self.is_integer() {
54            return Err(JsonNumberError::NotAnInteger);
55        }
56        self.repr
57            .parse::<i64>()
58            .map_err(|_| JsonNumberError::OutOfRange)
59    }
60
61    /// Converts to `u64`, or fails if the value is not a non-negative integer or
62    /// is out of range.
63    pub fn to_u64(&self) -> Result<u64, JsonNumberError> {
64        if !self.is_integer() {
65            return Err(JsonNumberError::NotAnInteger);
66        }
67        self.repr
68            .parse::<u64>()
69            .map_err(|_| JsonNumberError::OutOfRange)
70    }
71
72    /// Converts to `f64`. Fails only if the magnitude overflows `f64` to
73    /// infinity; ordinary rounding of the decimal value is not an error.
74    pub fn to_f64(&self) -> Result<f64, JsonNumberError> {
75        match self.repr.parse::<f64>() {
76            Ok(value) if value.is_finite() => Ok(value),
77            _ => Err(JsonNumberError::NotFinite),
78        }
79    }
80
81    /// Builds a number from an `f64`. Fails if the value is `NaN` or infinite.
82    pub fn try_from_f64(value: f64) -> Result<Self, JsonNumberError> {
83        if !value.is_finite() {
84            return Err(JsonNumberError::NotFinite);
85        }
86        let repr = value.to_string();
87        // `f64::to_string` produces a valid JSON number for every finite value,
88        // but validate defensively so the invariant always holds.
89        if is_valid_json_number(&repr) {
90            Ok(Self {
91                repr: repr.into_boxed_str(),
92            })
93        } else {
94            Err(JsonNumberError::InvalidNumber)
95        }
96    }
97}
98
99/// Validates a string against the RFC 8259 number grammar:
100/// `-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?`.
101pub(crate) fn is_valid_json_number(s: &str) -> bool {
102    let b = s.as_bytes();
103    let n = b.len();
104    let mut i = 0;
105
106    if i < n && b[i] == b'-' {
107        i += 1;
108    }
109
110    // Integer part: a single `0`, or a non-zero digit followed by digits.
111    match b.get(i) {
112        Some(b'0') => i += 1,
113        Some(d) if d.is_ascii_digit() => {
114            i += 1;
115            while i < n && b[i].is_ascii_digit() {
116                i += 1;
117            }
118        }
119        _ => return false,
120    }
121
122    // Optional fraction.
123    if i < n && b[i] == b'.' {
124        i += 1;
125        let start = i;
126        while i < n && b[i].is_ascii_digit() {
127            i += 1;
128        }
129        if i == start {
130            return false;
131        }
132    }
133
134    // Optional exponent.
135    if i < n && (b[i] == b'e' || b[i] == b'E') {
136        i += 1;
137        if i < n && (b[i] == b'+' || b[i] == b'-') {
138            i += 1;
139        }
140        let start = i;
141        while i < n && b[i].is_ascii_digit() {
142            i += 1;
143        }
144        if i == start {
145            return false;
146        }
147    }
148
149    i == n
150}