Lag time in starting function from On Edit trigger - Google Apps Script - google-apps-script

I have been using an On Edit trigger in a google apps script. While making this script, I have noticed that there is a short period of time between when a user finishes making an edit to the google sheet, and when the function called by the On Edit trigger begins to execute. Although this lag time is short, it is long enough for a user to quickly make additional edits to the sheet before running the function called by the On Edit trigger.
Is there any way to reduce/eliminate this lag time or to prevent the user from making any changes to the sheet while waiting for the function to begin?
I understand that the code within the function can cause a delay while the function is running, but I can resolve that issue by temporarily protecting the sheet while the function runs, and unprotecting it at the end of the function. This still leaves the issue with the lag time from the trigger because I can't protect the sheet until the function starts to run.
I can prove the existence of the lag time with the code below. The first thing this function does when called by the On Edit trigger is set the value of cell G1 to 69. If you delete the 69 in G1, you can quickly type in a different number and enter it before the function changes the value back to 69 again. The additional code and the other stuff on the sheet are just there to demonstrate what exactly I am trying to achieve with this and why. Here is a link to the related google sheet (it runs the function in the code block below when triggered by an edit): https://docs.google.com/spreadsheets/d/1BZB7YWguMdts-pvr7SSSEDUgSAOe023o9b_6VOhUoLw/edit?usp=sharing
// The goal of this function is to only allow the user to fill in column A with items from column D. The user should only be able to enter each item from column D once, but they can
// be moved around and replaced at will.
function triggerTest() {
//These two lines are just to quickly prove lag time. Nothing to do with the rest of this code...
let testCell = SpreadsheetApp.getActiveSheet().getRange('G1');
testCell.setValue(69);
//These next 4 lines are just putting the values in column A and column D into arrays (getDisplayValues() returns a 2d array)
//The cleanArray function deletes blank cells from the array and makes it a 1d array
let databaseValues = SpreadsheetApp.getActiveSheet().getRange('D2:D11').getDisplayValues();
let cleanDatabaseValues = cleanArray(databaseValues);
let itemsColumnValues = SpreadsheetApp.getActiveSheet().getRange('A2:A11').getDisplayValues();
let cleanItemsColumnValues = cleanArray(itemsColumnValues);
//These for loops find all items in the column D that are not already in column A and puts them into a new array - valuesInDatabaseAndNotInItemsColumn[]
let valuesInDatabaseAndNotInItemsColumn = cleanDatabaseValues;
for(let i=0; i<cleanItemsColumnValues.length; i++){
for(let j=0; j<valuesInDatabaseAndNotInItemsColumn.length; j++){
if(valuesInDatabaseAndNotInItemsColumn[j] === cleanItemsColumnValues[i]){
valuesInDatabaseAndNotInItemsColumn.splice(j,1);
j--;
}
}
}
//This is the data validation for column A. It only allows values that are in Database column and NOT already in the Items column. If there is a duplicate both entries are invalid.
//Since every cell undergoes validation, the cells that already contain entries must be allowed to keep them which is the reason for the .concat on line 44. Duplicate cells are not
//allowed to contain their current value, hence the absence of .concat on line 41.
for(let i=0; i<itemsColumnValues.length; i++){
let cellIndex = (i + 2).toString();
let cell = SpreadsheetApp.getActiveSheet().getRange('A'+ cellIndex);
let duplicate = false;
let cellValidation = SpreadsheetApp.newDataValidation();
for(let j=0; j<itemsColumnValues.length; j++){ //This for loop tests to see if there is a duplicate of the entry
if(j != i && itemsColumnValues[j][0] === itemsColumnValues[i][0]){
duplicate = true;
}
}
if(duplicate === true){
cellValidation.requireValueInList(valuesInDatabaseAndNotInItemsColumn, true).setAllowInvalid(false).build();
}
else{
cellValidation.requireValueInList(valuesInDatabaseAndNotInItemsColumn.concat(itemsColumnValues[i][0]), true).setAllowInvalid(false).build();
}
cell.setDataValidation(cellValidation);
}
}

Related

Using for and if loops in Google Apps Script

Dear programming Community,
at first I need to state, that I am not quite experienced in VBA and programming in general.
What is my problem? I have created a topic list in google sheets in order to collect topics for our monthly meeting among members in a little dance club. That list has a few columns (A: date of creation of topic; B: topic; C: Name of creator; ...). Since it is hard to force all the people to use the same format for the date (column A; some use the year, others not, ...), I decided to lock the entire column A (read-only) and put a formular there in all cells that looks in the adjacent cell in column B and sets the current date, if someone types in a new topic (=if(B2="";"";Now()). Here the problem is, that google sheets (and excel) does then always update the date, when you open the file a few days later again. I tried to overcome this problem by using a circular reference, but that doesn't work either. So now I am thinking of creating a little function (macro) that gets triggered when the file is closed.
Every cell in Column B (Topic) in the range from row 2 to 1000 (row 1 is headline) shall be checked if someone created a new topic (whether or not its empty). If it is not empty, the Date in the adjacent cell (Column A) shall be copied and reinserted just as the value (to get rid of the formular in that cell). Since it also can happen, that someone has created a topic, but a few days later decides to delete it again, in that case the formular for the date shall be inserted again. I thought to solve this with an If-Then-Else loop (If B is not empty, then copy/paste A, else insert formula in A) in a For loop (checking rows 1 - 1000). This is what I have so far, but unfortunately does not work. Could someone help me out here?
Thanks in advance and best regards,
Harry
function NeuerTest () {
var ss=SpreadsheetApp.getActive();
var s=ss.getSheetByName('Themenspeicher');
var thema = s.getCell(i,2);
var datum = s.getCell(i,1);
for (i=2;i<=100;i++) {
if(thema.isBlank){
}
else {
datum.copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}}
}
The suggested approach is to limit the calls to the Spreadsheet API, therefore instead of getting every cell, get all the data at once.
// this gets all the data in the Sheet
const allRows = s.getDataRange().getValues()
// here we will store what is written back into the sheet
const output = []
// now go through each row
allRows.forEach( (row, ind) => {
const currentRowNumber = ind+1
// check if column b is empty
if( !row[1] || row[1]= "" ){
// it is, therefore add a row with a formula
output.push( ["=YOUR_FORMULA_HERE"] )
} else {
// keep the existing value
output.push( [row[0]] )
}
})
Basically it could be something like this:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Themenspeicher');
var range = sheet.getRange('A2:B1000');
var data = range.getValues(); // <---- or: range.getDisplayValues();
for (let row in data) {
var formula = '=if(B' + (+row+2) + '="";"";Now())';
if (data[row][1] == '') data[row][0] = formula;
}
range.setValues(data);
}
But actual answer depends on what exactly you have, how your formula looks like, etc. It would be better if you show a sample of your sheet (a couple of screenshots would be enough) 'before the script' and 'after the script'.

How to set timer for a row in google sheets

I would like to set a timer for an entire row in google sheets where a user can start entering data in the second row only after a certain time after starting row one.
Example: If a user starts filling cells in row 1 then they should be able to fill the data in the second only after the timer ends.
Could anyone suggest me how to get started or suggest me a chrome extension for this use?
You could also suggest me on how to build the chrome extension I can try it along with my colleagues.
This function uses an onEdit trigger to impose a 20 second delay between editing rows. It may not be exactly what you want but perhaps it's a start. It uses PropertiesService to keep state. I think user properties would be a better choice but script properties are easier to develop with since you can modify them directly in the script editor.
function onEdit(e) {
const sh=e.range.getSheet();
const delay=20000;
let ms=Number(new Date().valueOf()).toFixed();
if(sh.getName()=='Sheet10') {
const ps=PropertiesService.getScriptProperties();
let dObj=ps.getProperties();
if(dObj.hasOwnProperty('row') && dObj.hasOwnProperty('delay')) {
if(dObj.row!=e.range.rowStart && Number(ms-dObj.delay)<delay) {
e.range.setValue(e.oldValue);
e.source.toast('Sorry you have ' + (delay-Number(ms-dObj.delay))/1000 + ' seconds left.');
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}
}
Issue with Protections:
Class Protection is commonly used to protect ranges from being edited. It is not appropriate for your situation, though, because, as specified here, users who are executing the script cannot remove themselves from the list of editors:
Neither the owner of the spreadsheet nor the current user can be removed.
Using oldValue:
Because of this, the best way to go would be to use the parameter oldValue from the onEdit event object.
An onEdit trigger runs every time a user edits a cell. In it, you can use:
PropertiesService to store useful information: (1) whether it is the first time row 1 is edited (isNotFirstTime), and (2) when was last time first row was edited (startTime).
Event object to get information on the edited cell (its row, its old value, etc.).
You can do something along the following lines (check comments):
function onEdit(e) {
var current = new Date(); // Current date
var range = e.range;
var editedRow = range.getRow();
var sheet = range.getSheet();
var props = PropertiesService.getScriptProperties();
var waitingTime = 20 * 1000; // 20 seconds
var isNotFirstTime = props.getProperty("isNotFirstTime"); // Check if first row was previously edited
var startTime = new Date(props.getProperty("startTime")); // Time when first row was first edited
if (editedRow === 1 && !isNotFirstTime) { // Check that (1) edited row is first one, (2) it was not edited before
props.setProperty("startTime", current.toString()); // If it's first time first row was edited, store current time
Utilities.sleep(waitingTime); // Wait for 20 seconds
props.setProperty("isNotFirstTime", true); // Store: first row was previously edited
}
// Check that (1) second row edited, (2) Less than 20 seconds passed since first time first row was edited:
if (editedRow === 2 && (current - startTime) < waitingTime) {
range.setValue(e.oldValue || ""); // Set previous value to edited cell (this avoids editing cells)
}
}
Reference:
onEdit(e)
onEdit Event object
Class PropertiesService

How slow can the google sheets api be? [duplicate]

This question already has answers here:
How much faster are arrays than accessing Google Sheets cells within Google Scripts?
(2 answers)
Closed 3 years ago.
In my script I iterate through cells each of which represents a day on The calendar grid. So it goes from an earlier date (from a cell changed by the user) to a later one. Top down and left to right.
The script starts when a user changed value of a cell. And it must fill every subsequent days-cells by the same value up to certain cell. Lets say it must stop on the cell with red font. Thus every iteration the script must get the cell font color.
...Or, The iterations must stop when the script gets a cell representing a certain date. Thus every iteration the script must verify which date the cell represets. To understand what date a cell represents I get The values from the helper cells (headers) and use getValue(). Whatever.
Everything is bearable: looping through cells, changing the values in each cell, getting helper cells(ranges). But! As soon as I add getValue() to the given headers it starts to work unbelievably slowly. Or even I just get font color... Any function starting from "get" included in iteration makes the job unbelievably slowly!
A script with looping as many as you like getRange(), setValue() works out in tolerable time, but with just one getFontColor() or getValue this job runs in the same time for just one cell.
Either I do somethin illegal or google ?
Is there an opportunity to accelerate this job significantly?
Or job like this should be done quite differently?
function onEdit(evt) {
var aSheet = evt.source.getActiveSheet();
// veryfy which sheet
switch( aSheet.getName().toLowerCase() ) {
case "wage":
// get range - calendar grid
var wageGrid = aSheet.getParent().getRangeByName("wageGrid");
var editedCell = evt.range;
// loop exit flag
var weBreak = false;
editedCell.setFontColor("red");
// loop through rows
for(var rowIndex = editedCell.getRow(); rowIndex <= wageGrid.getLastRow(); rowIndex++) { if(weBreak) break;
// loop through columns
for(var collIndex = (rowIndex == editedCell.getRow())?editedCell.getColumn():wageGrid.getColumn(); collIndex <= wageGrid.getLastColumn(); collIndex++) {
// as many as you like
var currentLoopCell = aSheet.getRange(rowIndex, collIndex);
var dayHeaderCell = aSheet.getRange(rowIndex, 1);
var monthHeaderCell = aSheet.getRange(1, collIndex);
cell.setValue(evt.value);
// but getValue() or getSomeAttribute() will slow down the process
//var cellFontColor = cell.getFontColor();
//if(cellFontColor=="red") weBreak = true; break;
}
}
break;
case "nonexistentyet":
break;
default:
Logger.log("What was it?")
}
}
What you have encountered is normal behaviour. Each call to the sheet such as getValue() and getFontColor() takes a fair amount of time, often 1 to 2 seconds each. You should avoid calling these functions in a loop.
When you want to loop over a large set of cells and work with their values, define the entire range with getRange() and use getValues() and getFontColors() instead. These functions will return the data from an entire range in a two-dimensional array. You can even bring in all the data in the sheet with getDataRange().
Similarly, it is best to write in blocks as well, using setValues() and setFontColors() rather than setting values/colors on cells one by one

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

Writing an 'Undo' Function for Google Spreadsheets Using GAS

Currently there is no undo() function for Google Apps Script in the Spreadsheet/Sheet/Range classes. There were a few issues opened on the Issue Tracker, I can only find one now (I don't know what Triaged means): here.
There have been suggested workarounds using the DriveApp and revision history but I took a look around and didn't find anything (maybe it's buried?). In any case, an undo() function is incredibly necessary for so many different operations. I could only think of one kind of workaround, but I haven't been able to get it to work (the way the data is stored, I don't know if it's even possible). Here is some pseudo -
function onOpen () {
// Get all values in the sheet(s)
// Stringify this/each (matrix) using JSON.stringify
// Store this/each stringified value as a Script or User property (character limits, ignore for now)
}
function onEdit () {
// Get value of edited cell
// Compare to some value (restriction, desired value, etc.)
// If value is not what you want/expected, then:
// -----> get the stringified value and parse it back into an object (matrix)
// -----> get the old data of the current cell location (column, row)
// -----> replace current cell value with the old data
// -----> notifications, coloring cell, etc, whatever else you want
// If the value IS what you expected, then:
// -----> update the 'undoData' by getting all values and re-stringifying them
// and storing them as a new Script/User property
}
Basically, when the Spreadsheet is opened store all values as a Script/User property, and only reference them when certain cell criteria(on) are met. When you want to undo, get the old data that was stored at the current cell location, and replace the current cell's value with the old data. If the value doesn't need to be undone, then update the stored data to reflect changes made to the Spreadsheet.
So far my code has been a bust, and I think it's because the nested array structure is lost when the object is stringified and stored (e.g., it doesn't parse correctly). If anyone has written this kind of function, please share. Otherwise, suggestions for how to write this will be helpful.
Edit: These documents are incredibly static. The number of rows/columns will not change, nor will the location of the data. Implementing a get-all-data/store-all-data-type function for temporary revision history will actually suit my needs, if it is possible.
I had a similar problem when I needed to protect the sheet yet allow edits via a sidebar. My solution was to have two sheets (one hidden). If you edit the first sheet, this triggers the onEdit procedure and reloads the values from the second sheet. If you unhide and edit the second sheet, it reloads from the first. Works perfectly, and quite entertaining to delete data on mass and watch it self repair!
As long as you will not add or remove rows and columns, you can rely on the row and column numbers as indices for historic values that you store in ScriptDb.
function onEdit(e) {
// Exit if outside validation range
// Column 3 (C) for this example
var row = e.range.getRow();
var col = e.range.getColumn();
if (col !== 3) return;
if (row <= 1) return; // skip headers
var db = ScriptDb.getMyDb();
// Query database for history on this cell
var dbResult = db.query({type:"undoHistory",
row:row,
col:col});
if (dbResult.getSize() > 0) {
// Found historic value
var historicObject = dbResult.next();
}
else {
// First change for this cell; seed historic value
historicObject = db.save({type:"undoHistory",
row:row,
col:col,
value:''});
}
// Validate the change.
if (valueValid(e.value,row,col)) {
// update script db with this value
historicObject.value = e.value;
db.save(historicObject);
}
else {
// undo the change.
e.range.getSheet()
.getRange(row,col)
.setValue(historicObject.value);
}
}
You need to provide a function that validates your data values. Again, in this example we only care about data in one column, so the validation is very simple. If you needed to perform different types of validation different columns, for instance, then you could switch on the col parameter.
/**
* Test validity of edited value. Return true if it
* checks out, false if it doesn't.
*/
function valueValid( value, row, col ) {
var valid = false;
// Simple validation rule: must be a number between 1 and 5.
if (value >= 1 && value <= 5)
valid = true;
return valid;
}
Collaboration
This undo function will work for spreadsheets that are edited collaboratively, although there is a race condition around storing of historic values in the script database. If multiple users made a first edit to a cell at the same time, the database could end up with multiple objects representing that cell. On subsequent changes, the use of query() and the choice to pick only the first result ensures that only one of those multiples would be selected.
If this became a problem, it could be resolved by enclosing the function within a Lock.
Revised the answer from the group to allow for range when user selects multiple cells:
I have used what I would call "Dual Sheets".
One sheet acts as a backup / master and the other as the active sheet
/**
* Test function for onEdit. Passes an event object to simulate an edit to
* a cell in a spreadsheet.
* Check for updates: https://stackoverflow.com/a/16089067/1677912
*/
function test_onEdit() {
onEdit({
user : Session.getActiveUser().getEmail(),
source : SpreadsheetApp.getActiveSpreadsheet(),
range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
authMode : "LIMITED"
});
}
function onEdit() {
// This script prevents cells from being updated. When a user edits a cell on the master sheet,
// it is checked against the same cell on a helper sheet. If the value on the helper sheet is
// empty, the new value is stored on both sheets.
// If the value on the helper sheet is not empty, it is copied to the cell on the master sheet,
// effectively undoing the change.
// The exception is that the first few rows and the first few columns can be left free to edit by
// changing the firstDataRow and firstDataColumn variables below to greater than 1.
// To create the helper sheet, go to the master sheet and click the arrow in the sheet's tab at
// the tab bar at the bottom of the browser window and choose Duplicate, then rename the new sheet
// to Helper.
// To change a value that was entered previously, empty the corresponding cell on the helper sheet,
// then edit the cell on the master sheet.
// You can hide the helper sheet by clicking the arrow in the sheet's tab at the tab bar at the
// bottom of the browser window and choosing Hide Sheet from the pop-up menu, and when necessary,
// unhide it by choosing View > Hidden sheets > Helper.
// See https://productforums.google.com/d/topic/docs/gnrD6_XtZT0/discussion
// modify these variables per your requirements
var masterSheetName = "Master" // sheet where the cells are protected from updates
var helperSheetName = "Helper" // sheet where the values are copied for later checking
var ss = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = ss.getActiveSheet();
if (masterSheet.getName() != masterSheetName) return;
var masterRange = masterSheet.getActiveRange();
var helperSheet = ss.getSheetByName(helperSheetName);
var helperRange = helperSheet.getRange(masterRange.getA1Notation());
var newValue = masterRange.getValues();
var oldValue = helperRange.getValues();
Logger.log("newValue " + newValue);
Logger.log("oldValue " + oldValue);
Logger.log(typeof(oldValue));
if (oldValue == "" || isEmptyArrays(oldValue)) {
helperRange.setValues(newValue);
} else {
Logger.log(oldValue);
masterRange.setValues(oldValue);
}
}
// In case the user pasted multiple cells this will be checked
function isEmptyArrays(oldValues) {
if(oldValues.constructor === Array && oldValues.length > 0) {
for(var i=0;i<oldValues.length;i++) {
if(oldValues[i].length > 0 && (oldValues[i][0] != "")) {
return false;
}
}
}
return true;
}