Slow Google Script - difficulties batching information into an array - arrays

I've been struggling with a little project of mine for a while now, and was looking for some assistance. The key issue I believe is simply me not being familiar with array script language and how to approach this. I've tried a few things after researching on here a bit and reading through the Best Practices section, but haven't been able to get it functioning adequately.
My script needs to be able to collect 200 rows x 200 columns of data from a spreadsheet, and depending on the number within each cell, it needs to select the corresponding number of columns next to that number and colour them in.
This was really simple to do with my basic programming knowledge, by just getting it to select each cell, check the number, select that range with an offset and then change the colour and move onto the next cell, however my code is incredibly slow because it does everything within the sheet without batching the data, and can't complete the full range within Google Script's time allowance. Any assistance on speeding it up would be greatly appreciated, as I haven't been able to get this working using arrays.
Here's the code I'm working with currently:
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var selection=ss.getRange("Sheet2!H2:FC140");
var columns=selection.getNumColumns();
var rows=selection.getNumRows();
for (var column=1; column < columns; column++) {
for (var row=1; row < rows; row++) {
var cell=selection.getCell(row,column);
var cellvalue=cell.getValue();
if (cellvalue >= 1) {
var range=cell.offset(0,0,1,cellvalue);
range.setBackground("blue");
}
else {;}
}
}
}
Here's a public spreadsheet with confidential info removed and the sheet I'm targeting is Sheet2. Any assistance I could get on this would be greatly appreciated! Thanks
https://docs.google.com/spreadsheets/d/1Oe0aacfSBMmHpZvGPmjay5Q1bqBebnGQV4xlsK8juxk/edit#gid=0

You need to get rid of the repeated calls to range.getValue(). You can get all of the values for the range in one call & then iterate over that array in-script.
For your script it would look something like this:
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var selection=ss.getRange("Sheet2!H1:FC140"); // <= include header, but we'll skip it when we get to stepping over the array
var values = selection.getValues(); // <= get all of the values now in one call
for (var r=1; r < values.length; r++) {
for (var c=0; c < values[r].length; c++) {
if (values[r][c] >= 1) {
var range=sheet.getRange(r+1, c+8, 1, values[r][c]); // r+1 & c+8 because of array offsets
range.setBackground("blue");
}
else {;}
}
}
}
Take a look at Google's documentation: range.GetValues() https://developers.google.com/apps-script/reference/spreadsheet/range#getValues()

How about following sample script? If this is not your expectation, I would like to modify this script.
Sample script :
function CreateCalendar() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getSheetByName('Sheet2');
var data = sheet.getRange("H2:FC140").getValues();
data.forEach(function(e1, i1){
e1.forEach(function(e2, i2){
if (e2 >= 1) {
sheet.getRange(i1+2, i2+8).offset(0,0,1,e2).setBackground("blue");
}
})
})
}
Result (sample) :
If I misunderstand your question, I'm sorry.

Related

Calling an API based on data within cells A2:A100 and outputting to cells G2:G100

I've been trying to figure out how to get a Google AppsScript to pull in an API for keyword rank tracking directly within Google Sheets.
The loop is required to dynamically pull in information from column A and output the keyword ranking position into column G.
The keywords are in cells A2-A100. The ranking position (which is the only thing we are pulling from the API) we are popping into the corresponding row in column G, starting from G2. For testing purposes, we've got the loop set from 1 to 3.
We're at a bit of a loss as to why this isn't working as expected, and would really appreciate a nudge in the right direction!
The issue is that the very first result always returns 'keyword = undefined' within the API, and returning a result of '-1', meaning that the first row is not read. We've tried updating the r to 0, to 2, and changing the r references to no avail.
This makes us think that there must be something wrong with the loop, rather than the rest of the code, but please do correct me if this is not the case.
The script we've gotten so far is;
function callAPI() {
//New loop
for (r = 1; r <= 3; r++) {
{
//Find keyword, encode query and url
var query = keyword;
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(response);
//Get column value for keyword
var sheet = SpreadsheetApp.getActiveSheet();
var keyword = sheet.getRange(1 + r, 1).getValue();
}
//Set value of column
var results = sheet.getRange(1 + r, 7).setValue(response);
}
}
Additional edit:
So this is crystal clear, the desired input is;
keyword in A2 is read using the API and the output found (ranking position) is fed into G2.
the loop should then read A3, find the corresponding ranking position within the API, and adds that value to G3
rinse and repeat until the end of the loop.
Hopefully this is enough to go on, would really appreciate any advice, thank you!
Basically from TheMaster's comments you switch up your statements to this:
function callAPI() {
var sheet = SpreadsheetApp.getActiveSheet();
var keywords = sheet.getRange(2,1,3).getValues();
var responses = [];
//New loop
for (r = 0; r <= 2; r++) {
//Find keyword, encode query and url
var query = keywords[r][0];
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(resp);
responses.push([resp]);
}
//Set value of column
sheet.getRange(2,7,3).setValues(responses);
}
Note that I moved the sheet declaration outside the loop, it needs to be only called once.
EDIT: I updated the code to follow best practices in the tag info page. Note the usage of arrays as return values of getValues() and parameter of setValues().

AppScript: 'number of columns in the data does not match the number of columns in the range.' setValues method not reading array correctly?

I'm trying to automate the collection of phone numbers from an API into a Google Sheet with app script. I can get the data and place it in an array with the following code:
const options = {
method: 'GET',
headers: {
Authorization: 'Bearer XXXXXXXXXXXXXXX',
Accept: 'Application/JSON',
}
};
var serviceUrl = "dummyurl.com/?params";
var data=UrlFetchApp.fetch(serviceUrl, options);
if(data.getResponseCode() == 200) {
var response = JSON.parse(data.getContentText());
if (response !== null){
var keys = Object.keys(response.call).length;
var phoneArray = [];
for(i = 0; i < keys; i++) {
phoneArray.push(response.call[i].caller.caller_id);
}
This works as expected - it grabs yesterday's caller ID values from a particular marketing campaign from my API. Next, I want to import this data into a column in my spreadsheet. To do this, I use the setValues method like so:
Logger.log(phoneArray);
var arrayWrapper = [];
arrayWrapper.push(phoneArray);
Logger.log(arrayWrapper);
for(i = 0; i < keys; i++) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var cell = sheet.getRange("A8");
cell.setValues(arrayWrapper);
}
}
}
}
I am aware that I need my array length to equal the length of the selected range of cells in my sheet. However, I get conflicting errors depending on the length I set for my getRange method. If I set it to a single cell, as you see above, the error I get is:
The number of columns in the data does not match the number of columns in the range. The data has 8 but the range has 1.
However, if I set the length of my range to 8 (or any value except 1), I get the error:
The number of columns in the data does not match the number of columns in the range. The data has 1 but the range has 8.
As you see, the error swaps values. Now I have the appropriate number of columns in the range, but my script only finds 1 cell of data. When I check the log, I see that my 2D array looks normal in both cases - 8 phone numbers in an array wrapped in another array.
What is causing this error? I cannot find reference to similar errors on SO or elsewhere.
Also, please note that I'm aware this code is a little wonky (weird variables and two for loops where one would do). I've been troubleshooting this for a couple hours and was originally using setValue instead of setValues. While trying to debug it, things got split up and moved around a lot.
The dimension of your range is one row and several columns
If you push an array into another array, the dimension will be [[...],[...],[...]] - i.e. you have one column and multiple rows
What you want instead is one row and multiple columns: [[...,...,...]]
To achieve this you need to create a two-dimensional array and push all entries into the first row of your array: phoneArray[0]=[]; phoneArray[0].push(...);
Sample:
var phoneArray = [];
phoneArray[0]=[];
for(i = 0; i < keys; i++) {
var phoneNumber = response.call[i].caller.caller_id;
phoneNumber = phoneNumber.replace(/-/g,'');
phoneArray[0].push(phoneNumber);
}
var range = sheet.getRange(1,8,1, keys);
range.setValues(phoneArray);
So I figured out how to make this work, though I can't speak to why the error is occurring, or rather why one receives reversed error messages depending on the setRange value.
Rather than pushing the whole list of values from the API to phoneArray, I structured my first for loop to reset the value of phoneArray each loop and push a single value array to my arrayWrapper, like so:
for(i = 0; i < keys; i++) {
var phoneArray = [];
var phoneNumber = response.call[i].caller.caller_id;
phoneNumber = phoneNumber.replace(/-/g,'');
phoneArray.push(phoneNumber);
arrayWrapper.push(phoneArray);
}
Note that I also edited the formatting of the phone numbers to suit my needs, so I pulled each value into a variable to make replacing a character simple. What this new for loop results in is a 2D array like so:
[[1235556789],[0987776543],[0009872345]]
Rather than what I had before, which was like this:
[[1235556789,0987776543,0009872345]]
It would appear that this is how the setValues method wants its data structured, although the documentation suggests otherwise.
Regardless, if anyone were to run into similar issues, this is the gist of what must be done to fix it, or at least the method I found worked. I'm sure there are far more performant and elegant solutions than mine, but I will be dealing with dozens of rows of data, not thousands or millions. Performance isn't a big concern for me.
var correct = [[data],[data]] -
is the data structure that is required for setValues()
therefore
?.setValues(correct)

Loop over a couple of columns

very beginning with Google sheet.
I managed to create the function I need:
function happy() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('C5').activate();
spreadsheet.getCurrentCell().setFormula('=G4');
spreadsheet.getRange('H4').activate();
var current_e15=spreadsheet.getRange('E15').getValue();
spreadsheet.getCurrentCell().setValue(current_e15);
spreadsheet.getRange('C5').activate();
spreadsheet.getCurrentCell().setFormula('=G5');
spreadsheet.getRange('H5').activate();
var current_e15=spreadsheet.getRange('E15').getValue();
spreadsheet.getCurrentCell().setValue(current_e15);
...
...
...
The function is reading from the G[i] column, assign it to C5, read the result produced by the new value in E15 and put that value in the cell H[i].
Obviously this can be done over a loop over i instead of copy past this same block and changing G4 by G5 and H4 by H5 etc..
I just don't know how to say it in this langage.
I tried this that is obviously not working.
function happyloop() {
var spreadsheet = SpreadsheetApp.getActive();
var CellsG = spreadsheet.getRange('G4:G15');
var CellsH = spreadsheet.getRange('H4:H15');
for (var i = 0; i < CellsG.length; i++) {
spreadsheet.getRange('C5').activate();
spreadsheet.getCurrentCell().setFormula('=CellsG[i]');
spreadsheet.getRange(CellsH[i]).activate();
var current_e15=spreadsheet.getRange('E15').getValue();
spreadsheet.getCurrentCell().setValue(current_e15);
}
};
I'd be glad to get help with how to properly this function in google sheet language.
Thanks if you can help :)
I finally get to this that is working.
If there is a more elegant way to do it please correct me.
Thank you
function happyloop() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheets()[0];
for (var i = 4; i < 16; i++) {
sheet.getRange('C5').activate();
var current_Gi=sheet.getRange(i,7).getValue();
sheet.getCurrentCell().setValue(current_Gi);
sheet.getRange(i,8).activate();
var current_e15=sheet.getRange('E15').getValue();
sheet.getCurrentCell().setValue(current_e15);
}
}
I took your working answer and made minor changes in it to achieve a more elegant way as you requested. The original scripts runs in 4.5 seconds, while this modified version runs in 2.5 seconds. This is the optimized version:
function happyLoop() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheets()[0];
var C5 = sheet.getRange('C5');
var E15 = sheet.getRange('E15').getValue();
for (var i = 4; i < 16; i++) {
C5.setValue(sheet.getRange(i, 7).getValue());
sheet.getRange(i, 8).setValue(E15);
}
}
I changed the variable names to make it clearer to me, but that isn't a speed improvement. To achieve more speed I took out of the for loop everything that doesn't depend on the i iterator. Notice also how I nested each method call.
To improve this code even more I'll have to understand your original scenario. A big possible improvement is to use the .setValues just once in the full code, because it is the slowest method and here it gets called in every iteration. Finally, I want to share the Apps Script best practices with you. Please, don't hesitate to ask me for deeper clarification.

Google Apps Script Replace and update cell within a range

I have a Google spreadsheet that I'm trying to remove the word "woo" within a range of cells
So far I've managed to loop through the results and log the results, however I haven't figured how to update that information in the spreadsheet itself.
Any guidance would be welcomed
Thank you
function myFunction () {
var ss = SpreadsheetApp.getActiveSheet().getRange('B:B')
var data = ss.getValues();
for (var i = 0; i < data.length; i++) {
var text = data[i].toString();
var finaltext = text.replace(/woo/g, "");
data[i] = finaltext;
Logger.log(data[i]);
}
}
Use setValues()
Notes:
Usually ss is used as a shorthand for spreadsheet, as it's used on the code for a range it's better to use range as a variable name.
setValues() returns a 2D array, so data[i] returns an array of row values rather than a cell value. To get/set cell values, use data[i][0] notation.
Considering the above replace
var ss = SpreadsheetApp.getActiveSheet().getRange('B:B')
by
var range = SpreadsheetApp.getActiveSheet().getRange('B:B')
then add the following line after the for block.
range.setValues(data);
Regarding text var declaration, replace
var text = data[i].toString();
to
var text = data[i][0].toString();
Using open ended references like B:B could lead to problems. To avoid them be sure to keep the sheet rows at minimum or better instead of using an open ended reference use something like B1:B10.

Store formatting information in an array then apply it to a range

I'm trying to create a script that will automatically format a selection based on the formatting of a table in another sheet. The idea is that a user can define a table style for header, rowOdd and rowEven in the Formats sheet, then easily apply it to a selected table using the script.
I've managed to get it working, but only by applying one type of formatting (background colour).
I based my code for reading the code into an array on this article.
As you will hopefully see from my code below, I am only able to read one formatting property into my array.
What I would like to do is read all formatting properties into the array, then apply them to the range in one go. I'm new to this so sorry if my code is a mess!
function formatTable() {
var activeRange = SpreadsheetApp.getActiveSpreadsheet().getActiveRange(); //range to apply formatting to
var arr = new Array(activeRange.getNumRows());
var tableStyleSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Formats"); //location of source styles
var tableColours = {
header: tableStyleSheet.getRange(1, 1, 1).getBackground(),
rowEven: tableStyleSheet.getRange(2, 1, 1).getBackground(),
rowOdd: tableStyleSheet.getRange(3, 1, 1).getBackground()
}
for (var x = 0; x < activeRange.getNumRows(); x++) {
arr[x] = new Array(activeRange.getNumColumns());
for (var y = 0; y < activeRange.getNumColumns(); y++) {
x == 0 ? arr[x][y] = tableColours.header :
x % 2 < 1 ? arr[x][y] = tableColours.rowOdd : arr[x][y] = tableColours.rowEven;
Logger.log(arr);
}
}
activeRange.setBackgrounds(arr);
}
Thanks!
I might be wrong but based from the list of methods given in Class Range, feature to save or store formatting details currently do not exist yet.
However, you may want to try using the following:
copyFormatToRange(gridId, column, columnEnd, row, rowEnd) or copyFormatToRange(sheet, column, columnEnd, row, rowEnd) wherein it copies the formatting of the range to the given location.
moveTo(target) wherein it cuts and paste (both format and values) from this range to the target range.
Did you know that you can get all of the different formatting elements for a range straight into an array?
E.g.
var backgrounds = sheet.getRange("A1:D50").getBackgrounds();
var fonts = sheet.getRange("A1:D50").getFontFamilies();
var fontcolors = sheet.getRange("A1:D50").getFontColors();
etc.
However, there's no way to get all of the formatting in one call unfortunately, so you have to handle each element separately. Then you can apply all of the formats in one go:
targetRng.setFontColors(fontcolors);
targetRng.setBackgrounds(backgrounds);
and so on.

Resources