highcharts
Version:
JavaScript charting framework
478 lines (477 loc) • 13.9 kB
JavaScript
/* *
*
* (c) 2009-2025 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* Authors:
* - Sophie Bremer
*
* */
'use strict';
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 columnNames = table
.getColumnNames()
.slice(range.beginColumn, range.endColumn + 1), values = [];
for (let i = 0, iEnd = columnNames.length, cell; i < iEnd; ++i) {
const cells = table.getColumn(columnNames[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.modified) {
// Look in the modified table for formula result
cell = table.modified.getCell(columnNames[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 columnName = table.getColumnNames()[reference.column];
if (columnName) {
const cell = table.getCell(columnName, reference.row);
if (typeof cell === 'string' &&
cell[0] === '=' &&
table !== table.modified) {
// Look in the modified table for formula result
const result = table.modified.getCell(columnName, reference.row);
return isValue(result) ? result : NaN;
}
return isValue(cell) ? cell : NaN;
}
return NaN;
}
/**
* Processes a formula array on the given table. If the formula does not contain
* references or ranges, then no table has to be provided.
*
* @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) {
let x;
for (let i = 0, iEnd = formula.length, item, operator, result, y; i < iEnd; ++i) {
item = formula[i];
// Remember operator for operation on next item
if (isOperator(item)) {
operator = item;
continue;
}
// Next item is a value
if (isValue(item)) {
y = item;
// Next item is a formula and needs to get processed first
}
else if (isFormula(item)) {
y = processFormula(formula, table);
// Next item is a function call and needs to get processed first
}
else if (isFunction(item)) {
result = processFunction(item, table);
y = (isValue(result) ? result : NaN); // Arrays are not allowed here
// Next item is a reference and needs to get resolved
}
else if (isReference(item)) {
y = (table && getReferenceValue(item, table));
}
// If we have a next value, lets do the operation
if (typeof y !== 'undefined') {
// Next value is our first value
if (typeof x === 'undefined') {
if (operator) {
x = basicOperation(operator, 0, y);
}
else {
x = y;
}
// Fail fast if no operator available
}
else if (!operator) {
return NaN;
// Regular next value
}
else {
const operator2 = formula[i + 1];
if (isOperator(operator2) &&
operatorPriority[operator2] > operatorPriority[operator]) {
y = basicOperation(operator2, y, processFormula(formula.slice(i + 2)));
i = iEnd;
}
x = basicOperation(operator, x, y);
}
operator = void 0;
y = void 0;
}
}
return isValue(x) ? x : NaN;
}
/**
* 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 (item instanceof Array) {
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;