use core::ops::Deref;
use core::str::FromStr;
use crate::ParseError;
#[cfg(feature = "int32")]
use crate::int_parser::from_ascii_i32;
#[cfg(feature = "int64")]
use crate::int_parser::from_ascii_i64;
#[cfg(feature = "int8")]
use crate::int_parser::from_ascii_i8;
#[cfg(feature = "int8")]
type ConfiguredInt = i8;
#[cfg(feature = "int32")]
type ConfiguredInt = i32;
#[cfg(feature = "int64")]
type ConfiguredInt = i64;
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum NumberResult {
Integer(ConfiguredInt),
IntegerOverflow,
Float(f64),
FloatDisabled,
FloatSkipped,
FloatTruncated(ConfiguredInt),
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum JsonNumber<'a, 'b> {
Borrowed {
raw: &'a str,
parsed: NumberResult,
},
Copied {
raw: &'b str,
parsed: NumberResult,
},
}
impl JsonNumber<'_, '_> {
pub fn from_slice(bytes: &[u8]) -> Result<JsonNumber<'_, '_>, ParseError> {
let parsed_result = if is_integer(bytes) {
parse_integer(bytes)
} else {
#[cfg(feature = "float")]
{
parse_float(bytes)
}
#[cfg(not(feature = "float"))]
{
parse_float(bytes)?
}
};
let number_str = crate::shared::from_utf8(bytes)?;
Ok(JsonNumber::Borrowed {
raw: number_str,
parsed: parsed_result,
})
}
pub fn parsed(&self) -> &NumberResult {
match self {
JsonNumber::Borrowed { parsed, .. } => parsed,
JsonNumber::Copied { parsed, .. } => parsed,
}
}
pub fn as_int(&self) -> Option<ConfiguredInt> {
let parsed = self.parsed();
match parsed {
NumberResult::Integer(val) => Some(*val),
#[cfg(all(not(feature = "float"), feature = "float-truncate"))]
NumberResult::FloatTruncated(val) => Some(*val),
_ => None,
}
}
#[cfg(feature = "float")]
pub fn as_f64(&self) -> Option<f64> {
let parsed = self.parsed();
match parsed {
NumberResult::Float(val) => Some(*val),
NumberResult::Integer(val) => Some(*val as f64),
_ => None,
}
}
pub fn as_str(&self) -> &str {
match self {
JsonNumber::Borrowed { raw, .. } => raw,
JsonNumber::Copied { raw, .. } => raw,
}
}
pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
T::from_str(self.as_str())
}
pub fn is_integer(&self) -> bool {
let parsed = self.parsed();
matches!(
parsed,
NumberResult::Integer(_) | NumberResult::IntegerOverflow
)
}
pub fn is_float(&self) -> bool {
!self.is_integer()
}
}
impl AsRef<str> for JsonNumber<'_, '_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Deref for JsonNumber<'_, '_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl core::fmt::Display for JsonNumber<'_, '_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let (raw, parsed) = match self {
JsonNumber::Borrowed { raw, parsed } => (raw, parsed),
JsonNumber::Copied { raw, parsed } => (raw, parsed),
};
match parsed {
NumberResult::Integer(val) => write!(f, "{val}"),
#[cfg(feature = "float")]
NumberResult::Float(val) => write!(f, "{val}"),
#[cfg(all(not(feature = "float"), feature = "float-truncate"))]
NumberResult::FloatTruncated(val) => write!(f, "{}", val),
_ => f.write_str(raw),
}
}
}
pub fn is_integer(bytes: &[u8]) -> bool {
for &b in bytes {
if b == b'.' || b == b'e' || b == b'E' {
return false;
}
}
true
}
pub const fn parse_integer(bytes: &[u8]) -> NumberResult {
#[cfg(feature = "int8")]
let result = from_ascii_i8(bytes);
#[cfg(feature = "int32")]
let result = from_ascii_i32(bytes);
#[cfg(feature = "int64")]
let result = from_ascii_i64(bytes);
match result {
Ok(val) => NumberResult::Integer(val),
Err(_) => NumberResult::IntegerOverflow,
}
}
#[cfg(feature = "float")]
pub fn parse_float(bytes: &[u8]) -> NumberResult {
let s = match crate::shared::from_utf8(bytes) {
Ok(s) => s,
Err(_) => return NumberResult::IntegerOverflow, };
match f64::from_str(s) {
Ok(val) if val.is_finite() => NumberResult::Float(val),
_ => NumberResult::IntegerOverflow, }
}
#[cfg(not(feature = "float"))]
pub fn parse_float(bytes: &[u8]) -> Result<NumberResult, ParseError> {
let s = match crate::shared::from_utf8(bytes) {
Ok(s) => s,
Err(_) => return Err(ParseError::InvalidNumber), };
#[cfg(feature = "float-error")]
{
let _ = s; Err(ParseError::FloatNotAllowed)
}
#[cfg(feature = "float-skip")]
{
let _ = s; Ok(NumberResult::FloatSkipped)
}
#[cfg(feature = "float-truncate")]
{
if s.contains(['e', 'E']) {
return Err(ParseError::InvalidNumber);
}
let int_part = if let Some(dot_pos) = s.find('.') {
s.get(..dot_pos).unwrap_or(s)
} else {
s };
match ConfiguredInt::from_str(int_part) {
Ok(val) => Ok(NumberResult::FloatTruncated(val)),
Err(_) => Ok(NumberResult::IntegerOverflow),
}
}
#[cfg(not(any(
feature = "float-error",
feature = "float-skip",
feature = "float-truncate"
)))]
{
let _ = s; Ok(NumberResult::FloatDisabled)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_number_from_slice_basic() {
let json_number = JsonNumber::from_slice(b"56").unwrap();
assert_eq!(json_number.as_str(), "56");
assert_eq!(json_number.as_int(), Some(56));
let json_number = JsonNumber::from_slice(b"89").unwrap();
assert_eq!(json_number.as_str(), "89");
assert_eq!(json_number.as_int(), Some(89));
let invalid_utf8 = &[0xFF, 0xFE, 0xFD]; let result = JsonNumber::from_slice(invalid_utf8);
assert!(result.is_err());
}
#[test]
fn test_json_number_integer() {
let number = JsonNumber::Borrowed {
raw: "42",
parsed: NumberResult::Integer(42),
};
assert_eq!(number.as_str(), "42");
assert_eq!(number.as_int(), Some(42));
assert!(number.is_integer());
assert!(!number.is_float());
}
#[test]
fn test_json_number_negative_integer() {
let number = JsonNumber::Borrowed {
raw: "-123",
parsed: NumberResult::Integer(-123),
};
assert_eq!(number.as_str(), "-123");
assert_eq!(number.as_int(), Some(-123));
assert!(number.is_integer());
}
#[test]
fn test_json_number_large_integer() {
let large_int_str = "12345678901234567890"; let number = JsonNumber::Borrowed {
raw: large_int_str,
parsed: NumberResult::IntegerOverflow,
};
assert_eq!(number.as_str(), large_int_str);
assert_eq!(number.as_int(), None); match number {
JsonNumber::Borrowed {
parsed: NumberResult::IntegerOverflow,
..
} => {}
_ => panic!("Expected IntegerOverflow"),
}
assert!(number.is_integer());
}
#[test]
#[cfg(feature = "float")]
fn test_json_number_float() {
let number = JsonNumber::Borrowed {
raw: "3.25",
parsed: NumberResult::Float(3.25),
};
assert_eq!(number.as_str(), "3.25");
assert_eq!(number.as_int(), None);
assert_eq!(number.as_f64(), Some(3.25));
assert!(!number.is_integer());
assert!(number.is_float());
}
#[test]
#[cfg(feature = "float")]
fn test_json_number_exponent() {
let number = JsonNumber::Borrowed {
raw: "1.5e10",
parsed: NumberResult::Float(1.5e10),
};
assert_eq!(number.as_str(), "1.5e10");
assert_eq!(number.as_f64(), Some(1.5e10));
assert!(number.is_float());
}
#[test]
#[cfg(not(feature = "float"))]
fn test_json_number_float_disabled() {
let number = JsonNumber::Borrowed {
raw: "3.14159",
parsed: NumberResult::FloatDisabled,
};
assert_eq!(number.as_str(), "3.14159");
assert_eq!(number.as_int(), None);
match number {
JsonNumber::Borrowed {
parsed: NumberResult::FloatDisabled,
..
} => {}
_ => panic!("Expected FloatDisabled"),
}
assert!(number.is_float());
}
#[test]
fn test_json_number_parse_custom() {
let number = JsonNumber::Borrowed {
raw: "42",
parsed: NumberResult::Integer(42),
};
let parsed: u32 = number.parse().unwrap();
assert_eq!(parsed, 42u32);
let float_number = JsonNumber::Borrowed {
raw: "3.14",
parsed: NumberResult::Integer(3), };
let parsed_f32: Result<f32, _> = float_number.parse();
assert!(parsed_f32.is_ok());
}
#[test]
fn test_is_integer_detection() {
assert!(is_integer("42".as_bytes()));
assert!(is_integer("-123".as_bytes()));
assert!(is_integer("0".as_bytes()));
assert!(!is_integer("3.14".as_bytes()));
assert!(!is_integer("1e10".as_bytes()));
assert!(!is_integer("2.5E-3".as_bytes()));
}
#[test]
fn test_from_slice_with_container() {
let data = b"56}"; let number_bytes = &data[0..2];
let json_number = JsonNumber::from_slice(number_bytes).unwrap();
assert_eq!(json_number.as_str(), "56"); assert_eq!(json_number.as_int(), Some(56));
}
#[test]
fn test_from_slice_at_eof() {
let data = b"89";
let number_bytes = &data[0..2];
let json_number = JsonNumber::from_slice(number_bytes).unwrap();
assert_eq!(json_number.as_str(), "89"); assert_eq!(json_number.as_int(), Some(89));
}
}