1use crate::data::{compare_cells, CellValue};
9use std::cmp::Ordering;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub enum AggregationFn {
14 Count,
16 #[default]
19 Sum,
20 Avg,
22 Min,
24 Max,
26}
27
28impl AggregationFn {
29 #[must_use]
31 pub fn all() -> [AggregationFn; 5] {
32 [Self::Count, Self::Sum, Self::Avg, Self::Min, Self::Max]
33 }
34
35 #[must_use]
37 pub fn label(&self) -> &'static str {
38 match self {
39 Self::Count => "Count",
40 Self::Sum => "Sum",
41 Self::Avg => "Avg",
42 Self::Min => "Min",
43 Self::Max => "Max",
44 }
45 }
46
47 #[must_use]
49 pub fn caption(&self, field_name: &str) -> String {
50 format!("{} of {}", self.label(), field_name)
51 }
52}
53
54#[derive(Clone, Debug)]
57pub struct Accumulator {
58 func: AggregationFn,
59 count: u64,
60 int_sum: i64,
61 float_sum: f64,
62 promoted: bool,
65 extreme: Option<CellValue>,
66}
67
68impl Accumulator {
69 #[must_use]
71 pub fn new(func: AggregationFn) -> Self {
72 Self {
73 func,
74 count: 0,
75 int_sum: 0,
76 float_sum: 0.0,
77 promoted: false,
78 extreme: None,
79 }
80 }
81
82 pub fn ingest(&mut self, value: &CellValue) {
87 if matches!(value, CellValue::None) {
88 return;
89 }
90 self.count += 1;
91 match self.func {
92 AggregationFn::Count => {}
93 AggregationFn::Sum | AggregationFn::Avg => match value {
94 CellValue::Integer(v) => {
95 if self.promoted {
96 self.float_sum += *v as f64;
97 } else {
98 match self.int_sum.checked_add(*v) {
99 Some(next) => self.int_sum = next,
100 None => {
101 self.promote();
102 self.float_sum += *v as f64;
103 }
104 }
105 }
106 }
107 CellValue::Decimal(v) => {
108 if !self.promoted {
109 self.promote();
110 }
111 self.float_sum += *v;
112 }
113 _ => self.count -= 1,
117 },
118 AggregationFn::Min => {
119 let replace = match &self.extreme {
120 Some(current) => compare_cells(value, current) == Ordering::Less,
121 None => true,
122 };
123 if replace {
124 self.extreme = Some(value.clone());
125 }
126 }
127 AggregationFn::Max => {
128 let replace = match &self.extreme {
129 Some(current) => compare_cells(value, current) == Ordering::Greater,
130 None => true,
131 };
132 if replace {
133 self.extreme = Some(value.clone());
134 }
135 }
136 }
137 }
138
139 fn promote(&mut self) {
140 self.float_sum += self.int_sum as f64;
141 self.int_sum = 0;
142 self.promoted = true;
143 }
144
145 #[must_use]
148 pub fn finish(&self) -> CellValue {
149 if self.count == 0 && !matches!(self.func, AggregationFn::Count) {
150 return CellValue::None;
151 }
152 match self.func {
153 AggregationFn::Count => CellValue::Integer(self.count as i64),
154 AggregationFn::Sum => {
155 if self.promoted {
156 CellValue::Decimal(self.float_sum)
157 } else {
158 CellValue::Integer(self.int_sum)
159 }
160 }
161 AggregationFn::Avg => {
162 let total = if self.promoted {
163 self.float_sum
164 } else {
165 self.int_sum as f64
166 };
167 CellValue::Decimal(total / self.count as f64)
168 }
169 AggregationFn::Min | AggregationFn::Max => {
170 self.extreme.clone().unwrap_or(CellValue::None)
171 }
172 }
173 }
174}
175
176#[must_use]
179pub fn aggregate(values: &[CellValue], func: AggregationFn) -> CellValue {
180 let mut acc = Accumulator::new(func);
181 for v in values {
182 acc.ingest(v);
183 }
184 acc.finish()
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use CellValue::{Boolean, Decimal, Integer, None as Null, Text};
191
192 #[test]
193 fn count_skips_nulls_and_counts_everything_else() {
194 let vals = vec![
195 Integer(1),
196 Null,
197 Text("x".into()),
198 Boolean(true),
199 Decimal(2.5),
200 Null,
201 ];
202 assert_eq!(aggregate(&vals, AggregationFn::Count), Integer(4));
203 }
204
205 #[test]
206 fn count_of_empty_is_zero() {
207 assert_eq!(aggregate(&[], AggregationFn::Count), Integer(0));
208 assert_eq!(aggregate(&[Null, Null], AggregationFn::Count), Integer(0));
209 }
210
211 #[test]
212 fn sum_of_integers_stays_integer() {
213 let vals = vec![Integer(1), Integer(2), Integer(3)];
214 assert_eq!(aggregate(&vals, AggregationFn::Sum), Integer(6));
215 }
216
217 #[test]
218 fn sum_promotes_on_mixed_numeric() {
219 let vals = vec![Integer(1), Decimal(0.5)];
220 assert_eq!(aggregate(&vals, AggregationFn::Sum), Decimal(1.5));
221 }
222
223 #[test]
224 fn sum_promotes_on_i64_overflow() {
225 let vals = vec![Integer(i64::MAX), Integer(1)];
226 match aggregate(&vals, AggregationFn::Sum) {
227 Decimal(v) => {
228 let expected = i64::MAX as f64 + 1.0;
229 assert!((v - expected).abs() < 1e3, "got {v}");
230 }
231 other => panic!("expected Decimal, got {other:?}"),
232 }
233 }
234
235 #[test]
236 fn sum_ignores_non_numeric_and_null() {
237 let vals = vec![Integer(4), Text("x".into()), Null, Boolean(true)];
238 assert_eq!(aggregate(&vals, AggregationFn::Sum), Integer(4));
239 }
240
241 #[test]
242 fn sum_of_empty_is_none() {
243 assert_eq!(aggregate(&[], AggregationFn::Sum), Null);
244 assert_eq!(aggregate(&[Null], AggregationFn::Sum), Null);
245 }
246
247 #[test]
248 fn avg_is_always_decimal() {
249 let vals = vec![Integer(1), Integer(2)];
250 assert_eq!(aggregate(&vals, AggregationFn::Avg), Decimal(1.5));
251 let vals = vec![Integer(2), Integer(2)];
252 assert_eq!(aggregate(&vals, AggregationFn::Avg), Decimal(2.0));
253 }
254
255 #[test]
256 fn avg_of_empty_is_none() {
257 assert_eq!(aggregate(&[], AggregationFn::Avg), Null);
258 }
259
260 #[test]
261 fn min_max_preserve_kind_and_skip_null() {
262 let vals = vec![Null, Integer(5), Integer(2), Integer(9)];
263 assert_eq!(aggregate(&vals, AggregationFn::Min), Integer(2));
264 assert_eq!(aggregate(&vals, AggregationFn::Max), Integer(9));
265 let dates = vec![CellValue::Date(200), CellValue::Date(100)];
266 assert_eq!(aggregate(&dates, AggregationFn::Min), CellValue::Date(100));
267 let texts = vec![Text("beta".into()), Text("alpha".into())];
268 assert_eq!(aggregate(&texts, AggregationFn::Max), Text("beta".into()));
269 }
270
271 #[test]
272 fn min_max_of_empty_is_none() {
273 assert_eq!(aggregate(&[], AggregationFn::Min), Null);
274 assert_eq!(aggregate(&[Null], AggregationFn::Max), Null);
275 }
276
277 #[test]
278 fn caption_formats_label_and_field() {
279 assert_eq!(AggregationFn::Sum.caption("Amount"), "Sum of Amount");
280 assert_eq!(AggregationFn::Count.caption("Id"), "Count of Id");
281 }
282
283 #[test]
284 fn all_lists_five_distinct_functions() {
285 let all = AggregationFn::all();
286 assert_eq!(all.len(), 5);
287 for (i, a) in all.iter().enumerate() {
288 for b in &all[i + 1..] {
289 assert_ne!(a, b);
290 }
291 }
292 }
293}