corium_query/
aggregate.rs1use corium_core::{TotalF64, Value};
8
9use crate::QueryError;
10use crate::builtins::compare;
11
12#[derive(Clone, Debug, PartialEq)]
15pub enum AggOut {
16 Value(Value),
18 Coll(Vec<Value>),
20 Set(Vec<Value>),
22}
23
24fn distinct_sorted(values: &[Value]) -> Result<Vec<Value>, QueryError> {
25 let mut out: Vec<Value> = Vec::new();
26 for value in values {
27 if !out.iter().any(|v| v == value) {
28 out.push(value.clone());
29 }
30 }
31 sort_values(&mut out)?;
32 Ok(out)
33}
34
35fn sort_values(values: &mut [Value]) -> Result<(), QueryError> {
36 for window in values.windows(2) {
38 compare(&window[0], &window[1])?;
39 }
40 values.sort_by(|left, right| compare(left, right).unwrap_or(std::cmp::Ordering::Equal));
41 Ok(())
42}
43
44#[allow(clippy::cast_precision_loss)]
45fn numeric_sum(values: &[Value]) -> Result<Value, QueryError> {
46 let mut long_sum: i64 = 0;
47 let mut double_sum: f64 = 0.0;
48 let mut any_double = false;
49 for value in values {
50 match value {
51 Value::Long(v) => {
52 long_sum = long_sum
53 .checked_add(*v)
54 .ok_or_else(|| QueryError::Type("sum overflowed".into()))?;
55 }
56 Value::Double(v) => {
57 any_double = true;
58 double_sum += v.0;
59 }
60 _ => return Err(QueryError::Type("sum requires numbers".into())),
61 }
62 }
63 if any_double {
64 Ok(Value::Double(TotalF64(double_sum + long_sum as f64)))
65 } else {
66 Ok(Value::Long(long_sum))
67 }
68}
69
70#[allow(clippy::cast_precision_loss)]
71fn as_f64(value: &Value) -> Result<f64, QueryError> {
72 match value {
73 Value::Long(v) => Ok(*v as f64),
74 Value::Double(v) => Ok(v.0),
75 _ => Err(QueryError::Type("aggregate requires numbers".into())),
76 }
77}
78
79#[allow(clippy::cast_precision_loss)]
86pub fn apply(op: &str, n: Option<i64>, values: &[Value]) -> Result<AggOut, QueryError> {
87 let take = |n: Option<i64>| -> Result<usize, QueryError> {
88 n.and_then(|n| usize::try_from(n).ok())
89 .ok_or_else(|| QueryError::Arity(format!("({op} n ?x) requires a positive count")))
90 };
91 match (op, n) {
92 ("count", None) => Ok(AggOut::Value(Value::Long(
93 i64::try_from(values.len()).unwrap_or(i64::MAX),
94 ))),
95 ("count-distinct", None) => Ok(AggOut::Value(Value::Long(
96 i64::try_from(distinct_sorted(values)?.len()).unwrap_or(i64::MAX),
97 ))),
98 ("sum", None) => numeric_sum(values).map(AggOut::Value),
99 ("avg", None) => {
100 if values.is_empty() {
101 return Err(QueryError::Type("avg of no values".into()));
102 }
103 let sum: f64 = values.iter().map(as_f64).sum::<Result<f64, _>>()?;
104 Ok(AggOut::Value(Value::Double(TotalF64(
105 sum / values.len() as f64,
106 ))))
107 }
108 ("median", None) => {
109 if values.is_empty() {
110 return Err(QueryError::Type("median of no values".into()));
111 }
112 let mut sorted = values.to_vec();
113 sort_values(&mut sorted)?;
114 Ok(AggOut::Value(sorted[(sorted.len() - 1) / 2].clone()))
115 }
116 ("min" | "max", None) => {
117 if values.is_empty() {
118 return Err(QueryError::Type(format!("{op} of no values")));
119 }
120 let mut best = values[0].clone();
121 for value in &values[1..] {
122 let ordering = compare(value, &best)?;
123 if (op == "min" && ordering.is_lt()) || (op == "max" && ordering.is_gt()) {
124 best = value.clone();
125 }
126 }
127 Ok(AggOut::Value(best))
128 }
129 ("min" | "sample", Some(_)) => {
130 let count = take(n)?;
131 let sorted = distinct_sorted(values)?;
132 Ok(AggOut::Coll(sorted.into_iter().take(count).collect()))
133 }
134 ("max", Some(_)) => {
135 let count = take(n)?;
136 let sorted = distinct_sorted(values)?;
137 let skip = sorted.len().saturating_sub(count);
138 Ok(AggOut::Coll(sorted.into_iter().skip(skip).collect()))
139 }
140 ("distinct", None) => Ok(AggOut::Set(distinct_sorted(values)?)),
141 ("variance" | "stddev", None) => {
142 if values.is_empty() {
143 return Err(QueryError::Type(format!("{op} of no values")));
144 }
145 let nums: Vec<f64> = values.iter().map(as_f64).collect::<Result<_, _>>()?;
146 let mean = nums.iter().sum::<f64>() / nums.len() as f64;
147 let variance = nums.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / nums.len() as f64;
148 let out = if op == "variance" {
149 variance
150 } else {
151 variance.sqrt()
152 };
153 Ok(AggOut::Value(Value::Double(TotalF64(out))))
154 }
155 _ => Err(QueryError::Unsupported(format!(
156 "unknown aggregate ({op} …)"
157 ))),
158 }
159}