Apps Script - How could this code be streamlined? - google-apps-script

I've recently started working with Apps Script to improve the scope of what my google sheets can do, and I wanted to ask more experienced people how I might make my script more efficient. I used a mixture of tutorials, documentation, and trial & error to make it. I find that although it usually completes the task it's meant for, sometimes it takes an unreasonably long time or exceeds its runtime and simply stops.
I would like to know which best practices I could implement to make it run more quickly overall, and which things I might be able to include in future scripts to avoid any pitfalls I'd landed in here.
Scope:
The script is meant to take each day's new data and apply it to a new sheet called 'TODAY.' It works as follows.
Rename the tab labeled 'TODAY' to the previous workday's date (if today is 2.3, it renames the sheet to 2.2.)
Hide this renamed tab.
Duplicate the 'TEMPLATE' tab, and rename it to 'TODAY.'
Pull data from the 'RAW DATA' tab, and paste it into the new 'TODAY' tab.
Paste a formula into the new 'TODAY' tab and drag it down to the bottom of the table so that the correct values populate and the conditional formatting occurs.
Any help would be greatly appreciated, I really just need some direction for how to improve my work.
Here is a link to an example sheet with editing permissions enabled: https://docs.google.com/spreadsheets/d/1F7bAd2DjKgk53e-haPgjWfFphMfu5YBn8iRQ3qwC3n0/edit?usp=sharing

In my humble opinion, a good Google Sheet App Script doesn't need to use activate to control the source or destination of data. The sheet and script developer should know what and where they want the data to come from and go. Activate is like using the mouse to click on something.
I've taken your script and rewritten to minimize the use of variables. I have only one sheet variable and reuse it throughout. In fact for the majority of the time it is the copy of the TEMPLATE called TODAY.
Also unless I have to use a sheet last row many times, I avoid using a variable and instead just use sheet.getLastRow(). Same for columns.
I always wrap my code in a try catch block as a matter of habit.
As a last note, unless you change the notation in column C and N you could have used your script to fill in column B.
function myDailyUpdate() {
try {
let spread = SpreadsheetApp.getActiveSpreadsheet();
// Step 1
let sheet = spread.getSheetByName("TODAY");
let oldDate = sheet.getRange("Q4").getValue();
let prevDate = Utilities.formatDate(oldDate,"GMT-5","M.d");
// Renames old 'TODAY' sheet to previous workday's date.
sheet.setName(prevDate);
// Sets the color to red.
sheet.setTabColor("990000");
// Hides the old 'TODAY' sheet
sheet.hideSheet();
// Step 2
sheet = spread.getSheetByName("TEMPLATE");
// Copies the contents of the 'TEMPLATE' sheet to a new sheet called 'TODAY.'
sheet = sheet.copyTo(spread);
sheet.setName("TODAY");
sheet.activate(); // required to move to 1st position
// Move TEMPLATE to first position
spread.moveActiveSheet(1);
// Step 3
// Colors the 'TODAY' tab green to signify it being active.
sheet.setTabColor("6aa85f")
// Identifies the 'RAWDATA' sheet for later use.
let source = spread.getSheetByName("RAWDATA");
// Identifies ranges and values for later use.
let values = source.getDataRange().getValues();
// sheet is still the "TODAY" sheet
// Identifies 'TODAY' sheet as the recipient of 'RAWDATA' data, and identifies the range.
// Sets the values from 'RAWDATA' into 'TODAY.'
sheet.getRange(12,2,values.length,values[0].length).setValues(values);
// Step 4
// sheet is still the "TODAY" sheet
let range = sheet.getRange("C12");
range.setFormula(
'=IFERROR(IFERROR(IFS(VLOOKUP($B12,INDIRECT'
+'('
+'"'
+'\''
+'"'
+'&$Q$4&'
+'"'
+'\''
+'!"&"!"&"A1:O2000")'
+',15,false)="D","D",$N12="Quote","Q",$N12="Important","I",$N12="On Hold","H",$N12="IN TRANSIT","T",$N12="REQUEST","R",$N12="INCOMPLETE","N",$N12="COMMENT","C"),VLOOKUP($N12,$B$3:$C$9,2,FALSE)),"")');
// Pastes the above formula into cell C12.
let fillRange = sheet.getRange(12,3,values.length,1);
range.copyTo(fillRange);
sheet.activate();
}
catch(err) {
console.log(err);
}
}

Related

How to create a function that triggers whenever a new sheet is created? [duplicate]

This question already has an answer here:
How to set sheet create event as a trigger in apps script
(1 answer)
Closed 1 year ago.
I have a spreadsheet with a sheet of modification times for each of my other sheets. For example, I have a 'Signing' and 'Profile' sheet, and in my modifications times sheet I have:
Sheet Name
Modification Time
Signing
1639335205000
Profile
1639335207338
I want to create a function that, whenever I create another sheet, automatically adds it to the modifications times sheet as a new row.
I have looked at ScriptApp triggers and events but haven't found anything that is related(onEdit for example might be useful if there was a way to know if the edit was creating a sheet (if it even catches those events) but would also be triggered all the time).
I've actually wondered this myself and I hope someone can post a better answer than my method.
First, I create a named range that's going to store the number of sheets in the workbook (in below code I used sheetCount). Make sure this is at the workbook level (which by default google does).
Then leverage onEdit with this:
function onEdit(e) {
//var range = e.range;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const storedSheetCount = ss.getRange("sheetCount")//<-- need to setup named range
var sCount = ss.getNumSheets();
if(storedSheetCount.getValue()!=sCount){
if(storedSheetCount.getValue()<sCount){
// more
Browser.msgBox("You addeded a spreadsheet!")
}else{
//for less
Browser.msgBox("you took one away")
}
//both cases update the value
storedSheetCount.setValue(sCount); //<--- updates stored count
}
}
I am well aware that this is not ideal for a variety of reasons including:
No code is executed until an edit ACTUALLY happens.
Stated differently, adding a sheet is not an edit event, so a user must then click into a cell or delete a blank one to kickoff the procedure.
Takes up space on the spreadsheet front end.
I hate helper columns/cells. Names is one area that Excel definitely crushes GoogleShhets as it allows direct references to values. Thus with Excel, I could avoid cluttering a spreadsheet by setting a named rage to the sheetCount (ie. refersTo:=3). One alternative to this would be to use the spreadsheet's file description, but this requires granting permissions to Drive Service which opens up all kinds of security risks for such a trivial request.
If anyone can do better, please share.

Google Sheets script that runs based on the face value of a cell while ignoring formulas

I made a Google Sheet to check every media that plays on a certain channel on TV using a lot of workaround formulas within the cells themselves. A part of this sheet is a column (G) that tells me whether or not the specific episode/media/whatever is currently playing, has played in the past or will be played later today/at a later date using a "NOW" function. Next to that column there is another (F) where the user is able to write a "V", and in the case the show is playing but the user hasn't checked it yet, it writes "Check Me" See Example.
I wanted to create a button that will automatically change that "Check Me" into a "V" but the problem is that "Check Me" is based on a simple formula written throughout column F (=IF(G5="Playing","Check Me","")), so when I tried to run a script I found here on StackOverflow:
function Test() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("F5:F700");
range.setValues(range.getValues().map(function(row) {
return [row[0].replace(/Check Me/, "V")];
}));
}
(Can't remember the exact thread I got it from and it's been two days since I found it with lots of similar searches in my history, so I apologize for not crediting)
together with its intended use, it also straight up deleted all the rest of the formulas from the column, probably due to the formula itself containing "Check Me" but I might be mistaken.
To be honest, before this week I barely ever worked with either Google Sheets, much less JavaScript or even coding in general, so I'm pretty much restrained to changing values and very minor modifications in scripts I find online.
The only idea I had as to how to solve it is to add an "if IsBlank" but regarding face value of the cell only rather than its contents, but I don't know how to do it or whether it is even possible in the first place. At the very least, google shows no results on the subject. Is there a way to add that function? or perhaps a different method altogether to make that button work? (it's a drawing I will assign a script to)
Because you're using a map function to update the range, you'll need to get the formulas using getFormulas() in addition to the display values using either getValues() or getDisplayValues(). Using only the display values, as you're currently doing, will cause you to lose the formulas when you update the sheet. Conversely, using only the formulas would cause you to lose all of the display values that don't have a formula, so you'll need both. Try this and see if does what you want:
function Test() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("F5:F700");
// Get all cell formulas and display values
var formulas = range.getFormulas();
var values = range.getValues();
range.setValues(formulas.map(function(row, index) {
var formula = row[0];
var value = values[index][0];
// Check only the display value
if (value == "Check Me") {
return ["V"];
} else {
// Return formula if it exists, else return value
return [formula || value];
}
}));
}

Apps script onedit triggered too fast causing code conflicting with itself

I'm trying to make an applet in Google Spreadsheets and Apps script that scrapes view data about youtube videos searched by users. When a user types in a search query, a new sheet should be copied from a template sheet and customized to the query. The problem I'm encountering is that when a user rapidly types in multiple queries in succession, the script would dump multiple copies of the template sheet and name them 'Copy of template 1', 'Copy of template 2' and so on, whereas the name of each sheet should be "KW: " + its associated keyword. I suspect this is because the function duplicates and renames a sheet, but if two or more instances of a function try this at nearly the same time, they target the same sheet, causing errors.
This is the culmination of what I've tried:
function main(e){
//spreadsheet=SpreadsheetApp.getActiveSpreadsheet() at the top of the function
//keyword=the string the user typed in
//...
while(true){
var random=Math.random().toString();
try{
//assign the template sheet a random name other processes will fail when they try to use it
spreadsheet.getSheetByName('template').setName(random);
//make a copy of the template
spreadsheet.getSheetByName(random).copyTo(spreadsheet);
//give the copy a proper name
spreadsheet.getSheetByName('Copy of '+random).setName("KW: "+keyword);
//reset the name of the template so other processes can use it
spreadsheet.getSheetByName(random).setName('template');
break;
}
//when a process fails, it should wait then try again
catch(e){Utilities.sleep(Math.round(random*250));}
//...
}
main is has a trigger set on edit. The above code prevents any 'Copy of template n' sheets from appearing, but it simply leaves out most of the sheets that should be produced. My guess is that the code encounters an error in the first line of the try block and loops until it times out. I'm very much at a loss as to what to do. I'd appreciate any help, thank you!
Try copying the template sheet first, then changing the name of the new sheet. Your solution runs into errors because you're modifying the template sheet, but you should really avoid doing that. With this approach, you'll never modify the template sheet.
function main(e){
//spreadsheet=SpreadsheetApp.getActiveSpreadsheet() at the top of the function
//keyword=the string the user typed in
//...
while(true){
try{
// Select the template sheet
var templateSheet = spreadsheet.getSheetByName("template");
// Set the new sheet name
var sheetName = "KW: "+keyword;
// Copy the template sheet and set name
var newSheet = templateSheet.copyTo(spreadsheet).setName(sheetName);
break;
}
//when a process fails, it should wait then try again
catch(e){Utilities.sleep(Math.round(random*250));}
//...
}
}
Granted that I don't have full knowledge of what you're doing with your script, I do have some major concerns with your approach. I don't think you should be creating a new sheet for every query. For one, that will make your data really challenging to aggregate. Secondly, since sheet names must be unique, any time someone searches for something that already has an existing sheet, your function will get stuck in the infinite while loop you set up.
I don't know what kind of data you're placing in the sheets, but you should consider trying to record the data in one sheet. Also, although your while loop works and will, in the case of errors, eventually be terminated by Google's script limitations, you should really try (1) implementing something more robust like appending a number to the sheet name and (2) properly logging the errors.

How to disable editing cells after a particular time in Google Spreadsheet?

Use Case - I have shared a Google Spreadsheet amongst a dozen friends and we are entering our prediction for matches. The catch is to enter it before the game starts.
Using Spreadsheet as everyone can see everyone's prediction.
Problem - Is there an AddOn or any feature which allows to disable editing a few cells after a particular time? Say post midnight A[7]-M[7] cells cannot be edited.
You can set a script to run at a specific time:
In the script editor, create a function:
function protectRangeAtMidnight() {
//My code will go here
}
In the script editor, click on the RESOURCES menu, and choose, CURRENT PROJECTS TRIGGERS.
Add a trigger for a specific date and time.
The problem is, what is the code going to do? If you protect the range, but the people you are sharing the spreadsheet with have edit authority, they can just unprotected the range. If you change their permissions to VIEW only, then you'd have to change it back at some point for the next game. That would work as long as there can be a time period where no one else can edit the sheet.
You can remove a user from the editor list:
Remove Editor
function protectRangeAtMidnight() {
SpreadsheetApp.openById('The SS ID').removeEditor(emailAddress);
}
You can also set file sharing permissions through DriveApp:
setSharing(accessType, permissionType)
I am actually doing exactly the same thing and came across the same problem. The solution I came up with does not lock the cells but uses data validation. Some of the solutions suggested online did not seem to take into account that you need to lock a row of results which have a date associated with it.
This is the layout I am using for my predictions:
Google sheet cropped image example
The cells in blue then have the following data validation (criteria is custom formula, reject input):
=if(isnumber(C1),and(now()<$A1,C1>=0,C1-int(C1)=0))
It checks that what is entered in C1 is a number. If it is, it then checks the following:
If the current date and time is before 'kick off'.
If the number is greater than or equal to zero.
That it is a whole number.
If so, it allows the cell to be changed. If the match has kicked-off, the cell cannot be altered and a red triangle will appear in the cell (because the data validation will be violated as now() will be after the date in question) but it stops the cells from being changed once the game has kicked off.
If you couple the above with locking the entire sheet (apart from the blue cells) it should allow users to make predictions prior to kick-off.
If it is necessary for cells to be altered after the game has begun you can modify the date in column A to then make the update before changing the date back.
Hope this helps!
What works is to:
Lock the cells or columns at a specific time, then remove the protection at another time: using a daily trigger (or even manually)
Modifications needed before running the functions:
Sheets names (In my example it's Sheet1, Sheet2)
Range to protect (in my example it's A:D) (editors won't be able to edit the specified range)
function Lock() {
var tabs = ['Sheet1', 'Sheet2'];
var ss = SpreadsheetApp.getActiveSpreadsheet();
for (var i = 0; i < tabs.length; i++) {
var spreadsheet = ss.getSheetByName(tabs[i]);
var protection = spreadsheet.getRange('A:D').protect();
protection.setDescription('Protected')
protection.removeEditors(protection.getEditors());
}
};
And to remove the protection you'll use the following script: Editors get "back" their permission to edit the specified range:
function Unlock() {
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Protected') {
protections[i].remove();
}
}
};

Spreadsheet Email Trigger

I have Written Script on Google Spreadsheet to send Email when spreadsheet is modified or any Data is added. Email Trigger is working but whenever any data is entered in next Row it send Email to previous email address also.
Please suggest solution
The below is written script :
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 1; // Number of rows to process
var dataRange = sheet.getRange(startRow, 1 , numRows,3) // Fetch the range of cells A2:B3
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
var emailAddress = row[2]; // First column
var message = row[0] + "requested" + row [1]; // Second column
var subject = "Sending emails from a Spreadsheet";
MailApp.sendEmail(emailAddress, subject, message);
}
}
Your question is unclear... nowhere in the script I see something that reads which cell is actually modified... your target range is hardcoded on row 2 so the only row that can be processed is row 2 (and the mail can only be sent once)...
So can you :
explain how it should work
explain how it works now , especially what do you mean by 'previous email'
remove typos in your code (row[2] is not First column)
explain how you trigger this function : the name onEdit(e) suggest an onEdit trigger but simple triggers cannot send mail so I suppose you have set some other trigger.
explain why (e) in your function parameter and not using it ?
EDIT : thanks for the complement of information.
The script you suggest is not sufficient to achieve what you want. The idea here is to check if something in the sheet has been modified either by adding (or inserting) a row of data or (if I understood well) by editing any row in the sheet with a new value.
This is not really as simple as it looks at the first glance ;-)
What I would do it to take a 'snapshot' of the sheet and -based on a timer or onEdit - compare that snapshot to the sheet's current state.
There is more than one way to get that result, you could have a second sheet in your spreadsheet that no one could modify and that is a copy of the main sheet that you update after each modification/mail send. So before updating the script should look for any difference between the sheets and send a report to the corresponding email when a difference is found.
Another way to do that is to store the sheet data converted to a string in the script properties, the principle is the same but it's more 'invisible' for normal users accessing the spreadsheet.
You could also use scriptDb or your userproperties but the script properties is probably better suited (simpler) for this use case.
Tell us what you think/prefer and I (or someone else) could probably give you some code to start with.
It appears that you're using a shared spreadsheet to collect the add-user-requests, and trusting the requesters to fill in the information. In the detail document you shared, it further appears that requests are ADDED, but not EDITED. (That's an important simplifying distinction.)
I suggest that what you really need is to use a form for receiving that input. Using a form will create a "data table" within your spreadsheet, a set of columns that you must not mess with. (You can edit the contents, add and delete rows, but must not add or remove columns.) However, you CAN add columns to the spreadsheet outside of this table, which gives you a handy place to store state information about the status of individual requests.
Further, you can trigger your processing to run on form submit, rather than a simple "onEdit" - this gets away from the problem that ScampMichael pointed out. Alternatively, you can use an installable edit trigger, as described in this answer.
Try this sheet, and this form. Save yourself a copy, go into the script and remove the comments that are stopping emails from being sent, and try it out. There's a menu item in the spreadsheet that can kick off processing; just clear the "Request State" column to re-run it. You can open the form (and find its URL), and add more entries to experiment.
It's the core of a similar system that I've written, and contains a discreet state machine for processing the requests. My system has large amounts of very complex data in multiple spreadsheets, so it often gets pre-empted, then needs to run again. (I use a timed trigger for that.) That's why requests are handled through states. If you find that too complex, pull out only the parts you need.