@borgar/fx
Version:
Utilities for working with Excel formulas
207 lines (198 loc) • 6.07 kB
text/typescript
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);
}
}
}
}
}
}