use std::cmp::Ordering;
#[derive(Debug, Clone)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
impl PartialEq for Value {
fn eq(&self, other: &Value) -> bool {
self.value_eq(other)
}
}
impl Value {
pub fn is_truthy(&self) -> bool {
!matches!(self, Value::Null | Value::Bool(false))
}
pub fn type_name(&self) -> &'static str {
match self {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Int(_) | Value::Float(_) => "number",
Value::Str(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Int(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
_ => None,
}
}
pub fn order(&self, other: &Value) -> Ordering {
fn rank(v: &Value) -> u8 {
match v {
Value::Null => 0,
Value::Bool(_) => 1,
Value::Int(_) | Value::Float(_) => 2,
Value::Str(_) => 3,
Value::Array(_) => 4,
Value::Object(_) => 5,
}
}
match (self, other) {
(Value::Null, Value::Null) => Ordering::Equal,
(Value::Bool(a), Value::Bool(b)) => a.cmp(b),
(Value::Str(a), Value::Str(b)) => a.cmp(b),
(Value::Array(a), Value::Array(b)) => {
for (x, y) in a.iter().zip(b.iter()) {
let c = x.order(y);
if c != Ordering::Equal {
return c;
}
}
a.len().cmp(&b.len())
}
(Value::Object(a), Value::Object(b)) => {
let mut ka: Vec<&String> = a.iter().map(|(k, _)| k).collect();
let mut kb: Vec<&String> = b.iter().map(|(k, _)| k).collect();
ka.sort();
kb.sort();
match ka.cmp(&kb) {
Ordering::Equal => {}
ne => return ne,
}
for k in ka {
let va = a.iter().find(|(kk, _)| kk == k).map(|(_, v)| v);
let vb = b.iter().find(|(kk, _)| kk == k).map(|(_, v)| v);
if let (Some(va), Some(vb)) = (va, vb) {
let c = va.order(vb);
if c != Ordering::Equal {
return c;
}
}
}
Ordering::Equal
}
_ => {
if let (Some(a), Some(b)) = (self.as_f64(), other.as_f64()) {
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
} else {
rank(self).cmp(&rank(other))
}
}
}
}
pub fn value_eq(&self, other: &Value) -> bool {
self.order(other) == Ordering::Equal
}
pub fn to_raw_string(&self) -> String {
match self {
Value::Str(s) => s.clone(),
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => format_f64(*f),
Value::Array(_) | Value::Object(_) => self.to_json(),
}
}
pub fn to_json(&self) -> String {
let mut out = String::new();
self.write_json(&mut out);
out
}
fn write_json(&self, out: &mut String) {
match self {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(f) => out.push_str(&format_f64(*f)),
Value::Str(s) => write_json_string(s, out),
Value::Array(a) => {
out.push('[');
for (i, v) in a.iter().enumerate() {
if i > 0 {
out.push(',');
}
v.write_json(out);
}
out.push(']');
}
Value::Object(m) => {
out.push('{');
for (i, (k, v)) in m.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, out);
out.push(':');
v.write_json(out);
}
out.push('}');
}
}
}
}
fn format_f64(f: f64) -> String {
if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
format!("{}", f as i64)
} else {
format!("{f}")
}
}
fn write_json_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
fn arr(v: &[Value]) -> Value {
Value::Array(v.to_vec())
}
fn obj(pairs: &[(&str, Value)]) -> Value {
Value::Object(
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect(),
)
}
#[test]
fn total_order_across_types() {
let ladder = [
Value::Null,
Value::Bool(false),
Value::Bool(true),
Value::Int(-1),
Value::Float(2.5),
Value::Str("a".into()),
arr(&[Value::Int(1)]),
obj(&[("k", Value::Int(1))]),
];
for i in 0..ladder.len() {
for j in 0..ladder.len() {
let want = i.cmp(&j);
if want != Ordering::Equal {
assert_eq!(
ladder[i].order(&ladder[j]),
want,
"{:?} vs {:?}",
ladder[i],
ladder[j]
);
}
}
}
}
#[test]
fn numbers_compare_across_int_and_float() {
assert!(Value::Int(1).value_eq(&Value::Float(1.0)));
assert_eq!(Value::Int(2).order(&Value::Float(2.5)), Ordering::Less);
assert_eq!(Value::Float(3.0).order(&Value::Int(3)), Ordering::Equal);
assert_eq!(Value::Int(1).as_f64(), Some(1.0));
assert_eq!(Value::Str("x".into()).as_f64(), None);
}
#[test]
fn arrays_and_objects_order_structurally() {
assert_eq!(
arr(&[Value::Int(1), Value::Int(2)]).order(&arr(&[Value::Int(1), Value::Int(3)])),
Ordering::Less
);
assert_eq!(
arr(&[Value::Int(1)]).order(&arr(&[Value::Int(1), Value::Int(0)])),
Ordering::Less
);
assert_eq!(
obj(&[("a", Value::Int(1))]).order(&obj(&[("b", Value::Int(1))])),
Ordering::Less
);
assert_eq!(
obj(&[("a", Value::Int(1))]).order(&obj(&[("a", Value::Int(2))])),
Ordering::Less
);
assert!(
obj(&[("a", Value::Int(1)), ("b", Value::Int(2))])
.value_eq(&obj(&[("b", Value::Int(2)), ("a", Value::Int(1))]))
);
}
#[test]
fn raw_string_and_truthiness() {
assert_eq!(Value::Float(1.0).to_raw_string(), "1");
assert_eq!(Value::Float(1.5).to_raw_string(), "1.5");
assert_eq!(Value::Null.to_raw_string(), "null");
assert_eq!(Value::Bool(true).to_raw_string(), "true");
assert_eq!(arr(&[Value::Int(1)]).to_raw_string(), "[1]");
assert_eq!(obj(&[("a", Value::Int(1))]).to_raw_string(), "{\"a\":1}");
assert!(Value::Int(0).is_truthy());
assert!(Value::Str("".into()).is_truthy());
assert!(!Value::Bool(false).is_truthy());
assert!(!Value::Null.is_truthy());
}
#[test]
fn json_encoding_escapes_and_floats() {
assert_eq!(
Value::Str("a\"b\\c\n\t\r".into()).to_json(),
"\"a\\\"b\\\\c\\n\\t\\r\""
);
assert_eq!(Value::Str("\u{0001}".into()).to_json(), "\"\\u0001\"");
assert_eq!(Value::Float(42.0).to_json(), "42");
assert_eq!(Value::Float(2.5).to_json(), "2.5");
assert_eq!(
obj(&[("n", Value::Null), ("xs", arr(&[Value::Bool(true)]))]).to_json(),
"{\"n\":null,\"xs\":[true]}"
);
assert_eq!(arr(&[]).to_json(), "[]");
}
#[test]
fn type_names() {
assert_eq!(Value::Null.type_name(), "null");
assert_eq!(Value::Bool(true).type_name(), "boolean");
assert_eq!(Value::Int(1).type_name(), "number");
assert_eq!(Value::Float(1.0).type_name(), "number");
assert_eq!(Value::Str("x".into()).type_name(), "string");
assert_eq!(arr(&[]).type_name(), "array");
assert_eq!(obj(&[]).type_name(), "object");
}
}