Skip to main content

formualizer_eval/builtins/
database.rs

1//! Database functions (D-functions)
2//!
3//! Excel D-functions perform aggregate operations on a database (range with header row)
4//! filtering rows that match specified criteria.
5//!
6//! Implementations:
7//! - DSUM(database, field, criteria) - Sums values in field column matching criteria
8//! - DAVERAGE(database, field, criteria) - Averages values in field column matching criteria
9//! - DCOUNT(database, field, criteria) - Counts numeric cells in field column matching criteria
10//! - DMAX(database, field, criteria) - Maximum value in field column matching criteria
11//! - DMIN(database, field, criteria) - Minimum value in field column matching criteria
12//!
13//! Database structure:
14//! - First row contains column headers (field names)
15//! - Subsequent rows contain data records
16//!
17//! Field argument:
18//! - String matching a column header (case-insensitive)
19//! - Number representing 1-based column index
20//!
21//! Criteria structure:
22//! - First row contains column headers (subset of database headers)
23//! - Subsequent rows contain criteria values (OR relationship between rows)
24//! - Multiple columns in same row have AND relationship
25//! - Supports comparison operators (>, <, >=, <=, <>), wildcards (*, ?)
26
27use super::utils::{ARG_ANY_ONE, coerce_num, criteria_match};
28use crate::args::{ArgSchema, CriteriaPredicate, parse_criteria};
29use crate::function::Function;
30use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
31use formualizer_common::{ExcelError, LiteralValue};
32use formualizer_macros::func_caps;
33
34/// Aggregation operation type for database functions
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36enum DAggregate {
37    Sum,
38    Average,
39    Count,
40    Max,
41    Min,
42    Product,
43}
44
45/// Resolve the field argument to a 0-based column index within the database.
46/// Field can be:
47/// - A string matching a column header (case-insensitive)
48/// - A number representing 1-based column index
49fn resolve_field_index(
50    field: &LiteralValue,
51    headers: &[LiteralValue],
52) -> Result<usize, ExcelError> {
53    match field {
54        LiteralValue::Text(name) => {
55            let name_lower = name.to_ascii_lowercase();
56            for (i, h) in headers.iter().enumerate() {
57                if let LiteralValue::Text(hdr) = h
58                    && hdr.to_ascii_lowercase() == name_lower
59                {
60                    return Ok(i);
61                }
62            }
63            Err(ExcelError::new_value()
64                .with_message(format!("Field '{}' not found in database headers", name)))
65        }
66        LiteralValue::Number(n) => {
67            let idx = *n as i64;
68            if idx < 1 || idx as usize > headers.len() {
69                return Err(ExcelError::new_value().with_message(format!(
70                    "Field index {} out of range (1-{})",
71                    idx,
72                    headers.len()
73                )));
74            }
75            Ok((idx - 1) as usize)
76        }
77        LiteralValue::Int(i) => {
78            if *i < 1 || *i as usize > headers.len() {
79                return Err(ExcelError::new_value().with_message(format!(
80                    "Field index {} out of range (1-{})",
81                    i,
82                    headers.len()
83                )));
84            }
85            Ok((*i - 1) as usize)
86        }
87        _ => Err(ExcelError::new_value().with_message("Field must be text or number")),
88    }
89}
90
91/// Parse criteria range into a list of criteria rows.
92/// Each row is a vector of (column_index, predicate) pairs.
93/// Multiple rows have OR relationship; columns within a row have AND relationship.
94fn parse_criteria_range(
95    criteria_view: &crate::engine::range_view::RangeView<'_>,
96    db_headers: &[LiteralValue],
97) -> Result<Vec<Vec<(usize, CriteriaPredicate)>>, ExcelError> {
98    let (crit_rows, crit_cols) = criteria_view.dims();
99    if crit_rows < 1 || crit_cols < 1 {
100        return Ok(vec![]);
101    }
102
103    // First row is criteria headers - map to database column indices
104    let mut crit_col_map: Vec<Option<usize>> = Vec::with_capacity(crit_cols);
105    for c in 0..crit_cols {
106        let crit_header = criteria_view.get_cell(0, c);
107        if let LiteralValue::Text(name) = &crit_header {
108            let name_lower = name.to_ascii_lowercase();
109            let mut found = None;
110            for (i, h) in db_headers.iter().enumerate() {
111                if let LiteralValue::Text(hdr) = h
112                    && hdr.to_ascii_lowercase() == name_lower
113                {
114                    found = Some(i);
115                    break;
116                }
117            }
118            crit_col_map.push(found);
119        } else if matches!(crit_header, LiteralValue::Empty) {
120            crit_col_map.push(None);
121        } else {
122            // Non-text, non-empty header - try to match as-is
123            crit_col_map.push(None);
124        }
125    }
126
127    // Parse criteria rows (starting from row 1)
128    let mut criteria_rows = Vec::new();
129    for r in 1..crit_rows {
130        let mut row_criteria = Vec::new();
131        let mut has_any_criteria = false;
132
133        for (c, db_col) in crit_col_map.iter().enumerate() {
134            let crit_val = criteria_view.get_cell(r, c);
135            if matches!(crit_val, LiteralValue::Empty) {
136                continue;
137            }
138
139            if let Some(db_col) = db_col {
140                let pred = parse_criteria(&crit_val)?;
141                row_criteria.push((*db_col, pred));
142                has_any_criteria = true;
143            }
144        }
145
146        if has_any_criteria {
147            criteria_rows.push(row_criteria);
148        }
149    }
150
151    Ok(criteria_rows)
152}
153
154/// Check if a database row matches any of the criteria rows (OR relationship).
155/// Each criteria row is a list of (column_index, predicate) pairs (AND relationship).
156fn row_matches_criteria(
157    db_view: &crate::engine::range_view::RangeView<'_>,
158    row: usize,
159    criteria_rows: &[Vec<(usize, CriteriaPredicate)>],
160) -> bool {
161    // If no criteria, all rows match
162    if criteria_rows.is_empty() {
163        return true;
164    }
165
166    // OR relationship between criteria rows
167    for crit_row in criteria_rows {
168        let mut all_match = true;
169        // AND relationship within a criteria row
170        for (col_idx, pred) in crit_row {
171            let cell_val = db_view.get_cell(row, *col_idx);
172            if !criteria_match(pred, &cell_val) {
173                all_match = false;
174                break;
175            }
176        }
177        if all_match {
178            return true;
179        }
180    }
181
182    false
183}
184
185/// Core evaluation function for all D-functions.
186fn eval_d_function<'a, 'b>(
187    args: &[ArgumentHandle<'a, 'b>],
188    _ctx: &dyn FunctionContext<'b>,
189    agg_type: DAggregate,
190) -> Result<CalcValue<'b>, ExcelError> {
191    if args.len() != 3 {
192        return Ok(CalcValue::Scalar(LiteralValue::Error(
193            ExcelError::new_value().with_message(format!(
194                "D-function expects 3 arguments, got {}",
195                args.len()
196            )),
197        )));
198    }
199
200    // Get database range
201    let db_view = match args[0].range_view() {
202        Ok(v) => v,
203        Err(_) => {
204            // Try to get as array literal
205            let val = args[0].value()?.into_literal();
206            if let LiteralValue::Array(arr) = val {
207                crate::engine::range_view::RangeView::from_owned_rows(
208                    arr,
209                    crate::engine::DateSystem::Excel1900,
210                )
211            } else {
212                return Ok(CalcValue::Scalar(LiteralValue::Error(
213                    ExcelError::new_value().with_message("Database must be a range or array"),
214                )));
215            }
216        }
217    };
218
219    let (db_rows, db_cols) = db_view.dims();
220    if db_rows < 2 || db_cols < 1 {
221        return Ok(CalcValue::Scalar(LiteralValue::Error(
222            ExcelError::new_value()
223                .with_message("Database must have headers and at least one data row"),
224        )));
225    }
226
227    // Get database headers (first row)
228    let headers: Vec<LiteralValue> = (0..db_cols).map(|c| db_view.get_cell(0, c)).collect();
229
230    // Get field argument and resolve to column index
231    let field_val = args[1].value()?.into_literal();
232    let field_idx = resolve_field_index(&field_val, &headers)?;
233
234    // Get criteria range
235    let crit_view = match args[2].range_view() {
236        Ok(v) => v,
237        Err(_) => {
238            let val = args[2].value()?.into_literal();
239            if let LiteralValue::Array(arr) = val {
240                crate::engine::range_view::RangeView::from_owned_rows(
241                    arr,
242                    crate::engine::DateSystem::Excel1900,
243                )
244            } else {
245                return Ok(CalcValue::Scalar(LiteralValue::Error(
246                    ExcelError::new_value().with_message("Criteria must be a range or array"),
247                )));
248            }
249        }
250    };
251
252    // Parse criteria
253    let criteria_rows = parse_criteria_range(&crit_view, &headers)?;
254
255    // Collect matching values from the field column
256    let mut values: Vec<f64> = Vec::new();
257
258    // Iterate over data rows (starting from row 1, skipping header)
259    for row in 1..db_rows {
260        if row_matches_criteria(&db_view, row, &criteria_rows) {
261            let cell_val = db_view.get_cell(row, field_idx);
262
263            // For DCOUNT, only count numeric cells
264            // For other functions, try to coerce to number
265            match &cell_val {
266                LiteralValue::Number(n) => values.push(*n),
267                LiteralValue::Int(i) => values.push(*i as f64),
268                LiteralValue::Boolean(b) => {
269                    // Include booleans for DCOUNT only when explicitly numeric context
270                    if agg_type != DAggregate::Count {
271                        values.push(if *b { 1.0 } else { 0.0 });
272                    }
273                }
274                LiteralValue::Empty => {
275                    // Empty cells are skipped for all D-functions
276                }
277                LiteralValue::Text(s) => {
278                    // Try numeric coercion for text
279                    if let Ok(n) = coerce_num(&cell_val) {
280                        values.push(n);
281                    }
282                    // Non-numeric text is skipped
283                }
284                LiteralValue::Error(e) => {
285                    // Propagate errors
286                    return Ok(CalcValue::Scalar(LiteralValue::Error(e.clone())));
287                }
288                _ => {}
289            }
290        }
291    }
292
293    // Compute aggregate result
294    let result = match agg_type {
295        DAggregate::Sum => {
296            let sum: f64 = values.iter().sum();
297            LiteralValue::Number(sum)
298        }
299        DAggregate::Average => {
300            if values.is_empty() {
301                LiteralValue::Error(ExcelError::new_div())
302            } else {
303                let sum: f64 = values.iter().sum();
304                LiteralValue::Number(sum / values.len() as f64)
305            }
306        }
307        DAggregate::Count => {
308            // DCOUNT counts only numeric cells
309            LiteralValue::Number(values.len() as f64)
310        }
311        DAggregate::Max => {
312            if values.is_empty() {
313                LiteralValue::Number(0.0)
314            } else {
315                let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
316                LiteralValue::Number(max)
317            }
318        }
319        DAggregate::Min => {
320            if values.is_empty() {
321                LiteralValue::Number(0.0)
322            } else {
323                let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
324                LiteralValue::Number(min)
325            }
326        }
327        DAggregate::Product => {
328            if values.is_empty() {
329                LiteralValue::Number(0.0)
330            } else {
331                let product: f64 = values.iter().product();
332                LiteralValue::Number(product)
333            }
334        }
335    };
336
337    Ok(CalcValue::Scalar(result))
338}
339
340/// Statistical operation type for database variance/stdev functions
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum DStatOp {
343    VarSample,   // DVAR - sample variance (n-1 denominator)
344    VarPop,      // DVARP - population variance (n denominator)
345    StdevSample, // DSTDEV - sample standard deviation (n-1 denominator)
346    StdevPop,    // DSTDEVP - population standard deviation (n denominator)
347}
348
349/// Core evaluation function for database statistical functions (DVAR, DVARP, DSTDEV, DSTDEVP).
350fn eval_d_stat_function<'a, 'b>(
351    args: &[ArgumentHandle<'a, 'b>],
352    _ctx: &dyn FunctionContext<'b>,
353    stat_op: DStatOp,
354) -> Result<CalcValue<'b>, ExcelError> {
355    if args.len() != 3 {
356        return Ok(CalcValue::Scalar(LiteralValue::Error(
357            ExcelError::new_value().with_message(format!(
358                "D-function expects 3 arguments, got {}",
359                args.len()
360            )),
361        )));
362    }
363
364    // Get database range
365    let db_view = match args[0].range_view() {
366        Ok(v) => v,
367        Err(_) => {
368            let val = args[0].value()?.into_literal();
369            if let LiteralValue::Array(arr) = val {
370                crate::engine::range_view::RangeView::from_owned_rows(
371                    arr,
372                    crate::engine::DateSystem::Excel1900,
373                )
374            } else {
375                return Ok(CalcValue::Scalar(LiteralValue::Error(
376                    ExcelError::new_value().with_message("Database must be a range or array"),
377                )));
378            }
379        }
380    };
381
382    let (db_rows, db_cols) = db_view.dims();
383    if db_rows < 2 || db_cols < 1 {
384        return Ok(CalcValue::Scalar(LiteralValue::Error(
385            ExcelError::new_value()
386                .with_message("Database must have headers and at least one data row"),
387        )));
388    }
389
390    // Get database headers (first row)
391    let headers: Vec<LiteralValue> = (0..db_cols).map(|c| db_view.get_cell(0, c)).collect();
392
393    // Get field argument and resolve to column index
394    let field_val = args[1].value()?.into_literal();
395    let field_idx = resolve_field_index(&field_val, &headers)?;
396
397    // Get criteria range
398    let crit_view = match args[2].range_view() {
399        Ok(v) => v,
400        Err(_) => {
401            let val = args[2].value()?.into_literal();
402            if let LiteralValue::Array(arr) = val {
403                crate::engine::range_view::RangeView::from_owned_rows(
404                    arr,
405                    crate::engine::DateSystem::Excel1900,
406                )
407            } else {
408                return Ok(CalcValue::Scalar(LiteralValue::Error(
409                    ExcelError::new_value().with_message("Criteria must be a range or array"),
410                )));
411            }
412        }
413    };
414
415    // Parse criteria
416    let criteria_rows = parse_criteria_range(&crit_view, &headers)?;
417
418    // Collect matching numeric values from the field column
419    let mut values: Vec<f64> = Vec::new();
420
421    for row in 1..db_rows {
422        if row_matches_criteria(&db_view, row, &criteria_rows) {
423            let cell_val = db_view.get_cell(row, field_idx);
424
425            match &cell_val {
426                LiteralValue::Number(n) => values.push(*n),
427                LiteralValue::Int(i) => values.push(*i as f64),
428                LiteralValue::Boolean(b) => {
429                    values.push(if *b { 1.0 } else { 0.0 });
430                }
431                LiteralValue::Text(s) => {
432                    if let Ok(n) = coerce_num(&cell_val) {
433                        values.push(n);
434                    }
435                }
436                LiteralValue::Error(e) => {
437                    return Ok(CalcValue::Scalar(LiteralValue::Error(e.clone())));
438                }
439                _ => {}
440            }
441        }
442    }
443
444    // Compute statistical result
445    let result = match stat_op {
446        DStatOp::VarSample | DStatOp::StdevSample => {
447            // Sample variance/stdev requires at least 2 values
448            if values.len() < 2 {
449                LiteralValue::Error(ExcelError::new_div())
450            } else {
451                let n = values.len() as f64;
452                let mean = values.iter().sum::<f64>() / n;
453                let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0);
454                if matches!(stat_op, DStatOp::VarSample) {
455                    LiteralValue::Number(variance)
456                } else {
457                    LiteralValue::Number(variance.sqrt())
458                }
459            }
460        }
461        DStatOp::VarPop | DStatOp::StdevPop => {
462            // Population variance/stdev requires at least 1 value
463            if values.is_empty() {
464                LiteralValue::Error(ExcelError::new_div())
465            } else {
466                let n = values.len() as f64;
467                let mean = values.iter().sum::<f64>() / n;
468                let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
469                if matches!(stat_op, DStatOp::VarPop) {
470                    LiteralValue::Number(variance)
471                } else {
472                    LiteralValue::Number(variance.sqrt())
473                }
474            }
475        }
476    };
477
478    Ok(CalcValue::Scalar(result))
479}
480
481/// Core evaluation function for DGET - returns single value matching criteria.
482fn eval_dget<'a, 'b>(
483    args: &[ArgumentHandle<'a, 'b>],
484    _ctx: &dyn FunctionContext<'b>,
485) -> Result<CalcValue<'b>, ExcelError> {
486    if args.len() != 3 {
487        return Ok(CalcValue::Scalar(LiteralValue::Error(
488            ExcelError::new_value()
489                .with_message(format!("DGET expects 3 arguments, got {}", args.len())),
490        )));
491    }
492
493    // Get database range
494    let db_view = match args[0].range_view() {
495        Ok(v) => v,
496        Err(_) => {
497            let val = args[0].value()?.into_literal();
498            if let LiteralValue::Array(arr) = val {
499                crate::engine::range_view::RangeView::from_owned_rows(
500                    arr,
501                    crate::engine::DateSystem::Excel1900,
502                )
503            } else {
504                return Ok(CalcValue::Scalar(LiteralValue::Error(
505                    ExcelError::new_value().with_message("Database must be a range or array"),
506                )));
507            }
508        }
509    };
510
511    let (db_rows, db_cols) = db_view.dims();
512    if db_rows < 2 || db_cols < 1 {
513        return Ok(CalcValue::Scalar(LiteralValue::Error(
514            ExcelError::new_value()
515                .with_message("Database must have headers and at least one data row"),
516        )));
517    }
518
519    // Get database headers (first row)
520    let headers: Vec<LiteralValue> = (0..db_cols).map(|c| db_view.get_cell(0, c)).collect();
521
522    // Get field argument and resolve to column index
523    let field_val = args[1].value()?.into_literal();
524    let field_idx = resolve_field_index(&field_val, &headers)?;
525
526    // Get criteria range
527    let crit_view = match args[2].range_view() {
528        Ok(v) => v,
529        Err(_) => {
530            let val = args[2].value()?.into_literal();
531            if let LiteralValue::Array(arr) = val {
532                crate::engine::range_view::RangeView::from_owned_rows(
533                    arr,
534                    crate::engine::DateSystem::Excel1900,
535                )
536            } else {
537                return Ok(CalcValue::Scalar(LiteralValue::Error(
538                    ExcelError::new_value().with_message("Criteria must be a range or array"),
539                )));
540            }
541        }
542    };
543
544    // Parse criteria
545    let criteria_rows = parse_criteria_range(&crit_view, &headers)?;
546
547    // Find matching values
548    let mut matching_values: Vec<LiteralValue> = Vec::new();
549
550    for row in 1..db_rows {
551        if row_matches_criteria(&db_view, row, &criteria_rows) {
552            matching_values.push(db_view.get_cell(row, field_idx));
553        }
554    }
555
556    // DGET returns:
557    // - #VALUE! if no match
558    // - #NUM! if more than one match
559    // - The single value if exactly one match
560    let result = if matching_values.is_empty() {
561        LiteralValue::Error(ExcelError::new_value().with_message("No record matches criteria"))
562    } else if matching_values.len() > 1 {
563        LiteralValue::Error(
564            ExcelError::new_num().with_message("More than one record matches criteria"),
565        )
566    } else {
567        matching_values.into_iter().next().unwrap()
568    };
569
570    Ok(CalcValue::Scalar(result))
571}
572
573/// Core evaluation function for DCOUNTA - counts non-blank cells matching criteria.
574fn eval_dcounta<'a, 'b>(
575    args: &[ArgumentHandle<'a, 'b>],
576    _ctx: &dyn FunctionContext<'b>,
577) -> Result<CalcValue<'b>, ExcelError> {
578    if args.len() != 3 {
579        return Ok(CalcValue::Scalar(LiteralValue::Error(
580            ExcelError::new_value()
581                .with_message(format!("DCOUNTA expects 3 arguments, got {}", args.len())),
582        )));
583    }
584
585    // Get database range
586    let db_view = match args[0].range_view() {
587        Ok(v) => v,
588        Err(_) => {
589            let val = args[0].value()?.into_literal();
590            if let LiteralValue::Array(arr) = val {
591                crate::engine::range_view::RangeView::from_owned_rows(
592                    arr,
593                    crate::engine::DateSystem::Excel1900,
594                )
595            } else {
596                return Ok(CalcValue::Scalar(LiteralValue::Error(
597                    ExcelError::new_value().with_message("Database must be a range or array"),
598                )));
599            }
600        }
601    };
602
603    let (db_rows, db_cols) = db_view.dims();
604    if db_rows < 2 || db_cols < 1 {
605        return Ok(CalcValue::Scalar(LiteralValue::Error(
606            ExcelError::new_value()
607                .with_message("Database must have headers and at least one data row"),
608        )));
609    }
610
611    // Get database headers (first row)
612    let headers: Vec<LiteralValue> = (0..db_cols).map(|c| db_view.get_cell(0, c)).collect();
613
614    // Get field argument and resolve to column index
615    let field_val = args[1].value()?.into_literal();
616    let field_idx = resolve_field_index(&field_val, &headers)?;
617
618    // Get criteria range
619    let crit_view = match args[2].range_view() {
620        Ok(v) => v,
621        Err(_) => {
622            let val = args[2].value()?.into_literal();
623            if let LiteralValue::Array(arr) = val {
624                crate::engine::range_view::RangeView::from_owned_rows(
625                    arr,
626                    crate::engine::DateSystem::Excel1900,
627                )
628            } else {
629                return Ok(CalcValue::Scalar(LiteralValue::Error(
630                    ExcelError::new_value().with_message("Criteria must be a range or array"),
631                )));
632            }
633        }
634    };
635
636    // Parse criteria
637    let criteria_rows = parse_criteria_range(&crit_view, &headers)?;
638
639    // Count non-blank cells in matching rows
640    let mut count = 0;
641
642    for row in 1..db_rows {
643        if row_matches_criteria(&db_view, row, &criteria_rows) {
644            let cell_val = db_view.get_cell(row, field_idx);
645
646            // DCOUNTA counts all non-blank cells (unlike DCOUNT which only counts numbers)
647            match &cell_val {
648                LiteralValue::Empty => {
649                    // Empty cells are NOT counted
650                }
651                LiteralValue::Text(s) if s.is_empty() => {
652                    // Empty strings are treated as blank and NOT counted
653                }
654                LiteralValue::Error(e) => {
655                    // Propagate errors
656                    return Ok(CalcValue::Scalar(LiteralValue::Error(e.clone())));
657                }
658                _ => {
659                    // All other values (numbers, non-empty text, booleans) are counted
660                    count += 1;
661                }
662            }
663        }
664    }
665
666    Ok(CalcValue::Scalar(LiteralValue::Number(count as f64)))
667}
668
669/* ─────────────────────────── DSUM ──────────────────────────── */
670#[derive(Debug)]
671pub struct DSumFn;
672
673impl Function for DSumFn {
674    func_caps!(PURE, REDUCTION);
675
676    fn name(&self) -> &'static str {
677        "DSUM"
678    }
679
680    fn min_args(&self) -> usize {
681        3
682    }
683
684    fn variadic(&self) -> bool {
685        false
686    }
687
688    fn arg_schema(&self) -> &'static [ArgSchema] {
689        &ARG_ANY_ONE[..]
690    }
691
692    fn eval<'a, 'b, 'c>(
693        &self,
694        args: &'c [ArgumentHandle<'a, 'b>],
695        ctx: &dyn FunctionContext<'b>,
696    ) -> Result<CalcValue<'b>, ExcelError> {
697        eval_d_function(args, ctx, DAggregate::Sum)
698    }
699}
700
701/* ─────────────────────────── DAVERAGE ──────────────────────────── */
702#[derive(Debug)]
703pub struct DAverageFn;
704
705impl Function for DAverageFn {
706    func_caps!(PURE, REDUCTION);
707
708    fn name(&self) -> &'static str {
709        "DAVERAGE"
710    }
711
712    fn min_args(&self) -> usize {
713        3
714    }
715
716    fn variadic(&self) -> bool {
717        false
718    }
719
720    fn arg_schema(&self) -> &'static [ArgSchema] {
721        &ARG_ANY_ONE[..]
722    }
723
724    fn eval<'a, 'b, 'c>(
725        &self,
726        args: &'c [ArgumentHandle<'a, 'b>],
727        ctx: &dyn FunctionContext<'b>,
728    ) -> Result<CalcValue<'b>, ExcelError> {
729        eval_d_function(args, ctx, DAggregate::Average)
730    }
731}
732
733/* ─────────────────────────── DCOUNT ──────────────────────────── */
734#[derive(Debug)]
735pub struct DCountFn;
736
737impl Function for DCountFn {
738    func_caps!(PURE, REDUCTION);
739
740    fn name(&self) -> &'static str {
741        "DCOUNT"
742    }
743
744    fn min_args(&self) -> usize {
745        3
746    }
747
748    fn variadic(&self) -> bool {
749        false
750    }
751
752    fn arg_schema(&self) -> &'static [ArgSchema] {
753        &ARG_ANY_ONE[..]
754    }
755
756    fn eval<'a, 'b, 'c>(
757        &self,
758        args: &'c [ArgumentHandle<'a, 'b>],
759        ctx: &dyn FunctionContext<'b>,
760    ) -> Result<CalcValue<'b>, ExcelError> {
761        eval_d_function(args, ctx, DAggregate::Count)
762    }
763}
764
765/* ─────────────────────────── DMAX ──────────────────────────── */
766#[derive(Debug)]
767pub struct DMaxFn;
768
769impl Function for DMaxFn {
770    func_caps!(PURE, REDUCTION);
771
772    fn name(&self) -> &'static str {
773        "DMAX"
774    }
775
776    fn min_args(&self) -> usize {
777        3
778    }
779
780    fn variadic(&self) -> bool {
781        false
782    }
783
784    fn arg_schema(&self) -> &'static [ArgSchema] {
785        &ARG_ANY_ONE[..]
786    }
787
788    fn eval<'a, 'b, 'c>(
789        &self,
790        args: &'c [ArgumentHandle<'a, 'b>],
791        ctx: &dyn FunctionContext<'b>,
792    ) -> Result<CalcValue<'b>, ExcelError> {
793        eval_d_function(args, ctx, DAggregate::Max)
794    }
795}
796
797/* ─────────────────────────── DMIN ──────────────────────────── */
798#[derive(Debug)]
799pub struct DMinFn;
800
801impl Function for DMinFn {
802    func_caps!(PURE, REDUCTION);
803
804    fn name(&self) -> &'static str {
805        "DMIN"
806    }
807
808    fn min_args(&self) -> usize {
809        3
810    }
811
812    fn variadic(&self) -> bool {
813        false
814    }
815
816    fn arg_schema(&self) -> &'static [ArgSchema] {
817        &ARG_ANY_ONE[..]
818    }
819
820    fn eval<'a, 'b, 'c>(
821        &self,
822        args: &'c [ArgumentHandle<'a, 'b>],
823        ctx: &dyn FunctionContext<'b>,
824    ) -> Result<CalcValue<'b>, ExcelError> {
825        eval_d_function(args, ctx, DAggregate::Min)
826    }
827}
828
829/* ─────────────────────────── DPRODUCT ──────────────────────────── */
830#[derive(Debug)]
831pub struct DProductFn;
832
833impl Function for DProductFn {
834    func_caps!(PURE, REDUCTION);
835
836    fn name(&self) -> &'static str {
837        "DPRODUCT"
838    }
839
840    fn min_args(&self) -> usize {
841        3
842    }
843
844    fn variadic(&self) -> bool {
845        false
846    }
847
848    fn arg_schema(&self) -> &'static [ArgSchema] {
849        &ARG_ANY_ONE[..]
850    }
851
852    fn eval<'a, 'b, 'c>(
853        &self,
854        args: &'c [ArgumentHandle<'a, 'b>],
855        ctx: &dyn FunctionContext<'b>,
856    ) -> Result<CalcValue<'b>, ExcelError> {
857        eval_d_function(args, ctx, DAggregate::Product)
858    }
859}
860
861/* ─────────────────────────── DSTDEV ──────────────────────────── */
862#[derive(Debug)]
863pub struct DStdevFn;
864
865impl Function for DStdevFn {
866    func_caps!(PURE, REDUCTION);
867
868    fn name(&self) -> &'static str {
869        "DSTDEV"
870    }
871
872    fn min_args(&self) -> usize {
873        3
874    }
875
876    fn variadic(&self) -> bool {
877        false
878    }
879
880    fn arg_schema(&self) -> &'static [ArgSchema] {
881        &ARG_ANY_ONE[..]
882    }
883
884    fn eval<'a, 'b, 'c>(
885        &self,
886        args: &'c [ArgumentHandle<'a, 'b>],
887        ctx: &dyn FunctionContext<'b>,
888    ) -> Result<CalcValue<'b>, ExcelError> {
889        eval_d_stat_function(args, ctx, DStatOp::StdevSample)
890    }
891}
892
893/* ─────────────────────────── DSTDEVP ──────────────────────────── */
894#[derive(Debug)]
895pub struct DStdevPFn;
896
897impl Function for DStdevPFn {
898    func_caps!(PURE, REDUCTION);
899
900    fn name(&self) -> &'static str {
901        "DSTDEVP"
902    }
903
904    fn min_args(&self) -> usize {
905        3
906    }
907
908    fn variadic(&self) -> bool {
909        false
910    }
911
912    fn arg_schema(&self) -> &'static [ArgSchema] {
913        &ARG_ANY_ONE[..]
914    }
915
916    fn eval<'a, 'b, 'c>(
917        &self,
918        args: &'c [ArgumentHandle<'a, 'b>],
919        ctx: &dyn FunctionContext<'b>,
920    ) -> Result<CalcValue<'b>, ExcelError> {
921        eval_d_stat_function(args, ctx, DStatOp::StdevPop)
922    }
923}
924
925/* ─────────────────────────── DVAR ──────────────────────────── */
926#[derive(Debug)]
927pub struct DVarFn;
928
929impl Function for DVarFn {
930    func_caps!(PURE, REDUCTION);
931
932    fn name(&self) -> &'static str {
933        "DVAR"
934    }
935
936    fn min_args(&self) -> usize {
937        3
938    }
939
940    fn variadic(&self) -> bool {
941        false
942    }
943
944    fn arg_schema(&self) -> &'static [ArgSchema] {
945        &ARG_ANY_ONE[..]
946    }
947
948    fn eval<'a, 'b, 'c>(
949        &self,
950        args: &'c [ArgumentHandle<'a, 'b>],
951        ctx: &dyn FunctionContext<'b>,
952    ) -> Result<CalcValue<'b>, ExcelError> {
953        eval_d_stat_function(args, ctx, DStatOp::VarSample)
954    }
955}
956
957/* ─────────────────────────── DVARP ──────────────────────────── */
958#[derive(Debug)]
959pub struct DVarPFn;
960
961impl Function for DVarPFn {
962    func_caps!(PURE, REDUCTION);
963
964    fn name(&self) -> &'static str {
965        "DVARP"
966    }
967
968    fn min_args(&self) -> usize {
969        3
970    }
971
972    fn variadic(&self) -> bool {
973        false
974    }
975
976    fn arg_schema(&self) -> &'static [ArgSchema] {
977        &ARG_ANY_ONE[..]
978    }
979
980    fn eval<'a, 'b, 'c>(
981        &self,
982        args: &'c [ArgumentHandle<'a, 'b>],
983        ctx: &dyn FunctionContext<'b>,
984    ) -> Result<CalcValue<'b>, ExcelError> {
985        eval_d_stat_function(args, ctx, DStatOp::VarPop)
986    }
987}
988
989/* ─────────────────────────── DGET ──────────────────────────── */
990#[derive(Debug)]
991pub struct DGetFn;
992
993impl Function for DGetFn {
994    func_caps!(PURE, REDUCTION);
995
996    fn name(&self) -> &'static str {
997        "DGET"
998    }
999
1000    fn min_args(&self) -> usize {
1001        3
1002    }
1003
1004    fn variadic(&self) -> bool {
1005        false
1006    }
1007
1008    fn arg_schema(&self) -> &'static [ArgSchema] {
1009        &ARG_ANY_ONE[..]
1010    }
1011
1012    fn eval<'a, 'b, 'c>(
1013        &self,
1014        args: &'c [ArgumentHandle<'a, 'b>],
1015        ctx: &dyn FunctionContext<'b>,
1016    ) -> Result<CalcValue<'b>, ExcelError> {
1017        eval_dget(args, ctx)
1018    }
1019}
1020
1021/* ─────────────────────────── DCOUNTA ──────────────────────────── */
1022#[derive(Debug)]
1023pub struct DCountAFn;
1024
1025impl Function for DCountAFn {
1026    func_caps!(PURE, REDUCTION);
1027
1028    fn name(&self) -> &'static str {
1029        "DCOUNTA"
1030    }
1031
1032    fn min_args(&self) -> usize {
1033        3
1034    }
1035
1036    fn variadic(&self) -> bool {
1037        false
1038    }
1039
1040    fn arg_schema(&self) -> &'static [ArgSchema] {
1041        &ARG_ANY_ONE[..]
1042    }
1043
1044    fn eval<'a, 'b, 'c>(
1045        &self,
1046        args: &'c [ArgumentHandle<'a, 'b>],
1047        ctx: &dyn FunctionContext<'b>,
1048    ) -> Result<CalcValue<'b>, ExcelError> {
1049        eval_dcounta(args, ctx)
1050    }
1051}
1052
1053/// Register all database functions.
1054pub fn register_builtins() {
1055    use std::sync::Arc;
1056    crate::function_registry::register_function(Arc::new(DSumFn));
1057    crate::function_registry::register_function(Arc::new(DAverageFn));
1058    crate::function_registry::register_function(Arc::new(DCountFn));
1059    crate::function_registry::register_function(Arc::new(DMaxFn));
1060    crate::function_registry::register_function(Arc::new(DMinFn));
1061    crate::function_registry::register_function(Arc::new(DProductFn));
1062    crate::function_registry::register_function(Arc::new(DStdevFn));
1063    crate::function_registry::register_function(Arc::new(DStdevPFn));
1064    crate::function_registry::register_function(Arc::new(DVarFn));
1065    crate::function_registry::register_function(Arc::new(DVarPFn));
1066    crate::function_registry::register_function(Arc::new(DGetFn));
1067    crate::function_registry::register_function(Arc::new(DCountAFn));
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073    use crate::test_workbook::TestWorkbook;
1074    use formualizer_parse::parser::{ASTNode, ASTNodeType};
1075    use std::sync::Arc;
1076
1077    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
1078        wb.interpreter()
1079    }
1080
1081    fn lit(v: LiteralValue) -> ASTNode {
1082        ASTNode::new(ASTNodeType::Literal(v), None)
1083    }
1084
1085    fn make_database() -> LiteralValue {
1086        // Simple database with headers: Name, Age, Salary
1087        LiteralValue::Array(vec![
1088            vec![
1089                LiteralValue::Text("Name".into()),
1090                LiteralValue::Text("Age".into()),
1091                LiteralValue::Text("Salary".into()),
1092            ],
1093            vec![
1094                LiteralValue::Text("Alice".into()),
1095                LiteralValue::Int(30),
1096                LiteralValue::Int(50000),
1097            ],
1098            vec![
1099                LiteralValue::Text("Bob".into()),
1100                LiteralValue::Int(25),
1101                LiteralValue::Int(45000),
1102            ],
1103            vec![
1104                LiteralValue::Text("Carol".into()),
1105                LiteralValue::Int(35),
1106                LiteralValue::Int(60000),
1107            ],
1108            vec![
1109                LiteralValue::Text("Dave".into()),
1110                LiteralValue::Int(30),
1111                LiteralValue::Int(55000),
1112            ],
1113        ])
1114    }
1115
1116    fn make_criteria_all() -> LiteralValue {
1117        // Criteria that matches all (just header, no criteria values)
1118        LiteralValue::Array(vec![vec![LiteralValue::Text("Name".into())]])
1119    }
1120
1121    fn make_criteria_age_30() -> LiteralValue {
1122        // Criteria: Age = 30
1123        LiteralValue::Array(vec![
1124            vec![LiteralValue::Text("Age".into())],
1125            vec![LiteralValue::Int(30)],
1126        ])
1127    }
1128
1129    fn make_criteria_age_gt_25() -> LiteralValue {
1130        // Criteria: Age > 25
1131        LiteralValue::Array(vec![
1132            vec![LiteralValue::Text("Age".into())],
1133            vec![LiteralValue::Text(">25".into())],
1134        ])
1135    }
1136
1137    #[test]
1138    fn dsum_all_salaries() {
1139        let wb = TestWorkbook::new().with_function(Arc::new(DSumFn));
1140        let ctx = interp(&wb);
1141
1142        let db = lit(make_database());
1143        let field = lit(LiteralValue::Text("Salary".into()));
1144        let criteria = lit(make_criteria_all());
1145
1146        let args = vec![
1147            crate::traits::ArgumentHandle::new(&db, &ctx),
1148            crate::traits::ArgumentHandle::new(&field, &ctx),
1149            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1150        ];
1151
1152        let f = ctx.context.get_function("", "DSUM").unwrap();
1153        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1154
1155        // Sum of all salaries: 50000 + 45000 + 60000 + 55000 = 210000
1156        assert_eq!(result.into_literal(), LiteralValue::Number(210000.0));
1157    }
1158
1159    #[test]
1160    fn dsum_age_30() {
1161        let wb = TestWorkbook::new().with_function(Arc::new(DSumFn));
1162        let ctx = interp(&wb);
1163
1164        let db = lit(make_database());
1165        let field = lit(LiteralValue::Text("Salary".into()));
1166        let criteria = lit(make_criteria_age_30());
1167
1168        let args = vec![
1169            crate::traits::ArgumentHandle::new(&db, &ctx),
1170            crate::traits::ArgumentHandle::new(&field, &ctx),
1171            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1172        ];
1173
1174        let f = ctx.context.get_function("", "DSUM").unwrap();
1175        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1176
1177        // Sum of salaries where Age = 30: 50000 + 55000 = 105000
1178        assert_eq!(result.into_literal(), LiteralValue::Number(105000.0));
1179    }
1180
1181    #[test]
1182    fn daverage_age_gt_25() {
1183        let wb = TestWorkbook::new().with_function(Arc::new(DAverageFn));
1184        let ctx = interp(&wb);
1185
1186        let db = lit(make_database());
1187        let field = lit(LiteralValue::Text("Salary".into()));
1188        let criteria = lit(make_criteria_age_gt_25());
1189
1190        let args = vec![
1191            crate::traits::ArgumentHandle::new(&db, &ctx),
1192            crate::traits::ArgumentHandle::new(&field, &ctx),
1193            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1194        ];
1195
1196        let f = ctx.context.get_function("", "DAVERAGE").unwrap();
1197        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1198
1199        // Average of salaries where Age > 25: (50000 + 60000 + 55000) / 3 = 55000
1200        assert_eq!(result.into_literal(), LiteralValue::Number(55000.0));
1201    }
1202
1203    #[test]
1204    fn dcount_age_30() {
1205        let wb = TestWorkbook::new().with_function(Arc::new(DCountFn));
1206        let ctx = interp(&wb);
1207
1208        let db = lit(make_database());
1209        let field = lit(LiteralValue::Text("Salary".into()));
1210        let criteria = lit(make_criteria_age_30());
1211
1212        let args = vec![
1213            crate::traits::ArgumentHandle::new(&db, &ctx),
1214            crate::traits::ArgumentHandle::new(&field, &ctx),
1215            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1216        ];
1217
1218        let f = ctx.context.get_function("", "DCOUNT").unwrap();
1219        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1220
1221        // Count of numeric cells in Salary where Age = 30: 2
1222        assert_eq!(result.into_literal(), LiteralValue::Number(2.0));
1223    }
1224
1225    #[test]
1226    fn dmax_all() {
1227        let wb = TestWorkbook::new().with_function(Arc::new(DMaxFn));
1228        let ctx = interp(&wb);
1229
1230        let db = lit(make_database());
1231        let field = lit(LiteralValue::Text("Salary".into()));
1232        let criteria = lit(make_criteria_all());
1233
1234        let args = vec![
1235            crate::traits::ArgumentHandle::new(&db, &ctx),
1236            crate::traits::ArgumentHandle::new(&field, &ctx),
1237            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1238        ];
1239
1240        let f = ctx.context.get_function("", "DMAX").unwrap();
1241        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1242
1243        // Max salary: 60000
1244        assert_eq!(result.into_literal(), LiteralValue::Number(60000.0));
1245    }
1246
1247    #[test]
1248    fn dmin_all() {
1249        let wb = TestWorkbook::new().with_function(Arc::new(DMinFn));
1250        let ctx = interp(&wb);
1251
1252        let db = lit(make_database());
1253        let field = lit(LiteralValue::Text("Salary".into()));
1254        let criteria = lit(make_criteria_all());
1255
1256        let args = vec![
1257            crate::traits::ArgumentHandle::new(&db, &ctx),
1258            crate::traits::ArgumentHandle::new(&field, &ctx),
1259            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1260        ];
1261
1262        let f = ctx.context.get_function("", "DMIN").unwrap();
1263        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1264
1265        // Min salary: 45000
1266        assert_eq!(result.into_literal(), LiteralValue::Number(45000.0));
1267    }
1268
1269    #[test]
1270    fn dsum_field_by_index() {
1271        let wb = TestWorkbook::new().with_function(Arc::new(DSumFn));
1272        let ctx = interp(&wb);
1273
1274        let db = lit(make_database());
1275        let field = lit(LiteralValue::Int(3)); // Column 3 = Salary
1276        let criteria = lit(make_criteria_all());
1277
1278        let args = vec![
1279            crate::traits::ArgumentHandle::new(&db, &ctx),
1280            crate::traits::ArgumentHandle::new(&field, &ctx),
1281            crate::traits::ArgumentHandle::new(&criteria, &ctx),
1282        ];
1283
1284        let f = ctx.context.get_function("", "DSUM").unwrap();
1285        let result = f.dispatch(&args, &ctx.function_context(None)).unwrap();
1286
1287        // Sum of all salaries: 210000
1288        assert_eq!(result.into_literal(), LiteralValue::Number(210000.0));
1289    }
1290}