Trigger a script when a formula changes a cell value - google-apps-script

I'm using a Google script then sends out an email when a certain column in a Google sheet is changed. The information in the cell is either inputted manually, or completes using a formula based on information in other cells.
The script works fine when information is manually entered, but not when the formula runs. I've read up on it and realise that a formula calculation doesn't count as an edit, so how do I get the script to run?
It's currently set up to trigger from the spreadsheet when there's an edit.
Below is the part of my script that covers the column/cell in question.
function sendEmail() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var row = sheet.getActiveRange().getRow();
var cellvalue = ss.getActiveCell().getValue().toString();
if(sheet.getActiveRange().getColumn() == 12)
There's a lot more included in the script so I haven't copied everything onto here. Many thanks in advance.

There is no trigger that can run when a formula changes.
Try figure out, what conditions is your formula depends on:
if it is another cell, entered manually, then use those cell to trigger it's changes with onEdit
if the formula imports data from external source, use random or time functions, you'd better use onTime trigger.
if the formula uses importrange then go to the range you import and see the original range, return to step 1 → 2...

Related

Apps Script: how do you copy values from specific columns to a different spreadsheet via a time trigger, button press or sheet update?

I need to copy the values of multiple ranges of data from my main spreadsheet into another spreadsheet and then copy a different range of data back to the main spreadsheet.
Here are my example spreadsheets:
The main spreadsheet (two sheets: 1 - A sheet of pasted values alongside columns of user comments. 2 - A sheet with a button that runs the Apps Script)
The live data spreadsheet (pulls and formats the required data from other sheets).
Previously, I’ve used .copyTo() to copy the values of data and paste them elsewhere on the same spreadsheet. However, this method can’t be used to copy data to a different spreadsheet. Copying data from multiple ranges is also causing me issues. Here is my code:
function RefreshSheetData() {
// 1a. Run the script when a button is pressed (Main Spreadsheet - ‘Update report button!A3’)
// 1b. Run the script at a certain time (every Monday at 10 AM)
// 1c. Run the script when data in a sheet is replaced with new data via a formula – NOT POSSIBLE.
// 2. Copy email address and user comments (Main Spreadsheet - Editable report - J3:J & AM3:AR)
// 3. Clear the “Updated user comments” sheet below the header row (Live data Spreadsheet - Updated user comments - A2:G)
// 4. Paste the values (Live data Spreadsheet - Updated user comments - A2:G)
// 5. Clear the “Editable report” below the header rows (Main Spreadsheet - Editable report – B3:AR)
// 6. Copy the Live sheet (which should now include the most recent user comments via array vlookup) (Live data Spreadsheet - live data – A3:AQ)
// 7. Paste the values (Main Spreadsheet - Editable report - B3:AR)
// 8. Add the (United Kingdom) time and date (Main Spreadsheet - Update report button - A10)
// 9. Add the time and date (Live data Spreadsheet - Updated user comments - J1)
ScriptApp.newTrigger('RefreshSheetData')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(10)
.create();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var startSheet = ss.getSheetByName('Editable report');
var sourceRange = startSheet.getRangeList(['J3:J', 'AM3:AR']);
var sourceValues = sourceRange.getValues();
var target = SpreadsheetApp.openById('1OHQHefYvE4vZZPr8jgziy_L3-UBf1WSoKzMWQ8LUz6w');
var targetSheet = target.getSheetByName('Updated user comments');
var clearTargetRange = targetSheet.getRange('A2:G').clearContent();
var targetRange = targetSheet.getRange('A2').setValues(sourceValues);
var liveTargetSheet = target.getSheetByName('Live data');
var liveSourceRange = liveTargetSheet.getRange('A3:AQ').getValues();
var clearMainRange = startSheet.getRange('B3:AR').clearContent();
var startRange = startSheet.getRange('B3').setValues(liveSourceRange);
SpreadsheetApp.getActive().getRange('A10').setValue(new Date());
targetSheet.getRange('J1').setValue(new Date())
}
Any tips would be appreciated. Thank you.
Part of my question was how to trigger a script when a formula output changes. That’s not possible. Neither .onEdit or .onChange triggers work as they only respond to user actions. They won't run when the value of an IMPORTRANGE or alternative formula changes.
Here some suggestions referring to each of the steps mentioned in your Apps Script file
1b. To run the script at a certain time: please use the installable trigger "Time driven"
1c. To run the script when data in the sheet is updated: please use the onEdit trigger
1d. You can combine all triggers by simply adding as many, as required.
To copy and paste data, you just need the methods getValues() and setValues(), which you were using already, the important thing is that you chose the "to copy" range correctly.
You can clear a range with clear()
See 3.
You can copy a sheet with copyTo(), however keep in mind that if using this method, your data in the copied sheet will automatically be updated if there is a change in the original sheet. If you want the values to remain static, you have to copy and paste them with copyValues() and setValues().
See 2. and 5.
See here how to get and format the date in Apps Script
Assign the date to a variable and use setValue()
I encourage you to try and build the script based on those steps
yourself, the Apps Script documentation provides you good reference
and guidance, how to do so. If you encounter specific problems during
one of the steps which you cannot solve with the documentation, feel
free to ask!

Google Sheets: how to copy formula down on rows added through Google Form responses

I am working with Google Form results data that is being dropped into a Google Sheet tab and I added a column to calculate the percentage of the quiz scores that are then being pulled onto a tracking tab in the same sheet that calculates their percentage completed. Right now the Percent Column is appearing as a blank field no matter which formula I've tried. I am wondering if there is a different formula would work that would automatically apply to the column when new responses are added? Or would a Google Script be a better option?
I am wanting to keep the raw result data on the same sheet since it is compiling all of the quizzes into one Google Sheet with one tab pulling the Percentages to show completion rate.
I have tried ARRAYFORMULA and the formula that works if you copy it manually to each entry is "=left(C2,find("/",C2)-1)/(right(C2,len(C2)-find("/",C2)))"
Try
=Arrayformula(if(len(C2:C), left(C2:C,find("/",C2:C)-1)/(right(C2:C,len(C2:C)-find("/",C2:C))),))
and see if that works?
This function will add a formula in the last column when a new formresponse will be added to the sheet:
function setFormula() {
var ss=SpreadsheetApp.openById('SHEET_ID');
var sheet=ss.getSheetByName('SHEET_NAME');
var lastRow = sheet.getLastRow();
var formulaRange1 = sheet.getRange(lastRow, sheet.getLastColumn());
formulaRange1.setValue('=IF($A'+lastRow+'="";"";TODAY()-$Q'+lastRow+')');
}
Your formula must be adjusted accordingly. Just make sure that lastRow is inside the string instead of the line number and add a onFormResponse Trigger. I've added this script to the form, not to the spreadsheet.

How to determine which row has changed/updated on Google Sheets Script Editor

For my google sheets, each row gets populated at random, but when it does get populated, I want a function to run. Each time something gets populated, it's the next row (it's not a cell or a random row).
I have the onChange trigger running right now. It is set to "run FunctionA from Spreadsheet on change". In that function, how do I grab the row that has been specifically changed/updated? What is the function/method I would use to manipulate whatever that row is?
I searched around but couldn't find an appropriate answer.
You should use the onEdit spreadsheet event. (https://developers.google.com/apps-script/guides/triggers/events)
You can call it this way :
function onEdit(e){
//Access the range with your parameter e.
var range = e.range;
var row = range.getRow();
var column = range.getColumn();
//do whatever you need with that range
}
It will automatically run when there is an edit on a cell of the spreadsheet.

Prevent a function in Google App Scripts from updating on a cell edit

I am trying to sort a list of 32 names in column A randomly to column B, I found this formula to do this:
=sort('Sheet1'!A2:A33,arrayFormula(randbetween(sign(row('Sheet1'!A2:A33)),100)),true)
This works great, but I then want to be able to use the new/random list in column B to use in other formulas. The problem is that any time you update any cell (even on another sheet) the list re-randomizes, preventing me from using any of the cells from the random list.
So I tried to take the formula out of the cell it was in and putting the formula in a menu option, thinking it would prevent the sheet/cell from updating with this Apps Script:
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('Menu1', 'menuOption')
.addToUi();
}
function menuOption() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var cell = sheet.getRange("B2");
cell.setFormula("=sort('Sheet1'!A2:A32,arrayFormula(randbetween(sign(row('Sheet1'!A2:A33)),100)),true)");
}
But it still re-randomizes the list in column B whenever a cell is edited. Is it possible to call a formula or App Script function only once, when ran?
Thanks,
Formulas inside cells update with every edit, for the problem you are having you must be calling it within a cell somewhere on the spreadsheet.
You need to remove this call and only run it via the scripts interface or the menu button.
or
If you don't mind wasting google server resources set a boolean value in a cell somewhere on your spreadsheet and check the value before you run the sorting aspect of the script.

onEdit timestamp based on other sheet

As of now, it appears that Google Spreadsheets does not allow locking of cell ranges but instead locking has to be done on a sheet by sheet basis in its entirety. I would like to share a sheet with another user and have them enter data on Sheet 1. To prevent tampering, I would like the data duplicated on Sheet 2 (locked by me) with a timestamp of the last change made on Sheet 1. I've been playing with this onEdit() function but it does not give an updated time when I edit Sheet 1 but still only if I edit Sheet 2. I just haven't figured out what I'm doing wrong.
function onEdit(e)
{
var ss = e.source.getActiveSheet();
var rr = e.source.getActiveRange();
//comment 2 lines below if you want it working on all sheets, not just on 2nd one
if(ss.getIndex()!= 2)
return;
///
var firstRow = rr.getSheetByName(Sheet2).getRow();
var lastRow = rr.getSheetByName(Sheet2).getLastRow();
//the last modified date will appear in 12th column
for(var r=firstRow; r<=lastRow; r++)
ss.getRange(r, 12).setValue(new Date());
}
Is there another way I could do this?
You cannot use scripts to prevent the other user from tampering, because
he can go to Tools>Script editor and edit your script so that it
becomes idle, then edit Sheet1, then restore your script.
See the issue tracker for this issue. You cannot protect your scripts
from being edited by other users with whom you share at least one worksheet of your spreadsheet.
On the other hand, leaving scripts aside, you can always see
the history of what was edited by whom and when: File>See revision history.
I see a couple of problems. First, you're trying to use .getSheetByName() on a range object, but that method is only supported by spreadsheet objects. You can run .getSheet() on a range and that will return the sheet the range is in. Secondly, formatting: GAS requires standard Javascript which means that your if and for functions need to have {}. This should work for what you want:
onEdit(e){
if(e.range.getSheet().getSheetName()!="Sheet2"){
var row1=e.range.getRow();
var col2=e.range.getColumn();
var row2=e.range.getLastRow();
var col2=e.range.getLastColumn();
var data=e.range.getValues();//the data to be copied
var copySheet=e.source.getSheetByName("Sheet2");//sheet you want to copy to
var copyRange=copySheet.getRange(row1,col1,row2-row1,col2-col1);// the range data will be copied to
var time=new Date();
var timeRange=copySheet.getRange(row,col);//wherever you want the time signature to be.
timeRange.setValue(time);//inserts time stamp into sheet
copyRange.setValues(data);//copies data to the range
}
}
Just replace row and col with the row and column you want to place the time into. It takes the edited range and copies it to the same location on sheet 2 so it only works if sheet 1 is the only sheet to be edited. Otherwise you'll end up with overlapping data. You could also set a time stamp to be adjusted to the range, meaning put its position as something like (row1,col2+5). That would put it 5 columns right of the edited range.