Google app script exceeded execution time limit - javascript

I have a Google sheet with almost 160 sub sheets. All the questions are in "All" sheet and B column is the actual spreadsheet name where they should be. The following code is reading data from "All" spreadsheet and sending them perfectly to the last row of desired spreadsheet perfectly but it is taking very long time! Probably because it has a lot of subsheets and is using getSheetByName again and again. Now I've stored all the sub sheets' name and ID in 'sheets' and 'sheetID' arrays at once. I'm thinking to compare between rangeValues[j][1] and sheetNames[k][0]. Below is the code and screenshot of the spreadsheet.
Is this a suitable way? Please help me to run the script faster!
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
function send() {
var rangeValues = searchRange.getValues();
var sheetNames = new Array();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheets.length ; i++) sheetNames.push( [ sheets[i].getName() ] );
//Logger.log (sheetNames.length);
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
//Logger.log (sheetID.length);
for ( j = 0 ; j < lastRow; j++)
{
for ( k = 0 ; k < sheetNames.length ; k++) //
{
if (rangeValues[j][1] === sheetNames[k][0])
{
var targetSheet = ss.getSheetByName(sheetNames[k][0]); // This line is working but taking very long to complete
var targetSheet = ss.getSheetByID(sheetIDs[k][0]); // This line is not code just to show what I'm thinking to do.
targetSheet.getRange(targetSheet.getLastRow()+1, 1, 1, 8).setValues([rangeValues[j]]);
}
}
}
}

I believe your goal as follows.
You want to reduce the process cost of your script.
Modification points:
There is no method of getSheetByID in Class Spreadsheet. By this, I think that in your case, the following script can be removed.
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
In your script, getSheetByName and getRange(###).setValues(###) are used in the loop. And also, even when the sheet is the same sheet, each row is put using setValues in the loop every row. In this case, the process cost will be high. I think that these situation might be the reason of your issue.
Fortunately, in your situation, all sheets are in the same Spreadsheet. So for reducing the process cost of your script, here, I would like to propose to put the values to each sheet using Sheets API. The flow of modified script is as follows.
Retrieve Spreadsheet and values.
This script is from your script.
Retrieve all sheets in the Spreadsheet.
Check whether the sheet names from the column "B" are existing.
Create the object for putting to each sheet using Sheets API.
Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
When above points are reflected to your script, it becomes as follows.
Modified script:
Please copy and paste the following script. And, please enable Sheets API at Advanced Google services. And then, please run the script.
function send() {
// 1. Retrieve Spreadsheet and values. This script is from your script.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
var rangeValues = searchRange.getValues();
// 2. Retrieve all sheets in the Spreadsheet.
var sheets = ss.getSheets().reduce((o, e) => Object.assign(o, {[e.getSheetName()]: e}), {});
// 3. Check whether the sheet names from the column "B" are existing.
// 4. Create the object for putting to each sheet using Sheets API.
var data = rangeValues.reduce((o, e) => {
if (sheets[e[1]]) {
if (o[e[1]]) {
o[e[1]].values.push(e);
} else {
o[e[1]] = {range: `'${e[1]}'!A${sheets[e[1]].getLastRow() + 1}`, values: [e]};
}
}
return o;
}, {});
// 5. Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
Sheets.Spreadsheets.Values.batchUpdate({data: Object.values(data), valueInputOption: "USER_ENTERED"}, ss.getId());
}
Note:
In this case, when you run the script, one API call is used. Because the method of spreadsheets.values.batchUpdate is used.
In my environment, when I tested above script and your script using a sample Spreadsheet, I could confirm the reduction of the process cost of about 70 % from your script.
References:
Advanced Google services
Method: spreadsheets.values.batchUpdate
reduce()

Try this:
Should be a lot faster
function send() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("All");
var rg=sh.getRange(1,1,sh.getLastRow(),8);
var v=rg.getValues();
var names=[];
var sheets=ss.getSheets();
sheets.forEach(function(sh,i){names.push(sh.getName());});
v.forEach(function(r){
let idx=names.indexOf(r[1]);//column2
if(idx!=-1) {
sheets[idx].getRange(sheets[idx].getLastRow()+1,1,1,8).setValues([r]);
}
});
}

Related

Exception: Document is missing (perhaps it was deleted, or you don't have read access?)

I'm working on a project that take "profiles" stored in a Google Sheet, makes a unique Google Doc for each profile, and then updates the unique Google Doc with any new information when you push a button on the Google Sheet.
I have some other automations built into my original code, but I simplified most of it to what's pertinent to the error I'm getting, which is this:
Exception: Document is missing (perhaps it was deleted, or you don't have read access?
It happens on Line 52 of my script in the fileUpdate funtion. Here's the appropriate line for reference:
var file = DocumentApp.openById(fileName);
And this is the rest of my code:
function manageFiles() {
//Basic setup. Defining the range and retrieving the spreadsheet to store as an array.
var date = new Date();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var array = sheet.getDataRange().getValues();
var arrayL = sheet.getLastRow();
var arrayW = sheet.getLastColumn();
for (var i = 1; i < arrayL; i++) {
if (array[i][arrayW-2] == "") {
//Collect the data from the current sheet.
//Create the document and retrieve some information from it.
var docTitle = array[i , 0]
var doc = DocumentApp.create(docTitle);
var docBody = doc.getBody();
var docLink = doc.getUrl();
//Use a for function to collect the unique data from each cell in the row.
docBody.insertParagraph(0 , "Last Updated: "+date);
for (var j = 2; j <= arrayW; j++) {
var colName = array[0][arrayW-j];
var data = array[i][arrayW-j];
if (colName !== "Filed?") {
docBody.insertParagraph(0 , colName+": "+data);
}
}
//Insert a hyperlink to the file in the cell containing the SID
sheet.getRange(i+1 , 1).setFormula('=HYPERLINK("'+docLink+'", "'+SID+'")');
//Insert a checkbox and check it.
sheet.getRange(i+1 , arrayW-1).insertCheckboxes();
sheet.getRange(i+1 , arrayW-1).setFormula('=TRUE');
}
else if (array[i][arrayW-2] !== "") {
updateFiles(i);
}
}
sheet.getRange(1 , arrayW).setValue('Last Update: '+date);
}
//Note: I hate how cluttered updateFiles is. I'm going to clean it up later.
function fileUpdate(rowNum) {
//now you do the whole thing over again from createFiles()
//Basic setup. Defining the range and retrieving the spreadsheet to store as an array.
var date = new Date();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var array = sheet.getDataRange().getValues();
var arrayL = sheet.getLastRow();
var arrayW = sheet.getLastColumn();
//Collect the data from the current sheet.
var fileName = array[rowNum][0];
var file = DocumentApp.openById(fileName);
//retrieve the body of the document and clear the text, making it blank.
file.getBody().setText("");
//Use a for function to collect the the unique date from every non-blank cell in the row.
file.getBody().insertParagraph(0 , "Last Updated: "+date);
for (var j = 2; j <= arrayW; j++) {
var colName = array[0][arrayW-j];
var data = array[rowNum][arrayW-j];
file.getBody().insertParagraph(0 , colName+": "+data);
}
}
If you'd like to take a look at my sample spreadsheet, you can see it here. I suggest you make a copy though, because you won't have permissions to the Google Docs my script created.
I've looked at some other forums with this same error and tried several of the prescribed solutions (signing out of other Google Accounts, clearing my cookies, completing the URL with a backslash, widening permissions to everyone with the link), but to no avail.
**Note to anyone offended by my janky code or formatting: I'm self-taught, so I do apologize if my work is difficult to read.
The problem (in the updated code attached to your sheet) comes from your URL
Side Note:
In your initial question, you define DocumentApp.openById(fileName);
I assume your realized that this is not correct, since you updated
your code to DocumentApp.openByUrl(docURL);, so I will discuss the
problem of the latter in the following.
The URLs in your sheet are of the form
https://docs.google.com/open?id=1pT5kr7V11TMH0pJea281VhZg_1bOt8YDRrh9thrUV0w
while DocumentApp.openByUrl expects a URL of form
https://docs.google.com/document/d/1pT5kr7V11TMH0pJea281VhZg_1bOt8YDRrh9thrUV0w/
Just adding a / is not enough!
Either create the expected URL manually, or - much easier / use the method DocumentApp.openById(id) instead.
For this, you can extract the id from your URL as following:
var id = docURL.split("https://docs.google.com/open?id=")[1];
var file = DocumentApp.openById(id)

Creating Nested For-Loop on Google Sheets Script Editor to Auto-Extract Data into Required Format

I am trying to come up with a function that could extract data from one workbook to another that has a separate format. I will refer to the workbooks as "Source" and "Destination". So, on the "Source", I have the data arranged in yearly tabs. However, on "Destination", I have tabs indicating the regions. I have attached snapshots of the two workbooks.
Since I have no basics in programming, I tried to come up with a code but it didn't quite work. The following is my attempt at a nested for-loop which first extracts the data from the "2015" tab of the first region ("Ampang"), and pastes it into the other workbook in the "Ampang" tab and the "2015" row. I ran the debugging function and it says that the "var data" is undefined.
function D() {
var ss = SpreadsheetApp.getActiveSpreadsheet();//open destination worksheet
var sss = ss.getSheets(); //get all destination sheets
//var sheetname = ss.getSheetName(); //get sheet names
var sheet1 = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1jJ8QysQTmS5tG4wvNCxqdbWvAbdbmJm5uatdgIHe6tw/edit#gid=793399042"); //open where data is located
var sheet = sheet1.getSheets(); //get all the sheets required
for (j = 0; j<sss.length; j++) {
for (i = 0; i <sheet.length; i++) {
switch(sheet[i].getSheetName()) {
case "Main Menu" :
break;
default:
var data = sheet.getSheets()[i].getRange(i+1, i+2, 1, 2); //get the data for Specific Region and then Changing the Year
var destination = sss[j].getRange(j,j+1,1,2); //pasting the data to the specific year at the Specific Region Tab/
data.copyTo(destination);
}
}
}
}
[Update 1]
So, I realized that the copyTo() function doesn't work when the data are in separate workbooks so I have come up with a new code below:
function D() {
// define the source sheets
var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1jJ8QysQTmS5tG4wvNCxqdbWvAbdbmJm5uatdgIHe6tw/edit#gid=793399042");
// get all source sheets
var source = ss.getSheets();
// define the target sheets
var ts = SpreadsheetApp.getActiveSpreadsheet();
// get all target sheets
var target = ts.getSheets();
// set nested for-loop
for (j = 0; j < target.length; j++) {
for (i = 0; i < source.length; i++) {
if (source[i].getSheetName() !== "Main Menu") {
var rangeSource = source[i].getRange(i+1,i+1,1,2); // get the source range
var values = rangeSource[i].getValues(); // get the values from the set range
}
if (target[j].getSheetName() !== "Main Menu") {
var rangeTarget = target[j].getRange(j+1,j+1,1,2); //get the target range
rangeTarget.setValues(values[i]); // paste the values to the target range
}
}
}
}
However, the var rangeSource and values are currently undefined. I still am trying to find the source of the error.
Source Sheets Snapshot
Destination Sheets Snapshot
You've defined sheet as all the sheets
var sheet = sheet1.getSheets(); //get all the sheets required
So data needs to be
var data = sheet[i].getRange(i+1, i+2, 1, 2); //get the data for Specific Region and then Changing the Year
See is that helps.

Removing invalid named ranges with Sheets API and GAS

Here is my function:
function deleteNamedRangeWithREF() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = ss.getId();
var spreadsheetObject = Sheets.Spreadsheets.get(spreadsheetId);
var namedRanges = spreadsheetObject.namedRanges;
Logger.log(namedRanges);
var namedRangesRef = [];
for(var i = 0; i < namedRanges.length; i++)
{
if((namedRanges[i].range.endColumnIndex - namedRanges[i].range.startColumnIndex) <= 0 || (namedRanges[i].range.endRowIndex - namedRanges[i].range.startRowIndex) <= 0)
namedRangesRef.push(namedRanges[i].namedRangeId);
}
var BUrequest = Sheets.newBatchUpdateSpreadsheetRequest();
BUrequest.requests = [];
for(var i = 0; i < namedRangesRef.length; i++)
{
var request = {
deleteNamedRange: {
namedRangeId: namedRangesRef[i]
}
}
BUrequest.requests.push(request)
}
if (BUrequest.requests.length)
Sheets.Spreadsheets.batchUpdate(BUrequest, spreadsheetId);
}
I'm using sheets API to delete bad #REF named ranges.
When coding this, this worked, but now looks like it's broken.
You can see by yourself #Logger.log(namedRanges); all listed named ranges are valid, it doesn't retrieve the #REF named ranges.
Did google changed something, is there an alternative?
At first, I have to apologize and modify my comments in your question. At Sheets API, when there is no value of object, the property is not created. For example, sheetId of gid=0 is not shown in the range object. (But I'm not sure whether it occurs at all response patterns of Sheets API.) About this, I had forgot it. I have to apologize for this situation.
By considering above situation, I investigated your sample Spreadsheet. As the results, it was found as follows.
NamedRange with PlageNommée1 could be retrieved by Sheets API. In this case, although the range is included as the grid range, sheetId is not included in the range object because the sheet ID is 0.
NamedRange with PlageNommée2 couldn't be retrieved by Sheets API. In this case, the range object related to PlageNommée2 couldn't be retrieved, although sheetId of PlageNommée2 is not 0.
I think that when the grid range is #REF!, Sheets API might use this as no namedRange.
From above situation, I thought that Spreadsheet Service instead of Sheets API might be able to retrieve values. So I tested this. The sample script using Spreadsheet Service is as follows.
Sample script:
var id = "### spreadsheetId ###"; // Please set this.
var namedRanges = SpreadsheetApp.openById(id).getNamedRanges();
for (var i = 0; i < namedRanges.length; i++) {
Logger.log("name: %s, range: %s!%s",
namedRanges[i].getName(),
namedRanges[i].getRange().getSheet().getSheetName(),
namedRanges[i].getRange().getA1Notation()
)
}
Result:
name: "PlageNommée1", range: "Feuille 1!A1"
name: "PlageNommée2", range: "Feuille 2!#REF!"
In this case, the range of Feuille 2 can be retrieved as #REF!. From this result, you can check the namedRanges.
It was found that the namedRange of PlageNommée2 could be retrieved by Spreadsheet Service, even if the range is #REF!.
Reference:
getNamedRanges()

Looking for one Google Script App for 4 sheet tabs to produce one json

Ok...not sure how to do this. Right now I 4 sheets and 4 scripts for each sheet producing 4 json feeds. What I am trying to experiment with is having one script that will produce 1 json that I can use in a web page and just call the type of class. They are all formatted the same with columns etc.
Here is the Google Script App code I have.
function doGet(e){
// Change Spread Sheet url
var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/SpreadsheetID/edit#gid=0");
// Sheet Name, Change Sheet to whatever name you used to name your sheet
var sheet = ss.getSheetByName("Class Sheet 1");
return getClasses(sheet);
}
function getClasses(sheet){
var dataObj = {};
var dataArray = [];
// collecting data starting from 2nd Row , 1st column to last row and last column
var rows = sheet.getRange(2,1,sheet.getLastRow()-1, sheet.getLastColumn()).sort([{column: 1, ascending: true}, 1]).getValues();
for(var i = 0, l= rows.length; i<l ; i++){
var dataRow = rows[i];
var record = {};
record['Name'] = dataRow[0];
record['Note'] = dataRow[1];
record['Address'] = dataRow[2];
record['StreetAddress'] = dataRow[3];
record['City'] = dataRow[4];
record['State'] = dataRow[5];
record['ZipCode'] = dataRow[6];
record['ContactName'] = dataRow[7];
record['EMailAddress'] = dataRow[8];
record['CustomerServicePhone'] = dataRow[9];
dataArray.push(record);
}
dataObj = dataArray;
var result = JSON.stringify(dataObj);
return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
}
Scratching my head on this a little bit....I'm sure its something simple and I am probably over thinking things, but any help would be appreciated.
Possible Solution:
The e object in your doGet(e) provides a way to send parameters to your script. You can access different sheets with different url parameters. You can then easily get the requested SheetName through e.parameter. Use
https://script.google.com/.../exec?sheet=ClassSheet1 //for ClassSheet1
https://script.google.com/.../exec?sheet=ClassSheet2 //for ClassSheet2
Code.gs:
function doGet(e){
var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/SpreadsheetID/edit#gid=0");
var sheetName = e.parameter.sheet;
var sheet = ss.getSheetByName(sheetName);
return getClasses(sheet);
}
You can also provide UI in your web-app to select a sheet.

How to set column to plain text format for every sheet?

I've read through this post >> set format as plain text
I'm creating a new thread because that thread is a few years old already and I didn't want to revive something from a few years ago.
the code:
function A1format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mainsheet = ss.getSheetByName("Sheet1");
var G = mainsheet.getRange("C15:BH3000").getGridId();
var illa = mainsheet.getRange("A13");
Logger.log(G);
illa.copyFormatToRange(G, 16, 3,200, 30);
}
This code is supposed to set plain text format for the sheet named Sheet1
I've tried var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3"); but this doesn't seem to work, I just get an error message.
This is the current code I have, this code works but is both inefficient and a real pain to maintain if something changes:
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheets()[0];
var sheet2 = ss.getSheets()[2];
var sheet3 = ss.getSheets()[4];
var sheet4 = ss.getSheets()[7];
var sheetColumn1 = sheet1.getRange("A1:A");
var sheetColumn2 = sheet2.getRange("A1:A");
var sheetColumn3 = sheet3.getRange("A1:A");
var sheetColumn4 = sheet4.getRange("A1:A");
sheetColumn1.setNumberFormat("#");
sheetColumn2.setNumberFormat("#");
sheetColumn3.setNumberFormat("#");
sheetColumn4.setNumberFormat("#");
}
Here I am changing each column A in every sheet to plain text by using the index number of the sheet, so I have to manually count the number for every sheet, this is a nightmare as I have a very large number of sheets, it will take too much time for me to manually count the sheets and then add it to my current code. I know there is a better more efficient way of doing this, but I don't know how due to my lack of knowledge in google apps scripting.
How do you do this for every sheet in the document regardless of how many sheets are present? I want to go through every sheet, from sheet1 till x number of sheets and then change every column A to plain text.
var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3") throws an error because there isn't a sheet named "Sheet1, Sheet2, Sheet3". "There isn't a way to reduce the calls to the Spreadsheet Service on your code, but you could make that it "looks better" by using some JavaScript features like loops and array handling.
Example (untested)
On the following code snippet, arrays, a for and indexOf are used to reduce a bit the number of lines of the original code.
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sheetsToProcess = [0,2,4,7]; // Array holding the sheets indexes to process
for(var i = 0; i < sheets.length; i++){
/* If i is not in the sheetsToProccess indexOf returns -1 which is parsed as false
otherwise the result is parsed as true */
if(sheetsToProcess.indexOf(i)){
var sheet = ss.getSheets()[i];
var sheetColumn = sheet.getRange("A1:A");
sheetColumn.setNumberFormat("#");
}
}
}
To make the above work for every sheet, comment out or remove if(sheetsToProcess.indexOf(i)){ and the corresponding }.
It's worth to note that if you are looking help to find the "best" way to improve your code, you could try Code Review.
This was the code I needed:
function setPlainText() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
for(var i = 0; i < sheets.length; i++) {
//Logger.log(sheets[i].getName());
var setPlainText = ss.getSheets()[i];
var sheetColumnA = setPlainText.getRange("A1:A");
sheetColumnA.setNumberFormat("#");
var sheetColumnB = setPlainText.getRange("B1:B");
sheetColumnB.setNumberFormat("#");
}
}
It will:
Iterate through a document with x number of sheets however big or small
Then for each iteration, set plain text format for every column A and B for every sheet in the document

Categories

Resources