@borgar/fx
Version:
Utilities for working with Excel formulas
148 lines (141 loc) • 3.53 kB
text/typescript
import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.ts';
import type { Token } from '../types.ts';
import { advRangeOp } from './advRangeOp.ts';
import { canEndRange } from './canEndRange.ts';
const BR_OPEN = 91; // [
const BR_CLOSE = 93; // ]
const UC_R = 82;
const LC_R = 114;
const UC_C = 67;
const LC_C = 99;
const PLUS = 43;
const MINUS = 45;
const EXCL = 33;
// C
// C\[[+-]?\d+\]
// C[1-9][0-9]{0,4}
// R
// R\[[+-]?\d+\]
// R[1-9][0-9]{0,6}
function lexR1C1Part (str: string, pos: number, isRow: boolean = false): number {
const start = pos;
const c0 = str.charCodeAt(pos);
if ((isRow ? c0 === UC_R || c0 === LC_R : c0 === UC_C || c0 === LC_C)) {
pos++;
let digits = 0;
let value = 0;
let stop = str.length;
const c1 = str.charCodeAt(pos);
let c;
let sign = 1;
const relative = c1 === BR_OPEN;
if (relative) {
stop = Math.min(stop, pos + (isRow ? 8 : 6));
pos++;
// allow +-
c = str.charCodeAt(pos);
if (c === PLUS || c === MINUS) {
pos++;
stop++;
sign = c === MINUS ? -1 : 1;
}
}
else if (c1 < 49 || c1 > 57 || isNaN(c1)) {
// char must be 1-9, or part is either just "R" or "C"
return 1;
}
do {
const c = str.charCodeAt(pos);
if (c >= 48 && c <= 57) { // 0-9
value = value * 10 + c - 48;
digits++;
pos++;
}
else {
break;
}
}
while (pos < stop);
const MAX = isRow ? MAX_ROWS : MAX_COLS;
if (relative) {
const c = str.charCodeAt(pos);
if (c !== BR_CLOSE) {
return 0;
}
// isRow: next char must not be a number!
pos++;
value *= sign;
return (digits && (-MAX <= value) && (value <= MAX))
? pos - start
: 0;
}
// isRow: next char must not be a number!
return (digits && value <= (MAX + 1)) ? pos - start : 0;
}
return 0;
}
export function lexRangeR1C1 (
str: string,
pos: number,
options: { allowTernary: boolean }
): Token | undefined {
let p = pos;
// C1
// C1:C1
// C1:R1C1 --partial
// R1
// R1:R1
// R1:R1C1 --partial
// R1C1
// R1C1:C1 --partial
// R1C1:R1 --partial
const r1 = lexR1C1Part(str, p, true);
p += r1;
const c1 = lexR1C1Part(str, p);
p += c1;
if ((c1 || r1) && str.charCodeAt(p) !== EXCL) {
const op = advRangeOp(str, p);
const preOp = p;
if (op) {
p += op;
const r2 = lexR1C1Part(str, p, true); // R1
p += r2;
const c2 = lexR1C1Part(str, p); // C1
p += c2;
// C1:R2C2 --partial
// R1:R2C2 --partial
// R1C1:C2 --partial
// R1C1:R2 --partial
if (
(r1 && !c1 && r2 && c2) ||
(!r1 && c1 && r2 && c2) ||
(r1 && c1 && r2 && !c2) ||
(r1 && c1 && !r2 && c2)
) {
if (options.allowTernary && canEndRange(str, p)) {
return { type: REF_TERNARY, value: str.slice(pos, p) };
}
}
// C1:C2 -- beam
// R1:R2 -- beam
else if (
(c1 && c2 && !r1 && !r2) ||
(!c1 && !c2 && r1 && r2)
) {
if (canEndRange(str, p)) {
return { type: REF_BEAM, value: str.slice(pos, p) };
}
}
// Note: we do not capture R1C1:R1C1, mergeRefTokens will join the parts
}
// R1
// C1
// R1C1
if (canEndRange(str, preOp)) {
return {
type: (r1 && c1) ? REF_RANGE : REF_BEAM,
value: str.slice(pos, preOp)
};
}
}
}