UNPKG

@entestat/formula

Version:

fast excel formula parser

748 lines (660 loc) 24.8 kB
const FormulaError = require('../error'); const {FormulaHelpers, Types, Factorials, Criteria} = require('../helpers'); const {Infix} = require('../operators'); const H = FormulaHelpers; // Max number in excel is 2^1024-1, same as javascript, thus I will not check if number is valid in some functions. // factorials const f = [], fd = []; function factorial(n) { if (n <= 100) return Factorials[n]; if (f[n] > 0) return f[n]; return f[n] = factorial(n - 1) * n; } function factorialDouble(n) { if (n === 1 || n === 0) return 1; if (n === 2) return 2; if (fd[n] > 0) return fd[n]; return fd[n] = factorialDouble(n - 2) * n; } // https://support.office.com/en-us/article/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb const MathFunctions = { ABS: number => { number = H.accept(number, Types.NUMBER); return Math.abs(number); }, AGGREGATE: (functionNum, options, ref1, ...refs) => { // functionNum = H.accept(functionNum, Types.NUMBER); // throw FormulaError.NOT_IMPLEMENTED('AGGREGATE'); }, ARABIC: text => { text = H.accept(text, Types.STRING).toUpperCase(); // Credits: Rafa? Kukawski if (!/^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/.test(text)) { throw new FormulaError('#VALUE!', 'Invalid roman numeral in ARABIC evaluation.'); } let r = 0; text.replace(/[MDLV]|C[MD]?|X[CL]?|I[XV]?/g, function (i) { r += { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 }[i]; }); return r; }, BASE: (number, radix, minLength) => { number = H.accept(number, Types.NUMBER); if (number < 0 || number >= 2 ** 53) throw FormulaError.NUM; radix = H.accept(radix, Types.NUMBER); if (radix < 2 || radix > 36) throw FormulaError.NUM; minLength = H.accept(minLength, Types.NUMBER, 0); if (minLength < 0) { throw FormulaError.NUM; } const result = number.toString(radix).toUpperCase(); return new Array(Math.max(minLength + 1 - result.length, 0)).join('0') + result; }, CEILING: (number, significance) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER); if (significance === 0) return 0; if (number / significance % 1 === 0) return number; const absSignificance = Math.abs(significance); const times = Math.floor(Math.abs(number) / absSignificance); if (number < 0) { // round down, away from zero const roundDown = significance < 0; return roundDown ? -absSignificance * (times + 1) : -absSignificance * (times); } else { return (times + 1) * absSignificance; } }, 'CEILING.MATH': (number, significance, mode) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER, number > 0 ? 1 : -1); // mode can be any number mode = H.accept(mode, Types.NUMBER, 0); // The Mode argument does not affect positive numbers. if (number >= 0) { return MathFunctions.CEILING(number, significance); } // if round down, away from zero, then significance const offset = mode ? significance : 0; return MathFunctions.CEILING(number, significance) - offset; }, 'CEILING.PRECISE': (number, significance) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER, 1); // always round up return MathFunctions.CEILING(number, Math.abs(significance)); }, COMBIN: (number, numberChosen) => { number = H.accept(number, Types.NUMBER); numberChosen = H.accept(numberChosen, Types.NUMBER); if (number < 0 || numberChosen < 0 || number < numberChosen) throw FormulaError.NUM; const nFactorial = MathFunctions.FACT(number), kFactorial = MathFunctions.FACT(numberChosen); return nFactorial / kFactorial / MathFunctions.FACT(number - numberChosen); }, COMBINA: (number, numberChosen) => { number = H.accept(number, Types.NUMBER); numberChosen = H.accept(numberChosen, Types.NUMBER); // special case if ((number === 0 || number === 1) && numberChosen === 0) return 1; if (number < 0 || numberChosen < 0) throw FormulaError.NUM; return MathFunctions.COMBIN(number + numberChosen - 1, number - 1); }, DECIMAL: (text, radix) => { text = H.accept(text, Types.STRING); radix = H.accept(radix, Types.NUMBER); radix = Math.trunc(radix); if (radix < 2 || radix > 36) throw FormulaError.NUM; const res = parseInt(text, radix); if (isNaN(res)) throw FormulaError.NUM; return res; }, DEGREES: (radians) => { radians = H.accept(radians, Types.NUMBER); return radians * (180 / Math.PI); }, EVEN: (number) => { return MathFunctions.CEILING(number, -2); }, EXP: (number) => { number = H.accept(number, Types.NUMBER); return Math.exp(number) }, FACT: (number) => { number = H.accept(number, Types.NUMBER); number = Math.trunc(number); // max number = 170 if (number > 170 || number < 0) throw FormulaError.NUM; if (number <= 100) return Factorials[number]; return factorial(number); }, FACTDOUBLE: (number) => { number = H.accept(number, Types.NUMBER); number = Math.trunc(number); // max number = 170 if (number < -1) throw FormulaError.NUM; if (number === -1) return 1; return factorialDouble(number); }, FLOOR: (number, significance) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER); if (significance === 0) return 0; if (number > 0 && significance < 0) throw FormulaError.NUM; if (number / significance % 1 === 0) return number; const absSignificance = Math.abs(significance); const times = Math.floor(Math.abs(number) / absSignificance); if (number < 0) { // round down, away from zero const roundDown = significance < 0; return roundDown ? -absSignificance * times : -absSignificance * (times + 1); } else { // toward zero return times * absSignificance; } }, 'FLOOR.MATH': (number, significance, mode) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER, number > 0 ? 1 : -1); // mode can be 0 or any other number, 0 means away from zero // the official documentation seems wrong mode = H.accept(mode, Types.NUMBER, 0); // The Mode argument does not affect positive numbers. if (mode === 0 || number >= 0) { // away from zero return MathFunctions.FLOOR(number, Math.abs(significance)); } // towards zero, add a significance return MathFunctions.FLOOR(number, significance) + significance; }, 'FLOOR.PRECISE': (number, significance) => { number = H.accept(number, Types.NUMBER); significance = H.accept(significance, Types.NUMBER, 1); // always round up return MathFunctions.FLOOR(number, Math.abs(significance)); }, GCD: (...params) => { const arr = []; H.flattenParams(params, null, false, (param) => { // allow array, range ref param = typeof param === 'boolean' ? NaN : Number(param); if (!isNaN(param)) { if (param < 0 || param > 9007199254740990) // 2^53 throw FormulaError.NUM; arr.push(Math.trunc(param)) } else throw FormulaError.VALUE; }, 0); // http://rosettacode.org/wiki/Greatest_common_divisor#JavaScript let i, y, n = params.length, x = Math.abs(arr[0]); for (i = 1; i < n; i++) { y = Math.abs(arr[i]); while (x && y) { (x > y) ? x %= y : y %= x; } x += y; } return x; }, INT: (number) => { number = H.accept(number, Types.NUMBER); return Math.floor(number); }, 'ISO.CEILING': (...params) => { return MathFunctions['CEILING.PRECISE'](...params); }, LCM: (...params) => { const arr = []; // always parse string to number if possible H.flattenParams(params, null, false, param => { param = typeof param === 'boolean' ? NaN : Number(param); if (!isNaN(param)) { if (param < 0 || param > 9007199254740990) // 2^53 throw FormulaError.NUM; arr.push(Math.trunc(param)) } // throw value error if can't parse to string else throw FormulaError.VALUE; }, 1); // http://rosettacode.org/wiki/Least_common_multiple#JavaScript let n = arr.length, a = Math.abs(arr[0]); for (let i = 1; i < n; i++) { let b = Math.abs(arr[i]), c = a; while (a && b) { a > b ? a %= b : b %= a; } a = Math.abs(c * arr[i]) / (a + b); } return a; }, LN: number => { number = H.accept(number, Types.NUMBER); return Math.log(number); }, LOG: (number, base) => { number = H.accept(number, Types.NUMBER); base = H.accept(base, Types.NUMBER, 10); return Math.log(number) / Math.log(base); }, LOG10: number => { number = H.accept(number, Types.NUMBER); return Math.log10(number); }, MDETERM: (array) => { array = H.accept(array, Types.ARRAY, undefined, false, true); if (array[0].length !== array.length) throw FormulaError.VALUE; // adopted from https://github.com/numbers/numbers.js/blob/master/lib/numbers/matrix.js#L261 const numRow = array.length, numCol = array[0].length; let det = 0, diagLeft, diagRight; if (numRow === 1) { return array[0][0]; } else if (numRow === 2) { return array[0][0] * array[1][1] - array[0][1] * array[1][0]; } for (let col = 0; col < numCol; col++) { diagLeft = array[0][col]; diagRight = array[0][col]; for (let row = 1; row < numRow; row++) { diagRight *= array[row][(((col + row) % numCol) + numCol) % numCol]; diagLeft *= array[row][(((col - row) % numCol) + numCol) % numCol]; } det += diagRight - diagLeft; } return det; }, MINVERSE: (array) => { // TODO // array = H.accept(array, Types.ARRAY, null, false); // if (array[0].length !== array.length) // throw FormulaError.VALUE; // throw FormulaError.NOT_IMPLEMENTED('MINVERSE'); }, MMULT: (array1, array2) => { array1 = H.accept(array1, Types.ARRAY, undefined, false, true); array2 = H.accept(array2, Types.ARRAY, undefined, false, true); const aNumRows = array1.length, aNumCols = array1[0].length, bNumRows = array2.length, bNumCols = array2[0].length, m = new Array(aNumRows); // initialize array of rows if (aNumCols !== bNumRows) throw FormulaError.VALUE; for (let r = 0; r < aNumRows; r++) { m[r] = new Array(bNumCols); // initialize the current row for (let c = 0; c < bNumCols; c++) { m[r][c] = 0; // initialize the current cell for (let i = 0; i < aNumCols; i++) { const v1 = array1[r][i], v2 = array2[i][c]; if (typeof v1 !== "number" || typeof v2 !== "number") throw FormulaError.VALUE; m[r][c] += array1[r][i] * array2[i][c]; } } } return m; }, MOD: (number, divisor) => { number = H.accept(number, Types.NUMBER); divisor = H.accept(divisor, Types.NUMBER); if (divisor === 0) throw FormulaError.DIV0; return number - divisor * MathFunctions.INT(number / divisor); }, MROUND: (number, multiple) => { number = H.accept(number, Types.NUMBER); multiple = H.accept(multiple, Types.NUMBER); if (multiple === 0) return 0; if (number > 0 && multiple < 0 || number < 0 && multiple > 0) throw FormulaError.NUM; if (number / multiple % 1 === 0) return number; return Math.round(number / multiple) * multiple; }, MULTINOMIAL: (...numbers) => { let numerator = 0, denominator = 1; H.flattenParams(numbers, Types.NUMBER, false, number => { if (number < 0) throw FormulaError.NUM; numerator += number; denominator *= factorial(number); }); return factorial(numerator) / denominator; }, MUNIT: (dimension) => { dimension = H.accept(dimension, Types.NUMBER); const matrix = []; for (let row = 0; row < dimension; row++) { const rowArr = []; for (let col = 0; col < dimension; col++) { if (row === col) rowArr.push(1); else rowArr.push(0); } matrix.push(rowArr); } return matrix; }, ODD: (number) => { number = H.accept(number, Types.NUMBER); if (number === 0) return 1; let temp = Math.ceil(Math.abs(number)); temp = (temp & 1) ? temp : temp + 1; return (number > 0) ? temp : -temp; }, PI: () => { return Math.PI; }, POWER: (number, power) => { number = H.accept(number, Types.NUMBER); power = H.accept(power, Types.NUMBER); return number ** power; }, PRODUCT: (...numbers) => { let product = 1; H.flattenParams(numbers, null, true, (number, info) => { const parsedNumber = Number(number); if (info.isLiteral && !isNaN(parsedNumber)) { product *= parsedNumber; } else { if (typeof number === "number") product *= number; } }, 1); return product; }, QUOTIENT: (numerator, denominator) => { numerator = H.accept(numerator, Types.NUMBER); denominator = H.accept(denominator, Types.NUMBER); return Math.trunc(numerator / denominator); }, RADIANS: (degrees) => { degrees = H.accept(degrees, Types.NUMBER); return degrees / 180 * Math.PI; }, RAND: () => { return Math.random(); }, RANDBETWEEN: (bottom, top) => { bottom = H.accept(bottom, Types.NUMBER); top = H.accept(top, Types.NUMBER); return Math.floor(Math.random() * (top - bottom + 1) + bottom); }, ROMAN: (number, form) => { number = H.accept(number, Types.NUMBER); form = H.accept(form, Types.NUMBER, 0); if (form !== 0) throw Error('ROMAN: only allows form=0 (classic form).'); // The MIT License // Copyright (c) 2008 Steven Levithan const digits = String(number).split(''); const key = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX']; let roman = '', i = 3; while (i--) { roman = (key[+digits.pop() + (i * 10)] || '') + roman; } return new Array(+digits.join('') + 1).join('M') + roman; }, ROUND: (number, digits) => { number = H.accept(number, Types.NUMBER); digits = H.accept(digits, Types.NUMBER); const multiplier = Math.pow(10, Math.abs(digits)); const sign = number > 0 ? 1 : -1; if (digits > 0) { return sign * Math.round(Math.abs(number) * multiplier) / multiplier; } else if (digits === 0) { return sign * Math.round(Math.abs(number)); } else { return sign * Math.round(Math.abs(number) / multiplier) * multiplier; } }, ROUNDDOWN: (number, digits) => { number = H.accept(number, Types.NUMBER); digits = H.accept(digits, Types.NUMBER); const multiplier = Math.pow(10, Math.abs(digits)); const sign = number > 0 ? 1 : -1; if (digits > 0) { const offset = 1 / multiplier * 0.5; return sign * Math.round((Math.abs(number) - offset) * multiplier) / multiplier; } else if (digits === 0) { const offset = 0.5; return sign * Math.round((Math.abs(number) - offset)); } else { const offset = multiplier * 0.5; return sign * Math.round((Math.abs(number) - offset) / multiplier) * multiplier; } }, ROUNDUP: (number, digits) => { number = H.accept(number, Types.NUMBER); digits = H.accept(digits, Types.NUMBER); const multiplier = Math.pow(10, Math.abs(digits)); const sign = number > 0 ? 1 : -1; if (digits > 0) { const offset = 1 / multiplier * 0.5; return sign * Math.round((Math.abs(number) + offset) * multiplier) / multiplier; } else if (digits === 0) { const offset = 0.5; return sign * Math.round((Math.abs(number) + offset)); } else { const offset = multiplier * 0.5; return sign * Math.round((Math.abs(number) + offset) / multiplier) * multiplier; } }, SERIESSUM: (x, n, m, coefficients) => { x = H.accept(x, Types.NUMBER); n = H.accept(n, Types.NUMBER); m = H.accept(m, Types.NUMBER); let i = 0, result; H.flattenParams([coefficients], Types.NUMBER, false, (coefficient) => { if (typeof coefficient !== "number") { throw FormulaError.VALUE; } if (i === 0) { result = coefficient * Math.pow(x, n); } else { result += coefficient * Math.pow(x, n + i * m); } i++; }); return result; }, SIGN: number => { number = H.accept(number, Types.NUMBER); return number > 0 ? 1 : number === 0 ? 0 : -1; }, SQRT: number => { number = H.accept(number, Types.NUMBER); if (number < 0) throw FormulaError.NUM; return Math.sqrt(number); }, SQRTPI: number => { number = H.accept(number, Types.NUMBER); if (number < 0) throw FormulaError.NUM; return Math.sqrt(number * Math.PI); }, SUBTOTAL: () => { // TODO: Finish this after statistical functions are implemented. }, SUM: (...params) => { // parse string to number only when it is a literal. (not a reference) let result = 0; H.flattenParams(params, Types.NUMBER, true, (item, info) => { // literal will be parsed to given type (Type.NUMBER) if (info.isLiteral) { result += item; } else { if (typeof item === "number") result += item; } }); return result }, /** * This functions requires instance of {@link FormulaParser}. */ SUMIF: (context, range, criteria, sumRange) => { const ranges = H.retrieveRanges(context, range, sumRange); range = ranges[0]; sumRange = ranges[1]; criteria = H.retrieveArg(context, criteria); const isCriteriaArray = criteria.isArray; // parse criteria criteria = Criteria.parse(H.accept(criteria)); let sum = 0; range.forEach((row, rowNum) => { row.forEach((value, colNum) => { const valueToAdd = sumRange[rowNum][colNum]; if (typeof valueToAdd !== "number") return; // wildcard if (criteria.op === 'wc') { if (criteria.match === criteria.value.test(value)) { sum += valueToAdd; } } else if (Infix.compareOp(value, criteria.op, criteria.value, Array.isArray(value), isCriteriaArray)) { sum += valueToAdd; } }) }); return sum; }, SUMIFS: () => { }, SUMPRODUCT: (array1, ...arrays) => { array1 = H.accept(array1, Types.ARRAY, undefined, false, true); arrays.forEach(array => { array = H.accept(array, Types.ARRAY, undefined, false, true); if (array1[0].length !== array[0].length || array1.length !== array.length) throw FormulaError.VALUE; for (let i = 0; i < array1.length; i++) { for (let j = 0; j < array1[0].length; j++) { if (typeof array1[i][j] !== "number") array1[i][j] = 0; if (typeof array[i][j] !== "number") array[i][j] = 0; array1[i][j] *= array[i][j]; } } }); let result = 0; array1.forEach(row => { row.forEach(value => { result += value; }) }); return result; }, SUMSQ: (...params) => { // parse string to number only when it is a literal. (not a reference) let result = 0; H.flattenParams(params, Types.NUMBER, true, (item, info) => { // literal will be parsed to given type (Type.NUMBER) if (info.isLiteral) { result += item ** 2; } else { if (typeof item === "number") result += item ** 2; } }); return result }, SUMX2MY2: (arrayX, arrayY) => { const x = [], y = []; let sum = 0; H.flattenParams([arrayX], null, false, (item, info) => { x.push(item); }); H.flattenParams([arrayY], null, false, (item, info) => { y.push(item); }); if (x.length !== y.length) throw FormulaError.NA; for (let i = 0; i < x.length; i++) { if (typeof x[i] === "number" && typeof y[i] === "number") sum += x[i] ** 2 - y[i] ** 2 } return sum; }, SUMX2PY2: (arrayX, arrayY) => { const x = [], y = []; let sum = 0; H.flattenParams([arrayX], null, false, (item, info) => { x.push(item); }); H.flattenParams([arrayY], null, false, (item, info) => { y.push(item); }); if (x.length !== y.length) throw FormulaError.NA; for (let i = 0; i < x.length; i++) { if (typeof x[i] === "number" && typeof y[i] === "number") sum += x[i] ** 2 + y[i] ** 2 } return sum; }, SUMXMY2: (arrayX, arrayY) => { const x = [], y = []; let sum = 0; H.flattenParams([arrayX], null, false, (item, info) => { x.push(item); }); H.flattenParams([arrayY], null, false, (item, info) => { y.push(item); }); if (x.length !== y.length) throw FormulaError.NA; for (let i = 0; i < x.length; i++) { if (typeof x[i] === "number" && typeof y[i] === "number") sum += (x[i] - y[i]) ** 2; } return sum; }, TRUNC: (number) => { number = H.accept(number, Types.NUMBER); return Math.trunc(number); }, }; module.exports = MathFunctions;