@borgar/fx
Version:
Utilities for working with Excel formulas
124 lines (117 loc) • 4.11 kB
text/typescript
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();
}