UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

207 lines (197 loc) 5.74 kB
import { REF_RANGE, REF_BEAM, REF_TERNARY, UNKNOWN, REF_STRUCT } from './constants.js'; import { parseA1Ref } from './a1.js'; import { parseStructRef } from './sr.js'; function getIDer () { let i = 1; return () => 'fxg' + (i++); } function sameValue (a, b) { if (a == null && b == null) { return true; } return a === b; } function sameArray (a, b) { if ((Array.isArray(a) !== Array.isArray(b)) || a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (!sameValue(a[i], b[i])) { return false; } } return true; } function sameStr (a, b) { if (!a && !b) { return true; } return String(a).toLowerCase() === String(b).toLowerCase(); } function isEquivalent (refA, refB) { // if named, name must match if ((refA.name || refB.name) && refA.name !== refB.name) { return false; } // if structured if ((refA.columns || refB.columns)) { if (refA.table !== refB.table) { return false; } if (!sameArray(refA.columns, refB.columns)) { return false; } if (!sameArray(refA.sections, refB.sections)) { return false; } } // if ranged, range must have the same dimensions (we don't care about $) if (refA.range || refB.range) { if ( !sameValue(refA.range.top, refB.range.top) || !sameValue(refA.range.bottom, refB.range.bottom) || !sameValue(refA.range.left, refB.range.left) || !sameValue(refA.range.right, refB.range.right) ) { return false; } } // must have same context if ( !sameStr(refA.workbookName, refB.workbookName) || !sameStr(refA.sheetName, refB.sheetName) ) { return false; } return true; } function addContext (ref, sheetName, workbookName) { if (!ref.sheetName) { ref.sheetName = sheetName; } if (!ref.workbookName) { ref.workbookName = workbookName; } return ref; } /** * Runs through a list of tokens and adds extra attributes such as matching * parens and ranges. * * The `context` parameter defines default reference attributes: * `{ workbookName: 'report.xlsx', sheetName: 'Sheet1' }`. * If supplied, these are used to match `A1` to `Sheet1!A1`. * * All tokens will be tagged with a `.depth` number value to indicating the * level of nesting in parentheses as well as an `.index` number indicating * their zero based position in the list. * * The returned output will be the same array of tokens but the following * properties will added to tokens (as applicable): * * #### Parentheses ( ) * * Matching parens will be tagged with `.groupId` string identifier as well as * a `.depth` number value (indicating the level of nesting). * * Closing parens without a counterpart will be tagged with `.error` * (boolean true). * * #### Curly brackets { } * * Matching curly brackets will be tagged with `.groupId` string identifier. * These may not be nested in Excel. * * Closing curly brackets without a counterpart will be tagged with `.error` * (boolean `true`). * * #### Ranges (`REF_RANGE` or `REF_BEAM` type tokens) * * All ranges will be tagged with `.groupId` string identifier regardless of * the number of times they occur. * * #### Tokens of type `UNKNOWN` * * All will be tagged with `.error` (boolean `true`). * * @param {Array<Token>} tokenlist An array of tokens (from `tokenize()`) * @param {object} [context={}] A contest used to match `A1` to `Sheet1!A1`. * @param {string} [context.sheetName=''] An implied sheet name ('Sheet1') * @param {string} [context.workbookName=''] An implied workbook name ('report.xlsx') * @returns {Array<TokenEnhanced>} The input array with the enchanced tokens */ export function addTokenMeta (tokenlist, { sheetName = '', workbookName = '' } = {}) { const parenStack = []; let arrayStart = null; const uid = getIDer(); const knownRefs = []; const getCurrDepth = () => parenStack.length + (arrayStart ? 1 : 0); tokenlist.forEach((token, i) => { token.index = i; token.depth = getCurrDepth(); if (token.value === '(') { parenStack.push(token); token.depth = getCurrDepth(); } else if (token.value === ')') { const counter = parenStack.pop(); if (counter) { const pairId = uid(); token.groupId = pairId; token.depth = counter.depth; counter.groupId = pairId; } else { token.error = true; } } else if (token.value === '{') { if (!arrayStart) { arrayStart = token; token.depth = getCurrDepth(); } else { token.error = true; } } else if (token.value === '}') { if (arrayStart) { const pairId = uid(); token.groupId = pairId; token.depth = arrayStart.depth; arrayStart.groupId = pairId; } else { token.error = true; } arrayStart = null; } else if ( token.type === REF_RANGE || token.type === REF_BEAM || token.type === REF_TERNARY || token.type === REF_STRUCT ) { const ref = (token.type === REF_STRUCT) ? parseStructRef(token.value, { xlsx: true }) : parseA1Ref(token.value, { allowTernary: true, xlsx: true }); if (ref && (ref.range || ref.columns)) { ref.source = token.value; addContext(ref, sheetName, workbookName); const known = knownRefs.find(d => isEquivalent(d, ref)); if (known) { token.groupId = known.groupId; } else { ref.groupId = uid(); token.groupId = ref.groupId; knownRefs.push(ref); } } } else if (token.type === UNKNOWN) { token.error = true; } }); return tokenlist; }