Why does the looping in Google Sheet App Script stop in middle of the looping process and the rest of the codes do not run? - for-loop

I tried the "for loop" and "do ... while ...", both of them stopped in the middle of the looping process, and the rest of the codes, which come after the loop, did not run. This becomes an issue when I loop through hundreds of rows.
I know that the use of array is a better solution as the code execution is faster, but I have a difficulty in setting the borders in batch as there is no ".setBorders(Array)" function in Google Sheets.
The sheet provided here has been simplified only to show the looping issues. The actual sheet is written to automatically create hundreds of tables with different values, font weights and horizontal alignments.
What I want to do:
Choose the option "Yes" to start the looping (the current row of looping is also tracked and recorded in "CURRENT ROW" and the "STATUS" shows "Processing ...")
The program will check the "SCORE" column, if "SCORE" is empty (""), than the font weight for that row is set to "bold", else, the font weight is set to "normal"
If the looping is done until the last row, the "STATUS" shows "Done".
The following is the copy of the Google App Script:
let app = SpreadsheetApp;
let ss = app.getActiveSpreadsheet();
let activeSheet = ss.getActiveSheet();
let sheetName = activeSheet.getName();
let sheet1 = ss.getSheetByName("Sheet1");
let loopOptionRange = sheet1.getRange(4, 4);
let loopOption = loopOptionRange.getValue();
let loopStatusRange = sheet1.getRange(4, 7);
function onEdit(e) {
}
function onOpen() {
loopOptionRange.setValue("Choose");
sheet1.getRange(4, 5).setValue(5);
loopStatusRange.setValue("");
}
function loopTest() {
const startRow = 4; //table head
const lastRow = sheet1.getLastRow();
sheet1.getRange(4, 6).setValue(lastRow);
const startRowLoop = startRow + 1; //first row of looping
try {
for (i = startRowLoop; i <= lastRow; i++) {
const testStatus = sheet1.getRange(i, 3).getValue();
sheet1.getRange(4, 5).setValue(i);
if (testStatus == "") {
sheet1.getRange(i, 1, 1, 2).setFontWeight("bold");
} else {
sheet1.getRange(i, 1, 1, 3).setFontWeight("normal");
}
}
loopStatusRange.setValue("Done");
loopOptionRange.setValue("Choose");
} catch (error) {
app.getUi().alert(`An error occured.`);
}
}
if (sheetName === "Sheet1"){
if (loopOption == "Yes") {
loopStatusRange.setValue("Processing ...");
loopTest();
} else if (loopOption === "Cancel") {
loopOptionRange.setValue("Choose");
}
}
LOOP TEST - Google Sheets file

When I saw your script, getValue, setValue and setFontWeight are used in a loop. In this case, the process cost will become high. I thought that this might be the reason for your issue. In order to reduce the process cost of your script, how about the following modification?
From:
for (i = startRowLoop; i <= lastRow; i++) {
const testStatus = sheet1.getRange(i, 3).getValue();
sheet1.getRange(4, 5).setValue(i);
if (testStatus == "") {
sheet1.getRange(i, 1, 1, 2).setFontWeight("bold");
} else {
sheet1.getRange(i, 1, 1, 3).setFontWeight("normal");
}
}
To:
const range = sheet1.getRange(startRow, 3, lastRow - startRowLoop);
const values = range.getDisplayValues().map(([c]) => [c ? null : "bold"]);
range.offset(0, -1).setFontWeights(values);
Note:
About Is it really because of "the high process cost" you mentioned in your answer?, when I saw your script for the 1st time, I thought that the reason for your issue might be due to the process cost. Because, when the script is run by OnEdit trigger, the maximum execution time is 30 seconds. And, when I tested your script and your Spreadsheet, when I saw the log after the script was stopped, an error related to the over of the maximum execution time. By this, I resulted that the reason for your issue is due to the process cost of the script.
References:
map()
setFontWeights(fontWeights)

Related

Add a status to each proccesed row in for loop that has an if statement with Google App Script

I've got a for loop in App script that is looking only at rows that have data in two columns. I'd like to set a status on each row that is actually processed, but the statuses get added to the wrong rows. When I add to i it adds to the whole length of the array, so I guess I shouldn't be trying to process each row, what am I doing wrong?
function auditReport() {
var sheetname = "Sheet1"; // name of data sheet ex. Form Responses 1
var colstoworkon = 10; // how many cols are filled with data f.e. by a form
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3,1,sheet.getLastRow()-1,colstoworkon).getValues(); // starting with row 2 and column 1 as our upper-left most column,
//This makes it loops continuously and checks all not done rows
for (var i in data) {
if(data[i][1] && data[i][2]){//if email or copy are undefined just skip
var setStatus = sheet.getRange(i,4).setValue("done")
} // end of if
} // End of Loop
} //End of email function
Modification points:
In your script, from getRange(3,1,sheet.getLastRow()-1,colstoworkon), in this case, it is required to be getRange(3,1,sheet.getLastRow()-2,colstoworkon).
In the case of for (var i in data) {, i is the string type.
When you want to use sheet.getRange(i,4).setValue("done"), it is required to be sheet.getRange(Number(i) + 3, 4).setValue("done").
I thought that this might be the reason of your issue of but the statuses get added to the wrong rows..
In the case of if (data[i][1] && data[i][2]) {, if the value is 0, data[i][1] && data[i][2] is false.
When these points are reflected to your script, it becomes as follows.
Modified script:
function auditReport() {
var sheetname = "Sheet1";
var colstoworkon = 10;
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3, 1, sheet.getLastRow() - 2, colstoworkon).getDisplayValues();
for (var i in data) {
if (data[i][1] && data[i][2]) {
var setStatus = sheet.getRange(Number(i) + 3, 4).setValue("done");
}
}
}
Or, your script can be also modified as follows. In this modification, done is put using the range list. By this, the process cost can be reduced a little.
function auditReport() {
var sheetname = "Sheet1";
var colstoworkon = 10;
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName(sheetname));
var sheet = ss.getSheetByName(sheetname);
var data = sheet.getRange(3, 1, sheet.getLastRow() - 2, colstoworkon).getDisplayValues();
var ranges = data.map(([,b,c], i) => b && c ? `D${i + 3}` : "").filter(String);
if (ranges.length == 0) return;
sheet.getRangeList(ranges).setValue("done");
}
References:
for...in
getRangeList(a1Notations)

For loop not looping through spreadsheet

I made a small search box to retrieve values from a datasheet. For some reason it only loops once. Also this is the first time I'm trying something in google sheets, so if I want to search a string in column B, then the SEARCH_COL_IDX should be 1 right?
I put in a few loggers to check the process. I do see Logger.log(str), Logger.log("Check") and Logger.log("Check2). The latter two only once, where it should be multiple times imo. The Logger.log(row[#]) I don't get to see at all, which means it doesn't find anything.
I also tried doing if(row[SEARCH_COL_IDX] = str) { to see where it is looking. Than the Logger.log(row[#]) do come back, but from different rows AND columns. I am doing something wrong, but I can't find it.. any suggestions?
var SPREADSHEET_NAME = "Database";
var SEARCH_COL_IDX = 1;
var RETURN_COL_IDX= 1;
function searchStr(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var formSS = ss.getSheetByName("Userform");
var str = formSS.getRange("d17").getValue();
Logger.log(str);
var values = ss.getSheetByName("Database").getDataRange().getValues();
for (var i = 0; i < values.length; i++) {
Logger.log("check");
var row = values[i];
Logger.log("check2");
if(row[SEARCH_COL_IDX] == str) {
formSS.getRange("d3").setValue(row[0]);
Logger.log(row[0]);
formSS.getRange("d5").setValue(row[1]);
Logger.log(row[1]);
formSS.getRange("d7").setValue(row[2]);
Logger.log(row[2]);
formSS.getRange("d9").setValue(row[3]);
Logger.log(row[3]);
formSS.getRange("d11").setValue(row[4]);
Logger.log(row[4]);
formSS.getRange("d13").setValue(row[5]);
Logger.log(row[5]);
}
Logger.log("nothing found");
return row[RETURN_COL_IDX];
}
Your function has a return command in the last line of your loop block.

GSheet Script: How to Optimize my Row and Sheet Iterator

Long story short, I have this bit of Google Script that clears content automatically in a GSheet. It is set on a trigger and it works...the code does what it's supposed to do. The issue is that it runs slow. It takes 2 to 3 minutes for the iterator to run. To help you scope the size of the task: there is 150 rows on each of the 8 sheets.
The objective of the code is to clear a number of rows on each sheet based on the value of the cell in the first column of a row.
So I would like to know if anyone has any insight or suggestion to improve the running time. I understand my method of using a for loop checks rows one by one, and that's a time-consuming task. I couldn't think of an alternate method with arrays or something?
Thanks all!
Here's the code:
function Reset_Button() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 1; i < sheets.length ; i++ ) {
var sheet = sheets[i];
sheet.getRange("C2").setValue(new Date());
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 1);
for ( j = 1 ; j < lastRow ; j++){
var value = sheet.getRange(j,1).getValue()
if(value === 0){
sheet.getRange(j,2,1,5).clearContent()
}}}}
Typically you want to do as few writes to the spreadsheet as possible. Currently your code goes through each line and edits it if necessary. Instead get the entire data range you will be working with into one variable (let's say dRange and use .getValues() to get a 2d array of all the values into a second variable (let's say dValues). Then simply iterate over dValues, setting a blank "" in each you want to clear. Once you are done going over all values, just do a dRange.setValues(dValues) (that's why I said to keep the range in a separate variable). So as an example, the following will clear columns B through F if column A has a 0
function test(){
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 1; i <sheets.length; i++) {
sheets[i].getRange("C2").setValue(new Date());
var dRange = sheets[i].getDataRange();
var dValues = dRange.getValues();
for (var j = 1; j < dRange.getLastRow(); j++){
if (dValues[j][0] == 0) {
for (var c = 1; c < 6; c++) {
dValues[j][c] = ""
}
}
}
dRange.setValues(dValues);
}
}
For a single sheet of ~170 rows this takes a few seconds. One thing to note is that I wrote it based on your script, you set a date value in C2 however in your sript (and thus in the one I wrote based on yours) that falls within the range you are checking to be cleared, so double check your ranges

How to repeat the values until next non-empty row and then repeat next row's data and so on with Google Apps Script?

The challenge here is to capture the customer ID and its name and repeat it down below until the script reaches the next non-empty row. Then, the next ID and customer name are the ones that will be repeated.
This is just how far I've gotten on my own, but I can't build the core of the logic mylself (yes).
function repeatValues() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
const originData = sheet.getRange(6, 1, sheet.getLastRow(), 3).getValues();
const targetSheet = ss.getSheetByName("Sheet2");
for (var a = 0; a < originData.length; a++) {
if (originData[a][0] != ''){
var customerID = originData[a][0];
var customer = originData[a][1];
}
if (originData == ''){
targetSheet.getRange(2,1,originData.length, originData.length).setValue(customerID);
}
}
}
Here's a link to the sample spreadsheet: https://docs.google.com/spreadsheets/d/1wU3dql6dnh_JBC6yMkrFzEYxdyzyNdkBmve2pfyxG0g/edit?usp=sharing
Expected Result includes these values in blue repeated:
Any help is appreciated!
I believe your goal as follows.
You want to achieve the following situation. (The following image is from your question.) You want to add the texts of blue font color.
You want to put the values to "Sheet2".
Modification points:
In your script, at const originData = sheet.getRange(6, 1, sheet.getLastRow(), 3).getValues();, the start row is 6. In this case, please modify sheet.getLastRow() to sheet.getLastRow() - 5
In order to create an array for putting to "Sheet2", it is required to check the columns "A" and "B" of "Sheet1". For this, in this modification, I used temp as follows.
When above points are reflected to your script, it becomes as follows.
Modified script:
function repeatValues() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
const originData = sheet.getRange(6, 1, sheet.getLastRow() - 5, 3).getValues(); // Modified
const targetSheet = ss.getSheetByName("Sheet2");
// I added below script.
let temp = ["", ""];
const putValues = originData.map(([a, b, c]) => {
if (a.toString() && b.toString() && temp[0] != a && temp[1] != b) {
temp = [a, b];
}
return temp.concat(c);
});
targetSheet.getRange(2, 1, putValues.length, putValues[0].length).setValues(putValues);
}
Note:
This modified script is for your sample Spreadsheet. So when the structure of Spreadsheet is changed, the script might not be able to be used. Please be careful this.
Reference:
map()

How to reduce execution time of this Google Apps Script?

I wrote a script that gets a rows data from a spreadsheet and loops through them, calling a function to send an SMS if the rows' data meets certain conditions (having a phone number and not having already been sent for example).
However after adding about 600 rows, the script execution time exceeds it's limit, that seems to be 5 minutes according to my research. I'm using JavaScript objects to read data and a for loop to iterate through the rows.
Can anyone tel me if it is possible to make it faster? I'm very new to programming but this seems such a light task for all this computing power that I can't understand why it takes so long
Thanks in advance!
Here's the code of the function I'm using:
// Will send SMS on the currently active sheet
function sendSms() {
// Use the send sms menu to trigger reconcile
var user = ScriptProperties.getProperty(PROPERTY_USER_RECONCILE);
if (user == null)
reconcileUser();
// The sheets
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Registo");
var settingsSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
// Fetch values for each row in the Range.
var startRow = 2;
var apiKey = settingsSheet.getRange("B2").getValue();
var apiSecret = settingsSheet.getRange("B3").getValue();
var prefix = settingsSheet.getRange("B4").getValue();
var numRows = sheet.getMaxRows() - 1;
var numCols = 16;
var statusColNum = 15; // IMPT: To keep track status in col 15
var dataRange = sheet.getRange(startRow, 1, numRows, numCols);
// Make sure there is API key and secret
if (apiKey == "" || apiSecret == "") {
Browser.msgBox("You MUST fill in your API key and secret in Settings sheet first!");
return;
}
// Create one JavaScript object per row of data.
var objects = getRowsData(sheet, dataRange);
var totalSent = 0;
for (var i = 0; i < objects.length; ++i) {
// Get a row object
var rowData = objects[i];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("SMS Modelo");
var template = templateSheet.getRange("A1").getValue();
// jump loop iteration if conditions not satisied
if (rowData.resolv == "x" || rowData.contactoUtente == null || rowData.contactoUtente == "" || rowData.reserv == null || rowData.reserv == "" || rowData.cont == "x" || rowData.sms !== null) continue;
var message = fillInTemplateFromObject(template, rowData);
var senderName = "Farm Cunha"
var mobile = rowData.contactoUtente;
// Send via Nexmo API
var response = nexmoSendSms(apiKey, apiSecret,"+351" + mobile, message, senderName);
if (response.getResponseCode() == 200) {
var object = JSON.parse(response.getContentText());
if (object.messages[0]['status'] == "0") {
// Set to QUEUE status - We assumed SENT, as we don't handle delivery status.
//sheet.getRange(startRow + i, statusColNum).setValue(STATUS_QUEUE);
sheet.getRange(startRow + i, statusColNum).setValue(STATUS_SENT);
// Set the reference id
sheet.getRange(startRow + i, 19).setValue(object.messages[0]['message-id']);
// sheet.getRange(startRow + i, statusColNum+3).setValue(new Date()); linha pode ser activada para fazer timestamp do envio
totalSent++;
}
else {
// If status is not 0, then it is an error.
// Set status to the error text
sheet.getRange(startRow + i, statusColNum).setValue(object.messages[0]['error-text']);
}
}
else {
// Non 200 OK response
sheet.getRange(startRow + i, statusColNum).setValue("Error Response Code: " + response.getResponseCode);
}
SpreadsheetApp.flush();
// Need a wait. Need to throttle else will have "Route Busy" error.
Utilities.sleep(2000);
}
// Update total sent
var lastTotalSent = parseInt(ScriptProperties.getProperty(PROPERTY_SMS_SENT_FOR_RECONCILE));
if (isNaN(lastTotalSent)) lastTotalSent = 0;
ScriptProperties.setProperty(PROPERTY_SMS_SENT_FOR_RECONCILE, (lastTotalSent + totalSent).toString());
Logger.log("Last sent: " + lastTotalSent + " now sent: " + totalSent);
reconcileApp();
}
You have a few things in your loop that are too time consuming : spreadsheet readings and API calls + 2 seconds sleep !.
I would obviously advise you to take these out of the loop (specially the template sheet reading that is always the same!). A possible solution would be to check the conditions from the row objects and to save the valid entries in an array... THEN iterate in this array to call the API.
If this is still too long then proceed by small batches, saving the end position of the partial iteration in scriptproperties and using a timer trigger that will continue the process every 5 minutes until it is completed (and kill the trigger at the end).
There are a few example of this kind of "mechanics" on this forum, one recent example I suggested is here (it's more like a draft but the idea is there)
Ok, I've solved it by taking these 3 lines out of the loop as Serge (thanks) had told me to:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("SMS Modelo");
var template = templateSheet.getRange("A1").getValue();
It's so simple that I don't know how I was not seeing that.
This simple change made the script much faster. For example, going through 600 rows would take more than 5 minutes. Now, more than 5000 rows only take seconds.

Resources