UNPKG

@adaptabletools/adaptable

Version:

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

1,053 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', si