@borgar/fx
Version:
Utilities for working with Excel formulas
241 lines (229 loc) • 5.53 kB
text/typescript
import { isWS } from './lexers/lexWhitespace.ts';
const AT = 64; // @
const BR_CLOSE = 93; // ]
const BR_OPEN = 91; // [
const COLON = 58; // :
const COMMA = 44; // ,
const HASH = 35; // #
const QUOT_SINGLE = 39; // '
const keyTerms = {
'headers': 1,
'data': 2,
'totals': 4,
'all': 8,
'this row': 16,
'@': 16
};
// only combinations allowed are: #data + (#headers | #totals | #data)
const fz = (...a: string[]) => Object.freeze(a);
const sectionMap = {
// no terms
0: fz(),
// single term
1: fz('headers'),
2: fz('data'),
4: fz('totals'),
8: fz('all'),
16: fz('this row'),
// headers+data
3: fz('headers', 'data'),
// totals+data
6: fz('data', 'totals')
};
function matchKeyword (str: string, pos: number): number {
let p = pos;
if (str.charCodeAt(p++) !== BR_OPEN) {
return;
}
if (str.charCodeAt(p++) !== HASH) {
return;
}
do {
const c = str.charCodeAt(p);
if (
(c >= 65 && c <= 90) || // A-Z
(c >= 97 && c <= 122) || // a-z
(c === 32) // space
) {
p++;
}
else {
break;
}
}
while (p < pos + 11); // max length: '[#this row'
if (str.charCodeAt(p++) !== BR_CLOSE) {
return;
}
return p - pos;
}
function skipWhitespace (str: string, pos: number): number {
let p = pos;
while (isWS(str.charCodeAt(p))) { p++; }
return p - pos;
}
function matchColumn (str: string, pos: number, allowUnbraced = true): [ string, string ] {
let p = pos;
let column = '';
if (str.charCodeAt(p) === BR_OPEN) {
p++;
let c;
do {
c = str.charCodeAt(p);
if (c === QUOT_SINGLE) {
p++;
c = str.charCodeAt(p);
// Allowed set: '#@[]
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE) {
column += String.fromCharCode(c);
p++;
}
else {
return;
}
}
// Allowed set is all chars BUT: '#@[]
else if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN) {
return;
}
else if (c === BR_CLOSE) {
p++;
return [ str.slice(pos, p), column ];
}
else {
column += String.fromCharCode(c);
p++;
}
}
while (p < str.length);
}
else if (allowUnbraced) {
let c;
do {
c = str.charCodeAt(p);
// Allowed set is all chars BUT: '#@[]:
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE || c === COLON) {
break;
}
else {
column += String.fromCharCode(c);
p++;
}
}
while (p < str.length);
if (p !== pos) {
return [ column, column ];
}
}
}
export type SRange = {
columns: string[],
sections: string[],
length: number,
token: string
};
export function parseSRange (str: string, pos: number = 0): SRange {
const columns: string[] = [];
const start = pos;
let m;
let terms = 0;
// structured refs start with a [
if (str.charCodeAt(pos) !== BR_OPEN) {
return;
}
// simple keyword: [#keyword]
if ((m = matchKeyword(str, pos))) {
const k = str.slice(pos + 2, pos + m - 1);
pos += m;
const term = keyTerms[k.toLowerCase()];
if (!term) { return; }
terms |= term;
}
// simple column: [column]
else if ((m = matchColumn(str, pos, false))) {
pos += m[0].length;
if (m[1]) {
columns.push(m[1]);
}
}
// use the "normal" method
// [[#keyword]]
// [[column]]
// [@]
// [@column]
// [@[column]]
// [@column:column]
// [@column:[column]]
// [@[column]:column]
// [@[column]:[column]]
// [column:column]
// [column:[column]]
// [[column]:column]
// [[column]:[column]]
// [[#keyword],column]
// [[#keyword],column:column]
// [[#keyword],[#keyword],column:column]
// ...
else {
let expect_more = true;
pos++; // skip open brace
pos += skipWhitespace(str, pos);
// match keywords as we find them
while (expect_more && (m = matchKeyword(str, pos))) {
const k = str.slice(pos + 2, pos + m - 1);
const term = keyTerms[k.toLowerCase()];
if (!term) { return; }
terms |= term;
pos += m;
pos += skipWhitespace(str, pos);
expect_more = str.charCodeAt(pos) === COMMA;
if (expect_more) {
pos++;
pos += skipWhitespace(str, pos);
}
}
// is there an @ specifier?
if (expect_more && (str.charCodeAt(pos) === AT)) {
terms |= keyTerms['@'];
pos += 1;
expect_more = str.charCodeAt(pos) !== BR_CLOSE;
}
// not all keyword terms may be combined
if (!sectionMap[terms]) {
return;
}
// column definitions
const leftCol = expect_more && matchColumn(str, pos, true);
if (leftCol) {
pos += leftCol[0].length;
columns.push(leftCol[1]);
if (str.charCodeAt(pos) === COLON) {
pos++;
const rightCol = matchColumn(str, pos, true);
if (rightCol) {
pos += rightCol[0].length;
columns.push(rightCol[1]);
}
else {
return;
}
}
expect_more = false;
}
// advance ws
pos += skipWhitespace(str, pos);
// close the ref
if (expect_more || str.charCodeAt(pos) !== BR_CLOSE) {
return;
}
// step over the closing ]
pos++;
}
const sections: string[] = sectionMap[terms];
return {
columns,
sections: sections ? sections.concat() : sections,
length: pos - start,
token: str.slice(start, pos)
};
}