Skip to main content

core_query/
value_ops.rs

1use core_storage::Value;
2use std::cmp::Ordering;
3
4pub fn values_equal(a: &Value, b: &Value) -> bool {
5    match (a, b) {
6        (Value::Int(x), Value::Int(y)) => x == y,
7        (Value::Float(x), Value::Float(y)) => x == y,
8        (Value::Int(x), Value::Float(y)) => *x as f64 == *y,
9        (Value::Float(x), Value::Int(y)) => *x == *y as f64,
10        (Value::Str(x), Value::Str(y)) => x == y,
11        (Value::Bool(x), Value::Bool(y)) => x == y,
12        (Value::List(x), Value::List(y)) => {
13            x.len() == y.len() && x.iter().zip(y.iter()).all(|(l, r)| values_equal(l, r))
14        }
15        (Value::Map(x), Value::Map(y)) => {
16            x.len() == y.len()
17                && x.iter()
18                    .zip(y.iter())
19                    .all(|((k1, v1), (k2, v2))| k1 == k2 && values_equal(v1, v2))
20        }
21        _ => false,
22    }
23}
24
25fn class_rank(v: &Value) -> u8 {
26    match v {
27        Value::Int(_) | Value::Float(_) => 0,
28        Value::Str(_) => 1,
29        Value::Bool(_) => 2,
30        Value::List(_) => 3,
31        // Map sorts after all other types.
32        Value::Map(_) => 4,
33    }
34}
35
36fn as_f64(v: &Value) -> f64 {
37    match v {
38        Value::Int(i) => *i as f64,
39        Value::Float(f) => *f,
40        _ => unreachable!("as_f64 only for numeric class"),
41    }
42}
43
44pub fn cmp_values(a: &Value, b: &Value) -> Ordering {
45    let ra = class_rank(a);
46    let rb = class_rank(b);
47    if ra != rb {
48        return ra.cmp(&rb);
49    }
50    match (a, b) {
51        (Value::Int(_) | Value::Float(_), Value::Int(_) | Value::Float(_)) => {
52            as_f64(a).total_cmp(&as_f64(b))
53        }
54        (Value::Str(x), Value::Str(y)) => x.cmp(y),
55        (Value::Bool(x), Value::Bool(y)) => x.cmp(y),
56        (Value::List(x), Value::List(y)) => {
57            for (l, r) in x.iter().zip(y.iter()) {
58                let c = cmp_values(l, r);
59                if c != Ordering::Equal {
60                    return c;
61                }
62            }
63            x.len().cmp(&y.len())
64        }
65        (Value::Map(x), Value::Map(y)) => {
66            // BTreeMap iterates in key-sorted order; compare key then value.
67            for ((k1, v1), (k2, v2)) in x.iter().zip(y.iter()) {
68                let ck = k1.cmp(k2);
69                if ck != Ordering::Equal {
70                    return ck;
71                }
72                let cv = cmp_values(v1, v2);
73                if cv != Ordering::Equal {
74                    return cv;
75                }
76            }
77            x.len().cmp(&y.len())
78        }
79        _ => unreachable!("same class rank implies matching variants"),
80    }
81}
82
83pub fn cmp_optional(a: Option<&Value>, b: Option<&Value>, descending: bool) -> Ordering {
84    match (a, b) {
85        (None, None) => Ordering::Equal,
86        // None sorts LAST regardless of ascending/descending.
87        (None, Some(_)) => Ordering::Greater,
88        (Some(_), None) => Ordering::Less,
89        (Some(x), Some(y)) => {
90            let c = cmp_values(x, y);
91            if descending {
92                c.reverse()
93            } else {
94                c
95            }
96        }
97    }
98}
99
100/// One element of a list value, openCypher subscript semantics.
101///
102/// A negative index counts from the end. A non-list base, a non-integer
103/// index, or an out-of-range index all give null — a subscript reads like a
104/// property that is not there, never an error.
105pub fn index_list(base: Option<Value>, index: Option<Value>) -> Option<Value> {
106    let (Some(Value::List(items)), Some(idx)) = (base, index) else {
107        return None;
108    };
109    let i = match idx {
110        Value::Int(n) => n,
111        Value::Float(f) if f.fract() == 0.0 && f.is_finite() => f as i64,
112        _ => return None,
113    };
114    let len = i64::try_from(items.len()).ok()?;
115    let pos = if i < 0 { len.checked_add(i)? } else { i };
116    if pos < 0 || pos >= len {
117        return None;
118    }
119    items.into_iter().nth(pos as usize)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::{cmp_optional, cmp_values, values_equal};
125    use core_storage::Value;
126    use std::cmp::Ordering::*;
127
128    #[test]
129    fn int_equals_float_numerically() {
130        assert!(values_equal(&Value::Int(2), &Value::Float(2.0)));
131        assert!(values_equal(&Value::Float(2.0), &Value::Int(2)));
132        assert!(!values_equal(&Value::Int(2), &Value::Float(2.1)));
133    }
134
135    #[test]
136    fn same_variant_equality() {
137        assert!(values_equal(&Value::Int(1), &Value::Int(1)));
138        assert!(!values_equal(&Value::Int(1), &Value::Int(2)));
139        assert!(values_equal(
140            &Value::Str("a".into()),
141            &Value::Str("a".into())
142        ));
143        assert!(!values_equal(
144            &Value::Str("a".into()),
145            &Value::Str("b".into())
146        ));
147        assert!(values_equal(&Value::Bool(true), &Value::Bool(true)));
148        assert!(!values_equal(&Value::Bool(true), &Value::Bool(false)));
149        assert!(values_equal(&Value::Float(1.5), &Value::Float(1.5)));
150        assert!(values_equal(
151            &Value::List(vec![Value::Int(1), Value::Float(2.0)]),
152            &Value::List(vec![Value::Float(1.0), Value::Int(2)]),
153        ));
154        assert!(!values_equal(
155            &Value::List(vec![Value::Int(1)]),
156            &Value::List(vec![Value::Int(1), Value::Int(2)]),
157        ));
158    }
159
160    #[test]
161    fn cross_variant_not_equal_except_int_float() {
162        assert!(!values_equal(&Value::Str("2".into()), &Value::Int(2)));
163        assert!(!values_equal(&Value::Bool(true), &Value::Int(1)));
164        assert!(!values_equal(&Value::List(vec![]), &Value::Str("".into())));
165        assert!(!values_equal(&Value::Bool(false), &Value::Float(0.0)));
166    }
167
168    #[test]
169    fn cmp_values_class_ranks() {
170        // numeric < Str < Bool < List
171        assert_eq!(cmp_values(&Value::Int(99), &Value::Str("a".into())), Less);
172        assert_eq!(cmp_values(&Value::Float(1.0), &Value::Bool(false)), Less);
173        assert_eq!(
174            cmp_values(&Value::Str("z".into()), &Value::Bool(false)),
175            Less
176        );
177        assert_eq!(cmp_values(&Value::Bool(true), &Value::List(vec![])), Less);
178        assert_eq!(cmp_values(&Value::List(vec![]), &Value::Int(0)), Greater);
179
180        // numerics via f64::total_cmp
181        assert_eq!(cmp_values(&Value::Int(1), &Value::Float(1.5)), Less);
182        assert_eq!(cmp_values(&Value::Int(2), &Value::Float(2.0)), Equal);
183        // f64::total_cmp: -0.0 < +0.0 (unlike IEEE ==).
184        assert_eq!(cmp_values(&Value::Float(-0.0), &Value::Float(0.0)), Less);
185
186        // str lexicographic
187        assert_eq!(
188            cmp_values(&Value::Str("a".into()), &Value::Str("b".into())),
189            Less
190        );
191
192        // bool false < true
193        assert_eq!(cmp_values(&Value::Bool(false), &Value::Bool(true)), Less);
194
195        // list elementwise, then length
196        assert_eq!(
197            cmp_values(
198                &Value::List(vec![Value::Int(1), Value::Int(2)]),
199                &Value::List(vec![Value::Int(1), Value::Int(3)]),
200            ),
201            Less
202        );
203        assert_eq!(
204            cmp_values(
205                &Value::List(vec![Value::Int(1)]),
206                &Value::List(vec![Value::Int(1), Value::Int(2)]),
207            ),
208            Less
209        );
210        // list elements use class ranks
211        assert_eq!(
212            cmp_values(
213                &Value::List(vec![Value::Int(1)]),
214                &Value::List(vec![Value::Str("a".into())]),
215            ),
216            Less
217        );
218    }
219
220    #[test]
221    fn cmp_optional_none_last_both_directions() {
222        let a = Value::Int(1);
223        let b = Value::Int(2);
224        assert_eq!(cmp_optional(None, Some(&a), false), Greater);
225        assert_eq!(cmp_optional(Some(&a), None, false), Less);
226        assert_eq!(cmp_optional(None, Some(&a), true), Greater);
227        assert_eq!(cmp_optional(Some(&a), None, true), Less);
228        assert_eq!(cmp_optional(None, None, false), Equal);
229        assert_eq!(cmp_optional(None, None, true), Equal);
230        assert_eq!(cmp_optional(Some(&a), Some(&b), false), Less);
231        assert_eq!(cmp_optional(Some(&a), Some(&b), true), Greater);
232        assert_eq!(cmp_optional(Some(&a), Some(&a), false), Equal);
233        assert_eq!(cmp_optional(Some(&a), Some(&a), true), Equal);
234    }
235}