UNPKG

@adaptabletools/adaptable

Version:

Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

1,420 lines • 69.3 kB
import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError';
import { SumArray } from '../Extensions/ArrayExtensions';
import { getTypedKeys } from '../Extensions/TypeExtensions';
import { extractColumnParameter, extractColumnParameters, extractParameter, getNumericValue, handleColumnFunction, validateColumnType, } from './expressionFunctionUtils';
import { isAfter } from 'date-fns';
import { parseDateValue } from '../Helpers/DateHelper';
export const aggregatedExpressionFunctions = [
    'SUM',
    'PERCENTAGE',
    'MIN',
    'MAX',
    'AVG',
    'COUNT',
    'WEIGHT',
    'COL',
    'GROUP_BY',
    'MEDIAN',
    'MODE',
    'DISTINCT',
    'ONLY',
    'STD_DEVIATION',
    'PREVIOUS',
    'NEXT',
    'FIRST',
    'LAST',
    'ABSOLUTE_DIFF',
    'PERCENT_DIFF',
];
export const cumulativeAggregatedExpressionFunctions = [
    'CUMUL',
    'OVER',
    'SUM',
    'PERCENTAGE',
    'MIN',
    'MAX',
    'QUANT',
    'PREVIOUS',
    'NEXT',
    'FIRST',
    'LAST',
    'AVG',
    'WEIGHT',
    'COL',
    'GROUP_BY',
    'ABSOLUTE_DIFF',
    'PERCENT_DIFF',
];
export const quantileAggregatedExpressionFunctions = [
    'QUANT',
    'QUARTILE',
    'PERCENTILE',
    'COL',
    'GROUP_BY',
];
export const aggregatedScalarExpressionFunctions = {
    CUMUL: {
        handler(args, context) {
            const aggregationParameter = extractParameter('CUMUL', 'aggregationScalar', ['SUM', 'PERCENTAGE', 'AVG', 'MIN', 'MAX', 'COUNT'], args);
            const overColumnParameter = extractParameter('CUMUL', 'operand', ['OVER'], args);
            const groupByParameter = extractParameter(aggregationParameter.name, 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (!!groupByParameter) {
                // ensure the GROUP_BY is NOT on the CUMUL function
                // it should be on the AGGREGATION function
                throw new ExpressionEvaluationError('GROUP_BY', `should be passed as an argument to the '${aggregationParameter.name}' aggregation function!`);
            }
            const cumulationExpressionEvaluation = mapAggregationToCumulation(aggregationParameter, overColumnParameter, context);
            const result = {
                name: 'CUMUL',
                type: 'aggregationScalar',
                value: cumulationExpressionEvaluation,
            };
            return result;
        },
        description: 'Performs a cumulative aggregation (running total) with the given aggregation operation over a provided column',
        signatures: ['CUMUL( aggregationOp: SUM|MIN|MAX, OVER( [colNameB] ))'],
        examples: [
            'CUMUL( SUM([colNameA]), OVER( [colNameB] ))',
            'CUMUL( MIN([colNameA]), OVER( [colNameB] ))',
            'CUMUL( MAX([colNameA]), OVER( [colNameB] ))',
        ],
        category: 'cumulative',
    },
    SUM: {
        handler(args, context) {
            const sumColumnParameter = extractColumnParameter('SUM', args);
            const sumColumnName = sumColumnParameter.value;
            const columnType = validateColumnType(sumColumnName, ['number'], 'SUM', context.adaptableApi);
            const groupByParameter = extractParameter('SUM', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        SUM: {
                            name: 'SUM',
                            field: sumColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + getNumericValue(rowValue);
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'SUM',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by summing up the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'SUM( [colName] )',
            'SUM( COL(name: string))',
            'SUM( [colNameA], GROUP_BY( [colNameB] ))',
            'SUM( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['SUM([colA])', 'SUM([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    PERCENTAGE: {
        handler(args, context) {
            const percentageColumnParameter = extractColumnParameter('PERCENTAGE', args);
            const percentageColumnName = percentageColumnParameter.value;
            const columnType = validateColumnType(percentageColumnName, ['number'], 'PERCENTAGE', context.adaptableApi);
            const sumOperand = extractParameter('PERCENTAGE', 'aggregationScalar', ['SUM'], args, {
                isOptional: true,
            });
            const groupByOperand = extractParameter('PERCENTAGE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (sumOperand && groupByOperand) {
                throw new ExpressionEvaluationError('PERCENTAGE', `expects either a SUM or a GROUP_BY argument`);
            }
            const sumAggregationColumnName = sumOperand
                ? sumOperand.value.aggregationParams.reducers['SUM'].field
                : percentageColumnName;
            const groupByColumnNames = groupByOperand
                ? groupByOperand?.value
                : sumOperand?.value.aggregationParams.groupBy?.map((param) => param.field);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        PERCENTAGE: {
                            name: 'PERCENTAGE',
                            field: sumAggregationColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + getNumericValue(rowValue);
                            },
                        },
                    },
                },
                rowValueGetter: (rowNode, aggregationValue) => {
                    return ((getNumericValue(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, percentageColumnName)) /
                        aggregationValue) *
                        100);
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByColumnNames, aggregationExpressionEvaluation);
            const result = {
                name: 'PERCENTAGE',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Divides each column value by the aggregated sum of a column\nThe column with the aggregated value and the column with the percentage value do not necessarily have to be identical',
        signatures: [
            'PERCENTAGE( [colName] )',
            'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
        ],
        examples: [
            'PERCENTAGE( [colName] )',
            'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
        ],
        category: 'aggregation',
    },
    AVG: {
        handler(args, context) {
            const avgColumnParameter = extractColumnParameter('AVG', args);
            const avgColumnName = avgColumnParameter.value;
            const columnType = validateColumnType(avgColumnName, ['number'], 'AVG', context.adaptableApi);
            const groupByParameter = extractParameter('AVG', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const weightParameter = extractParameter('AVG', 'operand', ['WEIGHT'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        AVG: {
                            name: 'AVG',
                            field: avgColumnName,
                            initialValue: {
                                sumOfValues: 0,
                                numberOfValues: 0,
                            },
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                if (isUndefinedValue(rowValue)) {
                                    return aggregatedValue;
                                }
                                const numericRowValue = getNumericValue(rowValue);
                                if (weightParameter) {
                                    // weighted average
                                    const weightValue = getNumericValue(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, weightParameter.value)) ?? 0;
                                    aggregatedValue.sumOfValues =
                                        aggregatedValue.sumOfValues + numericRowValue * weightValue;
                                    aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + weightValue;
                                }
                                else {
                                    aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + numericRowValue;
                                    aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + 1;
                                }
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                if (aggregatedValue.numberOfValues === 0) {
                                    return 0;
                                }
                                return aggregatedValue.sumOfValues / aggregatedValue.numberOfValues;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            if (weightParameter) {
                aggregationExpressionEvaluation.context = {
                    weightParam: weightParameter,
                };
            }
            const result = {
                name: 'AVG',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the average value (arithmetic mean) of the column values\nOptionally the aggregation may be weighted and/or grouped.',
        signatures: [
            'AVG( [colName] )',
            'AVG( COL(name: string))',
            'AVG( [colNameA], GROUP_BY( [colNameB] ))',
            'AVG( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['AVG([colA])', 'AVG([colA], GROUP_BY([colB]))', 'AVG([colA], WEIGHT([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MEDIAN: {
        handler(args, context) {
            const medianColumnParameter = extractColumnParameter('MEDIAN', args);
            const medianColumnName = medianColumnParameter.value;
            const columnType = validateColumnType(medianColumnName, ['number'], 'MEDIAN', context.adaptableApi);
            const groupByParameter = extractParameter('MEDIAN', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MEDIAN: {
                            name: 'MEDIAN',
                            field: medianColumnName,
                            initialValue: [],
                            reducer: (aggregatedValue, cellValue) => {
                                aggregatedValue.push(getNumericValue(cellValue));
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                if (aggregatedValue.length === 0) {
                                    return null;
                                }
                                if (aggregatedValue.length === 1) {
                                    return aggregatedValue[0];
                                }
                                const length = aggregatedValue.length;
                                const middle = Math.floor((length - 1) / 2);
                                if (length % 2) {
                                    return aggregatedValue[middle];
                                }
                                else {
                                    return (aggregatedValue[middle] + aggregatedValue[middle + 1]) / 2.0;
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MEDIAN',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the median of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'MEDIAN( [colName] )',
            'MEDIAN( COL(name: string))',
            'MEDIAN( [colNameA], GROUP_BY( [colNameB] ))',
            'MEDIAN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MEDIAN([colA])', 'MEDIAN([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MODE: {
        handler(args, context) {
            const modeColumnParameter = extractColumnParameter('mode', args);
            const modeColumnName = modeColumnParameter.value;
            const columnType = validateColumnType(modeColumnName, ['number', 'text', 'date'], 'mode', context.adaptableApi);
            const groupByParameter = extractParameter('MODE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MODE: {
                            name: 'MODE',
                            field: modeColumnName,
                            initialValue: new Map(),
                            reducer: (aggregatedValue, rowValue) => {
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                if (!aggregatedValue) {
                                    return new Map();
                                }
                                aggregatedValue.set(rowValue, (aggregatedValue.get(rowValue) ?? 0) + 1);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                const sorted = [...aggregatedValue.entries()].sort(([aVal, aFreq], [bVal, bFreq]) => {
                                    if (aFreq < bFreq) {
                                        return 1;
                                    }
                                    else if (aFreq > bFreq || aVal < bVal) {
                                        return -1;
                                    }
                                    else {
                                        return aVal === bVal ? 0 : 1;
                                    }
                                });
                                return sorted?.[0]?.[0];
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MODE',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the mode of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'MODE( [colName] )',
            'MODE( COL(name: string))',
            'MODE( [colNameA], GROUP_BY( [colNameB] ))',
            'MODE( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MODE([colA])', 'MODE([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    DISTINCT: {
        handler(args, context) {
            const distinctColumnParameter = extractColumnParameter('DISTINCT', args);
            const distinctColumnName = distinctColumnParameter.value;
            const groupByParameter = extractParameter('MODE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        DISTINCT: {
                            name: 'DISTINCT',
                            field: distinctColumnName,
                            initialValue: new Set(),
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                aggregatedValue.add(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                return aggregatedValue.size;
                            },
                        },
                    },
                },
                getRowNodes: context.getRowNodes,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'DISTINCT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the distinct number of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'DISTINCT( [colName] )',
            'DISTINCT( COL(name: string))',
            'DISTINCT( [colNameA], GROUP_BY( [colNameB] ))',
            'DISTINCT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['DISTINCT([colA])', 'DISTINCT([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    ONLY: {
        handler(args, context) {
            const distinctColumnParameter = extractColumnParameter('ONLY', args);
            const onlyColumnName = distinctColumnParameter.value;
            const columnType = validateColumnType(onlyColumnName, ['number'], 'ONLY', context.adaptableApi);
            const groupByParameter = extractParameter('ONLY', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        ONLY: {
                            name: 'ONLY',
                            field: onlyColumnName,
                            initialValue: new Set(),
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                aggregatedValue.add(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                return aggregatedValue.size === 1 ? aggregatedValue.values().next().value : null;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'ONLY',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing to a value that is common to all rows. \nOptionally the aggregation may be grouped.',
        signatures: [
            'ONLY( [colName] )',
            'ONLY( COL(name: string))',
            'ONLY( [colNameA], GROUP_BY( [colNameB] ))',
            'ONLY( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['ONLY([colA])', 'ONLY([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    STD_DEVIATION: {
        handler(args, context) {
            const columnParameter = extractColumnParameter('STD_DEVIATION', args);
            const columnName = columnParameter.value;
            const columnType = validateColumnType(columnName, ['number'], 'STD_DEVIATION', context.adaptableApi);
            const groupByParameter = extractParameter('STD_DEVIATION', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        STD_DEVIATION: {
                            name: 'STD_DEVIATION',
                            field: columnName,
                            initialValue: { sumOfValues: 0, values: [] },
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                // TODO: getNumericValue; allow numeric strings
                                if (typeof rowValue !== 'number' || isNaN(rowValue)) {
                                    return aggregatedValue;
                                }
                                aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + rowValue;
                                aggregatedValue.values.push(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                const { sumOfValues, values } = aggregatedValue;
                                if (values.length === 0) {
                                    return 0;
                                }
                                const mean = sumOfValues / values.length;
                                const variance = SumArray(values.map((value) => Math.pow(value - mean, 2))) / values.length;
                                return Math.sqrt(variance);
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'STD_DEVIATION',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing its standard deviation.\nOptionally the aggregation may be grouped.',
        signatures: [
            'STD_DEVIATION( [colName] )',
            'STD_DEVIATION( COL(name: string))',
            'STD_DEVIATION( [colNameA], GROUP_BY( [colNameB] ))',
            'STD_DEVIATION( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['STD_DEVIATION([colA])', 'STD_DEVIATION([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MIN: {
        handler(args, context) {
            const minColumnParameter = extractColumnParameter('MIN', args);
            const minColumnName = minColumnParameter.value;
            const columnType = validateColumnType(minColumnName, ['number', 'date'], 'MIN', context.adaptableApi);
            const groupByParameter = extractParameter('MIN', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MIN: {
                            name: 'MIN',
                            field: minColumnName,
                            initialValue: Number.MAX_VALUE,
                            reducer: (minValue, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return minValue;
                                }
                                if (minValue === Number.MAX_VALUE) {
                                    return rowValue;
                                }
                                if (columnType === 'number') {
                                    const numericRowValue = getNumericValue(rowValue);
                                    return numericRowValue < minValue ? numericRowValue : minValue;
                                }
                                else {
                                    // Date
                                    return isAfter(parseDateValue(rowValue), parseDateValue(minValue))
                                        ? minValue
                                        : rowValue;
                                }
                            },
                            done: (minValue) => {
                                if (minValue !== Number.MAX_VALUE) {
                                    return minValue;
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MIN',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the minimum of the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'MIN( [colName] )',
            'MIN( COL(name: string))',
            'MIN( [colNameA], GROUP_BY( [colNameB] ))',
            'MIN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MIN([colA])', 'MIN([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MAX: {
        handler(args, context) {
            const maxColumnParameter = extractColumnParameter('MAX', args);
            const maxColumnName = maxColumnParameter.value;
            const columnType = validateColumnType(maxColumnName, ['number', 'date'], 'MAX', context.adaptableApi);
            const groupByParameter = extractParameter('MAX', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MAX: {
                            name: 'MAX',
                            field: maxColumnName,
                            initialValue: Number.MIN_VALUE,
                            reducer: (maxValue, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return maxValue;
                                }
                                if (maxValue === Number.MIN_VALUE) {
                                    return rowValue;
                                }
                                if (columnType === 'number') {
                                    const numericRowValue = getNumericValue(rowValue);
                                    return numericRowValue > maxValue ? numericRowValue : maxValue;
                                }
                                else {
                                    // Date
                                    return isAfter(parseDateValue(rowValue), parseDateValue(maxValue))
                                        ? rowValue
                                        : maxValue;
                                }
                            },
                            done: (maxValue) => {
                                if (maxValue !== Number.MIN_VALUE) {
                                    return maxValue;
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MAX',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the maximum of the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'MAX( [colName] )',
            'MAX( COL(name: string))',
            'MAX( [colNameA], GROUP_BY( [colNameB] ))',
            'MAX( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MAX([colA])', 'MAX([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    QUANT: {
        handler(args, context) {
            const quantileColumnParameter = extractColumnParameter('QUANT', args);
            const quantileColumnName = quantileColumnParameter.value;
            const columnType = validateColumnType(quantileColumnName, ['number'], 'QUANT', context.adaptableApi);
            const qNumberCandidates = args.filter((arg) => typeof arg === 'number');
            if (qNumberCandidates.length !== 1) {
                throw new ExpressionEvaluationError('QUANT', 'expects a single positive numeric argument as q-quantile');
            }
            const qNumber = qNumberCandidates[0];
            const groupByOperand = extractParameter('QUANT', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const groupByColumnNames = groupByOperand?.value;
            let quantReducer;
            if (groupByColumnNames?.length) {
                // do the heavy-lifting in done()
                quantReducer = {
                    initialValue: {},
                    reducer: (nodeValueMap, rowValue, rowNode, rowIndex) => {
                        if (isUndefinedValue(rowValue)) {
                            return nodeValueMap;
                        }
                        nodeValueMap[rowNode.id] = rowValue;
                        return nodeValueMap;
                    },
                    done: (nodeValueMap, groupItems) => {
                        const populationSize = groupItems?.filter((rowNode) => {
                            const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                            return isDefinedValue(rowValue);
                        })?.length ?? 0;
                        const bucketsMap = new Map();
                        for (let step = 1; step <= qNumber; step++) {
                            bucketsMap.set(step, populationSize * (step / qNumber));
                        }
                        const indexBucketMap = new Map();
                        for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
                            for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
                                if (bucketsMap.get(quantIndex) < populationIndex + 1) {
                                    continue;
                                }
                                else {
                                    indexBucketMap.set(populationIndex, quantIndex);
                                    break;
                                }
                            }
                        }
                        const groupedRowBucketMap = {};
                        groupItems.forEach((subgroupRowNode, subgroupRowIndex) => {
                            const subgroupRowValue = nodeValueMap[subgroupRowNode.id];
                            if (subgroupRowValue) {
                                groupedRowBucketMap[subgroupRowValue] = indexBucketMap.get(subgroupRowIndex);
                            }
                        });
                        return groupedRowBucketMap;
                    },
                };
            }
            else {
                // in case of non-grouped quantiles, we are able to calculate the buckets beforehand (which is the most efficient)
                const populationSize = context.adaptableApi.gridApi.getAllRowNodes()?.filter((rowNode) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    return isDefinedValue(rowValue);
                })?.length ?? 0;
                const bucketsMap = new Map();
                for (let step = 1; step <= qNumber; step++) {
                    bucketsMap.set(step, populationSize * (step / qNumber));
                }
                const indexBucketMap = new Map();
                for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
                    for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
                        if (bucketsMap.get(quantIndex) < populationIndex + 1) {
                            continue;
                        }
                        else {
                            indexBucketMap.set(populationIndex, quantIndex);
                            break;
                        }
                    }
                }
                quantReducer = {
                    initialValue: {},
                    reducer: (rowBucketMap, rowValue, rowNode, rowIndex) => {
                        if (isUndefinedValue(rowValue)) {
                            return rowBucketMap;
                        }
                        rowBucketMap[rowValue] = indexBucketMap.get(rowIndex);
                        return rowBucketMap;
                    },
                };
            }
            const aggregationExpressionEvaluation = {
                sortByColumn: quantileColumnName,
                rowFilterFn: (rowNode) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    return isDefinedValue(rowValue);
                },
                getRowNodes: context.getRowNodes,
                aggregationParams: {
                    reducers: {
                        QUANT: {
                            name: 'QUANT',
                            field: quantileColumnName,
                            ...quantReducer,
                        },
                    },
                },
                rowValueGetter: (rowNode, rowBucketMap) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    if (rowValue == null) {
                        return;
                    }
                    return rowBucketMap[rowValue];
                },
                columnType,
            };
            addGroupByParams(groupByOperand?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'QUANT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Calculates the q-Quantiles of the given group and displays which q-quantile the given value is in\nCommon quantiles are 4-quantile or Quartile, 5-quantile or Quintile, 10-quantile or Decile and 100-quantile or Percentile',
        signatures: [
            'QUANT( [colName], q: number)',
            'QUANT( COL(name: string), q: number)',
            'QUANT( [colName1], q: number, GROUP_BY([colName2]) )',
        ],
        examples: [
            'QUANT( [col1], 5)',
            `QUANT( COL('col1'), 100))`,
            'QUANT( [col1], 4, GROUP_BY([col2]) )',
        ],
        category: 'aggregation',
    },
    QUARTILE: {
        handler(args, context) {
            const quartileArgs = [...args, 4];
            return aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
        },
        description: 'Calculates the Quartiles of the given group and displays which quartile the given value is in',
        signatures: ['QUARTILE( [colName])', 'QUARTILE( [colName1], GROUP_BY([colName2]) )'],
        examples: ['QUARTILE( [col1], 5)', 'QUARTILE( [col1], 4, GROUP_BY([col2]) )'],
        category: 'aggregation',
    },
    PERCENTILE: {
        handler(args, context) {
            const quartileArgs = [...args, 100];
            return aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
        },
        description: 'Calculates the Percentile of the given group and displays which percentile the given value is in',
        signatures: ['PERCENTILE( [colName])', 'PERCENTILE( [colName1], GROUP_BY([colName2]) )'],
        examples: ['PERCENTILE( [col1], 5)', 'PERCENTILE( [col1], 4, GROUP_BY([col2]) )'],
        category: 'aggregation',
    },
    COUNT: {
        handler(args, context) {
            const countColumnParameter = extractColumnParameter('COUNT', args);
            const countColumnName = countColumnParameter.value;
            const groupByParameter = extractParameter('COUNT', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        COUNT: {
                            name: 'COUNT',
                            field: countColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + 1;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'COUNT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Counts the number of non-null (i.e. not empty) values in a given column.\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'COUNT( [colName] )',
            'COUNT( COL(name: string))',
            'COUNT( [colNameA], GROUP_BY( [colNameB] ))',
            'COUNT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['COUNT([colA])', 'COUNT([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    OVER: {
        handler(args, context) {
            const columnParameter = extractColumnParameter('OVER', args);
            validateColumnType(columnParameter.value, ['number', 'date'], 'OVER', context.adaptableApi);
            const result = {
                type: 'operand',
                name: 'OVER',
                value: columnParameter.value,
            };
            return result;
        },
        description: 'Defines an accumulative dimension (order) for the enclosing cumulative aggregation',
        signatures: ['OVER( [colName] )', 'OVER( COL(name: string))'],
        examples: ['OVER( [colName] )', `OVER( COL('colName'))`, `CUMUL( SUM([colA]), OVER([colB]))`],
        category: 'cumulative',
    },
    WEIGHT: {
        handler(args, context) {
            const columnParameter = extractColumnParameter('WEIGHT', args);
            validateColumnType(columnParameter.value, ['number'], 'WEIGHT', context.adaptableApi);
            const result = {
                type: 'operand',
                name: 'WEIGHT',
                value: columnParameter.value,
            };
            return result;
        },
        description: 'Defines a weight for the enclosing AVG(Average) aggregation',
        signatures: ['WEIGHT( [colName] )', 'WEIGHT( COL(name: string))'],
        examples: [
            'WEIGHT( [colName] )',
            `WEIGHT( COL('colName'))`,
            `AVG( [colName1], WEIGHT([colName2]))`,
        ],
        category: 'operand',
    },
    GROUP_BY: {
        handler(args, context) {
            const columnParameters = extractColumnParameters('GROUP_BY', args);
            const result = {
                type: 'operand',
                name: 'GROUP_BY',
                value: columnParameters.map((columnParam) => columnParam.value),
            };
            return result;
        },
        description: 'Groups an aggregation operation within the rows that have the same value in the specified column',
        signatures: ['GROUP_BY( [colName] )', 'GROUP_BY( COL(name: string))'],
        examples: ['GROUP_BY( [colName] )', `GROUP_BY( COL('colName'))`],
        category: 'grouping',
    },
    COL: {
        handler(args, context) {
            return handleColumnFunction(args, context);
        },
        description: 'References a column by its unique identifier',
        signatures: ['[colName]', 'COL(name: string)'],
        examples: ['[col1]', 'COL("col1")'],
        category: 'special',
    },
    PREVIOUS: {
        handler(args, context) {
            const prevColumnParameter = extractColumnParameter('PREVIOUS', args);
            const prevColumnName = prevColumnParameter.value;
            const groupByParameter = extractParameter('PREVIOUS', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const overParameter = extractParameter('PREVIOUS', 'operand', ['OVER'], args);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        PREVIOUS: {
                            name: 'PREVIOUS',
                            field: prevColumnName,
                            initialValue: {
                                map: {},
                                previousValue: undefined,
                            },
                            reducer: (indexMapAndPrevious, rowValue, rowNode) => {
                                if (isDefinedValue(indexMapAndPrevious.previousValue)) {
                                    indexMapAndPrevious.map[rowNode.id] = indexMapAndPrevious.previousValue;
                                }
                                indexMapAndPrevious.previousValue = rowValue;
                                return indexMapAndPrevious;
                            },
                            done: (indexMapAndPrevious) => {
                                return indexMapAndPrevious.map;
                            },
                        },
                    },
                },
                rowValueGetter: (rowNode, indexMap) => {
                    return indexMap[rowNode.id];
                },
                rowFilterFn: context.filterFn,
                sortByColumn: overParameter.value,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'PREVIOUS',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Returns the value of the previous element in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
        signatures: [
            'PREVIOUS( [colNameA], OVER [colNameB] )',
            'PREVIOUS( COL(nameA: string), OVER( COL(nameB: string)))',
            'PREVIOUS( [colNameA], OVER( [colNameB] ), GROUP_BY( [colNameC] ))',
            'PREVIOUS( COL(nameA: string), OVER( COL(nameB: string)), GROUP_BY( COL(nameC: string)))',
        ],
        examples: ['PREVIOUS([colA], OVER[colB])', 'PREVIOUS([colA], OVER([colB]), GROUP_BY([colC]))'],
        category: 'refs',
    },
    NEXT: {
        handler(args, context) {
            const nextColumnParameter = extractColumnParameter('NEXT', args);
            const nextColumnName = nextColumnParameter.value;
            const groupByParameter = extractParameter('NEXT', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const overParameter = extractParameter('NEXT', 'operand', ['OVER'], args);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        NEXT: {
                            name: 'NEXT',
                            field: nextColumnName,
                            initialValue: {
                                map: {},
                                previousRowNode: undefined,
                            },
                            reducer: (indexMapAndPreviousRowNode, rowValue, rowNode) => {
                                if (isDefinedValue(rowValue) &&
                                    isDefinedValue(indexMapAndPreviousRowNode.previousRowNode)) {
                                    indexMapAndPreviousRowNode.map[indexMapAndPreviousRowNode.previousRowNode.id] =
                                        rowValue;
                                }
                                indexMapAndPreviousRowNode.previousRowNode = rowNode;
                                return indexMapAndPreviousRowNode;
                            },
                            done: (indexMapAndPreviousRowNode) => {
                                return indexMapAndPreviousRowNode.map;
                            },
                        },
                    },
                },
                rowValueGetter: (rowNode, indexMap) => {
                    return indexMap[rowNode.id];
                },
                rowFilterFn: context.filterFn,
                sortByColumn: overParameter.value,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'NEXT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Returns the value of the next element in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
        signatures: [
            'NEXT( [colNameA], OVER [colNameB] )',
            'NEXT( COL(nameA: string), OVER( COL(nameB: string)))',
            'NEXT( [colNameA], OVER( [colNameB] ), GROUP_BY( [colNameC] ))',
            'NEXT( COL(nameA: string), OVER( COL(nameB: string)), GROUP_BY( COL(nameC: string)))',
        ],
        examples: ['NEXT([colA], OVER[colB])', 'NEXT([colA], OVER([colB]), GROUP_BY([colC]))'],
        category: 'refs',
    },
    FIRST: {
        handler(args, context) {
            const firstColumnParameter = extractColumnParameter('FIRST', args);
            const firstColumnName = firstColumnParameter.value;
            const groupByParameter = extractParameter('FIRST', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const overParameter = extractParameter('FIRST', 'operand', ['OVER'], args);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        FIRST: {
                            name: 'FIRST',
                            field: firstColumnName,
                            initialValue: undefined,
                            reducer: (firstElement, rowValue) => {
                                if (isDefinedValue(firstElement)) {
                                    return firstElement;
                                }
                                return rowValue;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                sortByColumn: overParameter.value,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'FIRST',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Returns the first value in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
        signatures: [
            'FIRST( [colNameA], OVER [colNameB] )',
            'FIRST( COL(nameA: string), OVER( COL(nameB: string)))',
            'FIRST( [colNameA], OVER( [colNameB] ), GROUP_BY( [colNameC] ))',
            'FIRST( COL(nameA: string), OVER( COL(nameB: string)), GROUP_BY( COL(nameC: string)))',
        ],
        examples: ['FIRST([colA], OVER[colB])', 'FIRST([colA], OVER([colB]), GROUP_BY([colC]))'],
        category: 'refs',
    },
    LAST: {
        handler(args, context) {
            const lastColumnParameter = extractColumnParameter('LAST', args);
            const lastColumnName = lastColumnParameter.value;
            const groupByParameter = extractParameter('LAST', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const overParameter = extractParameter('LAST', 'operand', ['OVER'], args);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        LAST: {
                            name: 'LAST',
                            field: lastColumnName,
                            initialValue: undefined,
                            reducer: (lastElement, rowValue) => {
                                if (isDefinedValue(rowValue)) {
                                    return rowValue;
                                }
                                return lastElement;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                sortByColumn: overParameter.value,
            };
            addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
            const result = {
                name: 'LAST',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Returns the last value in the given column, when ordered by the second given column.\nOptionally the value may be computed within provided individual groups',
        signatures: [
            'LAST( [colNameA], OVER [colNameB] )',
            'LAST( COL(nameA: string), OVER( COL(nameB: string)))',
            'LAST( [colNameA], OVER( [colNameB] ), GROUP_BY( [colNameC] ))',
            'LAST( COL(nameA: string), OVER( COL(nameB: string)), GROUP_BY( COL(nameC: string)))',
        ],
        examples: ['LAST([colA], OVER[colB])', 'LAST([colA], OVER([colB]), GROUP_BY([colC]))'],
        category: 'refs',
    },
    ABSOLUTE_DIFF: {
        handler(args, context) {
            const absoluteDiffColumnParameter = extractColumnParameter('ABSOLUTE_DIFF', args);
            const absoluteDiffColumnName = absoluteDiffColumnParameter.value;
            const columnType = validateColumnType(absoluteDiffColumnName, ['number'], 'ABSOLUTE_DIFF', context.adaptableApi);
            const relativeValueParameter = extractParameter('ABSOLUTE_DIFF', 'aggregationScalar', ['AVG', 'MEDIAN', 'MODE', 'MIN', 'MAX', 'PREVIOUS', 'NEXT', 'FIRST', 'LAST'], args);
            const groupByParameter = extractParameter('ABSOLUTE_DIFF', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (!!groupByParameter) {
                // ensure the GROUP_BY is NOT on the ABSOLUTE_DIFF function
                // it should be on the AGGREGATION function
                throw new ExpressionEvaluationError('GROUP_BY', `should be passed as an argument to the '${relativeValueParameter.name}' aggregation function!`);
            }
            const absoluteDiffExpressionEvaluation = mapWrappedAggregationToDiff(context, 'ABSOLUTE_DIFF', relativeValueParameter, absoluteDiffColumnName, columnType);
            const result = {
                name: 'ABSOLUTE_DIFF',
                type: 'aggregationScalar',
                value: absoluteDiffExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the absolute difference between the column values and the provided relative value.\nOptionally the aggregation may be computed within provided individual groups.',
        signatures: ['ABSOLUTE_DIFF( [colName], aggregationOp: AVG|MIN|MAX|MEDIAN|MODE )'],
        examples: [
            'ABSOLUTE_DIFF([price], MAX([price]))',
            'ABSOLUTE_DIFF([price], AVG([price], GROUP_BY([currency])))',
        ],
        category: 'aggregation',
    },
    PERCENT_DIFF: {
        handler(args, context) {
            const percentDiffColumnParameter = extractColumnParameter('PERCENT_DIFF', args);
            const percentDiffColumnName = percentDiffColumnParameter.value;
            const columnType = validateColumnType(percentDiffColumnName, ['number'], 'PERCENT_DIFF', context.adaptableApi);
            const relativeValueParameter = extractParameter('PERCENT_DIFF', 'aggregationScalar', ['AVG', 'MEDIAN', 'MODE', 'MIN', 'MAX', 'PREVIOUS', 'NEXT', 'FIRST', 'LAST'], args);
            const groupByParameter = extractParameter('PERCENT_DIFF', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (!!groupByParameter) {
                // ensure the GROUP_BY is NOT on the ABSOLUTE_DIFF function
                // it should be on the AGGREGATION function
                throw new ExpressionEvaluationError('GROUP_BY', `should be passed as an argument to the '${relativeValueParameter.name}' aggregation function!`);
            }
            const percentDiffExpressionEvaluation = mapWrappedAggregationToDiff(context, 'PERCENT_DIFF', relativeValueParameter, percentDiffColumnName, columnType);
            const result = {
                name: 'PERCENT_DIFF',
                type: 'aggregationScalar',
                value: percentDiffExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the percentage difference between the column values and the provided relative value.\nOptionally the aggregation may be computed within provided individual groups.',
        signatures: ['PERCENT_DIFF( [colName], aggregationOp: AVG|MIN|MAX|MEDIAN|MODE )'],
        examples: [
            'PERCENT_DIFF([price], MAX([price]))',
            'PERCENT_DIFF([price], AVG([price], GROUP_BY([currency])))',
        ],
        category: 'aggregation',
    },
};
const mapAggregationToCumulation = (aggregationParameter, overColumnParameter, context) => {
    const aggregationEvaluation = aggregationParameter.value;
    const aggregationReducer = aggregationEvaluation.aggregationParams.reducers[aggregationParameter.name];
    const cumulationAggregationReducer = aggregationParameter.name === 'SUM'
        ? {
            name: 'CUMUL_SUM',
            field: aggregationReducer.field,
            initialValue: {
                currentValue: aggregationReducer.initialValue,
                cumulatedValues: {},
            },
            reducer: (cumulationBag, rowValue, rowNode) => {
                if (isDefinedValue(rowValue)) {
                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                }
                cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                return cumulationBag;
            },
        }
        : aggregationParameter.name === 'MIN'
            ? {
                name: 'CUMUL_MIN',
                field: aggregationReducer.field,
                initialValue: {
                    currentValue: aggregationReducer.initialValue,
                    cumulatedValues: {},
                },
                reducer: (cumulationBag, rowValue, rowNode) => {
                    if (isDefinedValue(rowValue)) {
                        if (aggregationEvaluation.columnType === 'date') {
                            if (cumulationBag.currentValue === aggregationReducer.initialValue ||
                                isAfter(parseDateValue(cumulationBag.currentValue), parseDateValue(rowValue))) {
                                cumulationBag.currentValue = rowValue;
                            }
                        }
                        else if (rowValue < cumulationBag.currentValue) {
                            cumulationBag.currentValue = rowValue;
                        }
                    }
                    if (cumulationBag.currentValue !== aggregationReducer.initialValue) {
                        cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                    }
                    return cumulationBag;
                },
            }
            : aggregationParameter.name === 'MAX'
                ? {
                    name: 'CUMUL_MAX',
                    field: aggregationReducer.field,
                    initialValue: {
                        currentValue: aggregationReducer.initialValue,
                        cumulatedValues: {},
                    },
                    reducer: (cumulationBag, rowValue, rowNode) => {
                        if (isDefinedValue(rowValue)) {
                            if (aggregationEvaluation.columnType === 'date') {
                                if (cumulationBag.currentValue === aggregationReducer.initialValue ||
                                    isAfter(parseDateValue(rowValue), parseDateValue(cumulationBag.currentValue))) {
                                    cumulationBag.currentValue = rowValue;
                                }
                            }
                            else if (rowValue > cumulationBag.currentValue) {
                                cumulationBag.currentValue = rowValue;
                            }
                        }
                        if (cumulationBag.currentValue !== aggregationReducer.initialValue) {
                            cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                        }
                        return cumulationBag;
                    },
                }
                : aggregationParameter.name === 'AVG'
                    ? {
                        name: 'CUMUL_AVG',
                        field: aggregationReducer.field,
                        initialValue: {
                            currentValue: 0,
                            cumulatedValues: {},
                            numberOfCumulatedValues: 0,
                        },
                        reducer: (cumulationBag, rowValue, rowNode) => {
                            if (isDefinedValue(rowValue)) {
                                if (aggregationEvaluation.context?.weightParam) {
                                    // weighted average
                                    const weightValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, aggregationEvaluation.context.weightParam.value) ?? 0;
                                    cumulationBag.currentValue =
                                        cumulationBag.currentValue + rowValue * weightValue;
                                    cumulationBag.numberOfCumulatedValues =
                                        cumulationBag.numberOfCumulatedValues + weightValue;
                                }
                                else {
                                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                                    cumulationBag.numberOfCumulatedValues =
                                        cumulationBag.numberOfCumulatedValues + 1;
                                }
                            }
                            if (cumulationBag.numberOfCumulatedValues !== 0) {
                                cumulationBag.cumulatedValues[rowNode.id] =
                                    cumulationBag.currentValue / cumulationBag.numberOfCumulatedValues;
                            }
                            return cumulationBag;
                        },
                    }
                    : aggregationParameter.name === 'PERCENTAGE'
                        ? {
                            name: 'CUMUL_PERCENTAGE',
                            field: aggregationReducer.field,
                            initialValue: {
                                currentValue: aggregationReducer.initialValue,
                                cumulatedValues: {},
                            },
                            reducer: (cumulationBag, rowValue, rowNode) => {
                                if (isDefinedValue(rowValue)) {
                                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                                }
                                cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                                return cumulationBag;
                            },
                            done: (cumulationBag) => {
                                cumulationBag.totalAggregationValue = cumulationBag.currentValue;
                                return cumulationBag;
                            },
                        }
                        : aggregationParameter.name === 'COUNT'
                            ? {
                                name: 'CUMUL_COUNT',
                                field: aggregationReducer.field,
                                initialValue: {
                                    currentValue: aggregationReducer.initialValue,
                                    cumulatedValues: {},
                                },
                                reducer: (cumulationBag, rowValue, rowNode) => {
                                    if (isDefinedValue(rowValue)) {
                                        cumulationBag.currentValue = cumulationBag.currentValue + 1;
                                    }
                                    cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                                    return cumulationBag;
                                },
                            }
                            : null;
    const rowValueGetter = aggregationParameter.name === 'PERCENTAGE'
        ? (rowNode, cumulationBag) => {
            const cumulatedSum = cumulationBag.cumulatedValues[rowNode.id];
            const totalValue = cumulationBag.totalAggregationValue;
            return (cumulatedSum / totalValue) * 100;
        }
        : (rowNode, aggregatedValue) => {
            return aggregatedValue.cumulatedValues[rowNode.id];
        };
    const cumulationExpressionEvaluation = {
        sortByColumn: overColumnParameter.value,
        aggregationParams: {
            reducers: {
                CUMUL: cumulationAggregationReducer,
            },
            groupBy: aggregationEvaluation.aggregationParams.groupBy,
        },
        rowValueGetter,
        rowFilterFn: aggregationEvaluation.rowFilterFn,
        getRowNodes: context.getRowNodes,
    };
    return cumulationExpressionEvaluation;
};
const mapWrappedAggregationToDiff = (context, diffWrapperFunction, aggregationParameter, columnName, columnType) => {
    const aggregationEvaluation = aggregationParameter.value;
    const absoluteDiffExpressionEvaluation = {
        aggregationParams: aggregationEvaluation.aggregationParams,
        rowValueGetter: (rowNode, wrappedAggregatedValue) => {
            const aggregatedValue = typeof aggregationEvaluation.rowValueGetter === 'function'
                ? aggregationEvaluation.rowValueGetter(rowNode, wrappedAggregatedValue)
                : wrappedAggregatedValue;
            const cellValue = getNumericValue(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, columnName));
            if (cellValue == null || aggregatedValue == null) {
                return null;
            }
            if (diffWrapperFunction === 'ABSOLUTE_DIFF') {
                return cellValue - aggregatedValue;
            }
            if (diffWrapperFunction === 'PERCENT_DIFF') {
                return ((cellValue - aggregatedValue) / aggregatedValue) * 100;
            }
            // should never happen
            return null;
        },
        rowFilterFn: aggregationEvaluation.rowFilterFn,
        getRowNodes: context.getRowNodes,
        sortByColumn: aggregationEvaluation.sortByColumn,
        context: aggregationEvaluation.context,
        columnType,
    };
    return absoluteDiffExpressionEvaluation;
};
export const aggregatedScalarExpressionFunctionNames = getTypedKeys(aggregatedScalarExpressionFunctions);
// !! mutates expressionEvaluation
export const addGroupByParams = (groupByColumnNames, expressionEvaluation) => {
    if (groupByColumnNames?.length) {
        expressionEvaluation.aggregationParams.groupBy = groupByColumnNames.map((columnName) => ({
            field: columnName,
        }));
    }
};
const isUndefinedValue = (input) => {
    return input == undefined || (typeof input === 'string' && input.trim() === '');
};
const isDefinedValue = (input) => {
    return !isUndefinedValue(input);
};