UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

124 lines (117 loc) 4.11 kB
import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT, REF_CELL } from './constants.ts'; import type { Token } from './types.ts'; const END = '$'; const validRunsMerge = [ [ REF_CELL, ':', REF_CELL ], [ REF_CELL, '.:', REF_CELL ], [ REF_CELL, ':.', REF_CELL ], [ REF_CELL, '.:.', REF_CELL ], [ REF_RANGE ], [ REF_BEAM ], [ REF_TERNARY ], [ CONTEXT, '!', REF_CELL, ':', REF_CELL ], [ CONTEXT, '!', REF_CELL, '.:', REF_CELL ], [ CONTEXT, '!', REF_CELL, ':.', REF_CELL ], [ CONTEXT, '!', REF_CELL, '.:.', REF_CELL ], [ CONTEXT, '!', REF_CELL ], [ CONTEXT, '!', REF_RANGE ], [ CONTEXT, '!', REF_BEAM ], [ CONTEXT, '!', REF_TERNARY ], [ CONTEXT_QUOTE, '!', REF_CELL, ':', REF_CELL ], [ CONTEXT_QUOTE, '!', REF_CELL, '.:', REF_CELL ], [ CONTEXT_QUOTE, '!', REF_CELL, ':.', REF_CELL ], [ CONTEXT_QUOTE, '!', REF_CELL, '.:.', REF_CELL ], [ CONTEXT_QUOTE, '!', REF_CELL ], [ CONTEXT_QUOTE, '!', REF_RANGE ], [ CONTEXT_QUOTE, '!', REF_BEAM ], [ CONTEXT_QUOTE, '!', REF_TERNARY ], [ REF_NAMED ], [ CONTEXT, '!', REF_NAMED ], [ CONTEXT_QUOTE, '!', REF_NAMED ], [ REF_STRUCT ], [ REF_NAMED, REF_STRUCT ], [ CONTEXT, '!', REF_NAMED, REF_STRUCT ], [ CONTEXT_QUOTE, '!', REF_NAMED, REF_STRUCT ] ]; type TypeNode = { [key: string]: TypeNode | boolean; }; // valid token runs are converted to a tree structure const refPartsTree: TypeNode = {}; function packList (f: string[], node: TypeNode) { if (f.length) { const key = f[0]; if (!node[key]) { node[key] = {}; } packList(f.slice(1), node[key] as TypeNode); } else { node[END] = true; } } validRunsMerge.forEach(run => packList(run.concat().reverse(), refPartsTree)); // attempt to match a backwards run of tokens from a given point // to a path in the tree const matcher = (tokens: Token[], currNode, anchorIndex, index = 0) => { let i = index; let node = currNode; const max = tokens.length - index; // keep walking as long as the next backward token matches a child key while (i <= max) { const token = tokens[anchorIndex - i]; if (token) { const value = token.value; let key = (token.type === OPERATOR) ? value : token.type; // we need to prevent merging ["A1:B2" ":" "C3"] as a range is only // allowed to contain a single ":" operator even if "A1:B2:C3" is // valid Excel syntax if (key === REF_RANGE && !value.includes(':')) { key = REF_CELL; } if (key in node) { node = node[key]; i += 1; continue; } } // can't advance further; accept only if current node is a terminal return node[END] ? i : 0; } }; /** * Merges context with reference tokens as possible in a list of tokens. * * When given a tokenlist, this function returns a new list with ranges returned * as whole references (`Sheet1!A1:B2`) rather than separate tokens for each * part: (`Sheet1`,`!`,`A1`,`:`,`B2`). * * @param tokenlist An array of tokens. * @returns A new list of tokens with range parts merged. */ export function mergeRefTokens (tokenlist: Token[]): Token[] { const finalTokens = []; // this seeks backwards because it's really the range part // that controls what can be joined. for (let i = tokenlist.length - 1; i >= 0; i--) { let token = tokenlist[i]; const type = token.type; // Quick check if token type could even start a valid run if (type === REF_RANGE || type === REF_BEAM || type === REF_TERNARY || type === REF_NAMED || type === REF_STRUCT) { const valid = matcher(tokenlist, refPartsTree, i); if (valid > 1) { token = { ...token, value: '' }; const start = i - valid + 1; for (let j = start; j <= i; j++) { token.value += tokenlist[j].value; } // adjust the offsets to include all the text if (token.loc && tokenlist[start].loc) { token.loc[0] = tokenlist[start].loc[0]; } i -= valid - 1; } } finalTokens[finalTokens.length] = token; } return finalTokens.reverse(); }