From 1a7b9e13984824740c298ea2ca0d1b2d0dfea1fe Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 13:33:00 +0200 Subject: [PATCH] feat(functions): add aggregate functions --- lib/src/functions/aggregate.dart | 192 +++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 lib/src/functions/aggregate.dart diff --git a/lib/src/functions/aggregate.dart b/lib/src/functions/aggregate.dart new file mode 100644 index 0000000..68522c3 --- /dev/null +++ b/lib/src/functions/aggregate.dart @@ -0,0 +1,192 @@ +import 'dart:math' as math; + +import '../ast.dart'; +import '../evaluator.dart'; +import '../values.dart'; +import 'helpers.dart'; + +void registerAggregate(Map registry) { + registry['SUM'] = (args, ctx) { + var total = 0.0; + for (final value in ctx.numbersFrom(args)) { + total += value; + } + return NumberValue(total); + }; + + registry['SUMSQ'] = (args, ctx) { + var total = 0.0; + for (final value in ctx.numbersFrom(args)) { + total += value * value; + } + return NumberValue(total); + }; + + registry['PRODUCT'] = (args, ctx) { + final numbers = ctx.numbersFrom(args); + if (numbers.isEmpty) return const NumberValue(0); + return NumberValue(numbers.reduce((a, b) => a * b)); + }; + + registry['MIN'] = (args, ctx) { + final numbers = ctx.numbersFrom(args); + if (numbers.isEmpty) return const NumberValue(0); + return NumberValue(numbers.reduce(math.min)); + }; + + registry['MAX'] = (args, ctx) { + final numbers = ctx.numbersFrom(args); + if (numbers.isEmpty) return const NumberValue(0); + return NumberValue(numbers.reduce(math.max)); + }; + + registry['SUMPRODUCT'] = (args, ctx) { + arity(args, 1); + final areas = [for (final arg in args) ctx.resolveArea(arg)]; + final length = areas.first.cells.length; + for (final area in areas) { + if (area.cells.length != length) { + throw const EvalException(ErrorValue(ErrorKind.value)); + } + } + var total = 0.0; + for (var i = 0; i < length; i++) { + var product = 1.0; + for (final area in areas) { + product *= _numeric(area.cells[i]); + } + total += product; + } + return NumberValue(total); + }; + + registry['SUMIF'] = (args, ctx) { + arity(args, 2, 3); + final range = ctx.resolveArea(args[0]).cells; + final criterion = ctx.eval(args[1]); + final sumCells = args.length == 3 ? ctx.resolveArea(args[2]).cells : range; + var total = 0.0; + final count = math.min(range.length, sumCells.length); + for (var i = 0; i < count; i++) { + if (matchesCriterion(range[i], criterion, ctx) && sumCells[i] is NumberValue) { + total += (sumCells[i] as NumberValue).value; + } + } + return NumberValue(total); + }; + + registry['COUNTIF'] = (args, ctx) { + arity(args, 2, 2); + final range = ctx.resolveArea(args[0]).cells; + final criterion = ctx.eval(args[1]); + var count = 0; + for (final candidate in range) { + if (matchesCriterion(candidate, criterion, ctx)) count++; + } + return NumberValue(count.toDouble()); + }; + + registry['AVERAGEIF'] = (args, ctx) { + arity(args, 2, 3); + final range = ctx.resolveArea(args[0]).cells; + final criterion = ctx.eval(args[1]); + final valueCells = args.length == 3 ? ctx.resolveArea(args[2]).cells : range; + var total = 0.0; + var count = 0; + final limit = math.min(range.length, valueCells.length); + for (var i = 0; i < limit; i++) { + if (matchesCriterion(range[i], criterion, ctx) && valueCells[i] is NumberValue) { + total += (valueCells[i] as NumberValue).value; + count++; + } + } + if (count == 0) throw const EvalException(ErrorValue(ErrorKind.divZero)); + return NumberValue(total / count); + }; + + registry['SUMIFS'] = (args, ctx) { + arity(args, 3); + final sumCells = ctx.resolveArea(args[0]).cells; + final matches = _multiCriteria(args.sublist(1), sumCells.length, ctx); + var total = 0.0; + for (final index in matches) { + if (sumCells[index] is NumberValue) total += (sumCells[index] as NumberValue).value; + } + return NumberValue(total); + }; + + registry['AVERAGEIFS'] = (args, ctx) { + arity(args, 3); + final valueCells = ctx.resolveArea(args[0]).cells; + final matches = _multiCriteria(args.sublist(1), valueCells.length, ctx); + var total = 0.0; + var count = 0; + for (final index in matches) { + if (valueCells[index] is NumberValue) { + total += (valueCells[index] as NumberValue).value; + count++; + } + } + if (count == 0) throw const EvalException(ErrorValue(ErrorKind.divZero)); + return NumberValue(total / count); + }; + + registry['COUNTIFS'] = (args, ctx) { + arity(args, 2); + final first = ctx.resolveArea(args[0]).cells; + final matches = _multiCriteria(args, first.length, ctx); + return NumberValue(matches.length.toDouble()); + }; + + registry['MAXIFS'] = (args, ctx) { + arity(args, 3); + final valueCells = ctx.resolveArea(args[0]).cells; + final matches = _multiCriteria(args.sublist(1), valueCells.length, ctx); + final numbers = [ + for (final index in matches) + if (valueCells[index] is NumberValue) (valueCells[index] as NumberValue).value, + ]; + if (numbers.isEmpty) return const NumberValue(0); + return NumberValue(numbers.reduce(math.max)); + }; + + registry['MINIFS'] = (args, ctx) { + arity(args, 3); + final valueCells = ctx.resolveArea(args[0]).cells; + final matches = _multiCriteria(args.sublist(1), valueCells.length, ctx); + final numbers = [ + for (final index in matches) + if (valueCells[index] is NumberValue) (valueCells[index] as NumberValue).value, + ]; + if (numbers.isEmpty) return const NumberValue(0); + return NumberValue(numbers.reduce(math.min)); + }; +} + +List _multiCriteria(List pairs, int length, EvalContext ctx) { + final ranges = >[]; + final criteria = []; + for (var i = 0; i + 1 < pairs.length; i += 2) { + ranges.add(ctx.resolveArea(pairs[i]).cells); + criteria.add(ctx.eval(pairs[i + 1])); + } + final matches = []; + for (var index = 0; index < length; index++) { + var ok = true; + for (var r = 0; r < ranges.length; r++) { + if (index >= ranges[r].length || !matchesCriterion(ranges[r][index], criteria[r], ctx)) { + ok = false; + break; + } + } + if (ok) matches.add(index); + } + return matches; +} + +double _numeric(CellValue value) { + if (value is NumberValue) return value.value; + if (value is BooleanValue) return value.value ? 1 : 0; + if (value is ErrorValue) throw EvalException(value); + return 0; +}