use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
List(Vec<Value>),
Record(Vec<(String, Value)>),
Instant {
secs: i64,
nanos: u32,
offset_min: Option<i16>,
},
Duration { secs: i64, nanos: u32 },
Quantity {
value: f64,
base: String,
written: Option<(f64, String)>,
},
}
impl Value {
pub fn is_truthy(&self) -> bool {
match self {
Value::Null => false,
Value::Bool(b) => *b,
Value::Int(n) => *n != 0,
Value::Float(f) => *f != 0.0,
Value::Str(s) => !s.is_empty(),
Value::List(l) => !l.is_empty(),
Value::Record(o) => !o.is_empty(),
Value::Instant { .. } => true,
Value::Duration { secs, nanos } => *secs != 0 || *nanos != 0,
Value::Quantity { value, .. } => *value != 0.0,
}
}
pub fn temporal_reading(&self) -> Option<(i64, u32)> {
match self {
Value::Instant { secs, nanos, .. } => Some((*secs, *nanos)),
Value::Int(n) => Some((*n, 0)),
Value::Float(f) if f.is_finite() => {
let secs = f.floor() as i64;
let nanos = ((f - f.floor()) * 1e9) as u32;
Some((secs, nanos))
}
Value::Str(s) => crate::temporal::parse_iso(s).map(|(s, n, _)| (s, n)),
_ => None,
}
}
pub fn unital_reading(&self) -> Option<(f64, String)> {
match self {
Value::Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::quantity::parse_unit_text(s).map(|(v, base, ..)| (v, base.to_string()))
}
_ => None,
}
}
pub fn durational_reading(&self) -> Option<(i64, u32)> {
match self {
Value::Duration { secs, nanos } => Some((*secs, *nanos)),
Value::Int(n) => Some((*n, 0)),
Value::Float(f) if f.is_finite() => {
let secs = f.floor() as i64;
let nanos = ((f - f.floor()) * 1e9) as u32;
Some((secs, nanos))
}
Value::Str(s) => crate::temporal::parse_span(s),
_ => None,
}
}
pub fn to_json(&self) -> String {
match self {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) => format_float(*f),
Value::Str(s) => json_string(s),
Value::List(l) => {
let inner: Vec<String> = l.iter().map(Value::to_json).collect();
format!("[{}]", inner.join(", "))
}
Value::Record(o) => {
let inner: Vec<String> = o
.iter()
.map(|(k, v)| format!("{}: {}", json_string(k), v.to_json()))
.collect();
format!("{{{}}}", inner.join(", "))
}
Value::Instant { .. } | Value::Duration { .. } | Value::Quantity { .. } => {
json_string(&self.to_string())
}
}
}
pub fn numeric(&self) -> Option<f64> {
match self {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
Value::Str(s) => s.trim().parse::<f64>().ok().filter(|f| f.is_finite()),
Value::Quantity { value, .. } => Some(*value),
_ => None,
}
}
pub fn numeric_reading(&self) -> Option<Value> {
match self {
Value::Int(n) => Some(Value::Int(*n)),
Value::Float(f) => Some(Value::Float(*f)),
Value::Str(s) => {
let s = s.trim();
if let Ok(n) = s.parse::<i64>() {
Some(Value::Int(n))
} else {
s.parse::<f64>()
.ok()
.filter(|f| f.is_finite())
.map(Value::Float)
}
}
_ => None,
}
}
pub fn compare(&self, other: &Value) -> std::cmp::Ordering {
use std::cmp::Ordering;
if matches!(self, Value::Instant { .. }) || matches!(other, Value::Instant { .. }) {
if let (Some(a), Some(b)) = (self.temporal_reading(), other.temporal_reading()) {
return a.cmp(&b);
}
}
if matches!(self, Value::Duration { .. }) || matches!(other, Value::Duration { .. }) {
if let (Some(a), Some(b)) = (self.durational_reading(), other.durational_reading()) {
return a.cmp(&b);
}
}
if matches!(self, Value::Quantity { .. }) || matches!(other, Value::Quantity { .. }) {
if let Some((a, b)) = quantital_pair(self, other) {
return a.partial_cmp(&b).unwrap_or(Ordering::Equal);
}
}
if let (Some(x), Some(y)) = (self.numeric(), other.numeric()) {
return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
}
self.to_string().cmp(&other.to_string())
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => Ok(()),
Value::Bool(b) => write!(f, "{b}"),
Value::Int(n) => write!(f, "{n}"),
Value::Float(x) => write!(f, "{x}"),
Value::Str(s) => write!(f, "{s}"),
Value::List(items) => {
let parts: Vec<String> = items.iter().map(|v| v.to_string()).collect();
write!(f, "{}", parts.join(", "))
}
Value::Record(_) => write!(f, "{}", self.to_json()),
Value::Instant {
secs,
nanos,
offset_min,
} => write!(
f,
"{}",
crate::temporal::format_instant(*secs, *nanos, *offset_min)
),
Value::Duration { secs, nanos } => {
write!(f, "{}", crate::temporal::format_duration(*secs, *nanos))
}
Value::Quantity {
value,
base,
written,
} => match written {
Some((v, u)) => write!(f, "{} {}", format_float(*v), u),
None => write!(f, "{} {}", format_float(*value), base),
},
}
}
}
pub(crate) fn quantital_pair(a: &Value, b: &Value) -> Option<(f64, f64)> {
quantital_pair_with(a, b, &crate::quantity::scale_expr)
}
pub(crate) fn quantital_pair_with(
a: &Value,
b: &Value,
scale: &dyn Fn(&str) -> Option<(f64, String)>,
) -> Option<(f64, f64)> {
let read = |v: &Value| -> Option<(f64, String)> {
match v {
Value::Quantity { value, base, .. } => Some((*value, base.clone())),
Value::Str(s) => {
crate::quantity::parse_unit_text_with(s, scale).map(|(bv, b, ..)| (bv, b))
}
_ => None,
}
};
match (read(a), read(b)) {
(Some((va, ba)), Some((vb, bb))) => (ba == bb).then_some((va, vb)),
(Some((va, _)), None) => b.numeric().map(|n| (va, n)),
(None, Some((vb, _))) => a.numeric().map(|n| (n, vb)),
(None, None) => None,
}
}
fn format_float(f: f64) -> String {
f.to_string()
}
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
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('"');
out
}
impl From<i64> for Value {
fn from(n: i64) -> Self {
Value::Int(n)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::Str(s)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::Str(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_rendering() {
let obj = Value::Record(vec![
("name".into(), Value::Str("A\"da\n".into())),
("age".into(), Value::Int(36)),
("score".into(), Value::Float(2.5)),
("ok".into(), Value::Bool(true)),
("gone".into(), Value::Null),
(
"tags".into(),
Value::List(vec![Value::Str("x".into()), Value::Int(1)]),
),
]);
assert_eq!(
obj.to_json(),
r#"{"name": "A\"da\n", "age": 36, "score": 2.5, "ok": true, "gone": null, "tags": ["x", 1]}"#
);
assert_eq!(obj.to_string(), obj.to_json());
assert!(!Value::Record(Vec::new()).is_truthy());
assert_eq!(
Value::List(vec![Value::Str("a".into()), Value::Str("b".into())]).to_string(),
"a, b"
);
}
}