UNPKG

@flighter/a1-notation

Version:

Work with A1 notation like "A1" or "A1:B2"

743 lines (737 loc) 20.8 kB
/** * @file Contains converters from string to number and vice versa */ /** * Converts column letter to number * @author AdamL * @see https://stackoverflow.com/questions/21229180/convert-column-index-into-corresponding-column-letter * @param {string} col * * @returns {number} */ const colStringToNumber1 = (col) => { const length = col.length; let column = 0; for (let i = 0; i < length; i++) column += (col.charCodeAt(i) - 64) * Math.pow(26, length - i - 1); return column; }; /** * Converts column letter to number * @author Flambino * @see https://codereview.stackexchange.com/questions/90112/a1notation-conversion-to-row-column-index * @param {string} col * * @returns {number} */ const colStringToNumber2 = (col) => { let i, l, chr, sum = 0, A = 'A'.charCodeAt(0), radix = 'Z'.charCodeAt(0) - A + 1; for (i = 0, l = col.length; i < l; i++) { chr = col.charCodeAt(i); sum = sum * radix + chr - A + 1; } return sum; }; /** * Converts column number to letter * @author AdamL * @see https://stackoverflow.com/questions/21229180/convert-column-index-into-corresponding-column-letter * @param {number} col * * @returns {string} */ const colNumberToString = (col) => { let letter = '', temp; while (col > 0) { temp = (col - 1) % 26; letter = String.fromCharCode(temp + 65) + letter; col = (col - temp - 1) / 26; } return letter; }; /** * Converts row string to number * @param {string} row * * @returns {number} */ const rowStringToNumber = (row) => parseInt(row, 10); /** * Converts row number to string * @param {number} row * * @returns {string} */ const rowNumberToString = (row) => String(row); /** * @file Contains secondary functions */ /** * Returns the type of a value * @param {unknown} some * * @returns {string} */ const type = (some) => typeof some; /** * Checks if a value is a string * @param {unknown} some * * @returns {boolean} */ const isString = (some) => type(some) === 'string'; /** * Checks if a value is a number * @param {unknown} some * * @returns {boolean} */ const isNumber = (some) => type(some) === 'number' && Number.isInteger(some); /** * Checks if a value is a positive number * @param {unknown} some * * @returns {boolean} */ const isPositiveNumber = (some) => isNumber(some) && some > 0; /** * Checks if a value is a stringified number > 0 like "1", "2", ... * @param {unknown} some * * @returns {boolean} */ const isStringifiedNumber = (some) => isString(some) && /^[0-9]+$/.test(some) && isPositiveNumber(+some); /** * Checks if a value is a letter between a-zA-Z * @param {unknown} some * * @returns {boolean} */ const isLetter = (some) => isString(some) && /^[a-z]+$/i.test(some); /** * Checks validation of A1 notation * @param {unknown} some * * @returns {boolean} */ const isValidA1 = (some) => isString(some) && /^[A-Z]+\d+(:[A-Z]+\d+)?$/i.test(some); /** * @fileOverview A1 notation errors */ class A1Error extends Error { constructor(something) { const str = JSON.stringify(something); super(str); this.name = 'A1Error'; this.message = str; } /** * Was string */ s() { this.message = `Invalid A1 notation: ${this.message}`; return this; } /** * Was number */ n() { this.message = `Invalid A1 number(s): ${this.message}`; return this; } /** * Was unknown */ u() { this.message = `Invalid A1 argument(s): ${this.message}`; return this; } } /** * @file Contains enums */ var Axis; (function (Axis) { Axis["X"] = "col"; Axis["Y"] = "row"; })(Axis || (Axis = {})); /** * @file Math operations and converting in A1 notation * Supports A1 notation like "A1" and "A1:B2" * @author FLighter */ class A1 { // Regular expression for parsing static _reg = /^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/; /** * Example: A1:B2 */ _colStart = 0; // A -> 1 _rowStart = 0; // 1 -> 1 _colEnd = 0; // B -> 2 _rowEnd = 0; // 2 -> 2 _converter = 1; // converter 1 | 2 /** * Parses A1 notation * @param {string} a1 * @param {1 | 2} converter * * @return {object} {cs: number, rs: number, ce: number, re: number} */ static _parse(a1, converter) { let [, cs, // col start // A rs, // row start // 1 ce, // col end // B re, // row end // 2 ] = a1.toUpperCase().match(this._reg) ?? []; ce = ce || cs; re = re || rs; const colStart = this._A1Col(cs, converter), colEnd = this._A1Col(ce, converter), rowStart = rowStringToNumber(rs), rowEnd = rowStringToNumber(re); // For non-standard A1 return { cs: colEnd > colStart ? colStart : colEnd, rs: rowEnd > rowStart ? rowStart : rowEnd, ce: colEnd > colStart ? colEnd : colStart, re: rowEnd > rowStart ? rowEnd : rowStart, }; } /** * Converts column letter to number using converter 1 or 2 * @param {string} a1 * @param {1 | 2} converter * * @return {number} */ static _A1Col(a1, converter) { return converter === 1 ? colStringToNumber1(a1) : colStringToNumber2(a1); } /****************** * STATIC METHODS ******************/ /** * Checks A1 notation * @param {string} a1 * * @return {boolean} */ static isValid(a1) { return isValidA1(a1); } /** * Converts the first column letter from A1 to number * @param {string} a1 * @param {1 | 2} [converter = 1] * * @return {number} */ static getCol(a1, converter = 1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); return this._parse(a1, converter).cs; } /** * Converts the last column letter from A1 to number * @param {string} a1 * @param {1 | 2} [converter = 1] * * @return {number} */ static getLastCol(a1, converter = 1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); return this._parse(a1, converter).ce; } /** * Converts number to column letter in A1 * @param {number} col * * @return {string} */ static toCol(col) { if (!isPositiveNumber(col)) throw new A1Error(col).n(); return colNumberToString(col); } /** * Converts the first row string to number * @param {string} a1 * * @return {number} */ static getRow(a1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); return this._parse(a1, 1).rs; } /** * Converts the last row string to number * @param {string} a1 * * @return {number} */ static getLastRow(a1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); return this._parse(a1, 1).re; } /** * Converts number to row string in A1 * @param {number} row * * @return {string} */ static toRow(row) { if (!isPositiveNumber(row)) throw new A1Error(row).n(); return rowNumberToString(row); } /** * @param {string} a1 * @param {1 | 2} [converter = 1] * * @return {number} columns count */ static getWidth(a1, converter = 1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); let { ce, cs } = this._parse(a1, converter); return ce - cs + 1; } /** * @param {string} a1 * * @return {number} rows count */ static getHeight(a1) { if (!isValidA1(a1)) throw new A1Error(a1).s(); let { re, rs } = this._parse(a1, 1); return re - rs + 1; } /*************** * CONSTRUCTOR ***************/ /** * It handles case: * constructor(object: options) * @param {options} options */ _initObject(options) { const { a1Start, a1End, colStart, colEnd, rowStart, rowEnd, nCols, nRows, converter, } = options; // Set converter this._converter = converter === 2 ? 2 : 1; let cs = 0; let ce = 0; let rs = 0; let re = 0; const getValue = (some, canBeLetter = true) => { if (isPositiveNumber(some) || isStringifiedNumber(some)) return +some; if (canBeLetter && isLetter(some)) return A1._A1Col(some, this._converter); return 0; }; /** * Define start range */ // From a1Start if (isValidA1(a1Start)) { const a1StartParsed = A1._parse(a1Start, this._converter); cs = a1StartParsed.cs; rs = a1StartParsed.rs; const equalCol = a1StartParsed.cs === a1StartParsed.ce, equalRow = a1StartParsed.rs === a1StartParsed.re, equal = equalCol && equalRow; if (!equal || (equal && a1Start.includes(':'))) { ce = a1StartParsed.ce; re = a1StartParsed.re; } } // From colStart & rowStart if (!cs && colStart) { cs = getValue(colStart); } if (!rs && rowStart) { rs = getValue(rowStart, false); } /** * Define end range */ // From a1End if (!ce && !re && isValidA1(a1End)) { const a1EndParsed = A1._parse(a1End, this._converter); ce = a1EndParsed.ce; re = a1EndParsed.re; } // From colEnd & rowEnd if (!ce && colEnd) ce = getValue(colEnd); if (!re && rowEnd) re = getValue(rowEnd, false); // From nCols & nRows if (!ce && cs && isPositiveNumber(nCols)) ce = cs + nCols - 1; if (!re && rs && isPositiveNumber(nRows)) re = rs + nRows - 1; /** * If only start/end range was defined */ (cs && !ce) && (ce = cs); (!cs && ce) && (cs = ce); (rs && !re) && (re = rs); (!rs && re) && (rs = re); /** * Check results */ if (!cs || !rs || !ce || !re) throw new A1Error(options).u(); /** * Set ranges */ this._colStart = cs; this._rowStart = rs; this._colEnd = ce; this._rowEnd = re; } /** * It handles cases: * constructor(col: number, row: number) * constructor(col: number, row: number, nRows: number) * constructor(col: number, row: number, nRows: number, nCols: number) * @param {number[]} args */ _initNumber(...args) { let [col, row, nRows, nCols] = args; nRows = nRows || 1; nCols = nCols || 1; let all = [col, row, nRows, nCols]; if (!all.every(n => isPositiveNumber(n))) throw new A1Error(all.join(', ')).n(); this._colStart = col; // the first col this._rowStart = row; // the first row this._colEnd = col + nCols - 1; // how many cols in total (cols length) this._rowEnd = row + nRows - 1; // how many rows in total (rows length) } /** * It handles cases: * constructor(range: string) * constructor(rangeStart: string, rangeEnd: string) * @param {string[]} args */ _initString(...args) { const [rangeStart, rangeEnd] = args; const range = rangeEnd ? `${rangeStart}:${rangeEnd}` // rangeStart: string, rangeEnd: string : rangeStart; // range: string if (!isValidA1(range)) throw new A1Error(range).s(); const { cs, rs, ce, re } = A1._parse(range, this._converter); this._colStart = cs; this._rowStart = rs; this._colEnd = ce; this._rowEnd = re; } constructor(something, something2, nRows, nCols) { // No arguments if (!arguments.length) throw new A1Error().u(); // Object if (something && type(something) === 'object') this._initObject(something); // Number else if (isNumber(something)) this._initNumber.apply(this, arguments); // String else if (isString(something)) this._initString.apply(this, arguments); // Unknown argument else throw new A1Error(something).u(); } /*********** * METHODS ***********/ /** * @return {string} in A1 notation */ get() { const start = colNumberToString(this._colStart) + rowNumberToString(this._rowStart), end = colNumberToString(this._colEnd) + rowNumberToString(this._rowEnd); return start === end ? start : `${start}:${end}`; } /** * @return {string} in A1 notation */ toString() { return this.get(); } /** * @typedef {Object} Result * @property {number} colStart * @property {number} rowStart * @property {number} colEnd * @property {number} rowEnd * @property {string} a1 * @property {number} rowsCount * @property {number} colsCount * * @return {Result} full information about the range */ toJSON() { return { colStart: this._colStart, rowStart: this._rowStart, colEnd: this._colEnd, rowEnd: this._rowEnd, a1: this.get(), rowsCount: this._rowEnd - this._rowStart + 1, colsCount: this._colEnd - this._colStart + 1, }; } /** * @return {number} start column */ getCol() { return this._colStart; } /** * @return {number} end column */ getLastCol() { return this._colEnd; } /** * @return {number} start row */ getRow() { return this._rowStart; } /** * @return {number} end row */ getLastRow() { return this._rowEnd; } /** * @return {number} columns count */ getWidth() { return this._colEnd - this._colStart + 1; } /** * @return {number} rows count */ getHeight() { return this._rowEnd - this._rowStart + 1; } /** * @return {A1} copy of this object */ copy() { return new A1(this.get()); } /** * Sets a value to the start column * @param {string | number} val * * @returns {this} */ setCol(val) { return this._setFields(val, '_colStart', Axis.X); } /** * Sets a value to the end column * @param {string | number} val * * @returns {this} */ setLastCol(val) { return this._setFields(val, '_colEnd', Axis.X); } /** * Sets a value to the start row * @param {string | number} val * * @returns {this} */ setRow(val) { return this._setFields(val, '_rowStart', Axis.Y, false); } /** * Sets a value to the end row * @param {string | number} val * * @returns {this} */ setLastRow(val) { return this._setFields(val, '_rowEnd', Axis.Y, false); } /** * Adds N cells to range along the x-axis * if count >= 0 - adds to right * if count < 0 - adds to left * @param {number} count * * @return {this} */ addX(count) { return this._addFields(count, Axis.X); } /** * Adds N cells to range along the y-axis * if count >= 0 - adds to bottom * if count < 0 - adds to top * @param {number} count * * @return {this} */ addY(count) { return this._addFields(count, Axis.Y); } /** * Adds N cells to range along the x/y-axis * @param {number} countX * @param {number} countY * * @return {this} */ add(countX, countY) { return this.addX(countX).addY(countY); } /** * Removes N cells from range along the x-axis * if count >= 0 - removes from right * if count < 0 - removes from left * @param {number} count * * @return {this} */ removeX(count) { return this._removeFields(count, Axis.X); } /** * Removes N cells from range along the y-axis * if count >= 0 - removes from bottom * if count < 0 - removes from top * @param {number} count * * @return {this} */ removeY(count) { return this._removeFields(count, Axis.Y); } /** * Removes N cells from range along the x/y-axis * @param {number} countX * @param {number} countY * * @return {this} */ remove(countX, countY) { return this.removeX(countX).removeY(countY); } /** * Shifts the range along the x-axis * If offset >= 0 - shifts to right * If offset < 0 - shifts to left * @param {number} offset * * @return {this} */ shiftX(offset) { return this._shiftFields(offset, Axis.X); } /** * Shifts the range along the y-axis * If offset >= 0 - shifts to bottom * If offset < 0 - shifts to top * @param {number} offset * * @return {this} */ shiftY(offset) { return this._shiftFields(offset, Axis.Y); } /** * Shifts the range along the x/y-axis * @param {number} offsetX * @param {number} offsetY * * @return {this} */ shift(offsetX, offsetY) { return this.shiftX(offsetX).shiftY(offsetY); } /** * Sets a value to the specified field * @param {string | number} val * @param {string} field * @param {Axis} axis * @param {boolean} [canBeLetter = true] * * @returns {this} */ _setFields(val, field, axis, canBeLetter = true) { if (isPositiveNumber(val) || isStringifiedNumber(val)) this[field] = +val; else if (canBeLetter && isLetter(val)) this[field] = A1._A1Col(val, this._converter); else throw new A1Error(val).u(); if (this[`_${axis}Start`] > this[`_${axis}End`]) throw new A1Error(`The first column or row can't be bigger than the last, got: ${val}`); return this; } /** * Adds N cells to the range along the x/y-axis * @param {number} count * @param {Axis} axis * * @returns {this} */ _addFields(count, axis) { if (!isNumber(count)) throw new A1Error(count).u(); const fieldStart = `_${axis}Start`, fieldEnd = `_${axis}End`; count >= 0 ? this[fieldEnd] += count : this[fieldStart] += count; (this[fieldStart] <= 0) && (this[fieldStart] = 1); return this; } /** * Removes N cells from the range along the x/y-axis * @param {number} count * @param {Axis} axis * * @returns {this} */ _removeFields(count, axis) { if (!isNumber(count)) throw new A1Error(count).u(); const fieldStart = `_${axis}Start`, fieldEnd = `_${axis}End`; if (count >= 0) { this[fieldEnd] -= count; (this[fieldEnd] < this[fieldStart]) && (this[fieldEnd] = this[fieldStart]); } else { this[fieldStart] -= count; (this[fieldStart] > this[fieldEnd]) && (this[fieldStart] = this[fieldEnd]); } return this; } /** * Shifts the specified fields along x/y-axis * @param {number} offset * @param {Axis} axis * * @returns {this} */ _shiftFields(offset, axis) { if (!isNumber(offset)) throw new A1Error(offset).u(); const fieldStart = `_${axis}Start`, fieldEnd = `_${axis}End`; const diff = this[fieldEnd] - this[fieldStart], start = this[fieldStart] + offset, end = this[fieldEnd] + offset; this[fieldStart] = start > 0 ? start : 1; this[fieldEnd] = start > 0 ? end : diff + 1; return this; } } export { A1 as default };