Skip to content

Commit

Permalink
fix: range parser
Browse files Browse the repository at this point in the history
  • Loading branch information
jroehl committed May 25, 2023
1 parent f27b8e0 commit 8786892
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 59 deletions.
4 changes: 2 additions & 2 deletions src/commands/data/append.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -25,7 +25,7 @@ Data successfully appended to "<worksheetTitle>"
const {
args: { data },
flags: { minCol, worksheetTitle = '', spreadsheetId, valueInputOption },
} = await this.parse(UpdateData);
} = await this.parse(AppendData);

try {
this.start('Appending data');
Expand Down
4 changes: 2 additions & 2 deletions src/commands/data/get.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 28 additions & 10 deletions src/lib/google-sheet.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -114,19 +116,36 @@ export default class GoogleSheet {
*/
async getData(options: GoogleSheetCli.QueryOptions = {}, spreadsheetId?: string): Promise<GoogleSheetCli.SheetData> {
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({
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
75 changes: 39 additions & 36 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};

/**
Expand All @@ -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<GoogleSheetCli.QueryOptions, 'maxCol' | 'minCol' | 'maxRow' | 'minRow' | 'worksheetTitle'> => {
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}"`);
}
};

/**
Expand Down
27 changes: 18 additions & 9 deletions test/lib.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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""');
});
});
});

0 comments on commit 8786892

Please sign in to comment.