I am using the Google API to fetch data from a Google Spreadsheet via Node.js.
In Excel VBA it is possible to either use
Range("A3") or Cells(3,1)
to reference the range of the cell.
And for a multiple cell range it would be like:
Range(Cells(1,1), Cells(2,3))
instead of:
Range("A1:C2")
I could not find out how to do that via the Google Sheets API.
Tried following this guideline, but I think it's not the thing I need here:
https://googlesheets4.tidyverse.org/articles/range-specification.html
My example code in Node.js looks like this:
async function getHelperData(cl){
const gsapi = google.sheets({version:'v4', auth: cl});
const opt = {
spreadsheetId: gs.gs_ID,
range: '_helperSheet!A1:C2'
};
let data = await gsapi.spreadsheets.values.get(opt);
let dataArray = data.data.values;
return dataArray;
};
Is it actually possible?
About Use Cell Range instead of A1-notation in Google Sheets and the use of a value like Range(Cells(1,1), Cells(2,3)), I thought that in the current stage, when Sheets API is used, the a1Notation is used for retrieving values from Google Spreadsheet. So, in this case, I thought that it is required to convert a value like Range(Cells(1,1), Cells(2,3)) to the a1Notation.
When this is reflected in your script, how about the following modification?
Modified script:
const gsapi = google.sheets({version:'v4', auth: cl});
const sheetName = "_helperSheet"; // Please set sheet name.
const rangeObj = { start: [1, 1], end: [2, 3] }; // This is from `Range(Cells(1,1), Cells(2,3))` of "A1:C2" in your question.
const columnIndexToLetter_ = (index) => (a = Math.floor(index / 26)) >= 0 ? columnIndexToLetter_(a - 1) + String.fromCharCode(65 + (index % 26)) : ""; // Ref: https://stackoverflow.com/a/53678158
const a1Notation = rangeObj.end ? `'${sheetName}'!${columnIndexToLetter_(rangeObj.start[1] - 1)}${rangeObj.start[0]}:${columnIndexToLetter_(rangeObj.end[1] - 1)}${rangeObj.end[0]}` : `'${sheetName}'!${columnIndexToLetter_(rangeObj.start[1] - 1)}${rangeObj.start[0]}`;
const opt = {
spreadsheetId: gs.gs_ID,
range: a1Notation,
};
let data = await gsapi.spreadsheets.values.get(opt);
let dataArray = data.data.values;
console.log(dataArray);
In this modification, it supposes that the value of gs.gs_ID has already been declared elsewhere. Please be careful about this.
In this modification, a1Notation returns '_helperSheet'!A1:C2.
If const rangeObj = { start: [1, 1] }; is used, a1Notation returns '_helperSheet'!A1.
Related
I have a google sheet with several named ranges.
I would like to import every single named range in the sheet and save them as individual objects named after the named range.
Furthermore, there are 4 similar blocks of data. Therefore the names of the ranges are structured like "block1_name1" "block1_name2" "block2_name1" "block2_name2" etc.
With the following code I can enter every range manually, but there are too many to enter them all manually:
const API_KEY = "###"; // Please set your API key.
const ID = "###"; // Please set your Spreadsheet ID.
const RANGE = ["Range1", "Range2"]; // Named ranges
const ranges = RANGE.map(e => `ranges=${encodeURIComponent(e)}`).join("&");
const response = await fetch(`https://sheets.googleapis.com/v4/spreadsheets/${ID}/values:batchGet?key=${API_KEY}&${ranges}`);
const { valueRanges } = await response.json();
const obj = valueRanges.reduce((o, { values }, i) => (o[RANGE[i]] = values, o), {});
console.log(obj);
How can I import every named range automatically?
How can I save them as a different objects for each datablock like block1.name1 etc?
I believe your goal is as follows.
You want to retrieve the named range list from Google Spreadsheet, and want to retrieve the values from the named ranges.
And, you want to export the values as {namedRange1: values1, namedRange2: values2,,,}.
You want to achieve this by directly requesting the endpoint of Sheets API with fetch API of Javascript.
In this case, how about the following modification?
Modified script:
const API_KEY = "###"; // Please set your API key.
const ID = "###"; // Please set your Spreadsheet ID.
// 1. Retrieve the named range list.
const base = `https:\/\/sheets.googleapis.com/v4/spreadsheets/${ID}`;
const res1 = await fetch(`${base}?key=${API_KEY}&fields=namedRanges(name)`);
const { namedRanges } = await res1.json();
// 2. Retrieve values from named ranges.
const ranges = namedRanges.map(({ name }) => `ranges=${encodeURIComponent(name)}`).join("&");
const res2 = await fetch(`${base}/values:batchGet?key=${API_KEY}&${ranges}`);
const { valueRanges } = await res2.json();
// 3. Create an output object.
const res = valueRanges.reduce((o, { values }, i) => (o[namedRanges[i].name] = values, o), {});
console.log(res);
// For your 2nd question.
const res3 = valueRanges.reduce((o, { values }, i) => {
const [k1, k2] = namedRanges[i].name.split("_");
if (o[k1]) {
o[k1][k2] = values;
} else {
o[k1] = { [k2]: values };
}
return o;
}, {});
console.log(res3);
Testing:
When this script is run, the following result can be seen at the console.
{
"block1_name2":[###values###],
"block2_name2":[###values###],
,
,
,
}
Note:
When you have a lot of named ranges, it is required to separate the request for retrieving the values, because of the limitation of the length of the URL. Please be careful about this.
About your 2nd question, when you want to convert from {"block1_name2":[###values###],,,} to {"block1": {"name2":[###values###]},,,}, as a premise, I think that it is required to decide the format of the name of the named ranges. In this case, from your showing sample named ranges, it supposes that your format of the name of all named ranges is like block#_name#. Please be careful about this.
References:
Method: spreadsheets.get
Method: spreadsheets.values.batchGet
You can use the Method: spreadsheets.get to list the ranges of a Google Spreadsheet. This is returned as an object.
{
"namedRangeId": "xxxxxxxxxxx",
"name": "test",
"range": {
"startRowIndex": 4,
"endRowIndex": 10,
"startColumnIndex": 0,
"endColumnIndex": 3
}
},
A simple sample using the method, and limiting the field for name ranges only:
function execute() {
return gapi.client.sheets.spreadsheets.get({
"spreadsheetId": "Google_Sheet_ID",
"fields": "namedRanges"
})
You can read more information about this method in the Google Documentation here
And there is a sample complete code on the same documentation. You can then use the name in "name": "test", to rename the objects.
Goals of the problem:
Retrieve the message from Gmail using the email address.
Retrieve the CSV files from the attachment files and put them on a sheet in Google Spreadsheet. Remove 1st 2 rows from the CSV data. (Remove the past data on Google Sheets and update it with the new CSV data whenever received from the specific email address).
Achieve this using Google Apps Script.
Already tried: Automate a CSV file received over Gmail from a specific email id to a specific Google Sheet
But nothing is solving the above 3 problems in specific.
function myFunction() {
const messageId = "###"; // Please set the message ID of Gmail.
const sheetName = "Sheet1"; // Please set the sheet name you want to put the values.
const delimiter = ","; // If your CSV data uses the specific delimiter, please set this.
const skipRows = 2; // 2 is from your question.
// 1. Retrieve message.
const message = GmailApp.getMessageById(messageId);
// 2. Retrieve attachment files.
const attachments = message.getAttachments();
if (attachments.length == 0) {
console.log("No attachment files.");
return;
}
// 3. Create an array for putting to Spreadsheet from the CSV data of attachment files.
const values = attachments.reduce((ar, e) => {
if (e.getContentType() == MimeType.CSV || e.getName().includes(".csv")) {
ar = [...ar, ...Utilities.parseCsv(e.getDataAsString(), delimiter).splice(skipRows)];
}
return ar;
}, []);
if (values.length == 0) {
console.log("No values.");
return;
}
// 4. Put the values to Spreadsheet.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); // and, you can also use. const sheet = SpreadsheetApp.openById("###spreadsheetId###").getSheetByName(sheetName);
sheet.getRange(sheet.getLastRow() + 1, 1, values.length, values[0].length).setValues(values);
}
Tried the above code but it shows an error Exception: Invalid argument: at myFunction(Code:8:28)
FYI, I also want to use Sheet ID instead of Sheet Name
Try (put the spreadsheet ID, the subject -if you have multiple words please specify as in the example hereafter- and the email of the sender hereafter):
const getGmailAttachment = () => {
const ssID = '############'
const searchQuery = 'from:#######gmail.com in:inbox has:attachment subject:##### subject:###';
const threads = GmailApp.search(searchQuery, 0, 1);
threads.forEach(thread => {
const message = thread.getMessages()[Number(thread.getMessageCount() - 1)];
const attachments = message.getAttachments();
attachments.forEach((attachment, i) => {
if (i == 0) {
console.log(attachment.getName())
const sheet = SpreadsheetApp.openById(ssID).getSheets()[0];
sheet.getDataRange().clearContent()
const csvData = Utilities.parseCsv(attachment.getDataAsString()).splice(2); // except 2 first rows
sheet.getRange(1, 1, csvData.length, csvData[0].length).setValues(csvData);
}
});
});
};
references
GmailApp.search
search syntax
getAttachments()
Utilities.parseCsv
Goal: To stack data from 90+ google workbooks, all with the same sheet name, into the one master sheet for reporting
Info:
All worksheets have the same number of columns.
I have the following script but it does not run properly, I think the issue is with how I am caching / Pushing the data to the array before pasting to the output sheet.
I am trying to build an array then paste it in one go.
The tables I am stacking have 47 columns, unknown number of rows.
The part that opens the sheets is all working perfectly.
// Get the data from the worksheets
var indexsheet = SpreadsheetApp.getActive().getSheetByName("Index");
var outputsheet = SpreadsheetApp.getActive().getSheetByName("Output");
var response = SpreadsheetApp.getUi().prompt('Current Cycle', 'Enter Cycle Name Exactly in YY-MMM-Cycle# format', SpreadsheetApp.getUi().ButtonSet.OK_CANCEL)
var CurrentCycleName = response.getResponseText()
// Assign datasets to variables
var indexdata = indexsheet.getDataRange().getValues();
// For each workbook in the index sheet, open it and copy the data to a cache
indexdata.forEach(function(row, r) {
try {
//open Entity specific workbook
var workbookid = indexsheet.getRange(r + 1, 7, 1, 1).getValues();
var Entityworkbook = SpreadsheetApp.openById(workbookid)
// Open workhseet
Entitysheet.getSheetByName(CurrentCycleName)
// Add PR Data to cache - stacking for all countrys
var PRDataCache = Entitysheet.getDataRange().push()
} catch {}
})
// Set the all values of the sheet at once
outputsheet.getRange(r + 1, 14).setValue('Issue Splitting Data')
Entitysheet.getRange(2, 1, PRDataCache.length || 1, 47).setValues(PRDataCache)
};
This is the index tab where we are getting the workbookid from to open each file
This is the output file, we are stacking all data from each country
I believe your goal is as follows.
You want to retrieve the Spreadsheet IDs from the column "G" of "Index" sheet.
You want to give the specific sheet name using a dialog.
You want to retrieve all values from the specification sheet in all Spreadsheets. In this case, you want to remove the header row.
You want to put the retrieved values on "Output" sheet.
In this case, how about the following sample script?
Sample script:
function myFunction() {
var ss = SpreadsheetApp.getActive();
var indexsheet = ss.getSheetByName("Index");
var outputsheet = ss.getSheetByName("Output");
var response = SpreadsheetApp.getUi().prompt('Current Cycle', 'Enter Cycle Name Exactly in YY-MMM-Cycle# format', SpreadsheetApp.getUi().ButtonSet.OK_CANCEL);
var CurrentCycleName = response.getResponseText();
var ids = indexsheet.getRange("G1:G" + indexsheet.getLastRow()).getValues();
var values = ids.reduce((ar, [id]) => {
try {
var [, ...values] = SpreadsheetApp.openById(id).getSheetByName(CurrentCycleName).getDataRange().getValues();
ar = [...ar, ...values];
} catch (e) {
console.log(`"${id}" was not found.`);
}
return ar;
}, []);
if (values.length == 0) return;
// If the number of columns is different in all Spreadsheets, please use the following script.
// var maxLen = Math.max(...values.map(r => r.length));
// values = values.map(r => r.length < maxLen ? [...r, ...Array(maxLen - r.length).fill("")] : r);
outputsheet.getRange(outputsheet.getLastRow() + 1, 1, values.length, values[1].length).setValues(values);
}
Note:
When the number of Spreadsheet IDs is large, the processing time might be over 6 minutes. I'm worried about this. At that time, how about separating the Spreadsheet IDs?
Reference:
reduce()
I am trying to avoid the following code because it is too slow:
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{sheet2.deleteColumn(c)}
}
Instead I tried to find a list of columns I want to delete from the array and then set the array. (I recently figure out that deleting rows/columns in a loop is very expensive: google script loop performance)
I found this Removing columns of data in javascript array and try to apply it to my code, but it is not working.
Here is the code.
var ary = sheet2.getRange(2,1,outData.length+1,outData[0].length).getValues();
var indexesToRemove = [];
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{
indexesToRemove.push(c);
}
}
The part above works well. What is not working is the function to remove the columns from the array once I found the indexes to remove. The array _row is not what I am looking for. What am I doing wrong?
removeColumns(ary, indexesToRemove);}
function removeColumns(data, indexes) {
return data.map(function (row) {
// when we remove columns, the indexing gets off by 1 each time, keep track of how many to adjust
var indexAdjustment = 0;
// copy row w/ .slice so we do not modify the original array
var _row = row.slice();
indexes.forEach(function (colIndex) {
// remove column
_row.splice(colIndex - indexAdjustment, 1);
// add 1 to adjustment to account for the column we just removed
indexAdjustment++
});
return _row;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet2 = ss.getSheetByName('Cache');
sheet2.clear();
sheet2.getRange(2,1,_row.length,_row[0].length).setValues(_row);
});
}
BTW, I have also tried this before, but still not working:
var ary = sheet2.getRange(2,1,outData.length+1,outData[0].length).getValues();
for (var c = 25; c>2; c--){
if (sheet2.getRange(1,c).getValue() == 0)
{ ary = ary.map(function(item){
return item.splice(0,c)});
}
}
You want to delete the columns that the value of is 0 in the cells C1:Y1.
You want to reduce the process cost of the script.
You want to achieve this without using Sheets API.
Pattern 1:
In this pattern, at first, the cells which have the value of 0 from the cells C1:Y1 using TextFinder, and the columns are deleted from the retrieved cells using deleteColumn().
Sample script:
const sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange("C1:Y1")
.createTextFinder(0)
.matchEntireCell(true)
.findAll()
.reverse()
.forEach(e => sheet.deleteColumn(e.getColumn()));
Pattern 2:
In this pattern, at first, all values are retrieved from "C1" to the last column for the all data rows, and delete the columns in the array and clear the range, and then, the values are put to the sheet. The method for directly processing the retrieved values has already been proposed. So as other pattern, I proposed the method which uses the transpose.
Sample script:
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getRange(1, 3, sheet.getLastRow(), sheet.getLastColumn() - 2);
const values = range.getValues();
const t = values[0].reduce((ar, r, i) => {
if (r != 0) ar.push(values.map(c => c[i]));
return ar;
}, []);
const v = t[0].map((_, i) => t.map(c => c[i]));
range.clearContent();
sheet.getRange(1, 3, v.length, v[0].length).setValues(v);
Pattern 3:
In this pattern, the request body for the batchUpdate method of Sheets API is created using the 1st row values, and the request body is used for requesting to Sheets API. By this, several columns can be deleted by one API call.
Before you run the script, please enable Sheets API at Advanced Google services.
Sample script:
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const sheetId = sheet.getSheetId();
// Create rerequests for DeleteDimensionRequest.
const requests = sheet.getRange("C1:Y1")
.createTextFinder(0)
.matchEntireCell(true)
.findAll()
.reverse()
.map(e => {
const col = e.getColumn();
return {deleteDimension: {range: {sheetId: sheetId, dimension: "COLUMNS", startIndex: col - 1, endIndex: col}}}
});
// Request to the batchUpdate method using the request body.
Sheets.Spreadsheets.batchUpdate({requests: requests}, spreadsheet.getId());
In this case, requests is created using the method of pattern 1. Each request is as follows. You can see about this structure at the document.
{
"deleteDimension": {
"range": {
"sheetId": "###",
"dimension": "COLUMNS",
"startIndex": ##,
"endIndex": ##
}
}
}
References:
Class TextFinder
Advanced Google services
Method: spreadsheets.batchUpdate
DeleteDimensionRequest
function runOne() {
var d=0;
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var hA=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];//header array
var vs=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn()).getValues();//data array
vs.forEach(function(r,j){
var d=0;
hA.forEach(function(h,i){
if(h==0)r.splice(i-d++,1);//removes elements in columns whose headers are == 0
});
});
Logger.log(vs);
}
Try using Sheets Advanced Service and batchUpdate.
Related
Remove all grouped rows / columns in a spreadsheet
Reference
https://developers.google.com/sheets/api/samples/rowcolumn#delete_rows_or_columns
I'm able to get values from a single sheet using code like you see below, but I was wondering if I can somehow get all the values from all the sheets in a Spreadsheet and then manipulate the date regarding on what I get. In other words, can you set the range to include sheets from sheet x to sheet y, or at least get all the data from all the sheets, and after that I'll somehow try to include/ exclude sheets. I'm using the Sheets API, and I can't use the Google App Scripts for this.
let { google } = require("googleapis");
let authentication = require("./authentication");
function getData(auth) {
var sheets = google.sheets('v4');
sheets.spreadsheets.values.get({
auth: auth,
spreadsheetId: '1XOqjUJ1eAMhl2g4KLIS4qPUzdwebSVeoE8OGJtPYyPw',
range: 'Test!A2:C', **//I would love to get all sheets. This is only one of them. Possible?**
}, (err, res) => {
if (err) {
console.log('The API returned an error: ' + err);
return res;
}
var rows = res.values;
if (rows.length === 0) {
console.log('No data found.');
} else {
for (var i = 0; i<= rows.length; i++) {
var row = rows[i];
console.log(row);
}
}
});
}
authentication.authenticate()
.then((auth) => {
getData(auth)
.catch((err) => {
console.log(err)
});
});
This is the right way of doing this. I'm answering for potential future references.
copyFormatToRange(sheet, column, columnEnd, row, rowEnd)
Copy the formatting of the range to the given location. If the destination is larger or smaller than the source range then the source is repeated or truncated accordingly. Note that this method copies the formatting only.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const source = ss.getSheets()[0];
const destination = ss.getSheets()[1];
const range = source.getRange("B2:D4");
// This copies the formatting in B2:D4 in the source sheet to
// D4:F6 in the second sheet
range.copyFormatToRange(destination, 4, 6, 4, 6);
Authorization
Scripts that use this method require authorization with one or more of the following scopes:
- https://www.googleapis.com/auth/spreadsheets.currentonly
- https://www.googleapis.com/auth/spreadsheets
Here is the whole article. Good luck with it :)
You may refer with this blog on how to combine data from multiple sheets using query formula. Below is a sample Query formula to combine the above two sheets’ data into a single sheet.
=query({junesheet!A2:H5;julysheet!A2:H5},”Select * where Col1 is not null “)
Here are additional links which might help:
https://productforums.google.com/forum/#!topic/docs/ZlhyAMmhJCI
https://productforums.google.com/forum/#!topic/docs/QK0IVoqyUjA
Hope this helps!