Google Spreadsheet App Script will not wait for results to load - javascript

I have a small script and what I'm trying to do is to write one value from 'Sheet 1' to 'Sheet 2'. Wait for the results to load and compare the cells to see if it is above 10% or not. I have some =importhtml functions in the spreadsheet and it takes along time to load. I've tried sleep, utilities sleep, and flush. None have been working, maybe because I might be putting it in the wrong area..
function compareCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var listSheet = ss.getSheetByName('Stocks');
var dataSheet = ss.getSheetByName('Summary');
var listSheetLastRow = listSheet.getLastRow();
var currRow = 1;
for (i = 1; i <= listSheetLastRow; i++) {
if (listSheet.getRange(1, 3).getValue() == 'asd') {
var ticker = listSheet.getRange(currRow, 1).getValue();
dataSheet.getRange(5, 4).setValue(ticker);
var value1 = dataSheet.getRange(15, 4).getValue();
var value2 = dataSheet.getRange(22, 4).getValue();
SpreadsheetApp.flush();
if (value1 > 0.10 && value2 > 0.10) {
listSheet.getRange(currRow, 8).setValue('True');
listSheet.getRange(currRow, 9).setValue(value1);
listSheet.getRange(currRow, 10).setValue(value2);
} else {
listSheet.getRange(currRow, 8).setValue('False');
}
} else {
Browser.msgBox('Script aborted');
return null;
}
currRow++;
}
}

If it is not important that you use the =IMPORTHTML() function in your sheet, the easiest way to do this will be to use UrlFetchApp within Apps Script. Getting the data this way will cause your script to block until the HTML response is returned. You can also create a time-based trigger so your data is always fresh, and the user will not have to wait for the URL fetch when looking at your sheet.
Once you get the HTML response, you can do all of the same processing you'd do in Sheet1 within your script. If that won't work because you have complex processing in Sheet1, you can:
use UrlFetchpApp.fetch('http://sample.com/data.html') to retrieve your data
write the data to Sheet1
call SpreadsheetApp.flush() to force the write and whatever subsequent processing
proceed as per your example above
By handling these steps sequentially in your script you guarantee that your later steps don't happen before the data is present.

I had a similar problem but came up with a solution which uses a while loop which forces the script to wait until at least 1 extra column or 1 extra row has been added. So for this to work the formula needs to add data to at least one extra cell other than the one containing the formula, and it needs to extend the sheet's data range (number of rows or columns), for example by adding the formula to the end of the sheet, which looks like what you are doing. Every 0.5 seconds for 10 seconds it checks if extra cells have been added.
dataSheet.getRange(5, 4).setValue(ticker);
var wait = 0;
var timebetween = 500;
var timeout = 10000;
var lastRow = dataSheet.getLastRow();
var lastColumn = dataSheet.getLastColumn();
while (dataSheet.getLastColumn() <= lastColumn && dataSheet.getLastRow() <= lastRow){
Utilities.sleep(timebetween);
wait += timebetween;
if (wait >= timeout){
Logger.log('ERROR: Source data for ' + ticker + ' still empty after ' + timeout.toString() + ' seconds.');
throw new Error('Source data for ' + ticker + ' still empty after ' + timeout.toString() + ' seconds.');
}
}

In case if you are getting these two values (
var value1 = dataSheet.getRange(15, 4).getValue();
var value2 = dataSheet.getRange(22, 4).getValue();
) after the =importhtml call, you have to add sleep function before these two lines of code.
You also can have a loop until you get some values into the range from =importhtml call and add some sleep in the loop. Also note that as of April 2014 the limitation of script runtime is 6 minutes.
I also found this link which might be helpful.
Hope that helps!

Related

How to write (SetValues) to a Google Sheet using a filtered forEach loop?

I've been trying for hours to make the following Google Apps Script work. What it needs to do, is send emails (from an html-template) to anyone that:
has a complete Event Schedule (which is completed if they have been
assigned to at least 4 events, which is counted in column Q);
has NOT been sent an email earlier (which is kept track of in column
R);
The script keeps track of errors in column S, i.e. if there's no email address provided.
It appears it only works:
if I comment out
data = data.filter(function(r){ return r[17] == true & r[16] > 3});
or if I comment out
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors);
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
How can I get this script to work properly?
A copy of the Google Sheet I'm referring to is this one:
https://docs.google.com/spreadsheets/d/1sbOlvLVVfiQMWxNZmtCLuizci2cQB9Kfd8tYz64gjP0/edit?usp=sharing
This is my code so far:
function SendEmail(){
var voornaam = 3;
var achternaam = 4;
var email = 5;
var event1 = 9;
var event2 = 10;
var event3 = 11;
var event4 = 12;
var event5 = 13;
var event6 = 14;
var event7 = 15;
var emailTemp = HtmlService.createTemplateFromFile("email");
var ws = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Events Day 1");
var datum = ws.getRange(1,3).getValue();
var spreker = ws.getRange(1,6).getValue();
var data = ws.getRange("A3:R" + ws.getLastRow()).getValues();
data = data.filter(function(r){ return r[17] == false && r[16] > 3}); //Either this needs to be commented out...
let errors = [];
let mailSucces = [];
data.forEach(function(row){
try{
emailTemp.voornaam = row[voornaam];
emailTemp.email = row[email];
emailTemp.datum = datum;
emailTemp.spreker = spreker;
emailTemp.event1 = row[event1];
emailTemp.event2 = row[event2];
emailTemp.event3 = row[event3];
emailTemp.event4 = row[event4];
emailTemp.event5 = row[event5];
emailTemp.event6 = row[event6];
emailTemp.event7 = row[event7];
var htmlMessage = emailTemp.evaluate().getContent();
GmailApp.sendEmail(
row[email],
"Here you go! Your personal schedule for the event of " + datum,
"Your emailprogramm doesn't support html.",
{
name: "Event Organisation Team", htmlBody: htmlMessage, replyTo: "info#fakeemail.com"
});
errors.push([""]);
mailSucces.push(["TRUE"]);
}
catch(err){
errors.push(["Error: no message sent."]);
mailSucces.push(["False"]);
}
}); //close forEach
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors); //or this and the next line need to be commented out.
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
}
Edit I have been trying and thinking en trying... but still haven't found out how to make it work. But I also got understanding of why it's not working; I just don't know how to get it fixed.
Let me elaborate on the problem a bit more:
The problem is, that within the forEach loop the range is a filtered variant of the data, pulled from the spreadsheet with getValues. Therefore, writing data back with ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces); results in mismatched checkmarks in te spreadsheet.
So, somehow I need to put the range of the previous used filter data = data.filter(function(r){ return r[17] == false & r[16] > 3}); in a variable...? I guess?
Furthermore, I don't think it's wise to use setValue within the loop, because (from what I understand from my searching on the topic) this results in a slow script, because every loop the script makes an API call to write in the spreadsheet. Hence the errors.push and mailSucces.push, and my attempt to do a setValue at the end, after the loop is finished.
Can someone help me to finish this problem?
The problem is different size of the range you write to and data you are writing in.
Try replacing:
ws.getRange("S3:S" + ws.getLastRow()).setValues(errors);
ws.getRange("R3:R" + ws.getLastRow()).setValues(mailSucces);
With:
ws.getRange(3, 19, errors.length, 1).setValues(errors);
ws.getRange(3, 18, mailSucces.length, 1).setValues(mailSucces);
You should use this variation of getRange
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow,-column,-numrows,-numcolumns
Your data has non-fixed number of rows and fixed number of columns (1). In general case your data will be matrix of X rows and Y columns. For that purpose you can make it completely dynamic:
sheet.getRange(startRow, startColumn, data.length, data[0].length)
Just make sure data.length is > 0 before you do this, otherwise data[0].length will break.
Edit:
I started writing a comment but it got too long. There are couple of things that may go wrong with sending emails. First thing I noticed is that you use & in filter, but in AppsScript/JavaScript/C-like-languages, you should use && for logical AND. Now the email: you only detect the code break with the catch block. At this point you don't know why the code breaks it could be anything. With GmailApp I recommend you to use createDraft while developing, then when all ok replace it with sendEmail for the final version, both functions have the exact same parameters, thank you Google devs ;-).
To find out the exact problem you should get the error message on break and display it. err.stack should tell you pretty much everything:
catch(err){
Logger.log(err.stack); // Added
errors.push(["Error: no message sent."]);
mailSucces.push(["False"]);
}
Run the sendEmail function from the code editor and you should see the Log for each catch(err) pass.

Advice on how to optimise Google AppScript code [duplicate]

I've just written my first google apps scripts, ported from VBA, which formats a column of customer order information (thanks to you all of your direction).
Description:
The code identifies state codes by their - prefix, then combines the following first name with a last name (if it exists). It then writes "Order complete" where the last name would have been. Finally, it inserts a necessary blank cell if there is no gap between the orders (see image below).
Problem:
The issue is processing time. It cannot handle longer columns of data. I am warned that
Method Range.getValue is heavily used by the script.
Existing Optimizations:
Per the responses to this question, I've tried to keep as many variables outside the loop as possible, and also improved my if statements. #MuhammadGelbana suggests calling the Range.getValue method just once and moving around with its value...but I don't understand how this would/could work.
Code:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getRange("A:A").getLastRow();
var row, range1, cellValue, dash, offset1, offset2, offset3;
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
range1 = s.getRange(row + 1, 1);
//if cell substring is number, skip it
//because substring cannot process numbers
cellValue = range1.getValue();
if (typeof cellValue === 'number') {continue;};
dash = cellValue.substring(0, 1);
offset1 = range1.offset(1, 0).getValue();
offset2 = range1.offset(2, 0).getValue();
offset3 = range1.offset(3, 0).getValue();
//if -, then merge offset cells 1 and 2
//and enter "Order complete" in offset cell 2.
if (dash === "-") {
range1.offset(1, 0).setValue(offset1 + " " + offset2);
//Translate
range1.offset(2, 0).setValue("Order complete");
};
//The real slow part...
//if - and offset 3 is not blank, then INSERT CELL
if (dash === "-" && offset3) {
//select from three rows down to last
//move selection one more row down (down 4 rows total)
s.getRange(row + 1, 1, lastRow).offset(3, 0).moveTo(range1.offset(4, 0));
};
};
}
Formatting Update:
For guidance on formatting the output with font or background colors, check this follow-up question here. Hopefully you can benefit from the advice these pros gave me :)
Issue:
Usage of .getValue() and .setValue() in a loop resulting in increased processing time.
Documentation excerpts:
Minimize calls to services:
Anything you can accomplish within Google Apps Script itself will be much faster than making calls that need to fetch data from Google's servers or an external server, such as requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Look ahead caching:
Google Apps Script already has some built-in optimization, such as using look-ahead caching to retrieve what a script is likely to get and write caching to save what is likely to be set.
Minimize "number" of read/writes:
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes.
Avoid alternating read/write:
Alternating read and write commands is slow
Use arrays:
To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Slow script example:
/**
* Really Slow script example
* Get values from A1:D2
* Set values to A3:D4
*/
function slowScriptLikeVBA(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
for(var row = 1; row <= 2; row++){
for(var col = 1; col <= 4; col++){
var sourceCellRange = sh.getRange(row, col, 1, 1);
var targetCellRange = sh.getRange(row + 2, col, 1, 1);
var sourceCellValue = sourceCellRange.getValue();//1 read call per loop
targetCellRange.setValue(sourceCellValue);//1 write call per loop
}
}
}
Notice that two calls are made per loop(Spreadsheet ss, Sheet sh and range calls are excluded. Only including the expensive get/set value calls). There are two loops; 8 read calls and 8 write calls are made in this example for a simple copy paste of 2x4 array.
In addition, Notice that read and write calls alternated making "look-ahead" caching ineffective.
Total calls to services: 16
Time taken: ~5+ seconds
Fast script example:
/**
* Fast script example
* Get values from A1:D2
* Set values to A3:D4
*/
function fastScript(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
var sourceRange = sh.getRange("A1:D2");
var targetRange = sh.getRange("A3:D4");
var sourceValues = sourceRange.getValues();//1 read call in total
//modify `sourceValues` if needed
//sourceValues looks like this two dimensional array:
//[//outer array containing rows array
// ["A1","B1","C1",D1], //row1(inner) array containing column element values
// ["A2","B2","C2",D2],
//]
//#see https://stackoverflow.com/questions/63720612
targetRange.setValues(sourceValues);//1 write call in total
}
Total calls to services: 2
Time taken: ~0.2 seconds
References:
Best practices
What does the range method getValues() return and setValues() accept?
Using methods like .getValue() and .moveTo() can be very expensive on execution time. An alternative approach is to use a batch operation where you get all the column values and iterate across the data reshaping as required before writing to the sheet in one call. When you run your script you may have noticed the following warning:
The script uses a method which is considered expensive. Each
invocation generates a time consuming call to a remote server. That
may have critical impact on the execution time of the script,
especially on large data. If performance is an issue for the script,
you should consider using another method, e.g. Range.getValues().
Using .getValues() and .setValues() your script can be rewritten as:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getLastRow(); // more efficient way to get last row
var row;
var data = s.getRange("A:A").getValues(); // gets a [][] of all values in the column
var output = []; // we are going to build a [][] to output result
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
var cellValue = data[row][0];
var dash = false;
if (typeof cellValue === 'string') {
dash = cellValue.substring(0, 1);
} else { // if a number copy to our output array
output.push([cellValue]);
}
// if a dash
if (dash === "-") {
var name = (data[(row+1)][0]+" "+data[(row+2)][0]).trim(); // build name
output.push([cellValue]); // add row -state
output.push([name]); // add row name
output.push(["Order complete"]); // row order complete
output.push([""]); // add blank row
row++; // jump an extra row to speed things up
}
}
s.clear(); // clear all existing data on sheet
// if you need other data in sheet then could
// s.deleteColumn(1);
// s.insertColumns(1);
// set the values we've made in our output [][] array
s.getRange(1, 1, output.length).setValues(output);
}
Testing your script with 20 rows of data revealed it took 4.415 seconds to execute, the above code completes in 0.019 seconds

Looping through data and setting IndexMatch formula based on cell value - too slow because of .getValue()

I have a code which loops through my rows and depending on what the value is in Column 2, returns different types of IndexMatch formulas. My code works very well and puts the formulas in the relevant columns, however it is very slow.
I get a message that .getValue() is very heavy and therefore slows down the code.
Now I need to figure out how to improve the speed of the code but since I am new to coding, I am not particularly sure how to go at this problem.
Here is what I have:
function setFormulas() {
var ss = SpreadsheetApp.getActiveSpreadsheet(); //gets the workbook being used
var sheet = ss.getSheetByName("Open Requests"); //gets the active worksheet
var sheetnametoWatch ="Open Requests"; //defines sheet
var columnnumbertoWatch = 14
var valuetoWatch = ""
var CostCenter = "'Open Requests'!"
var lastrow = sheet.getLastRow();
var datarange = sheet.getRange(11,2,lastrow -1,50).getValues()
var row = 10
for (i=0;i<datarange.length;i++){
var rowID = 10 + i + 1
if (datarange[i][0] == "CC"){
var addedCell = '=CONCATENATE(F'+rowID+',N'+rowID+',K'+rowID+')';
sheet.getRange(row + i+1,35).setFormula(addedCell);
var MatchedCell = '=INDEX(Input!$Q:$Q,MATCH('+CostCenter+'$AI$'+rowID+',Input!$K:$K,0))';
sheet.getRange(row + i+1,17).setFormula(MatchedCell);
sheet.getRange(row + i+1,18).setValue("kristin.j#ni.com")
}
else if (datarange[i][0] == "CC + TM1"){
var addedCell = '=CONCATENATE(F'+rowID+',N'+rowID+',K'+rowID+')';
sheet.getRange(row + i+1,35).setFormula(addedCell);
var MatchedCell = '=INDEX(Input!$Q:$Q,MATCH('+CostCenter+'$AI$'+rowID+',Input!$K:$K,0))';
sheet.getRange(row + i+1,17).setFormula(MatchedCell);
sheet.getRange(row + i+1,18).setValue("kristin.j#ni.com")
}
else if (datarange[i][0] == "GC"){
sheet.getRange(row + i+1,18).setValue("kristin.j#ni.com")
}
}
}
So as you can see, I am looping through the data and then depending on whether the column = CC, CC+TM1, GC it will set other cells equal to the relevant formula.
It would be great if somebody can help me in speeding up this process.
I'm having some trouble following exactly what the code does, but at a high level, I think what you want to do is this: instead of setting formulas for particular cells as you cycle through your for loop, use the loop to figure out which cells should be set to which formulas and store that information. Then, at the end of the for loop, call sheet.getRange(x,y).setFormula(Formula); just a few times, perhaps once for each formula.
Does that seem like it could work?

Concatenating strings in Google Apps Script

How do I get both values represented by i and j into the getSheetByName function?
Disclaimer: I am brand new at coding and am probably asking the wrong questions. My goal is to create a simple code that will delete sheets automatically by looping through the sheet names: Week 1, Week 2, etc.
Here's my code so far:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet()
var i = "Week "
var j = 32
var mysheet = sheet.getSheetByName(i&j)
sheet.deleteSheet(mysheet)
}
In your code you have written i&j that's not the syntax to concat in Google apps script. Instead you can simply use i + j.
For looping you'll need to use any loop, like for, while, do-while.
Combining these suggestions, here is my final suggestion.
Try something like this [By using 'try' here I mean, simply don't copy-n-paste and use. Try to understand and write your own code, curated for your specific need. Maybe that way, we'll grow/learn more]
function myFunction() {
var START_WEEK = 1; //Put your starting week count here
var END_WEEK = 2; //Put your ending week count here
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet()
var pre_name = "Week"
var i;
for (i = START_WEEK; i <= END_WEEK; i++) {
try {
spreadSheet.deleteSheet(spreadSheet.getSheetByName(pre_name + " " + i))
} catch (exp) {
//Maybe sheet with that name is not found
Logger.log(exp.toString());
}
}
}
This code loop through all sheet whose name start with "Week " and then followed by week count, up till the end week count and if that's found it's deleting them.
Make sure you put in START_WEEK and END_WEEK properly.
Let me know if this doesn't work for you or if you have any query.
Thanks

If/Else Statement not working to send different emails

I am trying to write a script in google sheets that will send one of two different emails based on the response to a multiple choice question. I can get my if/else statement to send either one or the other of the emails but it will not recognize the text of the multiple choice answer and send the correct email.
Here is the full script:
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 1;
// Fetch the range of cells A2:B3
var dataRange = sheet.getRange(startRow, 1, numRows, 8)
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var title = row[1]; // First column
var startDate = row[3]; // Second column
var endDate = row[4];
var description = row[2];
var location = row[6];
var eventImport = row[5];
var emailAddress = row[7];
var multchoice = row[8];
if (multchoice == "Part Time") {
var subject = "New Hire Part Time Email - " + startDate;
var emailBody = "Congradulations"
var htmlBody = "Congradulations! Part time person"
MailApp.sendEmail (emailAddress, subject, emailBody);
} else {
var subject = "New Hire Appointment - " + startDate;
var emailBody = "Congratulations! We are excited"
var htmlBody = "Congratulation! </i><br/> <br/> We are excited"
MailApp.sendEmail(emailAddress, subject, emailBody);
}
}
}
I believe the problem is here:
if (multchoice == "Part Time")
Any help is greatly appreciated! I am a novice
It looks like you are assigning your variables starting with '1' I stead of '0'. Start assigning them with 0 and counting up.
Without an example sheet to use, I won't be able to do a whole lot of debugging for you.
However, Apps Script comes with it's own debugger. Select the function you wish you debug and click the Little bug icon beside the play button.
Click on the sidebar where you want to set a breakpoint, where the code will stop executing.
Once it hits that breakpoint you can see all the variables currently within your scope. So the array, value, and i variables are visible to you.
Use this to your advantage and debug your code to find out where the issue is. Alternatively, you can use Logger.log() to log values at certain points within your code and then read back through the logs to try and determine where the problem lies.
The problem is not with your if/else statement. The problem is with how you are assigning your variables from your row[] array. While you use regular numbers in the getRange() function, the range that is returned is an array of those cells. Arrays always start with an index of [0]. Change var multchoice = row[8] to var multchoice = row[7] and your if/else statement will work (you'll want to change all of your other references, too).

Categories