From 87868921698bdec159c571865f00593ac8d56cc5 Mon Sep 17 00:00:00 2001 From: jroehl Date: Thu, 25 May 2023 07:00:53 +0200 Subject: [PATCH] fix: range parser --- src/commands/data/append.ts | 4 +- src/commands/data/get.ts | 4 +- src/lib/google-sheet.ts | 38 ++++++++++++++----- src/lib/utils.ts | 75 +++++++++++++++++++------------------ test/lib.test.ts | 27 ++++++++----- 5 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/commands/data/append.ts b/src/commands/data/append.ts index e221ace..521cee5 100644 --- a/src/commands/data/append.ts +++ b/src/commands/data/append.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; import Command, { data, spreadsheetId, valueInputOption, worksheetTitle } from '../../lib/base-class'; -export default class UpdateData extends Command { +export default class AppendData extends Command { static description = 'Append cells with the specified data after the last row in starting col'; static examples = [ @@ -25,7 +25,7 @@ Data successfully appended to "" const { args: { data }, flags: { minCol, worksheetTitle = '', spreadsheetId, valueInputOption }, - } = await this.parse(UpdateData); + } = await this.parse(AppendData); try { this.start('Appending data'); diff --git a/src/commands/data/get.ts b/src/commands/data/get.ts index cc402e4..2894317 100644 --- a/src/commands/data/get.ts +++ b/src/commands/data/get.ts @@ -1,7 +1,7 @@ import { Flags, ux } from '@oclif/core'; import Command, { spreadsheetId, worksheetTitle } from '../../lib/base-class'; -export default class UpdateData extends Command { +export default class GetData extends Command { static description = 'Returns cell data'; static examples = [ @@ -30,7 +30,7 @@ A3 B3 C3 async run() { const { flags: { spreadsheetId, rawOutput, minRow, maxRow, minCol, maxCol, range, hasHeaderRow, worksheetTitle, ...tableOptions }, - } = await this.parse(UpdateData); + } = await this.parse(GetData); this.start('Fetching data'); const res = await this.gsheet.getData({ minRow, maxRow, minCol, maxCol, range, hasHeaderRow, worksheetTitle }, spreadsheetId); diff --git a/src/lib/google-sheet.ts b/src/lib/google-sheet.ts index c85a02a..3337144 100644 --- a/src/lib/google-sheet.ts +++ b/src/lib/google-sheet.ts @@ -1,6 +1,6 @@ import { google, sheets_v4 } from 'googleapis'; import get from 'lodash.get'; -import { colToA, getLongestArray, getRange, parseRanges } from './utils'; +import { colToA, getLongestArray, getRange, parseRange } from './utils'; export namespace GoogleSheetCli { export interface Credentials { @@ -82,6 +82,8 @@ export default class GoogleSheet { const { data: sheet } = await this.sheets.spreadsheets.get({ spreadsheetId: spreadsheetId || this.spreadsheetId, }); + + this.sheets.spreadsheets.sheets; if (!sheet) throw `Spreadsheet "${spreadsheetId || this.spreadsheetId}" not found`; return sheet; } @@ -114,19 +116,36 @@ export default class GoogleSheet { */ async getData(options: GoogleSheetCli.QueryOptions = {}, spreadsheetId?: string): Promise { options.worksheetTitle = options.worksheetTitle || this.worksheetTitle; - const parsedOptions = parseRanges(options)[0]; - const { worksheetTitle: wsTitle } = parsedOptions; - if (!wsTitle) { + if (options.range) { + const parsedOptions = parseRange(options.range); + if (parsedOptions.worksheetTitle) { + options.worksheetTitle = parsedOptions.worksheetTitle; + } + if (parsedOptions.minCol) { + options.minCol = parsedOptions.minCol; + } + if (parsedOptions.maxCol) { + options.maxCol = parsedOptions.maxCol; + } + if (parsedOptions.minRow) { + options.minRow = parsedOptions.minRow; + } + if (parsedOptions.maxRow) { + options.maxRow = parsedOptions.maxRow; + } + } + + if (!options.worksheetTitle) { throw 'Option property "worksheetTitle" is required'; } - const sheet = await this.getWorksheet(wsTitle, spreadsheetId); + const sheet = await this.getWorksheet(options.worksheetTitle, spreadsheetId); const { rowCount = 0, columnCount = 0 } = sheet?.properties?.gridProperties || {}; const sanitizedOptions: GoogleSheetCli.QueryOptions = { - ...parsedOptions, - maxCol: parsedOptions.maxCol || columnCount || 0, - maxRow: parsedOptions.maxRow || rowCount || 0, + ...options, + maxCol: options.maxCol || columnCount || 0, + maxRow: options.maxRow || rowCount || 0, }; const res = await this.sheets.spreadsheets.values.get({ @@ -146,7 +165,7 @@ export default class GoogleSheet { spreadsheetId: spreadsheetId || this.spreadsheetId, range: getRange({ ...sanitizedOptions, - worksheetTitle: wsTitle, + worksheetTitle: options.worksheetTitle, minRow: 1, maxRow: 1, range: undefined, @@ -216,7 +235,6 @@ export default class GoogleSheet { options.worksheetTitle = options.worksheetTitle || this.worksheetTitle; if (!options.worksheetTitle) throw 'Specify worksheetTitle'; if (!Array.isArray(data) || !data.every(Array.isArray)) throw 'Check "data" property - has to be supplied as nested array ([["1", "2"], ["3", "4"]])'; - const range = getRange(options); await this.sheets.spreadsheets.values.update({ spreadsheetId: spreadsheetId || this.spreadsheetId, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a6e86cc..f7dbfcf 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -66,15 +66,15 @@ export const aToCol = (label: string): number => { * @returns {{ col: number; row: number }} */ const parseA1Notation = (a1Notation: string = ''): { col: number; row: number } => { - return a1Notation.split('').reduce( - (red, part: string) => { - if (part && !parseInt(part)) { - return { ...red, col: aToCol(part) }; - } - return { ...red, row: parseInt(part) }; - }, - { col: 0, row: 0 } - ); + const res = { col: 0, row: 0 }; + if (!a1Notation) return res; + const rowChar = a1Notation.match(/\d+/g)?.[0] ?? '0'; + + const colChar = a1Notation.match(/[a-zA-Z]+/g)?.[0]; + return { + col: colChar === undefined ? 0 : aToCol(colChar), + row: parseInt(rowChar), + }; }; /** @@ -83,35 +83,38 @@ const parseA1Notation = (a1Notation: string = ''): { col: number; row: number } * @param {GoogleSheetCli.QueryOptions} [options={}] * @returns {GoogleSheetCli.QueryOptions[]} */ -export const parseRanges = (options: GoogleSheetCli.QueryOptions = {}): GoogleSheetCli.QueryOptions[] => { - if (!options.range) return [options]; - const [title, a1Notations = ''] = options.range.split('!'); - - let worksheetTitle = title; - const sanitized = title.match(/^['"](.*)['"]$/); - if (sanitized) { - worksheetTitle = sanitized[1]; +export const parseRange = (range: string): Pick => { + let worksheetTitle: string | undefined | null; + let a1Notation: string; + + const split = range.match(/(["']?.*["']?)\!(.*)/); + if (split) { + [, worksheetTitle, a1Notation] = split; + } else { + a1Notation = range; } - return a1Notations - .replace(/[,;]/g, ',') - .split(',') - .map((a1Notation) => { - const [from, to] = a1Notation.split(':'); - const { col: minCol, row: minRow } = parseA1Notation(from); - const { col: maxCol, row: maxRow } = parseA1Notation(to); - if (!minCol && !minRow && !maxCol && !maxRow) { - return { ...options, worksheetTitle }; - } - return { - ...options, - maxCol: maxCol || minCol, - minCol, - maxRow: maxRow || minRow, - minRow, - worksheetTitle, - }; - }); + worksheetTitle = worksheetTitle?.match(/^['"](.*)['"]$/)?.[1]; + + const [from, to] = a1Notation.split(':'); + + try { + const { col: minCol, row: minRow } = parseA1Notation(from); + const { col: maxCol, row: maxRow } = parseA1Notation(to); + + if (!minCol && !minRow && !maxCol && !maxRow) { + return { worksheetTitle }; + } + return { + maxCol: maxCol || minCol, + minCol, + maxRow: maxRow || minRow, + minRow, + worksheetTitle, + }; + } catch (error) { + throw new Error(`Invalid range "${range}"`); + } }; /** diff --git a/test/lib.test.ts b/test/lib.test.ts index ecef2f1..8dc0b20 100644 --- a/test/lib.test.ts +++ b/test/lib.test.ts @@ -1,5 +1,5 @@ import { expect } from '@oclif/test'; -import { getLongestArray, colToA, aToCol, getRange, parseRanges } from '../src/lib/utils'; +import { aToCol, colToA, getLongestArray, getRange, parseRange } from '../src/lib/utils'; describe('lib', () => { it('getLongestArray', async () => { @@ -42,19 +42,28 @@ describe('lib', () => { }); describe('parseRanges', () => { - it('minCol & minRow & maxCol & maxRow', async () => { - const res = parseRanges({ range: '"foo"!B1:C2' }); - expect(res).to.eql([{ maxCol: 3, maxRow: 2, minCol: 2, minRow: 1, range: '"foo"!B1:C2', worksheetTitle: 'foo' }]); + it('minCol & minRow & maxCol & maxRow & worksheettitle', async () => { + const res = parseRange('"foo"!B1:C2'); + expect(res).to.eql({ maxCol: 3, maxRow: 2, minCol: 2, minRow: 1, worksheetTitle: 'foo' }); + }); + + it('minCol & maxCol & worksheettitle', async () => { + const res = parseRange('"foo"!B1'); + expect(res).to.eql({ maxCol: 2, maxRow: 1, minCol: 2, minRow: 1, worksheetTitle: 'foo' }); }); it('minCol & maxCol', async () => { - const res = parseRanges({ range: '"foo"!B1' }); - expect(res).to.eql([{ maxCol: 2, maxRow: 1, minCol: 2, minRow: 1, range: '"foo"!B1', worksheetTitle: 'foo' }]); + const res = parseRange('B1'); + expect(res).to.eql({ maxCol: 2, maxRow: 1, minCol: 2, minRow: 1, worksheetTitle: undefined }); + }); + + it('minCol & minRow & maxCol & maxRow', async () => { + const res = parseRange('B1:C2'); + expect(res).to.eql({ maxCol: 3, maxRow: 2, minCol: 2, minRow: 1, worksheetTitle: undefined }); }); - it('no minCol & maxCol', async () => { - const res = parseRanges({ range: '"foo"' }); - expect(res).to.eql([{ range: '"foo"', worksheetTitle: 'foo' }]); + it('invalid range', async () => { + expect(() => parseRange('"foo"')).to.throw('Invalid range ""foo""'); }); }); });