Skip to main content

nodedb_types/value/
coerce.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Type-coerced equality and ordering for `Value`.
4//!
5//! Single source of truth for type coercion in filter/sort evaluation.
6
7use super::core::Value;
8
9impl Value {
10    /// Coerced equality: `Value` vs `Value` with numeric/string coercion.
11    ///
12    /// Single source of truth for type coercion in filter evaluation.
13    /// Used by `matches_binary` (msgpack path) and `matches_value` (Value path).
14    pub fn eq_coerced(&self, other: &Value) -> bool {
15        match (self, other) {
16            (Value::Null, Value::Null) => true,
17            (Value::Bool(a), Value::Bool(b)) => a == b,
18            (Value::Integer(a), Value::Integer(b)) => a == b,
19            (Value::Integer(a), Value::Float(b)) => *a as f64 == *b,
20            (Value::Float(a), Value::Integer(b)) => *a == *b as f64,
21            (Value::Float(a), Value::Float(b)) => a == b,
22            (Value::String(a), Value::String(b)) => a == b,
23            // Coercion: number vs string
24            (Value::Integer(a), Value::String(s)) => {
25                s.parse::<i64>().is_ok_and(|n| *a == n)
26                    || s.parse::<f64>().is_ok_and(|n| *a as f64 == n)
27            }
28            (Value::String(s), Value::Integer(b)) => {
29                s.parse::<i64>().is_ok_and(|n| n == *b)
30                    || s.parse::<f64>().is_ok_and(|n| n == *b as f64)
31            }
32            (Value::Float(a), Value::String(s)) => s.parse::<f64>().is_ok_and(|n| *a == n),
33            (Value::String(s), Value::Float(b)) => s.parse::<f64>().is_ok_and(|n| n == *b),
34            // Structural equality on ND cells: same coords and same attrs.
35            (Value::ArrayCell(a), Value::ArrayCell(b)) => a == b,
36            _ => false,
37        }
38    }
39
40    /// Coerced ordering: `Value` vs `Value` with numeric/string coercion.
41    ///
42    /// Single source of truth for ordering in filter/sort evaluation.
43    pub fn cmp_coerced(&self, other: &Value) -> std::cmp::Ordering {
44        use std::cmp::Ordering;
45        // ND cells: lexicographic on coords, then attrs. Matches array
46        // engine cell ordering (coordinate-major).
47        if let (Value::ArrayCell(a), Value::ArrayCell(b)) = (self, other) {
48            for (x, y) in a.coords.iter().zip(b.coords.iter()) {
49                match x.cmp_coerced(y) {
50                    Ordering::Equal => continue,
51                    non_eq => return non_eq,
52                }
53            }
54            match a.coords.len().cmp(&b.coords.len()) {
55                Ordering::Equal => {}
56                non_eq => return non_eq,
57            }
58            for (x, y) in a.attrs.iter().zip(b.attrs.iter()) {
59                match x.cmp_coerced(y) {
60                    Ordering::Equal => continue,
61                    non_eq => return non_eq,
62                }
63            }
64            return a.attrs.len().cmp(&b.attrs.len());
65        }
66        let self_f64 = match self {
67            Value::Integer(i) => Some(*i as f64),
68            Value::Float(f) => Some(*f),
69            Value::String(s) => s.parse::<f64>().ok(),
70            _ => None,
71        };
72        let other_f64 = match other {
73            Value::Integer(i) => Some(*i as f64),
74            Value::Float(f) => Some(*f),
75            Value::String(s) => s.parse::<f64>().ok(),
76            _ => None,
77        };
78        if let (Some(a), Some(b)) = (self_f64, other_f64) {
79            return a.partial_cmp(&b).unwrap_or(Ordering::Equal);
80        }
81        let a_str = match self {
82            Value::String(s) => s.as_str(),
83            _ => return Ordering::Equal,
84        };
85        let b_str = match other {
86            Value::String(s) => s.as_str(),
87            _ => return Ordering::Equal,
88        };
89        a_str.cmp(b_str)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn eq_coerced_same_type() {
99        assert!(Value::Null.eq_coerced(&Value::Null));
100        assert!(Value::Bool(true).eq_coerced(&Value::Bool(true)));
101        assert!(!Value::Bool(true).eq_coerced(&Value::Bool(false)));
102        assert!(Value::Integer(42).eq_coerced(&Value::Integer(42)));
103        assert!(Value::Float(2.78).eq_coerced(&Value::Float(2.78)));
104        assert!(Value::String("hello".into()).eq_coerced(&Value::String("hello".into())));
105    }
106
107    #[test]
108    fn eq_coerced_int_float() {
109        assert!(Value::Integer(5).eq_coerced(&Value::Float(5.0)));
110        assert!(Value::Float(5.0).eq_coerced(&Value::Integer(5)));
111        assert!(!Value::Integer(5).eq_coerced(&Value::Float(5.1)));
112    }
113
114    #[test]
115    fn eq_coerced_string_number() {
116        assert!(Value::String("5".into()).eq_coerced(&Value::Integer(5)));
117        assert!(Value::Integer(5).eq_coerced(&Value::String("5".into())));
118        assert!(Value::String("2.78".into()).eq_coerced(&Value::Float(2.78)));
119        assert!(Value::Float(2.78).eq_coerced(&Value::String("2.78".into())));
120        assert!(!Value::String("abc".into()).eq_coerced(&Value::Integer(5)));
121        assert!(!Value::Integer(5).eq_coerced(&Value::String("abc".into())));
122    }
123
124    #[test]
125    fn eq_coerced_cross_type_false() {
126        assert!(!Value::Bool(true).eq_coerced(&Value::Integer(1)));
127        assert!(!Value::Null.eq_coerced(&Value::Integer(0)));
128        assert!(!Value::Null.eq_coerced(&Value::String("".into())));
129    }
130
131    #[test]
132    fn cmp_coerced_numeric() {
133        use std::cmp::Ordering;
134        assert_eq!(
135            Value::Integer(5).cmp_coerced(&Value::Integer(10)),
136            Ordering::Less
137        );
138        assert_eq!(
139            Value::Integer(10).cmp_coerced(&Value::Float(5.0)),
140            Ordering::Greater
141        );
142        assert_eq!(
143            Value::String("90".into()).cmp_coerced(&Value::Integer(80)),
144            Ordering::Greater
145        );
146        assert_eq!(
147            Value::Float(2.78).cmp_coerced(&Value::String("2.78".into())),
148            Ordering::Equal
149        );
150    }
151
152    #[test]
153    fn cmp_coerced_string_fallback() {
154        use std::cmp::Ordering;
155        assert_eq!(
156            Value::String("abc".into()).cmp_coerced(&Value::String("def".into())),
157            Ordering::Less
158        );
159        assert_eq!(
160            Value::String("z".into()).cmp_coerced(&Value::String("a".into())),
161            Ordering::Greater
162        );
163    }
164
165    #[test]
166    fn eq_coerced_symmetry() {
167        let cases = [
168            (Value::Integer(42), Value::String("42".into())),
169            (Value::Float(2.78), Value::String("2.78".into())),
170            (Value::Integer(5), Value::Float(5.0)),
171        ];
172        for (a, b) in &cases {
173            assert_eq!(
174                a.eq_coerced(b),
175                b.eq_coerced(a),
176                "symmetry violated for {a:?} vs {b:?}"
177            );
178        }
179    }
180}