Skip to main content

qubit_json/decode/
json_number_token.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Borrowed source JSON number tokens.
9
10use super::JsonNumberTokenError;
11
12/// A syntactically validated number token borrowed from the input document.
13///
14/// The token is only valid during the synchronous validated-decoding callback
15/// that supplied it; its source spelling cannot outlive that callback. Offsets
16/// are byte offsets from the beginning of the source document. The exact
17/// safe-integer range is the closed interval
18/// `[-9007199254740991, 9007199254740991]`. Decimal or exponent spellings are
19/// interpreted exactly, without an intermediate floating-point conversion.
20///
21/// # Examples
22///
23/// ```
24/// use qubit_json::decode::{JsonDecoder, JsonNumberPolicy, JsonValidatedDecodeOptions};
25///
26/// let options = JsonValidatedDecodeOptions::builder().build();
27/// let owner = JsonDecoder::unlimited().decode_unique_validated_str(
28///     "1.00e2", &options, |path, token| {
29///         assert!(path.is_empty());
30///         assert_eq!(token.raw(), "1.00e2");
31///         assert_eq!(token.offset(), 0);
32///         assert_eq!(token.exact_safe_integer(), Ok(100));
33///         Ok::<_, std::convert::Infallible>(JsonNumberPolicy::ExactSafeInteger)
34///     })?;
35/// assert_eq!(owner.as_value().as_i64(), Some(100));
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct JsonNumberToken<'call> {
40    /// Validated number spelling borrowed for the callback duration.
41    raw: &'call str,
42    /// Zero-based source byte offset.
43    offset: usize,
44}
45
46impl<'call> JsonNumberToken<'call> {
47    /// Constructs a token view for a number whose JSON syntax the scanner has
48    /// validated.
49    ///
50    /// `raw` must be the exact source spelling of that validated token, and
51    /// `offset` its zero-based byte offset in the same input. The borrow must
52    /// remain limited to the synchronous policy callback.
53    ///
54    /// # Parameters
55    ///
56    /// - `raw`: the validated token's source spelling.
57    /// - `offset`: its zero-based byte offset in the source document.
58    #[must_use]
59    #[inline]
60    pub(crate) const fn from_validated(raw: &'call str, offset: usize) -> Self {
61        Self { raw, offset }
62    }
63
64    /// Returns the original number spelling.
65    ///
66    /// # Returns
67    ///
68    /// The validated source spelling, borrowed for the callback duration.
69    #[must_use]
70    #[inline]
71    pub const fn raw(&self) -> &'call str {
72        self.raw
73    }
74
75    /// Returns the zero-based byte offset of the token in its source document.
76    #[must_use]
77    #[inline]
78    pub const fn offset(&self) -> usize {
79        self.offset
80    }
81
82    /// Converts an exact integral token in the IEEE-754 safe-integer range
83    /// without converting through floating point.
84    ///
85    /// # Returns
86    ///
87    /// The represented integer when its exact mathematical value is integral
88    /// and lies in `[-9007199254740991, 9007199254740991]`.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`JsonNumberTokenError::Fractional`] when the exact value has a
93    /// fractional part, and [`JsonNumberTokenError::OutOfSafeIntegerRange`]
94    /// when the exact integral value lies outside the safe-integer interval.
95    pub fn exact_safe_integer(&self) -> Result<i64, JsonNumberTokenError> {
96        let bytes = self.raw.as_bytes();
97        let negative = bytes.first() == Some(&b'-');
98        let start = usize::from(negative);
99        let exponent_marker = bytes[start..]
100            .iter()
101            .position(|byte| matches!(byte, b'e' | b'E'))
102            .map(|position| start + position);
103        let mantissa_end = exponent_marker.unwrap_or(bytes.len());
104        let exponent = exponent_marker.map_or(0_i64, |position| {
105            parse_saturated_exponent(
106                &bytes[position + 1..],
107                bytes.len().saturating_add(32) as i64,
108            )
109        });
110        let mut fractional_digits = 0_i64;
111        let mut digit_count = 0_usize;
112        let mut first_nonzero = None;
113        let mut last_nonzero = 0_usize;
114        let mut after_decimal = false;
115        for byte in &bytes[start..mantissa_end] {
116            match byte {
117                b'.' => after_decimal = true,
118                b'0'..=b'9' => {
119                    if after_decimal {
120                        fractional_digits = fractional_digits.saturating_add(1);
121                    }
122                    if *byte != b'0' {
123                        first_nonzero.get_or_insert(digit_count);
124                        last_nonzero = digit_count;
125                    }
126                    digit_count += 1;
127                }
128                _ => unreachable!("token is lexically validated"),
129            }
130        }
131        let Some(first_nonzero) = first_nonzero else {
132            return Ok(0);
133        };
134        let shift = exponent.saturating_sub(fractional_digits);
135        let trailing_zeroes = digit_count - last_nonzero - 1;
136        let (last_kept, zeroes_to_append) = if shift < 0 {
137            let remove = shift.unsigned_abs() as usize;
138            if remove > trailing_zeroes {
139                return Err(JsonNumberTokenError::Fractional);
140            }
141            (digit_count - 1 - remove, 0_usize)
142        } else {
143            (digit_count - 1, shift as usize)
144        };
145        let resulting_len = last_kept - first_nonzero + 1 + zeroes_to_append;
146        if resulting_len > 16 {
147            return Err(JsonNumberTokenError::OutOfSafeIntegerRange);
148        }
149        let mut magnitude = 0_u64;
150        let mut ordinal = 0_usize;
151        for byte in &bytes[start..mantissa_end] {
152            if byte.is_ascii_digit() {
153                if (first_nonzero..=last_kept).contains(&ordinal) {
154                    magnitude = magnitude * 10 + u64::from(*byte - b'0');
155                }
156                ordinal += 1;
157            }
158        }
159        for _ in 0..zeroes_to_append {
160            magnitude *= 10;
161        }
162        const MAX_SAFE: u64 = 9_007_199_254_740_991;
163        if magnitude > MAX_SAFE {
164            return Err(JsonNumberTokenError::OutOfSafeIntegerRange);
165        }
166        let value = magnitude as i64;
167        Ok(if negative { -value } else { value })
168    }
169}
170
171/// Parses validated exponent digits while bounding magnitude to avoid overflow.
172///
173/// # Parameters
174///
175/// - `bytes`: the exponent sign and validated decimal digits.
176/// - `bound`: the maximum magnitude retained while parsing.
177fn parse_saturated_exponent(bytes: &[u8], bound: i64) -> i64 {
178    let negative = bytes.first() == Some(&b'-');
179    let digits = if matches!(bytes.first(), Some(b'-' | b'+')) {
180        &bytes[1..]
181    } else {
182        bytes
183    };
184    let mut value = 0_i64;
185    for byte in digits {
186        value = value
187            .saturating_mul(10)
188            .saturating_add(i64::from(byte - b'0'))
189            .min(bound);
190    }
191    if negative { -value } else { value }
192}