UNPKG

highcharts

Version:
529 lines (528 loc) 15.5 kB
/* * * * (c) 2009-2026 Highsoft AS * * A commercial license may be required depending on use. * See www.highcharts.com/license * * * Authors: * - Sophie Bremer * * */ 'use strict'; import { defined } from '../../Shared/Utilities.js'; import FormulaTypes from './FormulaTypes.js'; const { isFormula, isFunction, isOperator, isRange, isReference, isValue } = FormulaTypes; /* * * * Constants * * */ const asLogicalStringRegExp = / */; const MAX_FALSE = Number.MAX_VALUE / 1.000000000001; const MAX_STRING = Number.MAX_VALUE / 1.000000000002; const MAX_TRUE = Number.MAX_VALUE; const operatorPriority = { '^': 3, '*': 2, '/': 2, '+': 1, '-': 1, '=': 0, '<': 0, '<=': 0, '>': 0, '>=': 0 }; const processorFunctions = {}; const processorFunctionNameRegExp = /^[A-Z][A-Z\.]*$/; /* * * * Functions * * */ /** * Converts non-number types to logical numbers. * * @param {Highcharts.FormulaValue} value * Value to convert. * * @return {number} * Logical number value. `NaN` if not convertable. */ function asLogicalNumber(value) { switch (typeof value) { case 'boolean': return value ? MAX_TRUE : MAX_FALSE; case 'string': return MAX_STRING; case 'number': return value; default: return NaN; } } /** * Converts strings to logical strings, while other types get passed through. In * logical strings the space character is the lowest value and letters are case * insensitive. * * @param {Highcharts.FormulaValue} value * Value to convert. * * @return {Highcharts.FormulaValue} * Logical string value or passed through value. */ function asLogicalString(value) { if (typeof value === 'string') { return value.toLowerCase().replace(asLogicalStringRegExp, '\0'); } return value; } /** * Converts non-number types to a logic number. * * @param {Highcharts.FormulaValue} value * Value to convert. * * @return {number} * Number value. `NaN` if not convertable. */ function asNumber(value) { switch (typeof value) { case 'boolean': return value ? 1 : 0; case 'string': return parseFloat(value.replace(',', '.')); case 'number': return value; default: return NaN; } } /** * Process a basic operation of two given values. * * @private * * @param {Highcharts.FormulaOperator} operator * Operator between values. * * @param {Highcharts.FormulaValue} x * First value for operation. * * @param {Highcharts.FormulaValue} y * Second value for operation. * * @return {Highcharts.FormulaValue} * Operation result. `NaN` if operation is not support. */ function basicOperation(operator, x, y) { switch (operator) { case '=': return asLogicalString(x) === asLogicalString(y); case '<': if (typeof x === typeof y) { return asLogicalString(x) < asLogicalString(y); } return asLogicalNumber(x) < asLogicalNumber(y); case '<=': if (typeof x === typeof y) { return asLogicalString(x) <= asLogicalString(y); } return asLogicalNumber(x) <= asLogicalNumber(y); case '>': if (typeof x === typeof y) { return asLogicalString(x) > asLogicalString(y); } return asLogicalNumber(x) > asLogicalNumber(y); case '>=': if (typeof x === typeof y) { return asLogicalString(x) >= asLogicalString(y); } return asLogicalNumber(x) >= asLogicalNumber(y); } x = asNumber(x); y = asNumber(y); let result; switch (operator) { case '+': result = x + y; break; case '-': result = x - y; break; case '*': result = x * y; break; case '/': result = x / y; break; case '^': result = Math.pow(x, y); break; default: return NaN; } // Limit decimal to 9 digits return (result % 1 ? Math.round(result * 1000000000) / 1000000000 : result); } /** * Converts an argument to Value and in case of a range to an array of Values. * * @function Highcharts.Formula.getArgumentValue * * @param {Highcharts.FormulaRange|Highcharts.FormulaTerm} arg * Formula range or term to convert. * * @param {Highcharts.DataTable} [table] * Table to use for references and ranges. * * @return {Highcharts.FormulaValue|Array<Highcharts.FormulaValue>} * Converted value. */ function getArgumentValue(arg, table) { // Add value if (isValue(arg)) { return arg; } // Add values of a range if (isRange(arg)) { return (table && getRangeValues(arg, table) || []); } // Add values of a function if (isFunction(arg)) { return processFunction(arg, table); } // Process functions, operations, references with formula processor return processFormula((isFormula(arg) ? arg : [arg]), table); } /** * Converts all arguments to Values and in case of ranges to arrays of Values. * * @function Highcharts.Formula.getArgumentsValues * * @param {Highcharts.FormulaArguments} args * Formula arguments to convert. * * @param {Highcharts.DataTable} [table] * Table to use for references and ranges. * * @return {Array<(Highcharts.FormulaValue|Array<Highcharts.FormulaValue>)>} * Converted values. */ function getArgumentsValues(args, table) { const values = []; for (let i = 0, iEnd = args.length; i < iEnd; ++i) { values.push(getArgumentValue(args[i], table)); } return values; } /** * Extracts cell values from a table for a given range. * * @function Highcharts.Formula.getRangeValues * * @param {Highcharts.FormulaRange} range * Formula range to use. * * @param {Highcharts.DataTable} table * Table to extract from. * * @return {Array<Highcharts.FormulaValue>} * Extracted values. */ function getRangeValues(range, table) { const columnIds = table .getColumnIds() .slice(range.beginColumn, range.endColumn + 1), values = []; for (let i = 0, iEnd = columnIds.length, cell; i < iEnd; ++i) { const cells = table.getColumn(columnIds[i], true) || []; for (let j = range.beginRow, jEnd = range.endRow + 1; j < jEnd; ++j) { cell = cells[j]; if (typeof cell === 'string' && cell[0] === '=' && table !== table.getModified()) { // Look in the modified table for formula result cell = table.getModified().getCell(columnIds[i], j); } values.push(isValue(cell) ? cell : NaN); } } return values; } /** * Extracts the cell value from a table for a given reference. * * @private * * @param {Highcharts.FormulaReference} reference * Formula reference to use. * * @param {Highcharts.DataTable} table * Table to extract from. * * @return {Highcharts.FormulaValue} * Extracted value. 'undefined' might also indicate that the cell was not found. */ function getReferenceValue(reference, table) { const columnId = table.getColumnIds()[reference.column]; if (columnId) { const cell = table.getCell(columnId, reference.row); if (typeof cell === 'string' && cell[0] === '=' && table !== table.getModified()) { // Look in the modified table for formula result const result = table.getModified().getCell(columnId, reference.row); return isValue(result) ? result : NaN; } if (isValue(cell)) { return reference.isNegative ? -cell : cell; } return NaN; } return NaN; } /** * Calculates a value based on the two top values and the related operator. * * Used to properly process the formula's values based on its operators. * * @private * @function Highcharts.applyOperator * * @param {Array<Highcharts.Value>} values * Processed formula values. * * @param {Array<Highcharts.Operator>} operators * Processed formula operators. */ function applyOperator(values, operators) { if (values.length < 2 || operators.length < 1) { values.push(NaN); } const secondValue = values.pop(); const firstValue = values.pop(); const operator = operators.pop(); if (!defined(secondValue) || !defined(firstValue) || !defined(operator)) { values.push(NaN); } else { values.push(basicOperation(operator, firstValue, secondValue)); } } /** * Processes a formula array on the given table. If the formula does not contain * references or ranges, then no table has to be provided. * * Performs formulas considering the operators precedence. * * // Example of the `2 * 3 + 4` formula: * 2 -> values: [2], operators: [] * * -> values: [2], operators: [*] * 3 -> values: [2, 3], operators: [*] * // Since the higher precedence operator exists (* > +), perform it first. * + -> values: [6], operators: [+] * 4 -> values: [6, 4], operators: [+] * // When non-higher precedence operators remain, perform rest calculations. * -> values: [10], operators: [] * * @private * @function Highcharts.processFormula * * @param {Highcharts.Formula} formula * Formula array to process. * * @param {Highcharts.DataTable} [table] * Table to use for references and ranges. * * @return {Highcharts.FormulaValue} * Result value of the process. `NaN` indicates an error. */ function processFormula(formula, table) { // Keeps all the values to calculate them in a proper priority, based on the // given operators. const values = []; // Keeps all the operators to calculate the values above, following the // proper priority. const operators = []; // Indicates if the next item is a value (not an operator). let expectingValue = true; for (let i = 0, iEnd = formula.length; i < iEnd; ++i) { const item = formula[i]; if (isOperator(item)) { if (expectingValue && item === '-') { // Split the negative values to be handled as a binary // operation if the next item is a value. values.push(0); operators.push('-'); expectingValue = true; } else { // Perform if the higher precedence operator exist. while (operators.length && operatorPriority[operators[operators.length - 1]] >= operatorPriority[item]) { applyOperator(values, operators); } operators.push(item); expectingValue = true; } continue; } let value; // Assign the proper value, starting from the most common types. if (isValue(item)) { value = item; } else if (isReference(item)) { value = table ? getReferenceValue(item, table) : NaN; } else if (isFunction(item)) { const result = processFunction(item, table); value = isValue(result) ? result : NaN; } else if (isFormula(item)) { value = processFormula(item, table); } if (typeof value !== 'undefined') { values.push(value); expectingValue = false; } else { return NaN; } } // Handle the remaining operators that weren't taken into consideration, due // to non-higher precedence. while (operators.length) { applyOperator(values, operators); } if (values.length !== 1) { return NaN; } return values[0]; } /** * Process a function on the given table. If the arguments do not contain * references or ranges, then no table has to be provided. * * @private * * @param {Highcharts.FormulaFunction} formulaFunction * Formula function to process. * * @param {Highcharts.DataTable} [table] * Table to use for references and ranges. * * @param {Highcharts.FormulaReference} [reference] * Table cell reference to use for relative references and ranges. * * @return {Highcharts.FormulaValue|Array<Highcharts.FormulaValue>} * Result value (or values) of the process. `NaN` indicates an error. */ function processFunction(formulaFunction, table, // eslint-disable-next-line @typescript-eslint/no-unused-vars reference // @todo ) { const processor = processorFunctions[formulaFunction.name]; if (processor) { try { return processor(formulaFunction.args, table); } catch { return NaN; } } const error = new Error(`Function "${formulaFunction.name}" not found.`); error.name = 'FormulaProcessError'; throw error; } /** * Registers a function for the FormulaProcessor. * * @param {string} name * Name of the function in spreadsheets notation with upper case. * * @param {Highcharts.FormulaFunction} processorFunction * ProcessorFunction for the FormulaProcessor. This is an object so that it * can take additional parameter for future validation routines. * * @return {boolean} * Return true, if the ProcessorFunction has been registered. */ function registerProcessorFunction(name, processorFunction) { return (processorFunctionNameRegExp.test(name) && !processorFunctions[name] && !!(processorFunctions[name] = processorFunction)); } /** * Translates relative references and ranges in-place. * * @param {Highcharts.Formula} formula * Formula to translate references and ranges in. * * @param {number} [columnDelta=0] * Column delta to translate to. Negative translate back. * * @param {number} [rowDelta=0] * Row delta to translate to. Negative numbers translate back. * * @return {Highcharts.Formula} * Formula with translated reference and ranges. This formula is equal to the * first argument. */ function translateReferences(formula, columnDelta = 0, rowDelta = 0) { for (let i = 0, iEnd = formula.length, item; i < iEnd; ++i) { item = formula[i]; if (Array.isArray(item)) { translateReferences(item, columnDelta, rowDelta); } else if (isFunction(item)) { translateReferences(item.args, columnDelta, rowDelta); } else if (isRange(item)) { if (item.beginColumnRelative) { item.beginColumn += columnDelta; } if (item.beginRowRelative) { item.beginRow += rowDelta; } if (item.endColumnRelative) { item.endColumn += columnDelta; } if (item.endRowRelative) { item.endRow += rowDelta; } } else if (isReference(item)) { if (item.columnRelative) { item.column += columnDelta; } if (item.rowRelative) { item.row += rowDelta; } } } return formula; } /* * * * Default Export * * */ const FormulaProcessor = { asNumber, getArgumentValue, getArgumentsValues, getRangeValues, getReferenceValue, processFormula, processorFunctions, registerProcessorFunction, translateReferences }; export default FormulaProcessor;