nodedb_query/
value_ops.rs1use std::cmp::Ordering;
8
9use nodedb_types::Value;
10
11pub 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
31pub 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
44pub 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
58pub 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
76pub 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 let json = serde_json::Value::from(other.clone());
96 json.to_string()
97 }
98 }
99}
100
101pub 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}