UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

342 lines (334 loc) 9.26 kB
/* ** RC notation works differently from A1 in that we can't merge static ** references joined by `:`. Merging can only work between references ** that are relative/absolute on the same axes, so: ** - R1C1:R2C2 will work, ** - R[1]C1:R[2]C2 will also work, but ** - R[1]C[1]:R2C2 doesn't have a direct rectangle represention without context. */ import { rangeOperator, trimDirection } from './a1.js'; import { MAX_ROWS, MAX_COLS } from './constants.js'; import { parseRef } from './parseRef.js'; import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js'; const clamp = (min, val, max) => Math.min(Math.max(val, min), max); function toCoord (value, isAbs) { if (isAbs) { return String(value + 1); } return value ? '[' + value + ']' : ''; } /** * Stringify a range object into R1C1 syntax. * * @ignore * @private * @see parseR1C1Ref * @param {RangeR1C1} range A range object * @returns {string} An R1C1-style string represenation of a range */ export function toR1C1 (range) { let { r0, c0, r1, c1 } = range; const { $c0, $c1, $r0, $r1 } = range; const nullR0 = r0 == null; const nullC0 = c0 == null; let nullR1 = r1 == null; let nullC1 = c1 == null; const op = rangeOperator(range.trim); const hasTrim = !!range.trim; r0 = clamp($r0 ? 0 : -MAX_ROWS, r0 | 0, MAX_ROWS); c0 = clamp($c0 ? 0 : -MAX_COLS, c0 | 0, MAX_COLS); if (!nullR0 && nullR1 && !nullC0 && nullC1) { r1 = r0; nullR1 = false; c1 = c0; nullC1 = false; } else { r1 = clamp($r1 ? 0 : -MAX_ROWS, r1 | 0, MAX_ROWS); c1 = clamp($c1 ? 0 : -MAX_COLS, c1 | 0, MAX_COLS); } // C:C const allRows = r0 === 0 && r1 >= MAX_ROWS; if ((allRows && !nullC0 && !nullC1) || (nullR0 && nullR1)) { const a = toCoord(c0, $c0); const b = toCoord(c1, $c1); return 'C' + (a === b && !hasTrim ? a : a + op + 'C' + b); } // R:R const allCols = c0 === 0 && c1 >= MAX_COLS; if ((allCols && !nullR0 && !nullR1) || (nullC0 && nullC1)) { const a = toCoord(r0, $r0); const b = toCoord(r1, $r1); return 'R' + (a === b && !hasTrim ? a : a + op + 'R' + b); } const s_r0 = toCoord(r0, $r0); const s_r1 = toCoord(r1, $r1); const s_c0 = toCoord(c0, $c0); const s_c1 = toCoord(c1, $c1); // RC:R, RC:C if (nullR0 || nullR1 || nullC0 || nullC1) { return ( (nullR0 ? '' : 'R' + s_r0) + (nullC0 ? '' : 'C' + s_c0) + op + (nullR1 ? '' : 'R' + s_r1) + (nullC1 ? '' : 'C' + s_c1) ); } // RC:RC if (s_r0 !== s_r1 || s_c0 !== s_c1) { return 'R' + s_r0 + 'C' + s_c0 + op + 'R' + s_r1 + 'C' + s_c1; } // RC return 'R' + s_r0 + 'C' + s_c0; } function parseR1C1Part (ref) { let r0 = null; let c0 = null; let $r0 = null; let $c0 = null; // R part const rm = /^R(?:\[([+-]?\d+)\]|(\d+))?/.exec(ref); if (rm) { if (rm[1]) { r0 = parseInt(rm[1], 10); $r0 = false; } else if (rm[2]) { r0 = parseInt(rm[2], 10) - 1; $r0 = true; } else { r0 = 0; $r0 = false; } ref = ref.slice(rm[0].length); } // C part const cm = /^C(?:\[([+-]?\d+)\]|(\d+))?/.exec(ref); if (cm) { if (cm[1]) { c0 = parseInt(cm[1], 10); $c0 = false; } else if (cm[2]) { c0 = parseInt(cm[2], 10) - 1; $c0 = true; } else { c0 = 0; $c0 = false; } ref = ref.slice(cm[0].length); } // must have at least one part (and nothing more) if ((!rm && !cm) || ref.length) { return null; } return [ r0, c0, $r0, $c0 ]; } /** * Parse a simple string reference to an R1C1 range into a range object. * * @private * @ignore * @see parseA1Ref * @param {string} rangeString A range string * @returns {(RangeR1C1|null)} An object representing a valid reference or null if it is invalid. */ export function fromR1C1 (rangeString) { let final = null; const [ part1, op, part2, overflow ] = rangeString.split(/(\.?:\.?)/); if (overflow) { return null; } const range = parseR1C1Part(part1); const trim = trimDirection(!!op && op[0] === '.', !!op && op[op.length - 1] === '.'); if (range) { const [ r0, c0, $r0, $c0 ] = range; if (part2) { const extendTo = parseR1C1Part(part2); if (extendTo) { final = {}; const [ r1, c1, $r1, $c1 ] = extendTo; // rows if (r0 != null && r1 != null) { final.r0 = $r0 === $r1 ? Math.min(r0, r1) : r0; final.$r0 = $r0; final.r1 = $r0 === $r1 ? Math.max(r0, r1) : r1; final.$r1 = $r1; } else if (r0 != null && r1 == null) { // partial RC:C final.r0 = r0; final.$r0 = $r0; final.r1 = null; final.$r1 = $r0; } else if (r0 == null && r1 != null) { // partial C:RC final.r0 = r1; final.$r0 = $r1; final.r1 = null; final.$r1 = $r1; } else if (r0 == null && r1 == null) { // C:C final.r0 = null; final.$r0 = false; final.r1 = null; final.$r1 = false; } // columns if (c0 != null && c1 != null) { final.c0 = $c0 === $c1 ? Math.min(c0, c1) : c0; final.$c0 = $c0; final.c1 = $c0 === $c1 ? Math.max(c0, c1) : c1; final.$c1 = $c1; } else if (c0 != null && c1 == null) { final.c0 = c0; final.$c0 = $c0; final.c1 = null; final.$c1 = $c0; } else if (c0 == null && c1 != null) { final.c0 = c1; final.$c0 = $c1; final.c1 = null; final.$c1 = $c1; } else if (c0 == null && c1 == null) { final.c0 = null; final.$c0 = false; final.c1 = null; final.$c1 = false; } } else { return null; } } // range only - no second part else if (r0 != null && c0 == null) { final = { r0: r0, c0: null, r1: r0, c1: null, $r0: $r0, $c0: false, $r1: $r0, $c1: false }; } else if (r0 == null && c0 != null) { final = { r0: null, c0: c0, r1: null, c1: c0, $r0: false, $c0: $c0, $r1: false, $c1: $c0 }; } else { final = { r0: r0 || 0, c0: c0 || 0, r1: r0 || 0, c1: c0 || 0, $r0: $r0 || false, $c0: $c0 || false, $r1: $r0 || false, $c1: $c0 || false }; } } if (final && trim) { final.trim = trim; } return final; } /** * Parse a string reference into an object representing it. * * ```js * parseR1C1Ref('Sheet1!R[9]C9:R[9]C9'); * // => { * // context: [ 'Sheet1' ], * // range: { * // r0: 9, * // c0: 8, * // r1: 9, * // c1: 8, * // $c0: true, * // $c1: true * // $r0: false, * // $r1: false * // } * // } * ``` * * @param {string} refString An R1C1-style reference string * @param {object} [options={}] Options * @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges. * @param {boolean} [options.allowTernary=false] Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md. * @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md) * @returns {(ReferenceR1C1|null)} An object representing a valid reference or null if it is invalid. */ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false, xlsx = false } = {}) { const d = parseRef(refString, { allowNamed, allowTernary, xlsx, r1c1: true }); if (d && (d.r0 || d.name)) { const range = d.r1 ? fromR1C1(d.r0 + d.operator + d.r1) : fromR1C1(d.r0); if (range) { return xlsx ? { workbookName: d.workbookName, sheetName: d.sheetName, range } : { context: d.context, range }; } if (d.name) { return xlsx ? { workbookName: d.workbookName, sheetName: d.sheetName, name: d.name } : { context: d.context, name: d.name }; } return null; } return null; } /** * Get an R1C1-style string representation of a reference object. * * ```js * stringifyR1C1Ref({ * context: [ 'Sheet1' ], * range: { * r0: 9, * c0: 8, * r1: 9, * c1: 8, * $c0: true, * $c1: true * $r0: false, * $r1: false * } * }); * // => 'Sheet1!R[9]C9:R[9]C9' * ``` * * @param {ReferenceR1C1} refObject A reference object * @param {object} [options={}] Options * @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md) * @returns {string} The reference in R1C1-style string format */ export function stringifyR1C1Ref (refObject, { xlsx = false } = {}) { const prefix = xlsx ? stringifyPrefixAlt(refObject) : stringifyPrefix(refObject); return prefix + ( refObject.name ? refObject.name : toR1C1(refObject.range) ); }