UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

241 lines (229 loc) 5.53 kB
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) }; }