Google Sheets Script "Exceeded maximum execution time" - javascript

I'm having issues trying to deal with the "Exceeded maximum execution time" error I get when running my script in Google sheets. I've found a few solutions on here that I couldn't get working with my script. Any help would be greatly appreciated, here is the script I am trying to modify:
function getGeocodingRegion() {
return PropertiesService.getDocumentProperties().getProperty('GEOCODING_REGION') || 'au';
}
function addressToPosition() {
// Select a cell with an address and two blank spaces after it
var sheet = SpreadsheetApp.getActiveSheet();
var cells = sheet.getActiveRange();
var addressColumn = 1;
var addressRow;
var latColumn = addressColumn + 1;
var lngColumn = addressColumn + 2;
var API_KEY = "xxx";
var options = {
muteHttpExceptions: true,
contentType: "application/json",
};
for (addressRow = 1; addressRow <= cells.getNumRows(); ++addressRow) {
var address = cells.getCell(addressRow, addressColumn).getValue();
var serviceUrl = "https://maps.googleapis.com/maps/api/geocode/json?address=" + address + "&key=" + API_KEY;
// Logger.log(address);
// Logger.log(serviceUrl);
var response = UrlFetchApp.fetch(serviceUrl, options);
if (response.getResponseCode() == 200) {
var location = JSON.parse(response.getContentText());
// Logger.log(response.getContentText());
if (location["status"] == "OK") {
//return coordinates;
var lat = location["results"][0]["geometry"]["location"]["lat"];
var lng = location["results"][0]["geometry"]["location"]["lng"];
cells.getCell(addressRow, latColumn).setValue(lat);
cells.getCell(addressRow, lngColumn).setValue(lng);
}
}
}
};
function positionToAddress() {
var sheet = SpreadsheetApp.getActiveSheet();
var cells = sheet.getActiveRange();
// Must have selected 3 columns (Address, Lat, Lng).
// Must have selected at least 1 row.
if (cells.getNumColumns() != 3) {
Logger.log("Must select at least 3 columns: Address, Lat, Lng columns.");
return;
}
var addressColumn = 1;
var addressRow;
var latColumn = addressColumn + 1;
var lngColumn = addressColumn + 2;
//Maps.setAuthentication("acqa-test1", "AIzaSyBzNCaW2AQCCfpfJzkYZiQR8NHbHnRGDRg");
var geocoder = Maps.newGeocoder().setRegion(getGeocodingRegion());
var location;
for (addressRow = 1; addressRow <= cells.getNumRows(); ++addressRow) {
var lat = cells.getCell(addressRow, latColumn).getValue();
var lng = cells.getCell(addressRow, lngColumn).getValue();
// Geocode the lat, lng pair to an address.
location = geocoder.reverseGeocode(lat, lng);
// Only change cells if geocoder seems to have gotten a
// valid response.
Logger.log(location.status);
if (location.status == 'OK') {
var address = location["results"][0]["formatted_address"];
cells.getCell(addressRow, addressColumn).setValue(address);
}
}
};
function generateMenu() {
var entries = [{
name: "Geocode Selected Cells (Address to Lat, Long)",
functionName: "addressToPosition"
}, {
name: "Geocode Selected Cells (Address from Lat, Long)",
functionName: "positionToAddress"
}];
return entries;
}
function updateMenu() {
SpreadsheetApp.getActiveSpreadsheet().updateMenu('Geocode', generateMenu())
};
/**
* Adds a custom menu to the active spreadsheet, containing a single menu item
* for invoking the readRows() function specified above.
* The onOpen() function, when defined, is automatically invoked whenever the
* spreadsheet is opened.
*
* For more information on using the Spreadsheet API, see
* https://developers.google.com/apps-script/service_spreadsheet
*/
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().addMenu('Geocode', generateMenu());
};
Or, any other script you may know of that does geocode in Google sheets and already properly handles max execution time that would be OK too, I'm not tied to this specific script, just getting the outcome I need!

This error's cause is due to script running more than 6 minutes.
A possible solution is to limit the time-consuming part of your script (which is the for loop) to only 5 minutes. Then create a trigger and continue the loop into another instance if it still isn't done.
Script:
function addressToPosition() {
// Select a cell with an address and two blank spaces after it
var sheet = SpreadsheetApp.getActiveSheet();
...
var options = {
muteHttpExceptions: true,
contentType: "application/json",
};
// if lastRow is set, get value, else 0
var continueRow = ScriptProperties.getProperty("lastRow") || 0;
var startTime = Date.now();
var resume = true;
for (addressRow = ++continueRow; addressRow <= cells.getNumRows(); ++addressRow) {
var address = cells.getCell(addressRow, addressColumn).getValue();
...
// if 5 minutes is done
if ((Date.now() - startTime) >= 300000) {
// save what's the last row you processed then exit loop
ScriptProperties.setProperty("lastRow", addressRow)
break;
}
// if you reached last row, assign flag as false to prevent triggering the next run
else if (addressRow == cells.getNumRows())
resume = false;
}
// if addressRow is less than getNumRows()
if (resume) {
// after execution of loop, prepare the trigger for the same function
var next = ScriptApp.newTrigger("addressToPosition").timeBased();
// run script after 1 second to continue where you left off (on another instance)
next.after(1000).create();
}
}
Do the same thing with your other functions.

Related

Functions Rerunning in GAS

I'm using GAS to send and receive texts. There is one function that send texts (sendTexts.gs) and one that receives (receiveTexts.gs). I have both of these functions linked to individual buttons on the sheet, but when I run one function, both are running (texts get sent every time). Is there a cache or something that needs to be cleared? The receiveTexts has no commands in the code that could send messages in it, and based on logger testing, I know that both are running when I only click one.
EDIT: This also occurs in the GAS "terminal". If I click run in the script editor both run.
Here is the code with personal/individual info (codes/phone numbers edited out):
function sendSms(to, body) {
var playerArray = getMeta();
Logger.log(playerArray);
var messages_url = "https://api.twilio.com/2010-04-01/Accounts/EDIT/Messages.json";
var payload = {
"To": to,
"Body" : body,
"From" : "EDIT"
};
var options = {
"method" : "post",
"payload" : payload
};
options.headers = {
"Authorization" : "Basic " + Utilities.base64Encode("EDIT")
};
UrlFetchApp.fetch(messages_url, options);
}
function sendAll() {
var spreadsheet = SpreadsheetApp.getActive();
var text = SpreadsheetApp.setActiveSheet(spreadsheet.getSheetByName('Meta')).getRange('B4').getValue();
var playerArray = getMeta();
Logger.log(text);
for (i=0; i<playerArray.length;i++) {
try {
var number = playerArray[i][1];
Logger.log(number);
response_data = sendSms(number, text);
status = "sent";
} catch(err) {
Logger.log(err);
status = "error";
}
Logger.log(status);
}
}
function sendTexts() {
sendAll();
}
Logger.log("ran send texts");
Here is the receive texts code with the same adjustments:
function receiveTexts() {
var spreadsheet = SpreadsheetApp.getActive();
var ACCOUNT_SID = "EDIT";
var ACCOUNT_TOKEN = "EDIT";
var toPhoneNumber = "+EDIT";
var sheet = spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Meta'), true);
var playerArray = getMeta();
var numberToRetrieve = playerArray.length;
var hoursOffset = 0;
var options = {
"method" : "get"
};
options.headers = {
"Authorization" : "Basic " + Utilities.base64Encode(ACCOUNT_SID + ":" + ACCOUNT_TOKEN)
};
var url="https://api.twilio.com/2010-04-01/Accounts/" + ACCOUNT_SID + "/Messages.json?To=" + toPhoneNumber + "&PageSize=" + numberToRetrieve;
var response = UrlFetchApp.fetch(url,options);
// -------------------------------------------
// Parse the JSON data and put it into the spreadsheet's active page.
// Documentation: https://www.twilio.com/docs/api/rest/response
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Meta'), true);
var numRounds = spreadsheet.getRange('B2').getValue();
var theSheet = SpreadsheetApp.setActiveSheet(spreadsheet.getSheetByName('Attacks'),true);
var theColumn = (numRounds*2)+1;
var dataAll = JSON.parse(response.getContentText());
for (i=0; i<dataAll.messages.length; i++){
var sentNumber = dataAll.messages[i].from;
Logger.log(sentNumber);
for (k=0; k<playerArray.length;k++){
Logger.log(playerArray[k][1]);
if (playerArray[k][1]==sentNumber){
var player = k;
Logger.log('Success');
Logger.log(player);
break;
}
}
var playerRow = playerArray[player][0];
Logger.log(playerRow);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Attacks'),true);
theSheet.getRange(playerRow, theColumn).setValue(dataAll.messages[i].body);
}
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Meta'), true);
sheet.getRange(2,2).setValue(numRounds +1);
}
Logger.log("texts received ran");
EDIT2: I separated out getMeta into a separate file. Now when I run getMeta, it also runs the other two scripts. Anytime any one script is run, all three run. This makes me think it's not related to the code, but related to some setting or something. Does the order of the scripts in the sidebar matter? I feel like that's not it because running any of the three causes all three to be run.

Google Apps Script Not Loading Functions for Everyone, only me

My onSheetOpen / showSidebar is not working for anyone but me when opening my events Google sheet. I have set up triggers to launch both the onSheetOpen and showSidebar for myself, but this is not working for anyone else who opens the spreadsheet. Can anyone help figure out why this might be the case? Below is the code I'm using.
/**
* Set up custom sidebar.
* TRIGGER: spreadsheet event 'on open'
*/
function onSheetOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Actions')
.addItem("Register interest in current opportunity", 'registerInterest')
.addItem("Show sidebar", 'showSidebar')
.addToUi();
showSidebar();
}
/**
* Renders the sidebar
* TRIGGER: spreadsheet event 'on open' & custom menu option
*/
function showSidebar() {
try {
var htmlOutput = HtmlService.createHtmlOutputFromFile('sidebar');
SpreadsheetApp.getUi().showSidebar(htmlOutput);
} catch(e) {
// Just ignore - means user doesn't have edit access to sheet
}
}
/**
* Main function. Grabs the required data from the currently-selected row, registers interest
* in that data in a separate sheet, and sends a confirmation email to the user.
*/
function registerInterest() {
//var spreadsheet = SpreadsheetApp.getActive();
// These are the attributes we capture and store in the 'interest' sheet
var who = Session.getActiveUser().getEmail();
var when = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
var data = getRowOfInterest(SpreadsheetApp.getActiveSheet());
var id = Utilities.getUuid();
Logger.log(id)
if(data.what && data.what.length > 0) {
// Get the Google Sheets ID of the target 'interest' sheet
var id = getConfigValue("InterestSheet");
if(id) {
// Now try and open that sheet for updating
var ss = SpreadsheetApp.openById(id);
if(ss) {
var sheet = ss.getSheetByName('Interest');
if(sheet) {
// All good; log interest. First, create the new row of data for the target sheet
sheet.appendRow([ when, who, data.what, data.subject, data.location, data.industry, data.capabilities ]);
// Second, grab our 'template' email from config & use that to send a confirmation
// email to the person registering their interest
var body = getConfigValue('InterestEmail');
MailApp.sendEmail(who, getConfigValue('InterestSubject'), null, { noReply: true, htmlBody: body });
} else {
throw Error('can\'t open the \'Interest expressed\' sheet (or it doesn\'t exist)');
}
} else {
throw Error('can\'t open the \'Interest expressed\' sheet');
}
} else {
throw Error('\'Interest expressed\' sheet not specified');
}
}
}
/**
* Utility to derive the required data for registering interest.
* Called from the registerInterest() function
*/
function getRowOfInterest(sheet) {
var result = {};
var row = sheet.getActiveRange().getRow(); // Get the currently-selected row index
var fullRange = sheet.getRange(row, 1, 1, sheet.getLastColumn()); // Get the entire row of data
//var fullRange = sheet.getRange(row, 1, 1, 9);
Logger.log(fullRange.getValues())
var data = fullRange.getValues();
if(data[0] && data[0].length > 0) {
for(var n = 0; n < data[0].length; n++) {
// Populate specific attributes in the 'result' object - tailor this as you see fit
if(n==0) result.subject = data[0][n];
if(n==6) result.what = data[0][n] + ' ';
//if(n==5 || n==7) result.what += data[0][n] + ' ';
if(n==7) result.location = data[0][n];
if(n==11) result.industry = data[0][n];
if(n==12) result.capabilities = data[0][n];
}
result.what += '(' + sheet.getName() + ')';
result.industry;
result.capabilities;
return result;
}
}
function getSomeRange(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var row = sheet.getActiveRange().getRow(); // Get the currently-selected row index
var myRange = sheet.getRange(row, 1, 1, sheet.getLastColumn());
//Logger.log("Number of rows in range is "+myRange.getNumRows()+ " Number of columns in range is "+ myRange.getNumColumns());
}
/**
* Utility to pull specified data from a config sheet. This assumes that a sheet called 'Config'
* is populated thus:
* Column contains a load of 'key' and column 2 contains the corresponding 'values'
* Called from registerInterest() function
*/
function getConfigValue(key) {
var sheet = SpreadsheetApp.getActive().getSheetByName('Config');
var result;
if(sheet) {
// Scan column 1 for key and return value in column 2
var data = sheet.getDataRange().getValues();
for(var n = 0; n < data.length; ++n) {
if(data[n][0].toString().match(key)) result = data[n][1];
}
}
return result;
}
function hideExpiredEvents() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Events');
var exsh=ss.getSheetByName('Expired Events');
var sr=2;
var rg=sh.getRange(sr,1,sh.getLastRow()-sr+1,sh.getLastColumn());
var vA=rg.getValues();
Logger.log(vA);
var dt=new Date();
var d=0;//deleted row counter
var today=new Date(dt.getFullYear(),dt.getMonth(),dt.getDate(0)).valueOf();
for(var i=0;i<vA.length;i++) {
if(new Date(vA[i][3]).valueOf()<today) {
exsh.appendRow(vA[i]);//move to bottom of expired events
sh.deleteRow(i+sr-d++);//increment deleted row counter
}
}
}
Unfortunately, it seems that the issue you are encountering might be a bug.
What you can do in this case is to star the issue on Issue Tracker here by clicking the ★ next to the issue number and post a comment as well saying that you are affected by the issue.

Adding event to calendar via Google Apps script - error when calculating end time more than one hour after start

I have a simple app in which users complete some basic details of a meeting in a google sheet and then export these to their google calendar. Script as follows:
function exportEvents() {
var sheet = SpreadsheetApp.getActiveSheet();
var headerRows = 1;
var range = sheet.getDataRange();
var data = range.getValues();
// var calId = sheet.getRange("H2:H2").getValue();
var calId = "xxxxxxxxxxxx";
var cal = CalendarApp.getCalendarById(calId);
for (i=0; i<data.length; i++) {
if (i < headerRows) continue;
var row = data[i];
var date = new Date(row[1]);
var title = row[2];
var initial = row[0];
var concatTitle = initial + " - " + title;
var tstart = new Date(row[3]);
tstart.setDate(date.getDate());
tstart.setMonth(date.getMonth());
tstart.setYear(date.getYear());
// var tstop = new Date(row[4]);
var tstop = new Date(tstart + row[4]);
tstop.setDate(date.getDate());
tstop.setMonth(date.getMonth());
tstop.setYear(date.getYear());
var loc = row[5];
var desc = row[6];
var id = row[7];
try {
var event = cal.getEventSeriesById(id);
}
catch (e) {
// do nothing - we just want to avoid the exception when event doesn't exist
}
if (!event) {
var newEvent = cal.createEvent(concatTitle, tstart, tstop, {description:desc,location:loc}).getId();
row[7] = newEvent;
}
else {
event.setTitle(concatTitle);
event.setDescription(desc);
event.setLocation(loc);
var recurrence = CalendarApp.newRecurrence().addDailyRule().times(1);
event.setRecurrence(recurrence, tstart, tstop);
}
debugger;
}
range.setValues(data);
}
The whole thing works great, except for when the user puts more than 60 mins as the length of the meeting in column 4. Anything up to 59 mins works great and the event is logged with the correct start and end time. However, when I enter 60 or more mins, the error reads 'Event start time must be before event end time."
I'm sure I'm doing something very simple very wrong, but any help would be greatly appreciated.

Run Google script on single sheet instead of entire spreadsheet

With my spreadsheet, I have 2 Google forms tied to 2 sheets. When a form gets submitted the script executes and does it's thing. However, I only want the script to execute based on a submission from a single sheet. As it is now, the script executes when either of the forms get submitted.
My two sheets are:
Job Submission
and Order Submission
Any advice?
// Work Order
// Get template from Google Docs and name it
var docTemplate = ""; // *** replace with your template ID ***
var docName = "Work Order";
var printerId = "";
function addDates() {
var date = new Date(); // your form date
var holiday = ["09/04/2017","10/09/2017","11/23/2017","12/24/2017","12/25/2017","01/01/2018"]; //Define holiday dates in MM/dd/yyyy
var days = 5; //No of days you want to add
date.setDate(date.getDate());
var counter = 0;
if(days > 0 ){
while (counter < days) {
date.setDate(date.getDate() + 1 );
var check = date.getDay();
var holidayCheck = holiday.indexOf(Utilities.formatDate(date, "EDT", "MM/dd/yyyy"));
if (check != 0 && check != 6 && holidayCheck == -1) {
counter++;
}
}
}
Logger.log(date) //for this example will give 08/16/2017
return date;
}
function createNewDoc(values) {
//Get information from form and set as variables
var email_address = "";
var job_name = values[1];
var order_count = values[2];
var order_form = values[7];
var print_services = values[3];
var priority = values[5];
var notes = values[6];
var formattedDate = Utilities.formatDate(new Date(), "EDT", "MM/dd/yyyy");
var expirationDate = Utilities.formatDate(addDates(), "EDT", "MM/dd/yyyy");
// Get document template, copy it as a new temp doc, and save the Doc's id
var copyId = DriveApp.getFileById(docTemplate)
.makeCopy(docName+' for '+job_name)
.getId();
// Open the temporary document
var copyDoc = DocumentApp.openById(copyId);
// Get the document's body section
var copyBody = copyDoc.getActiveSection();
// Replace place holder keys,in our google doc template
copyBody.replaceText('keyJobName', job_name);
copyBody.replaceText('keyOrderCount', order_count);
copyBody.replaceText('keyOrderForm', order_form);
copyBody.replaceText('keyPrintServices', print_services);
copyBody.replaceText('keyPriority', priority);
copyBody.replaceText('keyNotes', notes);
copyBody.replaceText('keyDate', formattedDate);
copyBody.replaceText('keyDue', expirationDate);
// Save and close the temporary document
copyDoc.saveAndClose();
// Convert temporary document to PDF by using the getAs blob conversion
var pdf = DriveApp.getFileById(copyId).getAs("application/pdf");
// Attach PDF and send the email
var subject = "New Job Submission - " + job_name;
var body = "Here is the work order for " + job_name + ". Job is due " + expirationDate + ".";
MailApp.sendEmail(email_address, subject, body, {htmlBody: body, attachments: pdf});
// Move file to folder
var file = DriveApp.getFileById(copyId);
DriveApp.getFolderById("").addFile(file);
file.getParents().next().removeFile(file);
var newDocName = docName + ' for ' + job_name;
return [copyId, newDocName];
}
function printGoogleDocument(copyId, docName) {
// For notes on ticket options see https://developers.google.com/cloud-print/docs/cdd?hl=en
var ticket = {
version: "1.0",
print: {
color: {
type: "STANDARD_COLOR"
},
duplex: {
type: "NO_DUPLEX"
},
}
};
var payload = {
"printerid" : "",
"content" : copyId,
"title" : docName,
"contentType" : "google.kix", // allows you to print google docs
"ticket" : JSON.stringify(ticket),
};
var response = UrlFetchApp.fetch('https://www.google.com/cloudprint/submit', {
method: "POST",
payload: payload,
headers: {
Authorization: 'Bearer ' + GoogleCloudPrint.getCloudPrintService().getAccessToken()
},
"muteHttpExceptions": true
});
// If successful, should show a job here: https://www.google.com/cloudprint/#jobs
response = JSON.parse(response);
if (response.success) {
Logger.log("%s", response.message);
} else {
Logger.log("Error Code: %s %s", response.errorCode, response.message);
}
return response;
}
// When Form Gets submitted
function onFormSubmit(e) {
var values = e.values;
var returnedDocValues = createNewDoc(values);
var copyId = returnedDocValues[0];
var docName= returnedDocValues[1];
printGoogleDocument(copyId, docName);
}
Edit:
I'm not sure how to do a complete or verifiable example since it's dependent on the form submission. I don't often work with javascript so I'm still learning.
Anyway, I updated my onFormSubmit function, however, my other functions have failed to execute. The script doesn't create the doc and in turn doesn't get sent to the google cloud print
// When Form Gets submitted
function onFormSubmit(e) {
// Initialize
var rng = e.range;
var sheet = rng.getSheet();
var name = sheet.getName();
// If the response was not submitted to the right sheet, exit.
if(name != "Job Submission") return;
var values = e.values;
var returnedDocValues = createNewDoc(values);
var copyId = returnedDocValues[0];
var docName= returnedDocValues[1];
printGoogleDocument(copyId, docName);
}
If your onFormSubmit function is on a script bounded to the spreadsheet, the event object includes a range object. You could use getSheet to get the sheet and then getName to get the sheet name.
Example:
function onFormSubmit(e){
// Initialize
var rng = e.range;
var sheet = rng.getSheet();
var name = sheet.getName();
// If the response was not submitted to the right sheet, exit.
if(name != "Responses 1") return;
//Otherwise continue
}
Going off of Ruben's suggestion, here is what I ended up with
function onFormSubmit(e) {
// Initialize
var name = e.range.getSheet().getName();
// If the response was not submitted to the right sheet, exit.
if (name != "Job Submission") return;
var values = e.values;
var returnedDocValues = createNewDoc(values);
var copyId = returnedDocValues[0];
var docName = returnedDocValues[1];
printGoogleDocument(copyId, docName);
}

How to call api again after clearing interval

I am trying to create a stack area graph from the values I get from api's.
My first api gives me a range of dates. Eg: June 1 - June 7.
My second api gives me values need for the graph. The data looks like this
My idea is to call the api and push the count values to function every time to a function. But somehow I could not make that possible so I am calling api once for every 100 ms and grabbing all the data and triggering the graph. Now when I want to call the second api after one iteration is complete the call interrupts the graph. Please help me fix this.
d3.json('/service/dates', function(error, dates) {
var dran = dates;
if (dates != null) {
sDt = new Date(dates.st);
eDt = new Date(dates.et);
var i = 0;
var start = function() {
if (sDt > eDt) {
clearInterval(interval);
$('.wrapper').trigger('newPoint');
return;
}
var sDate = sDt.toISOString();
var eDate = new Date(sDt.setMinutes(sDt.getMinutes() + 30)).toISOString();
//Calling the api for graph values
d3.json("/service/dat?s=" + sDate + "&e=" + eDate, function(error, results) {
if (results != null) {
numbers = numbers.push(results.numbers);
values = values.push(results.values);
}
});
i++;
}
var interval = setInterval(start, 100);
}
});
}
Put the entire API call inside a named function. Then call that from the start() function to restart everything.
function doAPILoop() {
d3.json('/service/dates', function(error, dates) {
var dran = dates;
if (dates != null) {
sDt = new Date(dates.st);
eDt = new Date(dates.et);
var i = 0;
var start = function() {
if (sDt > eDt) {
clearInterval(interval);
$('.wrapper').trigger('newPoint');
doAPILoop();
return;
}
var sDate = sDt.toISOString();
var eDate = new Date(sDt.setMinutes(sDt.getMinutes() + 30)).toISOString();
//Calling the api for graph values
d3.json("/service/dat?s=" + sDate + "&e=" + eDate, function(error, results) {
if (results != null) {
numbers = numbers.concat(results.numbers);
values[values.length] = results.values;
}
});
i++;
}
var interval = setInterval(start, 10);
}
});
}
doAPILoop();

Categories