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 pub(crate) const fn from_validated(raw: &'call str, offset: usize) -> Self {
54 Self { raw, offset }
55 }
56
57 /// Returns the original number spelling.
58 ///
59 /// # Returns
60 ///
61 /// The validated source spelling, borrowed for the callback duration.
62 pub const fn raw(&self) -> &'call str {
63 self.raw
64 }
65
66 /// Returns the zero-based byte offset of the token in its source document.
67 pub const fn offset(&self) -> usize {
68 self.offset
69 }
70
71 /// Converts an exact integral token in the IEEE-754 safe-integer range
72 /// without converting through floating point.
73 ///
74 /// # Returns
75 ///
76 /// The represented integer when its exact mathematical value is integral
77 /// and lies in `[-9007199254740991, 9007199254740991]`.
78 ///
79 /// # Errors
80 ///
81 /// Returns [`JsonNumberTokenError::Fractional`] when the exact value has a
82 /// fractional part, and [`JsonNumberTokenError::OutOfSafeIntegerRange`]
83 /// when the exact integral value lies outside the safe-integer interval.
84 pub fn exact_safe_integer(&self) -> Result<i64, JsonNumberTokenError> {
85 let bytes = self.raw.as_bytes();
86 let negative = bytes.first() == Some(&b'-');
87 let start = usize::from(negative);
88 let exponent_marker = bytes[start..]
89 .iter()
90 .position(|byte| matches!(byte, b'e' | b'E'))
91 .map(|position| start + position);
92 let mantissa_end = exponent_marker.unwrap_or(bytes.len());
93 let exponent = exponent_marker.map_or(0_i64, |position| {
94 parse_saturated_exponent(&bytes[position + 1..], bytes.len().saturating_add(32) as i64)
95 });
96 let mut fractional_digits = 0_i64;
97 let mut digit_count = 0_usize;
98 let mut first_nonzero = None;
99 let mut last_nonzero = 0_usize;
100 let mut after_decimal = false;
101 for byte in &bytes[start..mantissa_end] {
102 match byte {
103 b'.' => after_decimal = true,
104 b'0'..=b'9' => {
105 if after_decimal {
106 fractional_digits = fractional_digits.saturating_add(1);
107 }
108 if *byte != b'0' {
109 first_nonzero.get_or_insert(digit_count);
110 last_nonzero = digit_count;
111 }
112 digit_count += 1;
113 }
114 _ => unreachable!("token is lexically validated"),
115 }
116 }
117 let Some(first_nonzero) = first_nonzero else {
118 return Ok(0);
119 };
120 let shift = exponent.saturating_sub(fractional_digits);
121 let trailing_zeroes = digit_count - last_nonzero - 1;
122 let (last_kept, zeroes_to_append) = if shift < 0 {
123 let remove = shift.unsigned_abs() as usize;
124 if remove > trailing_zeroes {
125 return Err(JsonNumberTokenError::Fractional);
126 }
127 (digit_count - 1 - remove, 0_usize)
128 } else {
129 (digit_count - 1, shift as usize)
130 };
131 let resulting_len = last_kept - first_nonzero + 1 + zeroes_to_append;
132 if resulting_len > 16 {
133 return Err(JsonNumberTokenError::OutOfSafeIntegerRange);
134 }
135 let mut magnitude = 0_u64;
136 let mut ordinal = 0_usize;
137 for byte in &bytes[start..mantissa_end] {
138 if byte.is_ascii_digit() {
139 if (first_nonzero..=last_kept).contains(&ordinal) {
140 magnitude = magnitude * 10 + u64::from(*byte - b'0');
141 }
142 ordinal += 1;
143 }
144 }
145 for _ in 0..zeroes_to_append {
146 magnitude *= 10;
147 }
148 const MAX_SAFE: u64 = 9_007_199_254_740_991;
149 if magnitude > MAX_SAFE {
150 return Err(JsonNumberTokenError::OutOfSafeIntegerRange);
151 }
152 let value = magnitude as i64;
153 Ok(if negative { -value } else { value })
154 }
155}
156
157/// Parses validated exponent digits while bounding magnitude to avoid overflow.
158fn parse_saturated_exponent(bytes: &[u8], bound: i64) -> i64 {
159 let negative = bytes.first() == Some(&b'-');
160 let digits = if matches!(bytes.first(), Some(b'-' | b'+')) {
161 &bytes[1..]
162 } else {
163 bytes
164 };
165 let mut value = 0_i64;
166 for byte in digits {
167 value = value
168 .saturating_mul(10)
169 .saturating_add(i64::from(byte - b'0'))
170 .min(bound);
171 }
172 if negative { -value } else { value }
173}