UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

173 lines (164 loc) 5.45 kB
import { stringifyR1C1RefXlsx } from './stringifyR1C1Ref.ts'; import { tokenizeXlsx } from './tokenize.ts'; import { parseA1Range } from './parseA1Range.ts'; import type { RangeR1C1, ReferenceA1Xlsx, Token } from './types.ts'; import { stringifyTokens } from './stringifyTokens.ts'; import { cloneToken } from './cloneToken.ts'; import { FUNCTION, OPERATOR, REF_BEAM, REF_RANGE, REF_TERNARY } from './constants.ts'; import { splitContext } from './parseRef.ts'; import { isRCTokenValue } from './isRCTokenValue.ts'; const reLetLambda = /^l(?:ambda|et)$/i; const calc = (abs: boolean, vX: number, aX: number): number => { if (vX == null) { return null; } return abs ? vX : vX - aX; }; // We already know here that we're holding a token value from // one of: REF_RANGE | REF_BEAM | REF_TERNARY // So we can quickly scan for ! shortcut a bunch of parsing: const unquote = d => d.slice(1, -1).replace(/''/g, "'"); function quickParseA1 (ref: string): ReferenceA1Xlsx { const split = ref.lastIndexOf('!'); const data: Partial<ReferenceA1Xlsx> = {}; if (split > -1) { if (ref.startsWith('\'')) { splitContext(unquote(ref.slice(0, split)), data, true); } else { splitContext(ref.slice(0, split), data, true); } data.range = parseA1Range(ref.slice(split + 1)); } else { data.range = parseA1Range(ref); } return data as ReferenceA1Xlsx; } /** * Options for {@link translateFormulaToR1C1}. */ export type OptsTranslateToR1C1 = { /** * Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. * These are supported by Google Sheets but not Excel. * See: [References.md](./References.md). * @defaultValue true */ allowTernary?: boolean, }; /** * Translates ranges in a list of tokens from absolute A1 syntax to relative R1C1 syntax. * * ```js * translateFormulaToR1C1("=SUM(E10,$E$2,Sheet!$E$3)", "D10"); * // => "=SUM(RC[1],R2C5,Sheet!R3C5)"); * ``` * * @param tokens A token list that should be adjusted. * @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`). * @returns A token list. */ export function translateTokensToR1C1 ( tokens: Token[], anchorCell: string ): Token[] { const anchorRange = parseA1Range(anchorCell); if (!anchorRange) { throw new Error('translateTokensToR1C1 got an invalid anchorCell: ' + anchorCell); } const { top, left } = anchorRange; let withinCall = 0; let parenDepth = 0; let offsetSkew = 0; const outTokens: Token[] = []; for (let token of tokens) { const tokenType = token?.type; if (tokenType === OPERATOR) { if (token.value === '(') { parenDepth++; const lastToken = outTokens[outTokens.length - 1]; if (lastToken && lastToken.type === FUNCTION) { if (reLetLambda.test(lastToken.value)) { withinCall = parenDepth; } } } else if (token.value === ')') { parenDepth--; if (parenDepth < withinCall) { withinCall = 0; } } } if (tokenType === REF_RANGE || tokenType === REF_BEAM || tokenType === REF_TERNARY) { token = cloneToken(token); const tokenValue = token.value; // We can get away with using the xlsx ref-parser here because it is more permissive: const ref = quickParseA1(tokenValue); if (ref) { const d = ref.range; const range: RangeR1C1 = {}; range.r0 = calc(d.$top, d.top, top); range.r1 = calc(d.$bottom, d.bottom, top); range.c0 = calc(d.$left, d.left, left); range.c1 = calc(d.$right, d.right, left); range.$r0 = d.$top; range.$r1 = d.$bottom; range.$c0 = d.$left; range.$c1 = d.$right; if (d.trim) { range.trim = d.trim; } // @ts-expect-error -- reusing the object, switching it to R1C1 by swapping the range ref.range = range; let val = stringifyR1C1RefXlsx(ref); if (isRCTokenValue(val) && withinCall) { val += '[0]'; } token.value = val; } // if token includes offsets, those offsets are now likely wrong! if (token.loc) { token.loc[0] += offsetSkew; offsetSkew += token.value.length - tokenValue.length; token.loc[1] += offsetSkew; } } else if (offsetSkew && token.loc) { token = cloneToken(token); token.loc[0] += offsetSkew; token.loc[1] += offsetSkew; } outTokens[outTokens.length] = token; } return outTokens; } /** * Translates ranges in a formula from absolute A1 syntax to relative R1C1 syntax. * * ```js * translateFormulaToR1C1("=SUM(E10,$E$2,Sheet!$E$3)", "D10"); * // => "=SUM(RC[1],R2C5,Sheet!R3C5)"); * ``` * * @see {@link OptsTranslateToR1C1} * @param formula An Excel formula that should be adjusted. * @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`). * @param [options={}] The options * @returns A formula string. */ export function translateFormulaToR1C1 ( formula: string, anchorCell: string, options: OptsTranslateToR1C1 = {} ): string { if (typeof formula === 'string') { const tokens = tokenizeXlsx(formula, { mergeRefs: false, allowTernary: options.allowTernary ?? true }); return stringifyTokens(translateTokensToR1C1(tokens, anchorCell)); } throw new Error('translateFormulaToA1 expects a formula string'); }