e.range.getA1Notation() unable to track changes caused by formula update - google-apps-script

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.

Related

Getting email notification via Google Sheets when =importxml cell value changes

I'm working on a spreadsheet that uses multiple =IMPORTXML functions to import changing text and price values from a webpage. At this moment I have the following columns in my Google Sheet:
A: 'URL info'
B: 'URL'
F-N: 'Price' (in every column a different price value)
What I have
Via a script, found on this page (thank you Umesh Agarwal) I will receive an email notification once a change has been made within the spreadsheet. Once I make a change in a cell within the range of F2:N200 I will receive an email with the cell that have been changed. The problem is that I have the script to sent me an email with the changed cell once the cell with a value of the =importxml function is changing.
At this moment, when a cell is changing due to the =importxml function the script is sending me an email that cell A1 has changed... it is not sending me the right cell that has been changed which makes it difficult to see what changed. How can I solve this problem?
function sendEmailonEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cellValue = ss.getActiveSheet().getActiveRange().getA1Notation();
var getColumn = ss.getActiveSheet().getActiveRange().getColumn();
var sheetname = ss.getActiveSheet().getName();
var user = Session.getActiveUser().getEmail();
var Toemail = 'myemailid_1234#gmail.com';
var subject = 'New Entry in ' + data + '.' + ss.getName();
var body = 'Your file has a new entry in - ' + sheetname + ' Updated by - ' + user + data
' check file- ' + ss.getUrl();
if(data.indexOf('F2:N200')!=-1.23456789) {
MailApp.sendEmail(Toemail,subject, body);
}
};
I'm afraid this is not possible with triggers the way you have it set up.
For the trigger to fire when a formula is pulling data from an external source "On change" is the only trigger that will pick up the change. Unfortunately, it won't return which cell or value has changed, it will only return which sheet has changed.
The other alternative trigger you might run into is "on Edit", however, this trigger will not fire when the sheet is updated by formulas pulling data from an external source ¯\_(ツ)_/¯
Avenue for possible workaround:
You might be able to work around this with a Time-driven trigger AKA clock trigger. Writing a script that fires every so often to check for changes in the worksheet, and to send an e-mail if it does. You might copy all the data to another sheet and then compare the values, or use the Properties Service, to persist data within the script.

Google Sheets: Action Based on Birthday

I'm trying to send myself either an email or copy the row to a new sheet when it's someone's birthday or hire date anniversary. Copying the line to a new sheet would allow me to use zapier to notify me of the update. Either would work. The sheet uses a form to collect data.
I've built a few scripts but nothing that had to do with dates. I'm just struggling with this one and have tried a few examples I could find with no luck.
Here is this sheet. It's view only so just let me know if you need more access.
I understand that you want to replicate your form responses Sheet in another Sheet (let's call it Zapier Sheet) automatically each time that a new form response is added. You can achieve that goal developing an Apps Script code that runs at each form response. In that case you can use a code similar to this one:
function so62400514() {
var formSheet = SpreadsheetApp.openById(
'{FORM SHEET ID}').getSheets()[0];
var zapierSheet = SpreadsheetApp.openById(
'{ZAPIER SHEET ID}').getSheets()[0];
var formData = formSheet.getRange(1, 1, formSheet.getLastRow(), formSheet
.getLastColumn()).getValues();
var zapierData = zapierSheet.getRange(1, 1, zapierSheet.getLastRow(),
formSheet.getLastColumn()).getValues();
var recorded = false;
for (var fr = 0; fr < formData.length; fr++) {
for (var zr = 0; zr < zapierData.length; zr++) {
if (formData[fr].toLocaleString() == zapierData[zr].toLocaleString()) {
recorded = true;
}
}
if (recorded == false) {
zapierSheet.appendRow(formData[fr]);
} else {
recorded = false;
}
}
}
This code will first open both sheets (using SpreadsheetApp.openById() and Spreadsheet.getSheets()) to select the data with Sheet.getRange (setting boundaries with Sheet.getLastRow() and Sheet.getLastColumn()) and reading it using Range.getValues(). After that operation the data will get iterated using the property Array.length as the perimeter. The iteration compares each row from the form Sheet to every row of the zapier sheet (to accomplish that, I first parsed the row as a string with Date.toLocaleString()). If the form row is found in the zapier sheet, the boolean recorded will flag to true. After every row on the zapier sheet gets compared to the form row, the code will write it down on the zapier sheet based on the boolean flag.
As explained in the previous paragraph, this code will take the form sheet rows not present in the zapier sheet; and paste them on the zapier sheet. I used this approach to prevent missing any row (as it could happen when simultaneous users answer the form all at once). To make this fire automatically you'll need to set up an installable trigger with these settings:
As an example, let's say that we have these form responses:
And our initial sample zapier sheet looks like this one below. Please, notice how several past rows are missing;
After running the script (as it will do automatically) this would be the result:
I suggest running the script manually for an initial setup. If the timestamps diverge, please check if both spreadsheets share time zones. Don't hesitate to ask me further questions to clarify my answer.

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);
}
}

Trigger an email when a cell is written into from another app (IFTTT)

So here's what I've been working on. I'm a basketball coach and have a spreadsheet that pulls in all of my players' tweets from IFTTT.com (it basically takes the RSS feed of a twitter list and when it is updated, it updates the spreadsheet).
I have been working on coding that basically says "if a player tweets an inappropriate word, email me immediately."
I've got the code figured out that if I just type in an inappropriate word, it'll turn the cell red and email me. However, I have not figured out how to get the code to email me after IFTTT automatically updates the spreadsheet with tweets.
Here is my code thus far. Right now I've just got one "trigger" word that is "players" just to try and get the spreadsheet to work. Here's the code:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();//Get the spreadsheet
var sheet = ss.getActiveSheet()//Get the active sheet
var cell = ss.getActiveCell().activate();//Get the active cell.
var badCell = cell.getA1Notation();//Get the cells A1 notation.
var badCellContent = cell.getValue();//Get the value of that cell.
if (badCellContent.match("players")){
cell.setBackgroundColor("red")
MailApp.sendEmail("antadrag#gmail.com", "Notice of possible inappropriate tweet", "This tweet says: " + badCellContent + ".");
}
}
Here is a link to the spreadsheet I'm working with right now: https://docs.google.com/spreadsheets/d/1g5XaIycy69a3T2YcWhcbBy0hYrxSfoEEz8c4-zP63O8/edit#gid=0
Any help or guidance on this is greatly appreciated! Thanks!
I originally wrote this answer for your previous question, so it includes answers to some of your comments from there, but since you're continuing to go asking the community to write this step-by-step , here's the next step.
The issue I'm running into is that if three tweets come into the spreadsheet at the same time, with my code, it's only going to update the most recent cell, not all three. Does that make sense?
Yes, it does make sense.
When an onEdit() trigger function calls Spreadsheet Service functions to get current info from the sheet, it enters a "Race condition". If any changes occur in the sheet after the change that triggered onEdit(), and the time when it gets scheduled, those changes will be visible when it runs. That's what you see when you assume that the change you're processing is in the last row - by the time you're processing it, there may be a new last row.
Good news, though - the attributes of the event object passed to onEdit contain the details of the change. (The parameter e.) See Event objects.
By using e.range and e.value you'll find you have the location and content of the edited cell that kicked the trigger. If additional tweets arrive before the trigger is serviced, your function won't be tricked into processing the last row.
In new sheets, the onEdit() can get triggered for multiple-cell changes, such as cut & paste. However unlikely that it may happen, it's worth covering.
Well, after getting the spreadsheet all setup & actually using the trigger from IFTTT, it doesn't work. :( I'm assuming it's not dubbing it as the active cell whenever it automatically pulls it into the spreadsheet. Any idea on a workaround on that?
Q: When is an edit not an edit? A: When it's made by a script. In that case, it's a change. You can add an installable on Change function to catch those events. Unfortunately, the change event is less verbose than an edit event, so you are forced to read the spreadsheet to figure out what has changed. My habit is to have the change handler simulate an edit by constructing a fake event (just as we'd do for testing), and passing it to the onEdit function.
So give this a try. This script:
handles a list of "bad words". (Could just as easily be monitoring for mentions of your product or cause.)
has an onEdit() function that uses the event object to evaluate the row(s) that triggered the function call.
colors "bad" tweets
has a function for testing the onEdit() trigger, based on How can I test a trigger function in GAS?
includes playCatchUp(e), an installable trigger function (change and/or time-based) that will evaluate any rows that have not been evaluated before. Script property "Last Processed Row" is used to track that row value. (If you plan to remove rows, you'll need to adjust the property.)
Has the sendMail function commented out.
Enjoy!
// Array of bad words. Could be replaced with values from a range in spreadsheet.
var badWords = [
"array",
"of",
"unacceptable",
"words",
"separated",
"by",
"commas"
];
function onEdit(e) {
if (!e) throw new Error( "Event object required. Test using test_onEdit()" );
Logger.log( e.range.getA1Notation() );
// e.value is only available if a single cell was edited
if (e.hasOwnProperty("value")) {
var tweets = [[e.value]];
}
else {
tweets = e.range.getValues();
}
var colors = e.range.getBackgrounds();
for (var i=0; i<tweets.length; i++) {
var tweet = tweets[i][0];
for (var j=0; j< badWords.length; j++) {
var badWord = badWords[j];
if (tweet.match(badWord)) {
Logger.log("Notice of possible inappropriate tweet: " + tweet);
colors[i][0] = "red";
//MailApp.sendEmail(myEmail, "Notice of possible inappropriate tweet", tweet);
break;
}
}
}
e.range.setBackgrounds(colors);
PropertiesService.getDocumentProperties()
.setProperty("Last Processed Row",
(e.range.getRowIndex()+tweets.length-1).toString());
}
// Test function, adapted from https://stackoverflow.com/a/16089067/1677912
function test_onEdit() {
var fakeEvent = {};
fakeEvent.authMode = ScriptApp.AuthMode.LIMITED;
fakeEvent.user = "amin#example.com";
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
// e.value is only available if a single cell was edited
if (fakeEvent.range.getNumRows() === 1 && fakeEvent.range.getNumColumns() === 1) {
fakeEvent.value = fakeEvent.range.getValue();
}
onEdit(fakeEvent);
}
// Installable trigger to handle change or timed events
// Something may or may not have changed, but we won't know exactly what
function playCatchUp(e) {
// Check why we've been called
if (!e)
Logger.log("playCatchUp called without Event");
else {
// If onChange and the change is an edit - no work to do here
if (e.hasOwnProperty("changeType") && e.changeType === "EDIT") return;
// If timed trigger, nothing special to do.
if (e.hasOwnProperty("year")) {
var date = new Date(e.year, e.month, e["day-of-month"], e.hour, e.minute, e.second);
Logger.log("Timed trigger: " + date.toString() );
}
}
// Find out where to start processing tweets
// The first time this runs, the property will be null, yielding NaN
var lastProcRow = parseInt(PropertiesService.getDocumentProperties()
.getProperty("Last Processed Row"));
if (isNaN(lastProcRow)) lastProcRow = 0;
// Build a fake event to pass to onEdit()
var fakeEvent = {};
fakeEvent.source = SpreadsheetApp.getActiveSpreadsheet();
fakeEvent.range = fakeEvent.source.getActiveSheet().getDataRange();
var numRows = fakeEvent.range.getLastRow() - lastProcRow;
if (numRows > 0) {
fakeEvent.range = fakeEvent.range.offset(lastProcRow, 0, numRows);
onEdit(fakeEvent);
}
else {
Logger.log("All caught up.");
}
}

Google Apps Script - Spreadsheet run script onEdit?

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);
}