use std::borrow::Cow;
use std::cmp::Ordering;
use std::str::FromStr;
use bigdecimal::BigDecimal;
use crate::value::Value;
#[must_use]
pub fn as_number(v: &Value) -> Option<f64> {
match v {
Value::Int(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
Value::Decimal(s) => s.parse::<f64>().ok(),
_ => None,
}
}
fn as_text(v: &Value) -> Option<&str> {
match v {
Value::Str(s) | Value::DateTime(s) | Value::Date(s) | Value::Uuid(s) => Some(s),
_ => None,
}
}
fn decimal_cmp(x: &str, y: &str) -> Option<Ordering> {
match (BigDecimal::from_str(x), BigDecimal::from_str(y)) {
(Ok(a), Ok(b)) => Some(a.cmp(&b)),
_ => x.parse::<f64>().ok()?.partial_cmp(&y.parse::<f64>().ok()?),
}
}
#[must_use]
pub fn compare(a: &Value, b: &Value) -> Option<Ordering> {
match (a, b) {
(Value::Int(x), Value::Int(y)) => Some(x.cmp(y)),
(Value::Bool(x), Value::Bool(y)) => Some(x.cmp(y)),
(Value::Decimal(x), Value::Decimal(y)) => decimal_cmp(x, y),
_ => {
if let (Some(x), Some(y)) = (as_number(a), as_number(b)) {
return x.partial_cmp(&y);
}
if let (Some(x), Some(y)) = (as_text(a), as_text(b)) {
return Some(x.cmp(y));
}
if a.is_null() && b.is_null() {
return Some(Ordering::Equal);
}
None
}
}
}
#[must_use]
pub fn eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::List(x), Value::List(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(p, q)| eq(p, q))
}
(Value::Map(x), Value::Map(y)) => {
x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|w| eq(v, w)))
}
(Value::Str(x), Value::Str(y)) => x == y,
(Value::DateTime(x), Value::DateTime(y)) => x == y,
(Value::Date(x), Value::Date(y)) => x == y,
(Value::Uuid(x), Value::Uuid(y)) => x == y,
(Value::Bytes(x), Value::Bytes(y)) => x == y,
(Value::Int(x), Value::Int(y)) => x == y,
(Value::Decimal(x), Value::Decimal(y)) => decimal_cmp(x, y) == Some(Ordering::Equal),
_ => match (as_number(a), as_number(b)) {
(Some(x), Some(y)) => x == y,
_ => false,
},
}
}
#[must_use]
pub fn to_py_str_cow(v: &Value) -> Cow<'_, str> {
match v {
Value::Str(s)
| Value::DateTime(s)
| Value::Date(s)
| Value::Decimal(s)
| Value::Uuid(s) => Cow::Borrowed(s),
Value::Int(i) => Cow::Owned(i.to_string()),
Value::Float(f) => Cow::Owned(format!("{f:?}")),
Value::Bool(true) => Cow::Borrowed("True"),
Value::Bool(false) => Cow::Borrowed("False"),
Value::Null => Cow::Borrowed("None"),
Value::Bytes(b) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
Value::List(_) | Value::Map(_) => Cow::Owned(format!("{v:?}")),
}
}
#[must_use]
pub fn to_py_str(v: &Value) -> String {
to_py_str_cow(v).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use std::cmp::Ordering::{Equal, Greater, Less};
#[test]
fn numbers_compare_across_types() {
assert_eq!(compare(&Value::Int(2), &Value::Float(2.5)), Some(Less));
assert_eq!(compare(&Value::Float(3.0), &Value::Int(3)), Some(Equal));
assert_eq!(compare(&Value::Int(10), &Value::Int(2)), Some(Greater));
assert_eq!(compare(&Value::Bool(true), &Value::Int(0)), Some(Greater));
}
#[test]
fn text_compares_by_code_point() {
assert_eq!(
compare(&Value::Str("apple".into()), &Value::Str("banana".into())),
Some(Less)
);
assert_eq!(
compare(
&Value::DateTime("2025-01-01T00:00:00".into()),
&Value::DateTime("2025-06-01T00:00:00".into())
),
Some(Less)
);
}
#[test]
fn mismatched_kinds_are_incomparable() {
assert_eq!(compare(&Value::Str("1".into()), &Value::Int(1)), None);
assert_eq!(compare(&Value::Null, &Value::Int(1)), None);
}
#[test]
fn equality_is_python_like() {
assert!(eq(&Value::Int(1), &Value::Float(1.0)));
assert!(eq(&Value::Bool(true), &Value::Int(1)));
assert!(eq(&Value::Null, &Value::Null));
assert!(eq(&Value::Str("x".into()), &Value::Str("x".into())));
assert!(!eq(&Value::Str("x".into()), &Value::DateTime("x".into())));
assert!(!eq(&Value::Int(1), &Value::Str("1".into())));
}
#[test]
fn large_integers_compare_exactly() {
let a = Value::Int(9_007_199_254_740_992);
let b = Value::Int(9_007_199_254_740_993);
assert!(!eq(&a, &b));
assert_eq!(compare(&a, &b), Some(Less));
}
#[test]
fn large_decimals_compare_exactly() {
let a = Value::Decimal("9007199254740993".into());
let b = Value::Decimal("9007199254740992".into());
assert_eq!(compare(&a, &b), Some(Greater));
assert!(!eq(&a, &b));
assert!(eq(
&Value::Decimal("1.0".into()),
&Value::Decimal("1.00".into())
));
}
#[test]
fn stringification_matches_python_str() {
assert_eq!(to_py_str(&Value::Int(42)), "42");
assert_eq!(to_py_str(&Value::Float(2.0)), "2.0");
assert_eq!(to_py_str(&Value::Bool(true)), "True");
assert_eq!(to_py_str(&Value::Null), "None");
assert_eq!(to_py_str(&Value::Str("hi".into())), "hi");
}
}