UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

227 lines (213 loc) 6.37 kB
import { REF_RANGE, REF_BEAM, REF_TERNARY, UNKNOWN, REF_STRUCT } from './constants.ts'; import { parseA1RefXlsx } from './parseA1Ref.ts'; import { parseStructRefXlsx } from './parseStructRef.ts'; import type { ReferenceA1Xlsx, ReferenceStructXlsx, Token, TokenEnhanced } from './types.ts'; function getIDer (): () => string { let i = 1; return () => 'fxg' + (i++); } function sameValue (a: unknown, b: unknown): boolean { if (a == null && b == null) { return true; } return a === b; } function sameArray (a: unknown[], b: unknown[]): boolean { 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: string, b: string): boolean { if (!a && !b) { return true; } return String(a).toLowerCase() === String(b).toLowerCase(); } function isEquivalent (refA, refB): boolean { // 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: string, workbookName: string) { if (!ref.sheetName) { ref.sheetName = sheetName; } if (!ref.workbookName) { ref.workbookName = workbookName; } return ref; } type RefWithGroupId = (ReferenceA1Xlsx | ReferenceStructXlsx) & { groupId: string }; class Reflist { refs: RefWithGroupId[]; uid: () => string; constructor (uid: () => string) { this.refs = []; this.uid = uid; } getGroupId (ref: ReferenceA1Xlsx | ReferenceStructXlsx): string { const known = this.refs.find(d => isEquivalent(d, ref)); if (known) { return known.groupId; } else { const r: RefWithGroupId = Object.assign(ref, { groupId: this.uid() }); this.refs.push(r); return r.groupId; } } } /** * 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 tokenlist An array of tokens (from `tokenize()`) * @param [context={}] A contest used to match `A1` to `Sheet1!A1`. * @param [context.sheetName=''] An implied sheet name ('Sheet1') * @param [context.workbookName=''] An implied workbook name ('report.xlsx') * @returns The input array with the enchanced tokens */ export function addTokenMeta (tokenlist: Token[], { sheetName = '', workbookName = '' } = {}): TokenEnhanced[] { const parenStack = []; let arrayStart = null; const uid = getIDer(); const knownRefs = new Reflist(uid); 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 ) { const ref = parseA1RefXlsx(token.value, { allowTernary: true }); if (ref && 'range' in ref) { addContext(ref, sheetName, workbookName); token.groupId = knownRefs.getGroupId(ref); } } else if (token.type === REF_STRUCT) { const ref = parseStructRefXlsx(token.value); if (ref) { addContext(ref, sheetName, workbookName); token.groupId = knownRefs.getGroupId(ref); } } else if (token.type === UNKNOWN) { token.error = true; } }); return tokenlist as TokenEnhanced[]; }