use serde_json::{value::RawValue, Number};
pub type NumberText = Box<RawValue>;
pub fn is_number_literal(text: &str) -> bool {
let bytes = text.as_bytes();
let mut at = 0;
let digits = |at: &mut usize| {
let start = *at;
while *at < bytes.len() && bytes[*at].is_ascii_digit() {
*at += 1;
}
*at - start
};
if bytes.get(at) == Some(&b'-') {
at += 1;
}
match bytes.get(at) {
Some(b'0') => at += 1,
Some(b'1'..=b'9') => {
digits(&mut at);
}
_ => return false,
}
if bytes.get(at) == Some(&b'.') {
at += 1;
if digits(&mut at) == 0 {
return false;
}
}
if matches!(bytes.get(at), Some(b'e' | b'E')) {
at += 1;
if matches!(bytes.get(at), Some(b'+' | b'-')) {
at += 1;
}
if digits(&mut at) == 0 {
return false;
}
}
at == bytes.len()
}
pub fn is_number_text(raw: &RawValue) -> bool {
is_number_literal(raw.get())
}
pub fn number_text(text: &str) -> Result<NumberText, String> {
if !is_number_literal(text) {
return Err(format!("{text:?} is not a JSON number"));
}
RawValue::from_string(text.to_owned()).map_err(|e| e.to_string())
}
pub fn number_text_of(number: &Number) -> NumberText {
RawValue::from_string(number.to_string()).expect("a Number's text is JSON")
}
pub fn same_text(a: Option<&RawValue>, b: Option<&RawValue>) -> bool {
a.map(RawValue::get) == b.map(RawValue::get)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn number_literals() {
for good in ["0", "-0", "0.10", "1e-7", "1E+30", "12.500", "-3"] {
assert!(is_number_literal(good), "{good}");
}
for bad in [
"", "-", "01", "1.", ".5", "1e", "+1", " 1", "1 ", "NaN", "\"1\"", "null",
] {
assert!(!is_number_literal(bad), "{bad}");
}
}
#[test]
fn number_text_keeps_the_spelling_through_a_decode() {
let decoded: Vec<NumberText> = serde_json::from_str("[0.10,1e-7,1E+30,12.500]").unwrap();
let texts: Vec<&str> = decoded.iter().map(|raw| raw.get()).collect();
assert_eq!(texts, ["0.10", "1e-7", "1E+30", "12.500"]);
assert_eq!(
serde_json::to_string(&decoded).unwrap(),
"[0.10,1e-7,1E+30,12.500]"
);
assert!(number_text("0.10").is_ok());
assert!(number_text("\"x\"").is_err());
}
}