Google Apps Script - Spreadsheet run script onEdit? - google-apps-script

I have a script that I use in my spreadsheet. It is at cell B2 and it takes an argument like so =myfunction(A2:A12). Internally the function gets info from a large range of cells.
Nothing seems to work. I tried adding it to
Scripts > Resources > Current Project Triggers > On edit
Scripts > Resources > Current Project Triggers > On open
How can I have this function update the result with every document edit?

When you are making calls to Google Apps services inside your custom function (like getRange and getValues etc), unfortunately there is no way of updating such custom functions with each edit, other than passing all of the cells that you are "watching" for editing.
And, perhaps even more frustratingly, the workaround of passing say a single cell that references all of your "watched" cells with a formula doesn't trigger an update - it seems that one needs to reference the "watched" cells directly.
You could pass GoogleClock() as an argument which will at least update the function output every minute.
But the advice from many members on this forum (who have much more knowledge about this stuff than me) would simply be: "don't use custom functions".

I am not sure if this exact code will work but you can try something like this...
function onEdit(event){
var activeSheet = event.source.getActiveSheet();
if(activeSheet.getName() == "ActiveSheetName") {
var targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TargetSheetName");
targetSheet.getRange(2,2).setValue("=myfunction(A2:A12)");
}
}
Assuming that B2 cell in on the sheet "TargetSheetName" and assuming that the edited cell is on the sheet "ActiveSheetName", the function onEdit will trigger when you edited any cell in any sheet. Since there is an if statement to check if that edited cell is on the sheet "ActiveSheetName" it will trigger only if the edited cell is on that sheet and it will set the B" cell to the value =myfunction(A2:A12), forcing it to update (i guess).
hope that i am correct and that i was helpful

I had a similar issue, for me I wanted to "watch" one particular cell to trigger my function.
I did the following (pretending A1 is the cell i am watching)
IF(LEN(A1) < 1, " ", customFunction() )
This successfully triggered if I ever edited that cell. However:
"Custom functions return values, but they cannot set values outside
the cells they are in. In most circumstances, a custom function in
cell A1 cannot modify cell A5. However, if a custom function returns a
double array, the results overflow the cell containing the function
and fill the cells below and to the right of the cell containing the
custom function. You can test this with a custom function containing
return [[1,2],[3,4]];."
from: https://developers.google.com/apps-script/execution_custom_functions
which makes it almost useless, but it might work for your case?
If the custom function is assigned to a project trigger it has more power so personally I ended adding it to "Scripts > Resources > Current Project Triggers > On edit"
and basically "watched a column" so it only did things if the current cell was within the "edit range". This is a bit of a bodge and requires some hidden cells, but it works for me at the moment
var rowIndex = thisspreadsheet.getActiveCell().getRowIndex();
var colIndex = thisspreadsheet.getActiveCell().getColumn();
//clamp to custom range
if(rowIndex < 3 && colIndex != 5)
{
return 0;
}
//check against copy
var name = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 5).getValue();
var copy = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).getValue();
if(name != copy )
{
///value has been changed, do stuff
SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).setValue(name);
}

Related

Modifying e.range.getA1Notation() to accept wider range of inputs in Google Apps Script

I'm using a Google Apps script to update the active sheet's name with a cell (C6) value when another cell (I16) is updated. Currently, the script works fine when I16 is updated and the sheet is renamed as intended.
However, I would like the range of inputs to be much wider, like an A:J kind of range, or at least A1:J104. How can I achieve this?
I have also read this thread but I'm unsure of how to extract that info specific to this scenario.
The script I'm using is as follows:
function onEdit(e) {
if(e.range.getA1Notation() !== 'I16') return;
var s = e.source.getActiveSheet();
s = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().setName(s.getRange('C6').getValue());
}
Any assistance will be much appreciated. Thank you in advance!
EDIT: With the script, the following happens:
Upon editing cell I16, the contents of cell C6 changes via a formula. For example, when I enter "002" into cell I16, cell C6 updates to "Report-002", and so on.
At the same time, the script extracts the string in cell C6 and updates the name of the current sheet accordingly (i.e. the sheet is now named "Report-002".
The script works well for this specific scenario. However, I would like to trigger the script when I update a wider range of cells, and not just cell I16. Ideally, I'd like the script to run if I updated any cell in the sheet.
Does that help to clarify what I'm trying to achieve?
Try
function onEdit(e) {
e.source.getActiveSheet().setName(e.source.getActiveSheet().getRange('C6').getValue());
}
Change the if condition to account for bounds:
const [/*actual row*/ar, ac] = ['rangeStart','columnStart'].map(s => e.range[s])
const [/*row start*/rs, re, cs, ce] = [1,2,3,4];//C1:D2
if(ar >= rs && ar <= re && ac >= cs && ac <= ce){
// do stuff
}

Use Conditional formatting for whole Google Sheet Workbook to search for duplicates

Use Conditional formatting for whole Google Sheet Workbook
Currently I am using the single color CONDITIONAL FORMATTING for two columns in every worksheet that looks at the column and if there is a similar match with the single worksheet within that column it will highlight the column in yellow.
APPLY TO RANGE:
C1:C1000
FORMAT RULES
Format cells if ...
=if(C1<>"",Countif(C:C,left(C1,18)& "*") > 1)
Formatting Style
I selected yellow
And I do exactly the same thing for Column D
Now that I have over 10 worksheets and it is growing week by week I am finding that there are duplicates happening between the sheets, as conditional formatting only works per sheet.
So is there a way for me to do have Google Sheet conditionally format the entire workbook by looking at column C and column D and if there is a match that it highlights it in yellow.
If there is a script or macro that someone can actually code for me (as I don't know how to do it) I would really appreciate. Alternatively if anyone knows an easy way to do what I need done, I would be most grateful.
As an example here is a workbook to show you what I mean.
In the google workbook, in Sheet labelled "Visual Test Current" this is what I have been able to currently setup as described. It works perfectly.
In the sheet labelled "Visual Test Sheet 1" it works fine on the individual sheet itself, however it does not look at "Visual Test Current" and if there is a match based on the formula "=if(D1<>"",Countif(D:D,left(D1,18)& "") > 1)"
AND
=if(E1<>"",Countif(E:E,left(E1,18)& "") > 1)
It does not format the cell to tell me that there is a duplicate match in a previous sheet. That is what I am trying to figure out how to do.
How do I get/create a formula that looks at these two columns through every sheet in Google Workbook and if there is a match based on the parameters of Countif it formats the cell to yellow, highlighting to me that there is a duplicate of that cell somewhere in a sheet in Google Workbook.
Here is the link to the shared Test document to help you visual what I am trying to do.
https://docs.google.com/spreadsheets/d/e/2PACX-1vQryGa9Z325XMv1mU5hq0AhaO3uWbzZ7MSxfY7U4EcPS7hGg281wogKJE98IQjriR4cPIXTCatd-Y8d/pubhtml
Solution:
Return all sheets via Spreadsheet.getSheets() and iterate through them.
For each sheet, create the conditional formatting rule for columns C and D, using ConditionalFormatRuleBuilder.
Add the conditional format rules via Sheet.setConditionalFormatRules.
Code sample:
const EXCLUDED_SHEETS = ["Sheets that won't be updated"];
function updateAllSheets() {
const ss = SpreadsheetApp.getActive();
const sheets = ss.getSheets().filter(sheet => !EXCLUDED_SHEETS.includes(sheet.getName()));
const [cFormula, dFormula] = ["C","D"].map(column => sheets.reduce((acc, sheet, i) => {
const sheetName = sheet.getSheetName();
return acc + (i!=0 ? "+" : "") + `Countif(INDIRECT("'${sheetName}'!${column}:${column}"),left(${column}1,18)&\"*\")`;
}, `=if(${column}1<>\"\",`) + "> 1)");
sheets.forEach(sheet => {
const cRange = sheet.getRange("C1:C" + sheet.getLastRow());
const cRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(cFormula)
.setBackground("yellow")
.setRanges([cRange])
.build();
const dRange = sheet.getRange("D1:D" + sheet.getLastRow());
const dRule = SpreadsheetApp.newConditionalFormatRule()
.whenFormulaSatisfied(dFormula)
.setBackground("yellow")
.setRanges([dRange])
.build();
sheet.setConditionalFormatRules([cRule,dRule]);
});
}
Execute automatically:
If you want to execute this automatically, you should use a trigger. Here, I see two main options:
onEdit trigger. With this trigger, your function will execute every time a user edits the spreadsheet. To have this trigger, just rename your function updateAllSheets to onEdit.
Time-driven trigger. This kind of trigger would execute periodically (for example, every hour). In contrast with onEdit, to use this trigger you have to install it first. To install this, either follow these steps or execute a function like this once (the example below refers to a trigger that executes hourly, check this to see all the alternatives at your disposal):
function createTimeDrivenTrigger() {
// Trigger every 1 hour.
ScriptApp.newTrigger('updateAllSheets')
.timeBased()
.everyHours(1)
.create();

Is the pasted value recognized as an event object that is an argument of the onEdit function?

I ran the following program to check the contents of e:
function onEdit(e){
Browser.msgBox(e.value);
}
As a result, an empty string is displayed.
Is the pasted value recognized as an event object that is an argument of the onEdit function?
This seems to be a bug.
The value field in the onEdit event object returns undefined when the trigger is fired by a paste event. This happens for both single cell ranges and multiple cell ranges.
This behaviour has been reported several times in Issue Tracker:
Event object value returns undefined when pasting values
Installable onEdit function doesn't pass correct event.value when pasting cells into spreadsheet.
I'd suggest you to star the latter issue (which was filed internally, so Google is treating this as unintended behaviour) in order to give it more visibility and help prioritizing it.
Workaround:
Use e.range.getValue() instead of e.value (or e.range.getValues() if the pasted range has multiple cells):
function onEdit(e){
Browser.msgBox(e.range.getValue());
}
Your code should be able to display the content of one cell when a single cell is edited. But if you also want to include the values of a range of cells when multiple cells are edited, then use getValues():
function onEdit(e){
Browser.msgBox(e.range.getValues());
}
As it is also stated in the official documentation:
As a proof of concept you can try this:
function onEdit(e){
Browser.msgBox(typeof e.values);
}
The latter will return a string when you edit a single cell but undefined if you edit multiple cells at once, and this is why e.range returns nothing in the screen.
Edit: jan 2022
Try this to understand how it works and how to adapt your script:
function onEdit(e){
Browser.msgBox(JSON.stringify(e))
}
When a single cell is modified:
{"authMode":"LIMITED","value":"c","oldValue":"A","user":{"email":"xxxxxxxxxx#gmail.com","nickname":xxxxxxxxxx"},"range":{"columnEnd":3,"columnStart":3,"rowEnd":1,"rowStart":1},"source":{}}
When two or more cells are modified:
{"user":{"email":"xxxxxxxxxx#gmail.com","nickname":"xxxxxxxxxx"},"range":{"columnEnd":3,"columnStart":3,"rowEnd":4,"rowStart":1},"authMode":"LIMITED","source":{}}
You can therefore identify the range at any time by:
var rangeCoord = [e.range.rowStart,e.range.columnStart,e.range.rowEnd,e.range.columnEnd]
You can therefore proceed as you wish, for example:
function onEdit(e){
var sh = e.source.getActiveSheet()
for (var i = e.range.rowStart;i<=e.range.rowEnd;i++){
for (var j = e.range.columnStart;j<=e.range.columnEnd;j++){
// do something ... as
sh.getRange(i,j).setBackground('red')
}
}
}
the 'new values' are also available by sh.getRange(i,j).getValue() (not the old ones)

Custom function won't refresh as inputs are changed

I have a custom function that finds the value of another cell and displays it. When the source cell is changed, the function does not reflect.
https://docs.google.com/spreadsheets/d/1wfFe__g0VdXGAAaPthuhmWQo3A2nQtSVUhfGBt6aIQ0/edit?usp=sharing
Refreshing google sheets
function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}
If I change a rate in the rate tab, I need the custom function to recognize the new rate and update its value
You want to recalculate the custom function of =findRate(), when the cells of the sheet name of Rates are edited.
If my understanding is correct, how about adding the following sample script? Please think of this as just one of several answers.
Solution:
In order to recalculate the custom function, in this answer, the formula of =findRate() is overwritten by the script running with the OnEdit event trigger (in this case, it's the simple trigger.). By this, the recalculate is executed. But, when the formula is directly replaced by the same formula, the recalculate is not executed. So I used the following flow.
Retrieve all ranges of cells which have the formula of =findRate() from the sheet of "Projected Revenue".
Clear the formulas of the ranges.
Put the formulas to the ranges.
By this flow, when the cell of the sheet of "Rates" is edited, the custom function of =findRate() is recalculated by automatically running onEdit().
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue"; // If you want to change the sheet name, please modify this.
var formula = "=findRate()";// If you want to change the function name, please modify this.
var sheet = e.source.getSheetByName(sheetName);
var ranges = sheet.createTextFinder(formula).matchFormulaText(true).findAll().map(function(e) {return e.getA1Notation()});
sheet.getRangeList(ranges).clearContent();
SpreadsheetApp.flush();
sheet.getRangeList(ranges).setFormula(formula);
}
}
Note:
onEdit(e) is run by the OnEdit event trigger. So when you directly run onEdit(e), an error occurs. Please be careful this.
In this sample script, as a sample, even when the row 1 and column "A" of the sheet of "Rates" are edited, the custom function is not recalculated. If you want to modify this and give the limitation of range you want to edit, please modify the above script.
References:
Simple Triggers
Class TextFinder
Class RangeList
flush()
If I misunderstood your question and this was not the result you want, I apologize.
Added:
The proposal from TheMaster's comment was reflected to the script. When sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(formula) can be used, also I think that the process cost will be much reduced. But in my environment, it seemed that the formulas are required to be cleared once to refresh the custom function, even if flush() is used. So I have proposed above flow.
But, now I could notice a workaround using replaceAllWith() of TextFinder. So I would like to add it. The flow of this workaround is as follows.
Replace all values of =findRate() to a value in the sheet of Projected Revenue using replaceAllWith()..
In this case, as a test case, the formulas are replaced to sample.
Replace sample to =findRate() using replaceAllWith().
By this flow, I could confirm that =findRate() is recalculated. And also, it seems that flush() is not required for this situation.
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue";
var formula = "=findRate()";
var tempValue = "sample";
var sheet = e.source.getSheetByName(sheetName);
sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(tempValue);
sheet.createTextFinder(tempValue).matchFormulaText(true).replaceAllWith(formula);
}
}

e.range.getA1Notation() unable to track changes caused by formula update

I modified a script provided from this blog
How to have your spreadsheet automatically send out an email when a cell value changes
After some debugging an modifications, I can send emails by manually entering a value at position C7. That is, according to script, if the value is greater than 100, it will send a email to me. That only happens if I type the number manually into the cell.
The problem is, if the value is generated by a formula, then it doesn't work. (Say cell C7 is a formula=C4*C5 where the product value is >100)
After some trial-and-error, I think it is the code in the edit detection part causing the problem.
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
Since cell C7 is a formula, the formula itself doesn't change, what is changing is the values from formula calculations. So it may not think I have edited the cell.
How should I modify the script, so that the script also send email when value of C7 produced by a formula is greater than 100?
For reference, here is the code that I am using.
function checkValue(e)
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "C7")
{
if(valueToCheck >100)
{
MailApp.sendEmail("h********#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
}
}
}
onEdit(e) Trigger(Both simple and Installable) will not trigger unless a human explicitly edits the file. In your case, Your seem to be getting value from an external source (specifically, Google finance data).
Script executions and API requests do not cause triggers to run. For example, calling FormResponse.submit() to submit a new form response does not cause the form's submit trigger to run.
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
Also, for Google finance data,
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you will see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
Notes:
Having said that,
In cases where the change is made by a formula(like =IF(),=VLOOKUP()) other than auto-change formulas(like =GOOGLEFINANCE,=IMPORTRANGE,=IMPORTXML, etc), Usually a human would need to edit some other cell- In which case, You will be able to capture that event(Which caused a human edit) and make changes to the formula cell instead.
In cases where the change is done from sheets api, a installed onChange trigger may be able to capture the event.
To avoid repeated notifications due to edits in other places, send an email only when the value you are tracking is different from the previous one. Store the previous value in script properties.
function checkValue(e) {
var sp = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("sheet1");
var valueToCheck = sheet.getRange("C7").getValue();
var oldValue = sp.getProperty("C7") || 0;
if (valueToCheck > 100 && valueToCheck != oldValue) {
MailApp.sendEmail("***v#gmail.com", "Campaign Balance", "Balance is currently at: " + valueToCheck+ ".");
sp.setProperty("C7", valueToCheck);
}
}
Here the email is sent only when valueToCheck differs from the stored value. If this happens, the stored value is updated by setProperty.