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        (Value::Timestamp(a), Value::Timestamp(b)) => Some(a.cmp(b)),
48        (Value::Date(a), Value::Date(b)) => Some(a.cmp(b)),
49        (Value::Time(a), Value::Time(b)) => Some(a.cmp(b)),
50        _ => None,
51    }
52}
53
54/// Compares two values with total ordering (returns `Equal` for incomparable types).
55///
56/// Used by sort operators where a total order is required.
57pub fn compare_values_total(a: &Value, b: &Value) -> Ordering {
58    match (a, b) {
59        (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
60        (Value::Int64(a), Value::Int64(b)) => a.cmp(b),
61        (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
62        (Value::String(a), Value::String(b)) => a.cmp(b),
63        (Value::Int64(a), Value::Float64(b)) => {
64            (*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal)
65        }
66        (Value::Float64(a), Value::Int64(b)) => {
67            a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal)
68        }
69        (Value::Timestamp(a), Value::Timestamp(b)) => a.cmp(b),
70        (Value::Date(a), Value::Date(b)) => a.cmp(b),
71        (Value::Time(a), Value::Time(b)) => a.cmp(b),
72        _ => Ordering::Equal,
73    }
74}
75
76/// Returns `true` if `new` is less than `current` (for MIN aggregation).
77///
78/// Returns `true` when `current` is `None` (first value always wins).
79pub fn is_less_than(current: &Option<Value>, new: &Value) -> bool {
80    match current {
81        None => true,
82        Some(curr) => compare_values(new, curr) == Some(Ordering::Less),
83    }
84}
85
86/// Returns `true` if `new` is greater than `current` (for MAX aggregation).
87///
88/// Returns `true` when `current` is `None` (first value always wins).
89pub fn is_greater_than(current: &Option<Value>, new: &Value) -> bool {
90    match current {
91        None => true,
92        Some(curr) => compare_values(new, curr) == Some(Ordering::Greater),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn value_to_f64_int() {
102        assert_eq!(value_to_f64(&Value::Int64(42)), Some(42.0));
103    }
104
105    #[test]
106    fn value_to_f64_float() {
107        assert_eq!(value_to_f64(&Value::Float64(2.72)), Some(2.72));
108    }
109
110    #[test]
111    fn value_to_f64_numeric_string() {
112        assert_eq!(value_to_f64(&Value::String("2.5".into())), Some(2.5));
113    }
114
115    #[test]
116    fn value_to_f64_non_numeric_string() {
117        assert_eq!(value_to_f64(&Value::String("abc".into())), None);
118    }
119
120    #[test]
121    fn value_to_f64_null() {
122        assert_eq!(value_to_f64(&Value::Null), None);
123    }
124
125    #[test]
126    fn compare_same_type_int() {
127        assert_eq!(
128            compare_values(&Value::Int64(1), &Value::Int64(2)),
129            Some(Ordering::Less)
130        );
131    }
132
133    #[test]
134    fn compare_cross_type_int_float() {
135        assert_eq!(
136            compare_values(&Value::Int64(2), &Value::Float64(2.0)),
137            Some(Ordering::Equal)
138        );
139    }
140
141    #[test]
142    fn compare_rdf_numeric_strings() {
143        assert_eq!(
144            compare_values(&Value::String("10".into()), &Value::String("9".into())),
145            Some(Ordering::Greater)
146        );
147    }
148
149    #[test]
150    fn compare_incomparable() {
151        assert_eq!(compare_values(&Value::Bool(true), &Value::Int64(1)), None);
152    }
153
154    #[test]
155    fn total_ordering_incomparable_returns_equal() {
156        assert_eq!(
157            compare_values_total(&Value::Bool(true), &Value::Int64(1)),
158            Ordering::Equal
159        );
160    }
161
162    #[test]
163    fn is_less_than_none_always_true() {
164        assert!(is_less_than(&None, &Value::Int64(5)));
165    }
166
167    #[test]
168    fn is_less_than_smaller() {
169        assert!(is_less_than(&Some(Value::Int64(10)), &Value::Int64(5)));
170    }
171
172    #[test]
173    fn is_less_than_larger() {
174        assert!(!is_less_than(&Some(Value::Int64(3)), &Value::Int64(5)));
175    }
176
177    #[test]
178    fn is_greater_than_none_always_true() {
179        assert!(is_greater_than(&None, &Value::Int64(5)));
180    }
181
182    #[test]
183    fn is_greater_than_larger() {
184        assert!(is_greater_than(&Some(Value::Int64(3)), &Value::Int64(5)));
185    }
186
187    #[test]
188    fn is_greater_than_smaller() {
189        assert!(!is_greater_than(&Some(Value::Int64(10)), &Value::Int64(5)));
190    }
191}