UNPKG

@entestat/formula

Version:

fast excel formula parser

376 lines (343 loc) 13.7 kB
const FormulaError = require('../error'); const {FormulaHelpers, Types, WildCard, Address} = require('../helpers'); const Collection = require('../../grammar/type/collection'); const H = FormulaHelpers; const ReferenceFunctions = { ADDRESS: (rowNumber, columnNumber, absNum, a1, sheetText) => { rowNumber = H.accept(rowNumber, Types.NUMBER); columnNumber = H.accept(columnNumber, Types.NUMBER); absNum = H.accept(absNum, Types.NUMBER, 1); a1 = H.accept(a1, Types.BOOLEAN, true); sheetText = H.accept(sheetText, Types.STRING, ''); if (rowNumber < 1 || columnNumber < 1 || absNum < 1 || absNum > 4) throw FormulaError.VALUE; let result = ''; if (sheetText.length > 0) { if (/[^A-Za-z_.\d\u007F-\uFFFF]/.test(sheetText)) { result += `'${sheetText}'!`; } else { result += sheetText + '!'; } } if (a1) { // A1 style result += (absNum === 1 || absNum === 3) ? '$' : ''; result += Address.columnNumberToName(columnNumber); result += (absNum === 1 || absNum === 2) ? '$' : ''; result += rowNumber; } else { // R1C1 style result += 'R'; result += (absNum === 4 || absNum === 3) ? `[${rowNumber}]` : rowNumber; result += 'C'; result += (absNum === 4 || absNum === 2) ? `[${columnNumber}]` : columnNumber; } return result; }, AREAS: refs => { refs = H.accept(refs); if (refs instanceof Collection) { return refs.length; } return 1; }, CHOOSE: (indexNum, ...values) => { }, // Special COLUMN: (context, obj) => { if (obj == null) { if (context.position.col != null) return context.position.col; else throw Error('FormulaParser.parse is called without position parameter.') } else { if (typeof obj !== 'object' || Array.isArray(obj)) throw FormulaError.VALUE; if (H.isCellRef(obj)) { return obj.ref.col; } else if (H.isRangeRef(obj)) { return obj.ref.from.col; } else { throw Error('ReferenceFunctions.COLUMN should not reach here.') } } }, // Special COLUMNS: (context, obj) => { if (obj == null) { throw Error('COLUMNS requires one argument'); } if (typeof obj != 'object' || Array.isArray(obj)) throw FormulaError.VALUE; if (H.isCellRef(obj)) { return 1; } else if (H.isRangeRef(obj)) { return Math.abs(obj.ref.from.col - obj.ref.to.col) + 1; } else { throw Error('ReferenceFunctions.COLUMNS should not reach here.') } }, HLOOKUP: (lookupValue, tableArray, rowIndexNum, rangeLookup) => { // preserve type of lookupValue lookupValue = H.accept(lookupValue); try { tableArray = H.accept(tableArray, Types.ARRAY, undefined, false); } catch (e) { // catch #VALUE! and throw #N/A if (e instanceof FormulaError) throw FormulaError.NA; throw e; } rowIndexNum = H.accept(rowIndexNum, Types.NUMBER); rangeLookup = H.accept(rangeLookup, Types.BOOLEAN, true); // check if rowIndexNum out of bound if (rowIndexNum < 1) throw FormulaError.VALUE; if (tableArray[rowIndexNum - 1] === undefined) throw FormulaError.REF; const lookupType = typeof lookupValue; // 'number', 'string', 'boolean' // approximate lookup (assume the array is sorted) if (rangeLookup) { let prevValue = lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null; for (let i = 1; i < tableArray[0].length; i++) { const currValue = tableArray[0][i]; const type = typeof currValue; // skip the value if type does not match if (type !== lookupType) continue; // if the previous two values are greater than lookup value, throw #N/A if (prevValue > lookupValue && currValue > lookupValue) { throw FormulaError.NA; } if (currValue === lookupValue) return tableArray[rowIndexNum - 1][i]; // if previous value <= lookup value and current value > lookup value if (prevValue != null && currValue > lookupValue && prevValue <= lookupValue) { return tableArray[rowIndexNum - 1][i - 1]; } prevValue = currValue; } if (prevValue == null) throw FormulaError.NA; if (tableArray[0].length === 1) { return tableArray[rowIndexNum - 1][0] } return prevValue; } // exact lookup with wildcard support else { let index = -1; if (WildCard.isWildCard(lookupValue)) { index = tableArray[0].findIndex(item => { return WildCard.toRegex(lookupValue, 'i').test(item); }); } else { index = tableArray[0].findIndex(item => { return item === lookupValue; }); } // the exact match is not found if (index === -1) throw FormulaError.NA; return tableArray[rowIndexNum - 1][index]; } }, // Special INDEX: (context, ranges, rowNum, colNum, areaNum) => { // retrieve values rowNum = context.utils.extractRefValue(rowNum); rowNum = {value: rowNum.val, isArray: rowNum.isArray}; rowNum = H.accept(rowNum, Types.NUMBER); rowNum = Math.trunc(rowNum); if (colNum == null) { colNum = 1; } else { colNum = context.utils.extractRefValue(colNum); colNum = {value: colNum.val, isArray: colNum.isArray}; colNum = H.accept(colNum, Types.NUMBER, 1); colNum = Math.trunc(colNum); } if (areaNum == null) { areaNum = 1; } else { areaNum = context.utils.extractRefValue(areaNum); areaNum = {value: areaNum.val, isArray: areaNum.isArray}; areaNum = H.accept(areaNum, Types.NUMBER, 1); areaNum = Math.trunc(areaNum); } // get the range area that we want to index // ranges can be cell ref, range ref or array constant let range = ranges; // many ranges (Reference form) if (ranges instanceof Collection) { range = ranges.refs[areaNum - 1]; } else if (areaNum > 1) { throw FormulaError.REF; } if (rowNum === 0 && colNum === 0) { return range; } // query the whole column if (rowNum === 0) { if (H.isRangeRef(range)) { if (range.ref.to.col - range.ref.from.col < colNum - 1) throw FormulaError.REF; range.ref.from.col += colNum - 1; range.ref.to.col = range.ref.from.col; return range; } else if (Array.isArray(range)) { const res = []; range.forEach(row => res.push([row[colNum - 1]])); return res; } } // query the whole row if (colNum === 0) { if (H.isRangeRef(range)) { if (range.ref.to.row - range.ref.from.row < rowNum - 1) throw FormulaError.REF; range.ref.from.row += rowNum - 1; range.ref.to.row = range.ref.from.row; return range; } else if (Array.isArray(range)) { return range[colNum - 1]; } } // query single cell if (rowNum !== 0 && colNum !== 0) { // range reference if (H.isRangeRef(range)) { range = range.ref; if (range.to.row - range.from.row < rowNum - 1 || range.to.col - range.from.col < colNum - 1) throw FormulaError.REF; return {ref: {row: range.from.row + rowNum - 1, col: range.from.col + colNum - 1}}; } // cell reference else if (H.isCellRef(range)) { range = range.ref; if (rowNum > 1 || colNum > 1) throw FormulaError.REF; return {ref: {row: range.row + rowNum - 1, col: range.col + colNum - 1}}; } // array constant else if (Array.isArray(range)) { if (range.length < rowNum || range[0].length < colNum) throw FormulaError.REF; return range[rowNum - 1][colNum - 1]; } } }, MATCH: () => { }, // Special ROW: (context, obj) => { if (obj == null) { if (context.position.row != null) return context.position.row; else throw Error('FormulaParser.parse is called without position parameter.') } else { if (typeof obj !== 'object' || Array.isArray(obj)) throw FormulaError.VALUE; if (H.isCellRef(obj)) { return obj.ref.row; } else if (H.isRangeRef(obj)) { return obj.ref.from.row; } else { throw Error('ReferenceFunctions.ROW should not reach here.') } } }, // Special ROWS: (context, obj) => { if (obj == null) { throw Error('ROWS requires one argument'); } if (typeof obj != 'object' || Array.isArray(obj)) throw FormulaError.VALUE; if (H.isCellRef(obj)) { return 1; } else if (H.isRangeRef(obj)) { return Math.abs(obj.ref.from.row - obj.ref.to.row) + 1; } else { throw Error('ReferenceFunctions.ROWS should not reach here.') } }, TRANSPOSE: (array) => { array = H.accept(array, Types.ARRAY, undefined, false); // https://github.com/numbers/numbers.js/blob/master/lib/numbers/matrix.js#L171 const result = []; for (let i = 0; i < array[0].length; i++) { result[i] = []; for (let j = 0; j < array.length; j++) { result[i][j] = array[j][i]; } } return result; }, VLOOKUP: (lookupValue, tableArray, colIndexNum, rangeLookup) => { // preserve type of lookupValue lookupValue = H.accept(lookupValue); try { tableArray = H.accept(tableArray, Types.ARRAY, undefined, false); } catch (e) { // catch #VALUE! and throw #N/A if (e instanceof FormulaError) throw FormulaError.NA; throw e; } colIndexNum = H.accept(colIndexNum, Types.NUMBER); rangeLookup = H.accept(rangeLookup, Types.BOOLEAN, true); // check if colIndexNum out of bound if (colIndexNum < 1) throw FormulaError.VALUE; if (tableArray[0][colIndexNum - 1] === undefined) throw FormulaError.REF; const lookupType = typeof lookupValue; // 'number', 'string', 'boolean' // approximate lookup (assume the array is sorted) if (rangeLookup) { let prevValue = lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null; for (let i = 1; i < tableArray.length; i++) { const currRow = tableArray[i]; const currValue = tableArray[i][0]; const type = typeof currValue; // skip the value if type does not match if (type !== lookupType) continue; // if the previous two values are greater than lookup value, throw #N/A if (prevValue > lookupValue && currValue > lookupValue) { throw FormulaError.NA; } if (currValue === lookupValue) return currRow[colIndexNum - 1]; // if previous value <= lookup value and current value > lookup value if (prevValue != null && currValue > lookupValue && prevValue <= lookupValue) { return tableArray[i - 1][colIndexNum - 1]; } prevValue = currValue; } if (prevValue == null) throw FormulaError.NA; if (tableArray.length === 1) { return tableArray[0][colIndexNum - 1] } return prevValue; } // exact lookup with wildcard support else { let index = -1; if (WildCard.isWildCard(lookupValue)) { index = tableArray.findIndex(currRow => { return WildCard.toRegex(lookupValue, 'i').test(currRow[0]); }); } else { index = tableArray.findIndex(currRow => { return currRow[0] === lookupValue; }); } // the exact match is not found if (index === -1) throw FormulaError.NA; return tableArray[index][colIndexNum - 1]; } }, }; module.exports = ReferenceFunctions;