use serde_json::{Map as JsonMap, Number as JsonNumber, Value as Json};
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Value>),
Mapping(Mapping),
}
impl Value {
#[must_use]
pub const fn is_scalar(&self) -> bool {
!matches!(self, Value::Array(_) | Value::Mapping(_))
}
#[must_use]
pub fn to_json(&self) -> Json {
match self {
Value::Null => Json::Null,
Value::Bool(b) => Json::Bool(*b),
Value::Number(n) => number_json(*n),
Value::String(s) => Json::String(s.clone()),
Value::Array(items) => Json::Array(items.iter().map(Value::to_json).collect()),
Value::Mapping(m) => Json::Object(
m.entries
.iter()
.map(|(k, v)| (k.clone(), v.to_json()))
.collect::<JsonMap<String, Json>>(),
),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mapping {
pub entries: Vec<(String, Value)>,
}
impl Mapping {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn insert(&mut self, key: String, value: Value) -> bool {
if self.entries.iter().any(|(k, _)| *k == key) {
return false;
}
self.entries.push((key, value));
true
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
self.entries.iter().map(|(k, v)| (k.as_str(), v))
}
}
impl From<Vec<(String, Value)>> for Mapping {
fn from(entries: Vec<(String, Value)>) -> Self {
Self { entries }
}
}
#[must_use]
pub fn number_json(n: f64) -> Json {
if !n.is_finite() {
return Json::Null;
}
if n.fract() == 0.0 && n.abs() < 9_007_199_254_740_992.0 {
return Json::from(n as i64);
}
JsonNumber::from_f64(n).map_or(Json::Null, Json::Number)
}
#[must_use]
pub fn js_number_string(x: f64) -> String {
if x.is_nan() {
return "NaN".to_owned();
}
if x.is_infinite() {
return if x > 0.0 { "Infinity" } else { "-Infinity" }.to_owned();
}
if x == 0.0 {
return "0".to_owned();
}
let sci = format!("{:e}", x.abs());
let (mantissa, exp) = sci
.split_once('e')
.expect("LowerExp always has an exponent");
let exp: i32 = exp.parse().expect("LowerExp exponent is an integer");
let all_digits: String = mantissa.chars().filter(|c| *c != '.').collect();
let digits = all_digits.trim_end_matches('0');
let digits = if digits.is_empty() { "0" } else { digits };
let k = i32::try_from(digits.len()).expect("a double has at most 17 digits");
let n = exp + 1;
let mut out = String::new();
if x < 0.0 {
out.push('-');
}
if k <= n && n <= 21 {
out.push_str(digits);
out.extend(std::iter::repeat_n('0', (n - k) as usize));
} else if 0 < n && n <= 21 {
let split = n as usize;
out.push_str(&digits[..split]);
out.push('.');
out.push_str(&digits[split..]);
} else if -6 < n && n <= 0 {
out.push_str("0.");
out.extend(std::iter::repeat_n('0', (-n) as usize));
out.push_str(digits);
} else {
let e = n - 1;
out.push_str(&digits[..1]);
if k > 1 {
out.push('.');
out.push_str(&digits[1..]);
}
out.push('e');
out.push(if e >= 0 { '+' } else { '-' });
out.push_str(&e.abs().to_string());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn js_number_strings_match_v8() {
let cases: &[(f64, &str)] = &[
(0.0, "0"),
(-0.0, "0"),
(1.0, "1"),
(1.5, "1.5"),
(-1.5, "-1.5"),
(1000.0, "1000"),
(16.0, "16"),
(0.005, "0.005"),
(0.1, "0.1"),
(1e21, "1e+21"),
(1e20, "100000000000000000000"),
(123_456_789_012_345_680_000.0, "123456789012345680000"),
(1e-6, "0.000001"),
(1e-7, "1e-7"),
(1.5e-7, "1.5e-7"),
(1.0 / 3.0, "0.3333333333333333"),
(9_007_199_254_740_992.0, "9007199254740992"),
(f64::INFINITY, "Infinity"),
(f64::NEG_INFINITY, "-Infinity"),
(f64::NAN, "NaN"),
(2.5e25, "2.5e+25"),
(-1e-10, "-1e-10"),
];
for (x, want) in cases {
assert_eq!(js_number_string(*x), *want, "{x:e}");
}
}
#[test]
fn number_json_is_an_integer_when_integral() {
assert_eq!(number_json(1.0), Json::from(1));
assert_eq!(number_json(-0.0), Json::from(0));
assert_eq!(number_json(1.5).as_f64(), Some(1.5));
assert_eq!(number_json(1e21).as_f64(), Some(1e21));
assert_eq!(number_json(f64::NAN), Json::Null);
assert_eq!(number_json(f64::INFINITY), Json::Null);
}
#[test]
fn mapping_rejects_duplicates_and_keeps_order() {
let mut m = Mapping::new();
assert!(m.insert("b".into(), Value::Null));
assert!(m.insert("a".into(), Value::Bool(true)));
assert!(!m.insert("b".into(), Value::Number(1.0)));
assert_eq!(m.len(), 2);
assert_eq!(m.get("b"), Some(&Value::Null));
assert_eq!(
m.to_json_keys(),
vec!["b", "a"],
"insertion order, not sorted"
);
}
impl Mapping {
fn to_json_keys(&self) -> Vec<String> {
match Value::Mapping(self.clone()).to_json() {
Json::Object(o) => o.keys().cloned().collect(),
_ => unreachable!(),
}
}
}
}