Skip to main content

nodedb_query/
value_ops.rs

1//! Core value operations on `nodedb_types::Value`: comparison, coercion, truthiness.
2//!
3//! Equivalent to `json_ops.rs` but operates on the internal `Value` type directly,
4//! avoiding the `serde_json::Value` intermediate. Used by expression evaluation,
5//! computed projections, and aggregate expression paths.
6
7use std::cmp::Ordering;
8
9use nodedb_types::Value;
10
11/// Coerce a Value to f64.
12///
13/// - Integer/Float: direct conversion
14/// - String: parse as f64
15/// - Bool: `true` → `1.0`, `false` → `0.0` (when `coerce_bool` is true)
16/// - Other types: `None`
17pub fn value_to_f64(v: &Value, coerce_bool: bool) -> Option<f64> {
18    match v {
19        Value::Integer(i) => Some(*i as f64),
20        Value::Float(f) => Some(*f),
21        Value::String(s) => s.parse::<f64>().ok(),
22        Value::Bool(b) if coerce_bool => Some(if *b { 1.0 } else { 0.0 }),
23        Value::Decimal(d) => {
24            use rust_decimal::prelude::ToPrimitive;
25            d.to_f64()
26        }
27        _ => None,
28    }
29}
30
31/// Compare two Values with type coercion.
32///
33/// Tries numeric comparison first (with bool coercion), then falls
34/// back to string comparison.
35pub fn compare_values(a: &Value, b: &Value) -> Ordering {
36    if let (Some(na), Some(nb)) = (value_to_f64(a, true), value_to_f64(b, true)) {
37        return na.partial_cmp(&nb).unwrap_or(Ordering::Equal);
38    }
39    let sa = value_to_display_string(a);
40    let sb = value_to_display_string(b);
41    sa.cmp(&sb)
42}
43
44/// Check equality with type coercion.
45///
46/// Handles `"5" == 5` by coercing both sides to f64 when one is a
47/// number and the other is a numeric string.
48pub fn coerced_eq(a: &Value, b: &Value) -> bool {
49    if a == b {
50        return true;
51    }
52    if let (Some(af), Some(bf)) = (value_to_f64(a, true), value_to_f64(b, true)) {
53        return (af - bf).abs() < f64::EPSILON;
54    }
55    false
56}
57
58/// Check if a Value is truthy (for boolean contexts).
59///
60/// - `true` → true, `false` → false
61/// - `Null` → false
62/// - Numbers: non-zero → true
63/// - Strings: non-empty → true
64/// - Arrays/Objects: always true
65pub fn is_truthy(v: &Value) -> bool {
66    match v {
67        Value::Bool(b) => *b,
68        Value::Null => false,
69        Value::Integer(i) => *i != 0,
70        Value::Float(f) => *f != 0.0,
71        Value::String(s) => !s.is_empty(),
72        _ => true,
73    }
74}
75
76/// Convert a Value to a display string.
77///
78/// - Strings: returned as-is (no quotes)
79/// - Null: empty string
80/// - Numbers/Bools: `.to_string()`
81/// - Objects/Arrays: JSON serialization
82pub fn value_to_display_string(v: &Value) -> String {
83    match v {
84        Value::String(s) => s.clone(),
85        Value::Null => String::new(),
86        Value::Integer(i) => i.to_string(),
87        Value::Float(f) => f.to_string(),
88        Value::Bool(b) => b.to_string(),
89        Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => s.clone(),
90        Value::DateTime(dt) => dt.to_iso8601(),
91        Value::Duration(d) => d.to_human(),
92        Value::Decimal(d) => d.to_string(),
93        other => {
94            // Fallback: convert to JSON string representation.
95            let json = serde_json::Value::from(other.clone());
96            json.to_string()
97        }
98    }
99}
100
101/// Convert an f64 to a Value, preferring integer representation.
102///
103/// Returns `Null` for NaN/Infinity.
104pub fn to_value_number(n: f64) -> Value {
105    if n.is_nan() || n.is_infinite() {
106        Value::Null
107    } else if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
108        Value::Integer(n as i64)
109    } else {
110        Value::Float(n)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn coerced_eq_mixed_types() {
120        assert!(coerced_eq(&Value::Integer(5), &Value::String("5".into())));
121        assert!(!coerced_eq(&Value::Integer(5), &Value::String("6".into())));
122    }
123
124    #[test]
125    fn coerced_eq_bool_numeric() {
126        assert!(coerced_eq(&Value::Bool(true), &Value::Integer(1)));
127        assert!(coerced_eq(&Value::Bool(false), &Value::Integer(0)));
128        assert!(!coerced_eq(&Value::Bool(true), &Value::Integer(0)));
129    }
130
131    #[test]
132    fn compare_numeric_coercion() {
133        assert_eq!(
134            compare_values(&Value::Integer(5), &Value::String("4".into())),
135            Ordering::Greater
136        );
137    }
138
139    #[test]
140    fn truthiness() {
141        assert!(is_truthy(&Value::Bool(true)));
142        assert!(!is_truthy(&Value::Bool(false)));
143        assert!(!is_truthy(&Value::Null));
144        assert!(is_truthy(&Value::Integer(1)));
145        assert!(!is_truthy(&Value::Integer(0)));
146        assert!(is_truthy(&Value::String("hello".into())));
147        assert!(!is_truthy(&Value::String(String::new())));
148    }
149
150    #[test]
151    fn to_value_number_nan() {
152        assert_eq!(to_value_number(f64::NAN), Value::Null);
153    }
154
155    #[test]
156    fn to_value_number_integer() {
157        assert_eq!(to_value_number(42.0), Value::Integer(42));
158    }
159
160    #[test]
161    fn to_value_number_float() {
162        assert_eq!(to_value_number(3.15), Value::Float(3.15));
163    }
164}