Skip to content
This repository was archived by the owner on Nov 4, 2025. It is now read-only.

Commit b2a35a1

Browse files
committed
Migration from Sheets API v3 to v4 -- in progress
1 parent 2e1bb12 commit b2a35a1

9 files changed

Lines changed: 451 additions & 137 deletions

lib/sheets.js

Lines changed: 189 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22
const { google } = require('googleapis');
33
var Promise = require('polyfill-promise');
4+
var excelColumnName = require('excel-column-name');
45

56
/**
67
* Authorizationn via GoogleApis
@@ -96,6 +97,8 @@ Sheets.prototype.getSheets = async function getSheets(docId) {
9697
spreadsheetId: docId
9798
}
9899
const response = await sheets.spreadsheets.get(request);
100+
101+
console.log('v4 response-->', response.data.sheets )
99102
return(response.data.sheets.map((sheet) => {
100103
return {
101104
id: sheet.properties.sheetId,
@@ -118,6 +121,7 @@ Sheets.prototype.getSheets = async function getSheets(docId) {
118121
Sheets.prototype.getSheet = async function getSheet(docId, sheetId) {
119122
try {
120123
const sheetsInfo = await this.getSheets(docId);
124+
console.log('-----sheetsInfo-----', sheetsInfo)
121125
const { id, title, rowCount, colCount } = sheetsInfo
122126
.find((sheet) => sheet.id === +sheetId);
123127

@@ -149,6 +153,7 @@ Sheets.prototype.getList = async function getList(docId, sheetId) {
149153
range: title
150154
}
151155
const sheet = await sheets.spreadsheets.values.get(request);
156+
// console.log(JSON.stringify(sheet.data.values, null, 2));
152157
return sheet.data.values;
153158
} catch(error) {
154159
throw new Error(error);
@@ -164,12 +169,132 @@ Sheets.prototype.getList = async function getList(docId, sheetId) {
164169
Sheets.prototype.getCells = async function getCells(docId, sheetId) {
165170
try {
166171
const cells = await this.getRange(docId, sheetId);
167-
return cells.flat();
172+
return cells.flat().filter(cell => cell.content != '');
168173
} catch (e) {
169174
throw new Error(e);
170175
}
171176
};
172177

178+
179+
/**
180+
* Convert rangeInfo to valid Google Sheets API v4 range
181+
*
182+
* Examples:
183+
*
184+
* - "Sheet1!A1:B2" refers to the first two cells in the top two rows of Sheet1.
185+
* - "Sheet1!A:A" refers to all the cells in the first column of Sheet1.
186+
* - "Sheet1!1:2" refers to all the cells in the first two rows of Sheet1.
187+
* - "Sheet1!A5:A" refers to all the cells of the first column of Sheet 1, from row 5 onward.
188+
* - "Sheet1" refers to all the cells in Sheet1.
189+
* - "'My Custom Sheet'!A:A" refers to all the cells in a sheet named "My Custom Sheet."
190+
* Single quotes are required for sheet names with spaces, special characters, or an alphanumeric combination.
191+
*
192+
* @param {String} t Title, name of the sheet
193+
* @param {String} ri Range info, e.g "A1:B2", where "A" and "B" are columns and "1" and "2" are rows
194+
*/
195+
Sheets.prototype.toV4Range = (t, ri) => {
196+
// Titles with spaces need to be wrapped into single quotes
197+
let title = t.includes(" ") ? `'${t}'` : t;
198+
199+
// Return just the title "Sheet1" if there is no range info
200+
if ([undefined, null].includes(ri)) {
201+
return title;
202+
}
203+
204+
// "Sheet1!A3:" -> "Sheet1" -> start from A3 and go column per column on the response returning all cells that are on row 3 or above
205+
if (ri[ri.length - 1] === ":") {
206+
return title;
207+
}
208+
// "Sheet1!1:2" -> all the cells in the first two rows of Sheet1
209+
// const patternMatch = ri.match(/([a-zA-Z]*)(\d*):([a-zA-Z]*)(\d*)/);
210+
// if (patternMatch && patternMatch[1] === '' && patternMatch[3] === '') {
211+
// return title;
212+
// }
213+
214+
return `${title}!${ri}`;
215+
// return title;
216+
};
217+
218+
219+
Sheets.prototype.fullDataMatrix = function (data, range) {
220+
const parts = range//'D1:D5'//'C5:A1'//'A1:B2'//'A1:C5'//'B2:C2';
221+
const partsMatch = parts.match(/([a-zA-Z]*)(\d*):([a-zA-Z]*)(\d*)/);
222+
let noOfColumns = 0;
223+
let startCol = 0;
224+
let noOfRows = 0;
225+
let startRow = 0;
226+
227+
// No column specified --> '1:2'
228+
if(partsMatch[1] === '' && partsMatch[3] === ''){
229+
let col = 0;
230+
231+
for (let r = 0; r < data.length; r += 1) {
232+
for (let c = 0; c < data[r].length; c += 1) {
233+
if (c >= col) {
234+
col = col + c;
235+
}
236+
}
237+
}
238+
noOfRows = Math.abs(+partsMatch[4] - +partsMatch[2]) + 1;
239+
startRow = Math.min(+partsMatch[2], +partsMatch[4]);
240+
noOfColumns = col;
241+
startCol = 1;
242+
}
243+
// No row specified --> 'A:B' OR 'A:A' OR 'B:B'
244+
else if (partsMatch[2] === '' && partsMatch[4] === '') {
245+
noOfRows = data.length;
246+
startRow = 1;
247+
noOfColumns = Math.abs(
248+
excelColumnName.excelColToInt(partsMatch[3]) - excelColumnName.excelColToInt(partsMatch[1])
249+
) + 1;
250+
startCol = Math.min(excelColumnName.excelColToInt(partsMatch[1]), excelColumnName.excelColToInt(partsMatch[3]))
251+
}
252+
// Only 1st part of a range specified --> 'A3:'
253+
else if (partsMatch[3] === '' && partsMatch[4] === '') {
254+
let col = 0;
255+
256+
for (let r = 0; r < data.length; r += 1) {
257+
for (let c = 0; c < data[r].length; c += 1) {
258+
if (c >= col) {
259+
col = col + c;
260+
}
261+
}
262+
}
263+
startRow = +partsMatch[2];
264+
noOfRows = Math.abs(data.length - startRow) + 1;
265+
startCol = excelColumnName.excelColToInt(partsMatch[1]);
266+
noOfColumns = Math.abs(col - startCol) + 1;
267+
268+
console.log('results-->', startRow, startCol, noOfRows, noOfColumns, partsMatch[1], excelColumnName.excelColToInt(partsMatch[1]))
269+
}
270+
// full range specified --> 'A1:C5'
271+
else {
272+
noOfRows = Math.abs(+partsMatch[4] - +partsMatch[2]) + 1;
273+
startRow = Math.min(+partsMatch[2], +partsMatch[4]);
274+
noOfColumns = Math.abs(
275+
excelColumnName.excelColToInt(partsMatch[3]) - excelColumnName.excelColToInt(partsMatch[1])
276+
) + 1;
277+
startCol = Math.min(excelColumnName.excelColToInt(partsMatch[1]), excelColumnName.excelColToInt(partsMatch[3]))
278+
}
279+
280+
const nMatrix = [...Array(noOfRows)]
281+
.map((_ ,r) =>
282+
[...Array(noOfColumns)]
283+
.map((__, c) => ({ row: r+startRow, col: c+startCol, content: '' }))
284+
)
285+
console.log('nMatrix', nMatrix)
286+
for (let r = 0; r < nMatrix.length; r += 1) {
287+
for (let c = 0; c < nMatrix[r].length; c += 1) {
288+
// console.log('data--->', r, c, data[r+startRow-1][c+startCol-1], nMatrix[r][c])
289+
// const startRowIndex = startRow - 1;
290+
// const startColIndex = startCol - 1;
291+
// console.log('results-->', startRow, startCol, data[r+startRowIndex][c+startColIndex])
292+
nMatrix[r][c].content = (data === undefined || data[r][c] === undefined) ? '' : data[r][c];
293+
}
294+
}
295+
return nMatrix;
296+
}
297+
173298
/**
174299
* Retrieve cells based on given range
175300
* NOTE: If there are missing cells (no content) this function
@@ -179,33 +304,83 @@ Sheets.prototype.getCells = async function getCells(docId, sheetId) {
179304
* @param {String} docId Sheet document id
180305
* @param {String} sheetId Sheet id
181306
* @param {Mixed} rangeInfo Range info. use sheets A1 notation for this https://developers.google.com/sheets/api/guides/concepts
182-
* @return {Promise} Rows containing cells, like [[{A1}, {B1}], [{A2}, {B2}]]
307+
* @return {Promise<{ row: Number, column: String, content: String }[]>} Rows containing cells, like [[{A1}, {B1}], [{A2}, {B2}]]
183308
*/
184309
Sheets.prototype.getRange = async function(docId, sheetId, rangeInfo) {
185310
try {
186311
const [{ title }, sheets] = await Promise.all([
187312
this.getSheet(docId, sheetId),
188313
this.get()
189314
]);
190-
315+
const range = this.toV4Range(title, rangeInfo);
191316
const request = {
192317
spreadsheetId: docId,
193-
range: rangeInfo === undefined ? title : `${title}!${rangeInfo}`,
318+
range
194319
}
195320
const sheet = await sheets.spreadsheets.values.get(request);
196-
return sheet.data.values.map((row, index) => {
197-
const rowIndex = index + 1;
198-
return row.map((cell, cellIndex) => {
199-
return {
200-
row: rowIndex,
201-
column: this.columnLetters.split(',')[cellIndex],
202-
content: cell
203-
};
204-
});
205-
});
321+
console.log('Actual data-->', sheet.data.values)
322+
return this.fullDataMatrix(sheet.data.values, rangeInfo);
323+
// const patternMatch = ![undefined, null].includes(rangeInfo)
324+
// ? rangeInfo.match(/([a-zA-Z]*)(\d*):([a-zA-Z]*)(\d*)/)
325+
// : null
326+
// const sheet = await sheets.spreadsheets.values.get(request);
327+
// console.log('Actual data-->', sheet.data.values)
328+
// const fullMatrix = this.fullDataMatrix(sheet.data.values);
329+
330+
// if(patternMatch && patternMatch[1] === '' && patternMatch[3] === ''){ // range 'Sheet1!1:2'
331+
// return fullMatrix.slice(patternMatch[2] - 1, patternMatch[4]);
332+
// } else if (patternMatch && patternMatch[3] === '' && patternMatch[4] === '') { // range 'Sheet1!A3:'
333+
// return fullMatrix.slice(patternMatch[2] - 1, fullMatrix.length);
334+
// }
335+
// else { // range "Sheet1"
336+
// return fullMatrix;
337+
// }
338+
206339
} catch(error) {
207340
throw new Error(error);
208341
}
209342
};
210343

344+
/**
345+
* Parse given range from given string, like 'A2:C8' or 'A2:'
346+
* @param {String} str Range info
347+
* @return {Object} Range info { from: { col: 'A', row: 2 }, to: ...}
348+
*/
349+
Sheets.prototype.parseRangeInfo = function (str) {
350+
var rangeInfo = {
351+
from: {
352+
col: null,
353+
row: null,
354+
},
355+
to: {
356+
col: null,
357+
row: null,
358+
},
359+
};
360+
361+
// Match full range: A1:B20
362+
var match = str.match(/([a-zA-Z]+)(\d+):([a-zA-Z]+)(\d+)/);
363+
364+
if (match && match.length > 1) {
365+
rangeInfo.from.row = parseInt(match[2], 10);
366+
rangeInfo.from.col = match[1];
367+
rangeInfo.to.row = parseInt(match[4], 10);
368+
rangeInfo.to.col = match[3];
369+
370+
return rangeInfo;
371+
}
372+
373+
// Match full range: A1:
374+
var match = str.match(/(\w)(\d+):/);
375+
if (match && match.length > 1) {
376+
rangeInfo.from.row = parseInt(match[2], 10);
377+
rangeInfo.from.col = match[1];
378+
379+
return rangeInfo;
380+
}
381+
382+
return rangeInfo;
383+
};
384+
385+
211386
module.exports = Sheets;

package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"url": "https://github.com/SC5/google-sheets-api/issues"
2525
},
2626
"dependencies": {
27+
"excel-column-name": "^1.0.1",
2728
"googleapis": "^59.0.0",
2829
"lodash": "^4.17.20",
2930
"polyfill-promise": "^4.0.0",

spec/cells.json

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,7 @@
11
[
2-
{
3-
"row": "1",
4-
"column": "A",
5-
"content": "A1"
6-
},
7-
{
8-
"row": "1",
9-
"column": "B",
10-
"content": "B1"
11-
},
12-
{
13-
"row": "1",
14-
"column": "C",
15-
"content": "C1"
16-
},
17-
{
18-
"row": "2",
19-
"column": "A",
20-
"content": "A2"
21-
},
22-
{
23-
"row": "2",
24-
"column": "C",
25-
"content": "C2"
26-
},
27-
{
28-
"row": "3",
29-
"column": "A",
30-
"content": "A3"
31-
},
32-
{
33-
"row": "4",
34-
"column": "A",
35-
"content": "A4"
36-
},
37-
{
38-
"row": "5",
39-
"column": "A",
40-
"content": "A5"
41-
},
42-
{
43-
"row": "6",
44-
"column": "A",
45-
"content": "A5"
46-
},
47-
{
48-
"row": "7",
49-
"column": "A",
50-
"content": "A7"
51-
},
52-
{
53-
"row": "8",
54-
"column": "A",
55-
"content": "A8"
56-
},
57-
{
58-
"row": "9",
59-
"column": "A",
60-
"content": "A9"
61-
},
62-
{
63-
"row": "10",
64-
"column": "A",
65-
"content": "A10"
66-
}
2+
["A1", "B1", "C1"],
3+
["A2", "B2", "C2"],
4+
["A3", "B3", "C3"],
5+
["A4", "B4", "C4"],
6+
["A5", "B5", "C5"]
677
]

spec/range-of-values-1.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"data": {
3+
"range": "B1:C2",
4+
"majorDimension": "ROWS",
5+
"values": [
6+
["B1", "C1"],
7+
["", "C2"]
8+
]
9+
}
10+
}
11+

spec/range-of-values-2.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"data": {
3+
"range": "A3:A",
4+
"majorDimension": "ROWS",
5+
"values": [
6+
[ "A3" ],
7+
[ "A4" ],
8+
[ "A5" ],
9+
[ "A5" ],
10+
[ "A7" ],
11+
[ "A8" ],
12+
[ "A9" ],
13+
[ "A10" ]
14+
]
15+
}
16+
}

0 commit comments

Comments
 (0)