UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

207 lines (198 loc) 6.07 kB
import { MAX_COLS, MAX_ROWS } from './constants.ts'; import type { RangeA1 } from './types.ts'; type TrimString = 'both' | 'head' | 'tail'; export function fromRow (rowStr: string): number { return +rowStr - 1; } const CHAR_DOLLAR = 36; const CHAR_PERIOD = 46; const CHAR_COLON = 58; const CHAR_A_LC = 97; const CHAR_A_UC = 65; const CHAR_Z_LC = 122; const CHAR_Z_UC = 90; const CHAR_0 = 48; const CHAR_1 = 49; const CHAR_9 = 57; function advRangeOp (str: string, pos: number): [ number, TrimString | '' ] { const c0 = str.charCodeAt(pos); if (c0 === CHAR_PERIOD) { const c1 = str.charCodeAt(pos + 1); if (c1 === CHAR_COLON) { return str.charCodeAt(pos + 2) === CHAR_PERIOD ? [ 3, 'both' ] : [ 2, 'head' ]; } } else if (c0 === CHAR_COLON) { const c1 = str.charCodeAt(pos + 1); return c1 === CHAR_PERIOD ? [ 2, 'tail' ] : [ 1, '' ]; } return [ 0, '' ]; } function advA1Col (str: string, pos: number): [ number, number, boolean ] { // [A-Z]{1,3} const start = pos; const lock = str.charCodeAt(pos) === CHAR_DOLLAR; if (lock) { pos++; } const stop = pos + 3; let col = 0; do { const c = str.charCodeAt(pos); if (c >= CHAR_A_UC && c <= CHAR_Z_UC) { col = (26 * col) + c - (CHAR_A_UC - 1); pos++; } else if (c >= CHAR_A_LC && c <= CHAR_Z_LC) { col = (26 * col) + c - (CHAR_A_LC - 1); pos++; } else { break; } } while (pos < stop && pos < str.length); return (col && col <= MAX_COLS + 1) ? [ pos - start, col - 1, lock ] : [ 0, 0, false ]; } function advA1Row (str: string, pos: number): [number, number, boolean] { // [1-9][0-9]{0,6} const start = pos; const lock = str.charCodeAt(pos) === CHAR_DOLLAR; if (lock) { pos++; } const stop = pos + 7; let row = 0; let c = str.charCodeAt(pos); if (c >= CHAR_1 && c <= CHAR_9) { row = (row * 10) + c - CHAR_0; pos++; do { c = str.charCodeAt(pos); if (c >= CHAR_0 && c <= CHAR_9) { row = (row * 10) + c - CHAR_0; pos++; } else { break; } } while (pos < stop && pos < str.length); } return (row && row <= MAX_ROWS + 1) ? [ pos - start, row - 1, lock ] : [ 0, 0, false ]; } function makeRange ( top: number | null, $top: boolean | null, left: number | null, $left: boolean | null, bottom: number | null, $bottom: boolean | null, right: number | null, $right: boolean | null, trim: TrimString | '' ): RangeA1 { // flip left/right and top/bottom as needed // for partial ranges we perfer the coord on the left-side of the: if (right != null && (left == null || (left != null && right < left))) { [ left, right, $left, $right ] = [ right, left, $right, $left ]; } if (bottom != null && (top == null || (top != null && bottom < top))) { [ top, bottom, $top, $bottom ] = [ bottom, top, $bottom, $top ]; } const range: RangeA1 = { top, left, bottom, right, $top, $left, $bottom, $right }; if (trim) { range.trim = trim; } return range; } /** * Parse A1-style range string into a RangeA1 object. * * @param rangeString A1-style range string. * @param [allowTernary] Permit ternary ranges like A2:A or B2:2. * @return A reference object. */ export function parseA1Range (rangeString: string, allowTernary = true): RangeA1 | undefined { let p = 0; const [ leftChars, left, $left ] = advA1Col(rangeString, p); let right = 0; let $right = false; let bottom = 0; let $bottom = false; let rightChars: number; let bottomChars: number; if (leftChars) { // TLBR: could be A1:A1 // TL R: could be A1:A (if allowTernary) // TLB : could be A1:1 (if allowTernary) // LBR: could be A:A1 (if allowTernary) // L R: could be A:A p += leftChars; const [ topChars, top, $top ] = advA1Row(rangeString, p); p += topChars; const [ op, trim ] = advRangeOp(rangeString, p); if (op) { p += op; [ rightChars, right, $right ] = advA1Col(rangeString, p); p += rightChars; [ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p); p += bottomChars; if (topChars && bottomChars && rightChars) { if (p === rangeString.length) { return makeRange(top, $top, left, $left, bottom, $bottom, right, $right, trim); } } else if (!topChars && !bottomChars) { if (p === rangeString.length) { return makeRange(null, false, left, $left, null, false, right, $right, trim); } } else if (allowTernary && (bottomChars || rightChars) && p === rangeString.length) { if (!topChars) { return makeRange(null, false, left, $left, bottom, $bottom, right, $right, trim); } else if (!bottomChars) { return makeRange(top, $top, left, $left, null, false, right, $right, trim); } else { return makeRange(top, $top, left, $left, bottom, $bottom, null, false, trim); } } } // LT : this is A1 if (topChars && p === rangeString.length) { return makeRange(top, $top, left, $left, top, $top, left, $left, trim); } } else { // T B : could be 1:1 // T BR: could be 1:A1 (if allowTernary) const [ topChars, top, $top ] = advA1Row(rangeString, p); if (topChars) { p += topChars; const [ op, trim ] = advRangeOp(rangeString, p); if (op) { p += op; [ rightChars, right, $right ] = advA1Col(rangeString, p); p += rightChars; [ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p); p += bottomChars; if (rightChars && bottomChars && allowTernary) { if (p === rangeString.length) { return makeRange(top, $top, null, false, bottom, $bottom, right, $right, trim); } } else if (!rightChars && bottomChars) { if (p === rangeString.length) { return makeRange(top, $top, null, false, bottom, $bottom, null, false, trim); } } } } } }