Skip to main content

nodedb_query/
value_ops.rs

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