UNPKG

@adaptabletools/adaptable

Version:

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

743 lines (742 loc) 30 kB
import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError'; import { parseISO } from 'date-fns'; import { startOfDay } from 'date-fns'; import { startOfWeek } from 'date-fns'; import { startOfMonth } from 'date-fns'; import { startOfYear } from 'date-fns'; import { addDays } from 'date-fns'; import { addWeeks } from 'date-fns'; import { addMonths } from 'date-fns'; import { addYears } from 'date-fns'; import { differenceInDays } from 'date-fns'; import { differenceInWeeks } from 'date-fns'; import { differenceInMonths } from 'date-fns'; import { differenceInYears } from 'date-fns'; import { evaluateExpressionNode, getNumericValue, isTextSearchCaseInsensitive, } from './expressionFunctionUtils'; import { normalizeDateParams } from './dateUtils'; import StringExtensions from '../Extensions/StringExtensions'; import { getTypedKeys } from '../Extensions/TypeExtensions'; import { evaluate } from '../../parser/src'; import { evaluateNode } from '../../parser/src/evaluator'; const flattenArguments = (values) => { if (!Array.isArray(values)) { return values; } return values.flat(Infinity); }; const sanitizeArguments = (values, allowNaN) => { return values.filter((value) => value != undefined && value != null && value !== '' && (allowNaN || !isNaN(value))); }; const sanitizeNumericResult = (value) => { if (isNaN(value)) { return ''; } return value; }; export const scalarExpressionFunctions = { VAR: { handler(args, context) { const [functionName, ...optionalArgs] = args; if (StringExtensions.IsNullOrEmpty(functionName)) { throw new ExpressionEvaluationError('VAR', 'should have a name'); } return context.evaluateCustomQueryVariable(functionName, optionalArgs); }, description: 'Returns the variable evaluation', signatures: ['VAR(varName)', 'VAR(varName, arg1, arg2)'], examples: ['VAR(CURRENT_USER)', 'VAR(IS_VALID_VALUE, IS_BLANK([col1]), [col2] < [col3])'], category: 'special', returnType: 'any', }, COL: { handler(args, context) { const columnId = args[0]; const column = context.adaptableApi?.columnApi.getColumnWithColumnId(columnId); if (!column) { throw new ExpressionEvaluationError('COL', `Column name "${columnId}" is not found`); } if (!column.queryable) { throw new ExpressionEvaluationError('COL', `Column name "${columnId}" is not queryable`); } if (context.pivotResultColumn) { const baseColumnId = context.pivotResultColumn.getColDef()?.pivotValueColumn?.getColId(); if (baseColumnId === columnId) { return context.adaptableApi?.gridApi.getNormalisedValueFromRowNode(context.node, context.pivotResultColumn.getColId()); } } return context.adaptableApi?.gridApi.getNormalisedValueFromRowNode(context.node, columnId); }, description: 'Returns the value of a Column', signatures: ['[colName]', 'COL(name: string)'], examples: ['[col1]', 'COL("col1")'], category: 'special', returnType: 'any', }, FIELD: { handler(args, context) { const fieldName = args[0]; if (StringExtensions.IsNullOrEmpty(fieldName)) { throw new ExpressionEvaluationError('FIELD', 'requires a field name'); } return context.adaptableApi?.internalApi.getValueUsingField(context.node?.data, fieldName); }, description: 'Returns the value of a row field (not necessarily mapped to a column). If the field is nested, use dot notation (e.g. "nested.fieldName")', signatures: ['FIELD(fieldName:string)'], examples: ['FIELD("fieldName")', 'FIELD("nested.fieldName")'], category: 'special', returnType: 'any', }, $SCOPE: { handler(args, context) { const scopeColumnId = context.columnScope; if (!scopeColumnId) { throw new ExpressionEvaluationError('$SCOPE', `$SCOPE is not available in this context`); } const column = context.adaptableApi?.columnApi.getColumnWithColumnId(scopeColumnId); if (!column) { throw new ExpressionEvaluationError('$SCOPE', `Column name "${scopeColumnId}" is not found`); } if (!column.queryable) { throw new ExpressionEvaluationError('$SCOPE', `Column name "${scopeColumnId}" is not queryable`); } if (context.pivotResultColumn) { const baseColumnId = context.pivotResultColumn.getColDef()?.pivotValueColumn?.getColId(); if (baseColumnId === scopeColumnId) { return context.adaptableApi?.gridApi.getNormalisedValueFromRowNode(context.node, context.pivotResultColumn.getColId()); } } return context.adaptableApi?.gridApi.getNormalisedValueFromRowNode(context.node, scopeColumnId); }, description: 'Returns the value of the column in the current scope', signatures: ['$SCOPE()'], examples: ['$SCOPE() > 10'], category: 'special', returnType: 'any', }, QUERY: { handler(args, context) { const namedQueryName = args[0]; if (StringExtensions.IsNullOrEmpty(namedQueryName)) { return false; } const namedQuery = context.adaptableApi?.namedQueryApi.getNamedQueryByName(namedQueryName); if (!namedQuery) { throw new ExpressionEvaluationError('QUERY', `Named Query with name ${namedQueryName} not found!`); } if (!context.namedQueryCallStack) { context.namedQueryCallStack = []; } context.namedQueryCallStack.push(namedQueryName); const firstIndex = context.namedQueryCallStack.indexOf(namedQueryName); const lastIndex = context.namedQueryCallStack.lastIndexOf(namedQueryName); if (firstIndex !== lastIndex) { const cycle = context.namedQueryCallStack.slice(firstIndex, lastIndex + 1); throw new ExpressionEvaluationError(`${namedQueryName}`, ` contains a circular reference: ${cycle.join(' -> ')}`); } const queryEvaluationResult = evaluate(namedQuery.BooleanExpression, context); context.namedQueryCallStack?.pop(); return queryEvaluationResult; }, category: 'special', description: 'Returns the evaluation result of the Named Query with the given name', signatures: ['QUERY("anyNamedQuery")'], examples: ['QUERY("anyNamedQuery")'], returnType: 'boolean', }, TO_ARRAY: { handler(args) { if (!args?.length) { return []; } return [...args]; }, description: 'Creates an array containing all given parameters (which can be scalar values or expressions)', signatures: ['TO_ARRAY(value1, expression1, value2)'], examples: [`TO_ARRAY([col1], AVG([col2],[col3]), QUERY("maxValue"))`], category: 'special', returnType: 'any', }, COALESCE: { handler(args) { return flattenArguments(args).find((arg) => !(arg === null || arg === undefined)); }, description: 'Returns the first argument which is not null', signatures: ['COALESCE(value, value, ...value)'], examples: ['COALESCE([col1], [col2], [col3], 0)'], category: 'special', returnType: 'string', }, NULL: { handler: () => { return null; }, description: 'Returns the NULL literal which represents the intentional absence of any object value', category: 'special', returnType: 'null', signatures: ['NULL'], examples: ['[status] = "accepted" ? 100 : NULL'], }, IS_BLANK: { handler(args) { return args[0] === undefined || args[0] === null || args[0] === ''; }, category: 'special', description: 'Returns true if input value is undefined, null, or an empty string', signatures: ['IS_BLANK(input: any)'], examples: ['IS_BLANK([col1])'], returnType: 'boolean', }, IS_NOT_BLANK: { handler(args) { return args[0] !== undefined && args[0] !== null && String(args[0]).trim() !== ''; }, category: 'special', description: 'Returns true if input value is not empty', signatures: ['IS_NOT_BLANK(input: any)'], examples: ['IS_NOT_BLANK([col1])'], returnType: 'boolean', }, ABS: { handler(args) { return sanitizeNumericResult(Math.abs(args[0])); }, isHiddenFromMenu: true, description: 'Returns the absolute value of the given number', signatures: ['ABS(a: number)'], examples: ['ABS([columnName])'], category: 'maths', returnType: 'number', }, CEILING: { handler(args) { return sanitizeNumericResult(Math.ceil(args[0])); }, isHiddenFromMenu: true, description: 'Returns smallest integer greater than or equal to the given number', signatures: ['CEILING(a: number)'], examples: ['CEILING([columnName])'], category: 'maths', returnType: 'number', }, FLOOR: { handler(args) { return sanitizeNumericResult(Math.floor(args[0])); }, isHiddenFromMenu: true, description: 'Returns largest integer less than or equal to the given number', signatures: ['FLOOR(a: number)'], examples: ['FLOOR([columnName])'], category: 'maths', returnType: 'number', }, ROUND: { handler(args) { return sanitizeNumericResult(Math.round(args[0])); }, isHiddenFromMenu: true, description: 'Returns value of the given number rounded to the nearest integer', signatures: ['ROUND(a: number)'], examples: ['ROUND([columnName])'], category: 'maths', returnType: 'number', }, MIN: { handler(args) { return Math.min(...sanitizeArguments(flattenArguments(args))); }, description: 'Returns the smallest of given numbers', signatures: ['MIN(number, number, ...number)'], examples: ['MIN([col1], 5)'], category: 'maths', returnType: 'number', }, MAX: { handler(args) { return Math.max(...sanitizeArguments(flattenArguments(args))); }, description: 'Returns the highest of given numbers', signatures: ['MAX(number, number, ...number)'], examples: ['MAX([col1], 5)'], category: 'maths', returnType: 'number', }, AVG: { handler(args) { const sanitizedArguments = sanitizeArguments(flattenArguments(args)); if (args.length && !sanitizedArguments.length) { return; } return sanitizedArguments.reduce((a, b) => a + b) / sanitizedArguments.length; }, description: 'Returns the average of inputted numbers', signatures: ['AVG(number, number, ...number)'], examples: ['AVG([col1], 5)'], category: 'maths', returnType: 'number', }, ADD: { handler(args) { const sanitizedArguments = sanitizeArguments(args, true); if (args.length && !sanitizedArguments.length) { return; } return sanitizedArguments.reduce((a, b) => a + b); }, isHiddenFromMenu: true, description: 'Returns the sum of 2 numbers', signatures: ['number + number', 'ADD(a: number, b: number)'], examples: ['[col1] + 5', 'ADD([col1], 5)'], category: 'maths', returnType: 'number', }, SUB: { handler(args) { const sanitizedArguments = sanitizeArguments(args); if (args.length && !sanitizedArguments.length) { return; } return sanitizedArguments.reduce((a, b) => a - b); }, isHiddenFromMenu: true, description: 'Returns the difference of 2 numbers', signatures: ['number - number', 'SUB(a: number, b: number)'], examples: ['[col1] - 5', 'SUB([col1], 5)'], category: 'maths', returnType: 'number', }, MUL: { handler(args) { return sanitizeNumericResult(args.reduce((a, b) => a * b)); }, isHiddenFromMenu: true, description: 'Returns the product of 2 numbers', signatures: ['number * number', 'mul(a: number, b: number)'], examples: ['[col1] * 5', 'mul([col1], 5)'], category: 'maths', returnType: 'number', }, DIV: { handler(args) { return sanitizeNumericResult(args.reduce((a, b) => a / b)); }, isHiddenFromMenu: true, description: 'Returns the division of 2 numbers', signatures: ['number / number', 'DIV(a: number, b: number)'], examples: ['[col1] / 5', 'DIV([col1], 5)'], category: 'maths', returnType: 'number', }, MOD: { handler(args) { return sanitizeNumericResult(args[0] % args[1]); }, isHiddenFromMenu: true, description: 'Returns the modulo of 2 numbers', signatures: ['number % number', 'MOD(a: number, b: number)'], examples: ['[col1] % 5', 'MOD([col1], 5)'], category: 'maths', returnType: 'number', }, IF: { handler(args) { return args[0] ? args[1] : args[2]; }, isHiddenFromMenu: true, description: 'Evaluates a condition expression and returns the result of one of the two expressions, depending on whether the condition expression evaluates to true or false', signatures: ['condition_expr ? true_statement : false_statement'], examples: ['[col1] > [col2] ? "BIG" : "SMALL"'], category: 'conditional', returnType: 'any', }, CASE: { handler([expressionValueNode, whenThenListNodes, defaultValueNode], context) { const caseExpressionValue = expressionValueNode != undefined ? evaluateExpressionNode(expressionValueNode, context) : true; const matchingWhen = whenThenListNodes.find((whenThenStatement) => { const whenValue = evaluateExpressionNode(whenThenStatement.WHEN, context); return whenValue === caseExpressionValue; }); if (!matchingWhen) { if (defaultValueNode) { return evaluateExpressionNode(defaultValueNode, context); } return; } const matchingThen = evaluateExpressionNode(matchingWhen.THEN, context); return matchingThen; }, isHiddenFromMenu: true, hasEagerEvaluation: true, description: `The syntax of CASE supports 2 variants: 1. CASE takes an expression and compares its value with each WHEN clause until one of them is equal, whereupon the corresponding THEN clause is executed. 2. Each expression in the WHEN clause is evaluated until one of them is true, whereupon the corresponding THEN clause is executed. If no WHEN clause is satisfied, the ELSE clause is executed if one exists, otherwise NULL is returned`, signatures: [ `CASE <expression> [WHEN <expression> THEN <expression>] [ELSE <expression>] END`, `CASE [WHEN <expression> THEN <expression>] [ELSE <expression>] END`, ], examples: [ `CASE [day] WHEN 'Saturday' THEN 'weekend' WHEN 'Sunday' THEN 'weekend' ELSE 'workday' END`, `CASE WHEN [price] < 10 THEN 'low price' WHEN [price] < 50 THEN 'medium price' ELSE 'high price' END`, ], category: 'conditional', returnType: 'any', }, POW: { handler(args) { return sanitizeNumericResult(Math.pow(args[0], args[1])); }, isHiddenFromMenu: true, description: 'Returns the pow of 2 numbers', signatures: ['number ^ number', 'POW(a: number, b: number)'], examples: ['[col1] ^ 5', 'POW([col1], 5)'], category: 'maths', returnType: 'number', }, DATE: { handler(args) { return parseISO(args[0]); }, description: 'Returns a new date by parsing the given string in ISO 8601 format', signatures: [ 'DATE(input: string)', 'DATE("YYYYMMDD")', 'DATE("YYYY-MM-DD")', 'DATE("YYYY-MM-DD HH:SS")', ], examples: ['DATE("20210101")', 'DATE("2021-01-01")', 'DATE("2021-01-01 10:10")'], category: 'dates', returnType: 'date', }, NOW: { handler() { return new Date(); }, description: 'Returns the current date', signatures: ['NOW()'], examples: ['[col1] > NOW()'], category: 'dates', returnType: 'date', }, CURRENT_DAY: { handler() { return startOfDay(new Date()); }, description: 'Returns the current day', signatures: ['CURRENT_DAY()'], examples: ['[col1] > CURRENT_DAY()'], category: 'dates', returnType: 'date', }, DAY: { handler(args) { return startOfDay(args[0]); }, description: 'Returns the day from a date', signatures: ['DAY(input: date)'], examples: ['DAY([col1]) = DAY(NOW())'], category: 'dates', returnType: 'date', }, WEEK: { handler(args) { return startOfWeek(args[0]); }, description: 'Returns the week from a date', signatures: ['WEEK(input: date)'], examples: ['WEEK([col1]) = WEEK(NOW())'], category: 'dates', returnType: 'number', }, MONTH: { handler(args) { return startOfMonth(args[0]); }, description: 'Returns the month from a date', signatures: ['MONTH(input: date)'], examples: ['MONTH([col1]) = MONTH(NOW())'], category: 'dates', returnType: 'number', }, YEAR: { handler(args) { return startOfYear(args[0]); }, description: 'Returns the year from a date', signatures: ['YEAR(input: date)'], examples: ['YEAR([col1]) = YEAR(NOW())'], category: 'dates', returnType: 'date', }, ADD_DAYS: { handler(args) { return addDays(args[0], args[1]); }, description: 'Returns a date based on input data and days to add', signatures: ['ADD_DAYS(input: date, days: number)'], examples: ['ADD_DAYS(CURRENT_DAY(), 5)', 'ADD_DAYS([col1], 5)'], category: 'dates', returnType: 'date', }, ADD_WEEKS: { handler(args) { return addWeeks(args[0], args[1]); }, description: 'Returns a date based on input data and weeks to add', signatures: ['ADD_WEEKS(input: date, weeks: number)'], examples: ['ADD_WEEKS(CURRENT_DAY(), 5)', 'ADD_WEEKS([col1], 5)'], category: 'dates', returnType: 'date', }, ADD_MONTHS: { handler(args) { return addMonths(args[0], args[1]); }, description: 'Returns a date based on input data and months to add', signatures: ['ADD_MONTHS(input: date, months: number)'], examples: ['ADD_MONTHS(CURRENT_DAY(), 5)', 'ADD_MONTHS([col1], 5)'], category: 'dates', returnType: 'date', }, ADD_YEARS: { handler(args) { return addYears(args[0], args[1]); }, description: 'Returns a date based on input data and years to add', signatures: ['ADD_YEARS(input: date, years: number)'], examples: ['ADD_YEARS(CURRENT_DAY(), 5)', 'ADD_YEARS([col1], 5)'], category: 'dates', returnType: 'date', }, DIFF_DAYS: { handler(args) { const [first, second] = normalizeDateParams(args); const result = differenceInDays(first, second); return sanitizeNumericResult(result); }, description: 'Returns the difference in days between 2 dates', signatures: ['DIFF_DAYS(a: date, b: date)'], examples: ['DIFF_DAYS([col1], CURRENT_DAY())'], category: 'dates', returnType: 'number', }, DIFF_WEEKS: { handler(args) { const [first, second] = normalizeDateParams(args); const result = differenceInWeeks(first, second); return sanitizeNumericResult(result); }, description: 'Returns the difference in weeks between 2 dates', signatures: ['DIFF_WEEKS(a: date, b: date)'], examples: ['DIFF_WEEKS([col1], CURRENT_DAY())'], category: 'dates', returnType: 'number', }, DIFF_MONTHS: { handler(args) { const [first, second] = normalizeDateParams(args); const result = differenceInMonths(first, second); return sanitizeNumericResult(result); }, description: 'Returns the difference in months between 2 dates', signatures: ['DIFF_MONTHS(a: date, b: date)'], examples: ['DIFF_MONTHS([col1], CURRENT_DAY())'], category: 'dates', returnType: 'number', }, DIFF_YEARS: { handler(args) { const [first, second] = normalizeDateParams(args); const result = differenceInYears(first, second); return sanitizeNumericResult(result); }, description: 'Returns the difference in years between 2 dates', signatures: ['DIFF_YEARS(a: date, b: date)'], examples: ['DIFF_YEARS([col1], CURRENT_DAY())'], category: 'dates', returnType: 'number', }, LEN: { handler(args) { return args[0] == null || args[0] == undefined ? 0 : String(args[0]).length; }, description: 'Returns the length of a string', signatures: ['LEN(a: string)'], examples: ['LEN([col1])'], category: 'strings', returnType: 'number', }, SUB_STRING: { handler(args) { return String(args[0]).substring(args[1], args[2]); }, description: 'Extracts characters from string, between 2 indices, returning new sub string', signatures: ['SUB_STRING(a: string, b: number, c:number)'], examples: ['SUB_STRING([col1], 1, 5)'], category: 'strings', returnType: 'string', }, UPPER: { handler(args) { return String(args[0]).toUpperCase(); }, description: 'Converts a string to Upper Case', signatures: ['UPPER(a: string)'], examples: ['UPPER([col1])'], category: 'strings', returnType: 'string', }, LOWER: { handler(args) { return String(args[0]).toLowerCase(); }, description: 'Converts a string to Lower Case', signatures: ['LOWER(a: string)'], examples: ['LOWER([col1])'], category: 'strings', returnType: 'string', }, REPLACE: { handler(args, context) { if (isTextSearchCaseInsensitive(context)) { return String(args[0]).replace(new RegExp(args[1], 'ig'), args[2]); } else { return String(args[0]).replace(args[1], args[2]); } }, description: 'Searches string for specified value returning new string with replaced values', signatures: ['REPLACE(a: string, b: string, c:string)'], examples: ['REPLACE([col1], "GBP", "EUR")'], category: 'strings', returnType: 'string', }, CONCAT: { handler(args) { return args.join(' '); }, description: 'Concatenates multiple strings', signatures: ['CONCAT(value1, value2, value3)'], examples: ['CONCAT([col1], [col2], [col3]'], category: 'strings', returnType: 'string', }, PERCENT_CHANGE: { handler(args, context) { if (!context.dataChangedEvent) { throw new ExpressionEvaluationError('PERCENT_CHANGE', 'is only valid in changed cell contexts'); } const currentValue = getNumericValue(args[0], true); if (Number.isNaN(currentValue)) { throw new ExpressionEvaluationError('PERCENT_CHANGE', 'First argument should be numeric'); } const previousValue = context.dataChangedEvent.oldValue; const increaseDecrease = args[1]; let result; if (!increaseDecrease) { result = sanitizeNumericResult(Math.abs(((currentValue - previousValue) / previousValue) * 100)); } else if (increaseDecrease === 'INCREASE') { result = currentValue - previousValue > 0 ? sanitizeNumericResult(((currentValue - previousValue) / previousValue) * 100) : undefined; } else if (increaseDecrease === 'DECREASE') { result = currentValue - previousValue < 0 ? sanitizeNumericResult(((previousValue - currentValue) / previousValue) * 100) : undefined; } else { throw new ExpressionEvaluationError('PERCENT_CHANGE', 'Optional second argument must be "INCREASE" or "DECREASE"'); } return result; }, description: "Returns percentage difference between a cell's current value and previous value", signatures: [ 'PERCENT_CHANGE( [colName], <INCREASE|DECREASE> )', 'PERCENT_CHANGE( COL(name: string), <INCREASE|DECREASE> )', ], examples: [ 'PERCENT_CHANGE([col1])', 'PERCENT_CHANGE([col1], "INCREASE")', 'PERCENT_CHANGE([col1], "DECREASE")', ], category: 'changes', returnType: 'number', }, ABSOLUTE_CHANGE: { handler(args, context) { if (!context.dataChangedEvent) { throw new ExpressionEvaluationError('ABSOLUTE_CHANGE', 'is only valid in changed cell contexts'); } const currentValue = getNumericValue(args[0], true); if (Number.isNaN(currentValue)) { throw new ExpressionEvaluationError('ABSOLUTE_CHANGE', 'First argument should be numeric'); } const previousValue = context.dataChangedEvent.oldValue; const increaseDecrease = args[1]; let result; if (!increaseDecrease) { result = sanitizeNumericResult(Math.abs(previousValue - currentValue)); } else if (increaseDecrease === 'INCREASE') { result = currentValue - previousValue > 0 ? sanitizeNumericResult(currentValue - previousValue) : undefined; } else if (increaseDecrease === 'DECREASE') { result = currentValue - previousValue < 0 ? sanitizeNumericResult(previousValue - currentValue) : undefined; } else { throw new ExpressionEvaluationError('ABSOLUTE_CHANGE', 'Optional second argument must be "INCREASE" or "DECREASE"'); } return result; }, description: "Returns absolute difference between a cell's current value and previous value", signatures: [ 'ABSOLUTE_CHANGE( [colName] )', 'ABSOLUTE_CHANGE( COL(name: string) )', 'ABSOLUTE_CHANGE( COL(name: string), <INCREASE|DECREASE> )', ], examples: [ 'ABSOLUTE_CHANGE([col1])', 'ABSOLUTE_CHANGE([col1], "INCREASE")', 'ABSOLUTE_CHANGE([col1], "DECREASE")', ], category: 'changes', returnType: 'number', }, ANY_CHANGE: { handler(args, context) { if (!context.dataChangedEvent) { throw new ExpressionEvaluationError('ANY_CHANGE', 'is only valid in changed cell contexts'); } const columnArg = args[0]; if (!columnArg) { return context.dataChangedEvent.oldValue !== context.dataChangedEvent.newValue; } if (columnArg.type !== 'COL' && columnArg.type !== '$SCOPE') { throw new ExpressionEvaluationError('ANY_CHANGE', 'accepts only a column reference or scope as an argument'); } const currentColumnValue = evaluateNode(columnArg, context); const previousValue = context.dataChangedEvent.oldValue; return currentColumnValue !== previousValue; }, description: "Returns true if cell's current value is different from previous value, otherwise false; if no column is provided, it checks if any value has changed", signatures: ['ANY_CHANGE( [colName] )', 'ANY_CHANGE()'], examples: ['ANY_CHANGE([col1])', 'ANY_CHANGE()'], category: 'changes', returnType: 'boolean', hasEagerEvaluation: true, }, }; export const scalarExpressionFunctionNames = getTypedKeys(scalarExpressionFunctions);