Script time limit for Google sheet - google-apps-script

I need to change some formulas at the same cells at an specific sheet (LISTAFINAL) present in a great number of spreadsheets, these one located at the same folder. But it stops at Google script time limit of 6 minutes, making changes only in 9 spreadsheets, and comes the message: Erro - Exceeded maximum execution time.
My goals:
I would like to know if there's any way to or speed up this process or make changes in a bigger number of spreadsheets or both. Here is the code:
function validacao(){
var folder = DriveApp.getFolderById("FOLDER ID");
var x = folder.getFilesByType(MimeType.GOOGLE_SHEETS);
while (x.hasNext()) {
SpreadsheetApp.open(x.next()).getSheets().forEach(sheet => {
sheet.getRange('LISTAFINAL!F5:F15').activate();
sheet.getRange('LISTAFINAL!F5').setValue('=ALUNO1!$F$167');
sheet.getRange('LISTAFINAL!F6').setValue('=ALUNO2!$F$167');
sheet.getRange('LISTAFINAL!F7').setValue('=ALUNO3!$F$167');
sheet.getRange('LISTAFINAL!F8').setValue('=ALUNO4!$F$167');
sheet.getRange('LISTAFINAL!F9').setValue('=ALUNO5!$F$167');
sheet.getRange('LISTAFINAL!F10').setValue('=ALUNO6!$F$167');
sheet.getRange('LISTAFINAL!F11').setValue('=ALUNO7!$F$167');
sheet.getRange('LISTAFINAL!F12').setValue('=ALUNO8!$F$167');
sheet.getRange('LISTAFINAL!F13').setValue('=ALUNO9!$F$167');
sheet.getRange('LISTAFINAL!F14').setValue('=ALUNO10!$F$167');
sheet.getRange('LISTAFINAL!F15').setValue('=ALUNO11!$F$167');
});
}
}

Explanation:
You iterate over all sheets for every spreadsheet file. Your goal is to just get a single sheet and put the formulas in specific cells. Therefore, you can get rid of the forEach loop.
It is a best practice to work with arrays instead of iteratively using multiple google apps script functions.
For example, in your code you are using getRange and setValue 11 times. If you store the formula values in an array you will be able to store them by using only a single getRange and setValues.
Solution:
function validacao(){
const folder = DriveApp.getFolderById("FOLDER ID");
const x = folder.getFilesByType(MimeType.GOOGLE_SHEETS);
const formulas = Array(11).fill().map((_, i) => [`=ALUNO${i+1}!$F$167`]);
while (x.hasNext()) {
let ss_target = SpreadsheetApp.open(x.next());
let sh = ss_target.getSheetByName("LISTAFINAL");
sh.getRange('F5:F15').setValues(formulas);
}
}

Related

I want to make a script in Google sheet to find and match strings of texts between different sheets

I am working on a Google sheet script to manage stocks of items in a game, which is supposed to work as such:
People can make request to deposite or withdraw items using a Google form, which send all the infos, including what resource and in what amount, to a first "log" sheet. I then want a script to read these logs, and use them to update a different sheet, which show the actual stocks.
I should mention, there's about 800 different items to stock, and we like to move them around (up or down the list) because we're dumb.
So my idea what the have the script first retrieve the name of the item we made a request for, then try to match it in the stock sheet.
If it can, it should then add or substract the amount to the stock.
If it can't, it should just colour the log line in red so we can see it and redo the request.
My first problem is that I have no idea if a script in Gsheet can stay active for a long time, and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Thank you !
From the question
My first problem is that I have no idea if a script in Gsheet can stay active for a long time,
Google Apps Script have quotas. In this case, the corresponding quota is the execution time limit. For free accounts the limit is 6 minutes, for Workspace accounts the limit is 30 minutes.
and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Start by reading https://developers.google.com/apps-script/guides/sheets
Tl;Dr.
You need to learn the pretty basics of JavaScript.
You might use the Spreadsheet Service (Class SpreadsheetApp) or the Advanced Sheets Service, i.e.
/**
* Returns the values from the data range of the active sheet
*
*/
function readData(){
const sheet = spreadsheet.getActiveSheet();
const values = sheet.getDataRange().getValues();
return values;
}
You should decide where do you will store the values, then use JavaScript comparison expressions. You might use loops (for, while, do..while, or use Array methods like Array.prototype.forEach()
Here is an example how it could be done for simplest case, for manual firing of the functions.
Let's say you have the log sheet that look like this:
And your data sheet looks like this:
Here is the function that takes all items from the log sheet, sums them and put on the data sheet:
function add_all_items_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName('log').getDataRange().getValues();
// put all data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
for (let [date, item, q] of log) {
if (item in obj) obj[item] += q; else obj[item] = q;
}
console.log(obj);
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the data sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
The result:
Here is the function that takes item from the last line of the log sheet and add the item to the data sheet:
function add_last_item_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get item from the last row of the log sheet
var [date, item, q] = ss.getSheetByName('log').getDataRange().getValues().pop();
console.log(date, item, q);
// get data from the data sheet
var sheet = ss.getSheetByName('data');
var [header, ...data] = sheet.getDataRange().getValues();
// put the data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
data.forEach(row => obj[row[0]] = row[1]);
console.log(obj);
// add the item to the object
if (item in obj) obj[item] += q; else obj[item] = q;
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
Here is my sheet.
You can run these function manually from Text Editor. Just to see how it works. But actually, as far as I can tell, you better to run the last function (or its variant) automatically every time the log sheet is updated from the Form submit. It can be done with the trigger onFormSubmit().
And this is a simplest case. If you have 800+ items and many columns the code may require some optimizations.

Google Script timeout after 6 min

I want to list all the folderName and their folderID present in a team Drive(more than 3000 folders). I am using speedsheet and running following code in script-
function listFilesInFolder(folderName) {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.appendRow(['Name','File-Id']);
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
var folder = DriveApp.getFolderById('folder ID');
var contents = folder.getFolders();
var cnt = 0;
var folder;
while (contents.hasNext()) {
var folder = contents.next();
cnt++;
data = [
folder.getName(),
folder.getId(),
];
sheet.appendRow(data);
};
}
But this is getting Error Exceeded maximum execution time which is 6 min by default.
I tried adding triggers from script app, but after triggering it get start from beginning and script still ends after 6min.
How to add a triggers which starts from where it left?
Answer:
The slow part of this script is the repeated call to sheet.appendRow(). You can speed this up by pushing the results to an array and setting the values at the end of the loop, rather than appending a row on each iteration of the while loop.
More Information:
Using the built-in services such as SpreadsheetApp often can be slow when making many changes to a sheet in a short space of time. You can combat this by minimising the number of calls to the built-in Apps Script services as possible, relying on pure JavaScript to do your processing.
Code Change:
function listFilesInFolder(folderName) {
const sheet = SpreadsheetApp.getActiveSheet()
//change the folder ID below to reflect your folder's ID (look in the URL when you're in your folder)
let folder = DriveApp.getFolderById('')
let contents = folder.getFolders()
let cnt = 0
let data = [['Name','File-Id']]
while (contents.hasNext()) {
folder = contents.next()
cnt++
data.push([
folder.getName(),
folder.getId(),
])
}
sheet.insertRows(sheet.getMaxRows(), data.length)
sheet.getRange(2, 1, data.length, 2).setValues(data)
}
Code changes:
data is declared as an array initialised with the header row, as opposed to appending it directly to the sheet
On each iteration of the loop, the current folder's name and ID is appended to the data array as a new row of data.
After all folders have been looped through, the number of rows in the sheet is extended by the number of rows in data so to not hit an out of bounds error
All rows inside data are added to the sheet using setValues().
In my test environment, I had the following set up:
Drive folder containing 3424 folders
Using the appendRow() method inside the while loop, execution took 1105.256 seconds (or 18 minutes)
Using push() with the .setValues() method outside the loop, execution took 4.478 seconds.
References:
Class Range - setValues() | Apps Script | Google Developers

Script that adds new row to sheets in selected AND another Google Spreadsheet

I'm working on a Spreadsheet to keep track of team member's project hours. I've created a Spreadsheet per team member for them to fill out weekly, and a project overview Spreadsheet that takes in all data through IMPORTRANGE.
To be able to quickly add a new project I want a macro to insert a new row in the Project overview + the separate Spreadsheets per team member. However I can't figure out how to write the correct code for the separate team member Spreadsheets. What's going wrong here?
If possible I'd also like to make a macro to DELETE a row in the project overview + team member spreadsheets, and one to HIDE a row...
Project overview
Team member Kate
Team member David
My current code:
function InsertRow() {
var ss = SpreadsheetApp.getActive();
var allsheets = ss.getSheets();
var row = SpreadsheetApp.getActiveRange().getRow();
// Array holding the names of the sheets to exclude from the execution
// I only managed to make it work when I exclude the sheet that I actually want to affect instead of the other way around?
var exclude = (["PROJECTS"] ||
SpreadsheetApp.openById("1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI").getSheets() ||
SpreadsheetApp.openById("1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU").getSheets())
for(var s in allsheets){
var sheet = allsheets[s];
// Stop iteration execution if the condition is meet.
if(exclude.indexOf(sheet.getName())==-1) continue;
sheet.insertRowBefore(row);
}
}
As I see it you have a couple of options, which I'll be listing here as A, B, and C. Please note that you might need two different .GS files as you are linking to two sheets
A
Try code found on google app script documentation
I found the google apps script documentation for this command found here, so you might want to check that for this questions and others , but here is the exact code included
// The code below opens a spreadsheet using its ID and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openById("abc1234567");
Logger.log(ss.getName());
B
use open by url instead of open by id
Your issue might be that your current id isn't correct, I have no way of knowing, so here is some alternate code here (link to documentation here)
// The code below opens a spreadsheet using its id and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openByUrl(
'https://docs.google.com/spreadsheets/d/abc1234567/edit');
Logger.log(ss.getName());
C
Tie the google script to one sheet
This last option doesn't require any code, just an explanation. Instead of trying to link your script to two separate sheets, you might be able to automatically link it to a single google sheet and create two pages in the sheets file that you treat as two different sheets but are one thing. This might not be what you want, but I included it anyways. You link the sheet to the code automatically by:
1 opening your sheet
2 going to "tools"
3 clicking script editor
4 copy and paste your code (except for the "open by id" part)
5 success!
Your exclude variable doesn't contain what you think it does. You're using an "or" operator (||), which will take the first "truthy" value and skip the rest.
console.log((["PROJECTS"] || 'something else')); // ["PROJECTS"]
Moreover, you don't have a good way of telling which spreadsheet belongs to which team member. To solve that problem, you can create an object.
const teamSpreadsheetIds = {
'DAVID': 'ABC',
'KATE': '123',
};
console.log(teamSpreadsheetIds['DAVID']); // ABC
With the teamSpreadsheetIds object, you can now go about updating your team member sheets locally as well as their individual spreadsheets. The "PROJECTS" sheet is unique, so there's only one check for it.
function InsertRow() {
const ss = SpreadsheetApp.getActive();
const allSheets = ss.getSheets();
const row = SpreadsheetApp.getActiveRange().getRow();
const teamSpreadsheetIds = {
'DAVID': '1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU',
'KATE': '1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI',
};
for (let sheet of allSheets) {
const sheetName = sheet.getName();
const memberSpreadsheetId = teamSpreadsheetIds[sheetName];
const isSkippable = memberSpreadsheetId === undefined && sheetName !== 'PROJECTS';
if (isSkippable) { continue };
// Insert a row in the local sheet
sheet.insertRowBefore(row);
// Get the member sheet and insert a row
if (memberSpreadsheetId) {
const memberSpreadsheet = SpreadsheetApp.openById(memberSpreadsheetId);
const memberSheet = memberSpreadsheet.getSheets()[0]; // Assumes the first sheet is the one to modify
memberSheet.insertRowBefore(row);
}
}
}

How to get values from unspecified number of sheets

I have a spreadsheet that may have any number of sheets on it at any given time. These "Side Sheets" have a total value added and placed in a specified cell. We'll say this total is in cell "A1" on every side sheet. I want to total all of these side sheet totals, and place the total in-cell on another sheet.
I've coded a solution I think should work, but it displays "loading" forever. I'm certain there's an easier way to do this, I just can't see it.
function GETSIDESHEETTOTALS(){
var totalCell = "A1"
var total = 0;
var cur_ss = SpreadsheetApp.getActive();
cur_ss.getSheets().forEach(function(sheet){
total += sheet.getRange(totalCell).getValue();
});
return total;
}
I'm expecting the totals from each sheet to add together and display in the cell I've specified on the main sheet. I've placed the function "=GETSIDESHEETTOTALS()" into a cell on the main page of my spreadsheet. I would prefer it to be a cell-called function if possible.
Does anyone have an alternate solution, or can tell me what I'm doing wrong?
For those familiar with Excel, this could be rephrased as, "How do I use Google App Script to sum using 3D cell references?".
Briefly looking at yours, you do not exclude the sheet on which you aggregate the total. Perhaps you're recursively adding the values together?
My very quick example from scratch:
function sum3D(cellRef) {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var cumTotal = 0;
for (var i=0;i<sheets.length;i++){
if (sheets[i].getIndex()!==1){ // specifically omit the first sheet
cumTotal += sheets[i].getRange(cellRef).getValue();
}
}
return cumTotal;
}
This is implemented in the first sheet in my Google Sheet as "=sum3d('A1')".
However, I would recommend designing this more generally to simply return an array upon which you can perform any function (average, multiplications, etc.).
E.g.
function get3D(cellRef) {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var arr = [];
for (var i=0;i<sheets.length;i++){
if (sheets[i].getIndex()!==1){
arr.push( sheets[i].getRange(cellRef).getValue());
}
}
return arr;
}
and implemented as, e.g., "=sum(get3d('A1'))".
EDIT
Some parts unnecessarily separated in the code have been consolidated (but the overall function remains the same)
EDIT 2
There are obvious improvements regarding how you designate the aggregator sheet. For example, you could simply pass in the sheet name in the formula and omit that based on the return value of "sheets[i].getName()".

Google Docs: Get Sheet Without Name or ID

I'm writing some scripts for a client with the end goal of complete autonomy -- when complete, the spreadsheet will always work forever. Ideally, anyways.
Because I need information from other sheets, I have to access them in a way other than .getActiveSheet(). Because the client might re-name or re-order the sheets, I have to access the sheet in a way that works even after those changes. This rules out getSheetByName() and getSheets()[SHEET_NUMBER] (again, the client might re-name or re-order the sheets). However, it should be possible because of the "gid." Each sheet has a different gid and they do not change when you re-order or re-name the sheets (scroll to the end of the URL for each sheet to see what I mean).
All of the URL accesses only open the FIRST sheet. For instance,
SpreadsheetApp.openById(SHEET_ID).getDataRange().getValues()
returns the values of the first sheet, even if I include the "gid" part at the end. Same with openById and openFile.
So my question is, how do I access a sheet in a way that will work even after renaming the sheet or reordering the sheets within the spreadsheet?
There's no getSheetById method, but you can build your own using getSheetId(). Here:
function sheetsIdMap() {
var sheetsById = {};
SpreadsheetApp.getActive().getSheets().forEach(function(s){ sheetsById[s.getSheetId()] = s; });
//just checking that it worked
for( var id in sheetsById )
Logger.log(id+' - '+sheetsById[id].getName());
//usage example
var sId2 = sheetsById[2];
Logger.log('\n'+sId2.getName());
}
-- edit
Let's try a more straightforward function (although I don't like to do such a loop and don't store the data on a map for subsequent use o(1)).
function getSheetById(ssID, sheetID) {
var sheets = SpreadsheetApp.openById(ssID).getSheets();
for( var i in sheets )
if( sheets[i].getSheetId() == sheetID )
return sheets[i];
return null; //sheet id not found in spreadsheet, probably deleted?
}
Yes there is a sheet id. Its sheet.getSheetId. this id can be used from apps script and can also be transformed into a "real" gid for making a sheet url. Do (sheetId ^ 31578).toString(36) to get the gid.
I had to reverse-eng it to get it and I cant guarantee it will work forever.