Skip to main content

grafeo_core/execution/operators/
value_utils.rs

1//! Shared value comparison and conversion utilities.
2//!
3//! Used by pull-based and push-based aggregate, filter, and sort operators
4//! to avoid duplicating comparison logic across six different files.
5
6use std::cmp::Ordering;
7
8use grafeo_common::types::Value;
9
10/// Converts a value to `f64` for numeric aggregations.
11///
12/// Supports RDF values stored as strings by attempting numeric parsing.
13pub fn value_to_f64(value: &Value) -> Option<f64> {
14    match value {
15        Value::Int64(i) => Some(*i as f64),
16        Value::Float64(f) => Some(*f),
17        // RDF stores numeric literals as strings - try to parse them
18        Value::String(s) => s.parse::<f64>().ok(),
19        _ => None,
20    }
21}
22
23/// Compares two values with partial ordering (returns `None` for incomparable types).
24///
25/// Handles cross-type comparisons between Int64/Float64/String, including
26/// RDF numeric strings that need parsing before comparison.
27pub fn compare_values(a: &Value, b: &Value) -> Option<Ordering> {
28    match (a, b) {
29        (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
30        (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b),
31        (Value::String(a), Value::String(b)) => {
32            // Try numeric comparison first if both look like numbers
33            if let (Ok(a_num), Ok(b_num)) = (a.parse::<f64>(), b.parse::<f64>()) {
34                a_num.partial_cmp(&b_num)
35            } else {
36                Some(a.cmp(b))
37            }
38        }
39        (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
40        (Value::Int64(a), Value::Float64(b)) => (*a as f64).partial_cmp(b),
41        (Value::Float64(a), Value::Int64(b)) => a.partial_cmp(&(*b as f64)),
42        // String-to-numeric comparisons for RDF
43        (Value::String(s), Value::Int64(i)) => s.parse::<f64>().ok()?.partial_cmp(&(*i as f64)),
44        (Value::String(s), Value::Float64(f)) => s.parse::<f64>().ok()?.partial_cmp(f),
45        (Value::Int64(i), Value::String(s)) => (*i as f64).partial_cmp(&s.parse::<f64>().ok()?),
46        (Value::Float64(f), Value::String(s)) => f.partial_cmp(&s.parse::<f64>().ok()?),
47        _ => None,
48    }
49}
50
51/// Compares two values with total ordering (returns `Equal` for incomparable types).
52///
53/// Used by sort operators where a total order is required.
54pub fn compare_values_total(a: &Value, b: &Value) -> Ordering {
55    match (a, b) {
56        (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
57        (Value::Int64(a), Value::Int64(b)) => a.cmp(b),
58        (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
59        (Value::String(a), Value::String(b)) => a.cmp(b),
60        (Value::Int64(a), Value::Float64(b)) => {
61            (*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal)
62        }
63        (Value::Float64(a), Value::Int64(b)) => {
64            a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal)
65        }
66        _ => Ordering::Equal,
67    }
68}
69
70/// Returns `true` if `new` is less than `current` (for MIN aggregation).
71///
72/// Returns `true` when `current` is `None` (first value always wins).
73pub fn is_less_than(current: &Option<Value>, new: &Value) -> bool {
74    match current {
75        None => true,
76        Some(curr) => compare_values(new, curr) == Some(Ordering::Less),
77    }
78}
79
80/// Returns `true` if `new` is greater than `current` (for MAX aggregation).
81///
82/// Returns `true` when `current` is `None` (first value always wins).
83pub fn is_greater_than(current: &Option<Value>, new: &Value) -> bool {
84    match current {
85        None => true,
86        Some(curr) => compare_values(new, curr) == Some(Ordering::Greater),
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn value_to_f64_int() {
96        assert_eq!(value_to_f64(&Value::Int64(42)), Some(42.0));
97    }
98
99    #[test]
100    fn value_to_f64_float() {
101        assert_eq!(value_to_f64(&Value::Float64(2.72)), Some(2.72));
102    }
103
104    #[test]
105    fn value_to_f64_numeric_string() {
106        assert_eq!(value_to_f64(&Value::String("2.5".into())), Some(2.5));
107    }
108
109    #[test]
110    fn value_to_f64_non_numeric_string() {
111        assert_eq!(value_to_f64(&Value::String("abc".into())), None);
112    }
113
114    #[test]
115    fn value_to_f64_null() {
116        assert_eq!(value_to_f64(&Value::Null), None);
117    }
118
119    #[test]
120    fn compare_same_type_int() {
121        assert_eq!(
122            compare_values(&Value::Int64(1), &Value::Int64(2)),
123            Some(Ordering::Less)
124        );
125    }
126
127    #[test]
128    fn compare_cross_type_int_float() {
129        assert_eq!(
130            compare_values(&Value::Int64(2), &Value::Float64(2.0)),
131            Some(Ordering::Equal)
132        );
133    }
134
135    #[test]
136    fn compare_rdf_numeric_strings() {
137        assert_eq!(
138            compare_values(&Value::String("10".into()), &Value::String("9".into())),
139            Some(Ordering::Greater)
140        );
141    }
142
143    #[test]
144    fn compare_incomparable() {
145        assert_eq!(compare_values(&Value::Bool(true), &Value::Int64(1)), None);
146    }
147
148    #[test]
149    fn total_ordering_incomparable_returns_equal() {
150        assert_eq!(
151            compare_values_total(&Value::Bool(true), &Value::Int64(1)),
152            Ordering::Equal
153        );
154    }
155
156    #[test]
157    fn is_less_than_none_always_true() {
158        assert!(is_less_than(&None, &Value::Int64(5)));
159    }
160
161    #[test]
162    fn is_less_than_smaller() {
163        assert!(is_less_than(&Some(Value::Int64(10)), &Value::Int64(5)));
164    }
165
166    #[test]
167    fn is_less_than_larger() {
168        assert!(!is_less_than(&Some(Value::Int64(3)), &Value::Int64(5)));
169    }
170
171    #[test]
172    fn is_greater_than_none_always_true() {
173        assert!(is_greater_than(&None, &Value::Int64(5)));
174    }
175
176    #[test]
177    fn is_greater_than_larger() {
178        assert!(is_greater_than(&Some(Value::Int64(3)), &Value::Int64(5)));
179    }
180
181    #[test]
182    fn is_greater_than_smaller() {
183        assert!(!is_greater_than(&Some(Value::Int64(10)), &Value::Int64(5)));
184    }
185}