'onEdit' function doesn't always fire when editing multiple cells quickly - google-apps-script

Purpose
This is for a checklist.
People type 'x' into a cell. If x is the only thing in the cell, it should change to ✓
Script
function onEdit(e) {
var r = e.range;
if (r.getValue() === 'x' || r.getValue() === 'X') {
r.setValue('✓');
r.setHorizontalAlignment('center');
}
}
Problem
Going slowly works.
But when inputting quickly (ie: type 'x', move to another cell with the arrow keys, type 'x', move, [quickly repeat this multiple times]), some cells change, but some remain as x.
EDIT:
I now have a working (but inelegant) solution. I'd still like to know if there's a more elegant solution.
This works
// Checks 300 rows and 11 columns, starting from E10 (row 10, col 5)
// if the value is 'x' or 'X', it changes it to a '✓'
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSheet();
// getRange(row, column, numRows, numColumns)
var values = sheet.getRange(10, 5, 300, 11).getValues();
for (var i=0; i<values.length; i++) {
for (var j=0; j<values[i].length; j++) {
if ( values[i][j] === 'x' || values[i][j] === 'X') {
var cell = sheet.getRange( 10 + i, 5 + j );
cell.setValue('✓');
cell.setHorizontalAlignment('center');
}
}
}
}
This checks every cell in the tables (3300 of them) each time onEdit fires. Since onEdit seems to always catch the last edit (but not the ones in between), this changes every x to ✓.
Background
At work, I've been asked to convert a checklist into a Google Doc. There are hundreds of checkboxes in various tables.
I've been asked to make the boxes have checks, ✓, since we print and show this checklist to clients.
Many of my colleagues are not computer literate, so the input method has to be very simple.
Questions
Is there a way to fix the original script, or is there another better way to do this?

I don't know if having data validation is an option? Else you can do this;
type in a cell: =char(10003) which will output as ✓
create data validation and choose list with items
copy the symbol (not the formula) and paste it in the box (and put a comma behind it)
if you need another symbol for not completed, repeat the previous steps with =char(10005), which will output as ✕
Besides that you can maybe use the square root symbol, which on my mac is easily entered using option-shift-V and looks like √ (you will have to look up that combination for your OS).

Related

Hide rows based on multiple checkbox values

The project I am working on is to calculate costs of remaining sets in a mobile game. It has a spreadsheet with a list of all the sets, and checkboxes for all 5 pieces, Columns B:F. I want to include the option to hide all sets that are completed, so all Checkboxes are checked. This is done via another Checkbox, H16.
I have modified the Checkbox values to use Yes and No.
I have never used Google Apps Script before, and am very new to coding in general. I think what I need is, to use onEdit, then every time a cell is edited, check if H16 is TRUE, then scan through each row to check the B:F values. If all are true, hide that row. I don't know the best way to type that out, though.
Bonus points, I also want to include a reset checkbox, so when checked, set all values in B:F to false, and show the rows.
Here is a link to the spreadsheet
EDIT: My current GAS code, which isn't much because I don't know what I am doing:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var maxSheet = 100;
if(H16 == true)
{
for(i, i<=maxSheet, i = i + 1) {
}
} else {
sheet.showRows(1, maxSheet);
}
}
Hiding rows when all five columns are true
This may not be exactly what you wish but I think it's close. I did not use yes and no values because it's easier for me to leave it true false but you can change that. I'm using Sheet0 and you can change that as well. I used less rows so you can also change that. But the basic idea is that when H16 is clicked it hides rows that have all five columns checked.
Code:
function onEdit(e) {
e.source.toast('entry');//debug
const sh = e.range.getSheet();
const sr = 2;//data start row
const lr = 15;//last row of data
sh.getRange('K1').setValue(JSON.stringify(e));//debug
if(sh.getName() == "Sheet0" && e.range.columnStart == 8 && e.range.rowStart == 16 & e.value == "TRUE" ) {
e.source.toast('past if');//debug
e.range.setValue("FALSE");
let vs = sh.getRange(sr,2,lr - sr + 1, 5).getValues();
vs.forEach((r,i) => {
if(r[0] && r[1] && r[2] && r[3] && r[4]) {
e.source.toast(`Row: ${sr + i}`);//debug
sh.hideRows(sr + i);
}
});
}
}
Image of Sheet0:
I use K1 to provide me with the event object while I debug the script. And I also use the e.source.toast in several location to get an idea of what is going on.
Animation:
an incomplete description of the event object
You can get a better understanding of the event object by using the JSON.stringify code as show in my example.
Most new people want to run the code from the script editor so I'll tell upfront that unless you provide the event object to populate the e then it's not going to work. Just copy and past it and get you unique stuff set like sheet name and data space and then proceed to edit the page and figure out how it works.

Popup box if a negative number based from formula

I am trying to figure out a script that will popup a box if a number in column F goes negative. I think I may be running into an issue due to the negative number generating from a formula as opposed to someone manually typing it in. Here is the script I have been working with:
function onEdit(event){
var sheet = event.source.getActiveSheet().getName()
var editedCell = event.range.getSheet().getActiveCell();
if(sheet=="Numbers"){
if(editedCell.getColumn() == 6 && event.value==-0.1){
Browser.msgBox('It looks like there is a negative number in Column F.');
}}}
I think it may have something to do with the 'editedcell' function but I am not 100%. Does anyone know of a better script that will popup a box even if a formula is generating a negative number (the value in column F will be a formula that may generate a negative)?
At this point, nothing happens with this code unless I manually type "-1" or some other negative in Column F.
Answer:
The event object returns the range of the cells you edited, not the ones edited by a script or formulae. As well as this, there are a few things you can do to fix or optimise your code.
Code Fixes:
Firstly, you can change your conditional to optimise the script and only make calls when you need them - this I have done with a slightly different conditional order.
As well as this, rather than event.range.getSheet().getActiveCell() you need to specify the values of column F explicitly, and use the getDisplayValues() function as this will also pick up the values if the cell contains a formula:
function onEdit(event) {
var sheet = event.source.getActiveSheet();
if (sheet.getName() != "Numbers") {
return;
}
var values = sheet.getRange('F:F').getDisplayValues()
for (var i = 0; i < values.length; i++) {
if (values[i] < 0) {
Browser.msgBox('It looks like there is a negative number in Column F.');
return;
}
}
}
References:
Google Apps Script - Event Objects
Google Apps Script - Best Practices

Google Sheets OnEdit script: copy value from specific columns

We are using Google sheets for tracking attendance. Previously, the teachers were entering P, T, or A (for present, tardy, absent) for each period. I would still like users to have the option to enter a value for each period in a week, however it would be a great time saver if they could enter one value for the whole day.
What I'd like is that if a value is entered into any one of the "0" periods (green columns) with a "P" or "A" (data validation limits those options) an OnEdit function would copy that same letter ("P" or "A") to the following 8 columns and then delete the original value. (without the deletion the totals on the far right columns will be off). I would not want the OnEdit to be activitated based on edits in any of the non-green columns.
I will eventually have several tabs, each one a different week, but each exactly the same... so I'm thinking the function should work within whatever the activesheet is.
https://docs.google.com/spreadsheets/d/1NKIdNY4k66r0zhJeFv8jYYoIwuTq0tCWlWin5GO_YtM/edit?usp=sharing
Thank you for your help,
I wrote some code to get you started with your project. (I am also a teacher) You will have to make some changes based on what you are going for and it can probably be optimised to run faster. Good luck!
function onEdit(e) {
//create an array of the columns that will be affected
var allColumns = [2, 10];
//get the number values of the column and row
var col = e.range.getColumn();
var row = e.range.getRow();
//get the A1 notation of the editted cell for clearing it out
var cell = e.range.getA1Notation();
//only run if the cell is in a column in the allColumns array
if(allColumns.indexOf(col) > -1) {
//run the for loop for the next 8 cells
for(var i = col + 1; i < col + 9; i++) {
SpreadsheetApp.getActiveSheet().getRange(row, i).setValue(e.value);
SpreadsheetApp.getActiveSheet().getRange(cell).setValue('');
}
}
}

Change cell background color onEdit based on value

How can I change the cell background color based on the cell's content in an onEdit() function?
I've had many versions of code that I tested for this - some working almost right, some not working at all.
But I have yet to get this to work the way I need it to.
Please forgive the lack of elegance in the way that this is written, but I actually need to keep the code as straightforward as possible since there will be many cell changes, many conditionals, and many differing numbers of cells that will be changed depending on what gets changed on the worksheet.
Ok, so here goes...
function onEdit(event)
{
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var changedCell= event.source.getActiveRange().getA1Notation();
if (changedCell == 'B3') {
var c = ss.getRange("B3").getValue();
if (c < 2); {
ss.getRange("B3").setBackgroundColor('#ff0000');
ss.getRange("B12").setBackgroundColor('#ff0000');
}
if (c > 1); {
ss.getRange("B3").setBackgroundColor('#000000');
ss.getRange("B12").setBackgroundColor('#000000');
}
}
}
A few things to note
1.The name of the method is setBackground and not setBackgroundColor
2.How is the cell B3 formatted. The comparison works only if it is formatted as an integer. In most cases, Google Spreadsheet automatically formats the cells based on the data type, but if I'm writing code, I'd double check. So use something like
var c = parseInt(ss.getRange("B3").getValue()) ;
3.The semicolon is not needed after the if condition. That will terminate the if condition immediately. So use
if (c < 2) {
and
if (c > 1) {
4.Finally, I don't know how data comes into B3, but if you have 1.5 in B3, then both the if conditions become true and your background color is overwritten. So, I suggest that you use a if..elseif
For better readability, I'd use setBackground('red') and setBackground('white') for the common colours.
In addition to the advice from Srik, you should consider the efficiency of your onEdit() function. The general idea is to determine whether you should bail out as soon and as cheaply as possible, and then optimize the rest of the code according to the Best Practices (minimize Service calls, mainly).
It looks like you want the onEdit() to perform only on "Sheet2", but the way you have written it, it will trigger for a change on any sheet, but mess around with colors on "Sheet2". (Not a visible problem, since the conditions on "Sheet2" are the ones being used to decide on coloring, but you'll be burning up script execution time needlessly.) By using the event info to figure out what sheet has been updated, and exiting if it's not "Sheet2", the cost of the onEdit() drops.
I agree with Srik about the if statements... I just can't tell what you wanted. I don't think the suggestion of else if alone will solve it, though, because it would treat c > 1 as c > 2... if you wanted that, why not write it? Maybe you wanted no color for 1 < c < 2, with red for c < 1 and black for c > 2? You should sort out the logic in that part.
Here's how an optimized onEdit() would look, making maximum use of the event info:
function onEdit(event)
{
var ss = event.range.getSheet();
if (ss.getName() !== "Sheet2") return; // Get out quickly
var changedCell = event.source.getActiveRange();
var changedCellA1 = changedCell.getA1Notation();
if (changedCellA1 !== 'B3') return;
var c = event.value; // We know we edited cell B3, just get the value
var background = 'white'; // Assume 1 <= c <= 2
if (c > 2) {
background = 'red';
}
else if (c < 1) {
background = 'black';
}
changedCell.setBackground(background);
ss.getRange("B12").setBackground(background);
}

Format row color based on cell value

I am trying to adapt the example script from this previous, related question. For rows where the cell value in column K is zero, I want to make the row yellow.
Here is my current adapted code:
function colorAll() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 3;
var endRow = sheet.getLastRow();
for (var r = startRow; r <= endRow; r++) {
colorRow(r);
}
}
function colorRow(r){
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
var dataRange = sheet.getRange(r, 1, 1, c);
var data = dataRange.getValue();
var row = data[0];
if(row[0] === "0"){
dataRange.setBackground("white");
}else{
dataRange.setBackground("yellow");
}
SpreadsheetApp.flush();
}
function onEdit(event)
{
var r = event.source.getActiveRange().getRowIndex();
if (r >= 3) {
colorRow(r);
}
}
function onOpen(){
colorAll();
}
My problem is, I can't figure out how to reference column K. In the linked answer above, the script's creator claims, "[h]ere is a Google Apps Script example of changing the background color of an entire row based on the value in column A." First, and most importantly, I can't figure out where he's referencing column A. I thought changing "var dataRange = sheet.getRange(r, 1, 1, c);" to "var dataRange = sheet.getRange(r, 11, 1, c);" would do it, but that just added 10 blank columns to the end of my sheet, and then the script crashed. I do not understand why.
Secondly, but more as an aside, his claim that the script affects entire rows is inaccurate, as his original "var dataRange = sheet.getRange(r, 1, 1, 3);" only colored the first three columns - which is why I added "var c" and changed "3" to "c".
Furthermore, when I play/debug the script, or run "onEdit" from the spreadsheet script manager, I get "TypeError: Cannot read property "source" from undefined." I can see that "source" is undefined - I had mistakenly assumed it was a Method at first - but I'm not sure how to fix this issue either.
Lastly, column K will not always be the reference column, as I mean to add more columns to the left of it. I assume I'll have to update the script every time I add columns, but there is a column heading in row 2 that will never change, so if someone can help me devise a bit of code that will look for a specific string in row 2, then get that column reference for use in function colorRow(), I would appreciate it.
I can't tell if this script is structured efficiently, but ideally, I want my spreadsheet to be reactive - I don't want to have to rerun this script after editing a driving cell, or upon opening; it reads like it's supposed to do that (were it not buggy), but this is my first attempt at using Google Apps Script, and I don't feel certain of anything.
I'm not great with scripting, but I took a programming fundamentals/Python class in grad school back in 2006, and spent 4 years working with Excel & Access shortly after that, often creating and adapting Macros. I can't really design from scratch, but I understand the basic principles and concepts, even if I can't translate everything (e.g., I don't understand what the "++" means in the third argument in the "for" statement I'm using: "for (var r = startRow; r <= endRow; r++)." I think I'm allegorically equivalent to a literate Spanish speaker trying to read Italian.
Help, and educational explanations/examples, will be much appreciated. Thank you kindly for reading/skimming/skipping to this sentence.
Rather than rewriting the code which you have already got some help with, I will try to give you explanations to the specific questions you asked. I see that you have some of the answers already but I am putting thing in completely as it helps understanding.
My problem is, I can't figure out how to reference column K.
Column A = 1, B = 2,... K = 10.
I can't figure out where he's referencing column A.
You were close when you altered the .getRange. .getRange does different things depending on how many arguments are in the (). With 4 arguments it is getRange(row, column, numRows, numColumns).
sheet.getRange(r, 1, 1, c) // the first '1' references column A
starts at row(r) which is initially row(3), and column(1). So this is cell(A3). The range extends for 1 row and (c) columns. As c = sheet.getLastColumn(), this means you have taken the range to be 1 row and all the columns.
When you changed this to
var dataRange = sheet.getRange(r, 11, 1, c) // the '11' references column L
You have got a range starting at row(3) column(L) as 11 = L. This runs to row(3) column(getLastColumn()).
This is going to do weird things if you have gone out of range.
You may have pushed it in to an infinite for loop which would cause the script to crash
Secondly, but more as an aside, his claim that the script affects entire rows is inaccurate, as his original "var dataRange = sheet.getRange(r, 1, 1, 3);"
only colored the first three columns - which is why I added "var c" and changed "3" to "c".
You are correct. The (3) says that the range extend for 3 columns.
"TypeError: Cannot read property "source" from undefined."
What is happening here is not intuitively clear. You can't run the function onEdit(event) from the spreadsheet script manager because it is expecting an "event".
onEdit is a special google trigger that runs whenever any edits the spreadsheet.
it is passed the (event) that activated it and
event.source. refers to the sheet where the event happened so
var r = event.source.getActiveRange().getRowIndex(); gets the row number where the edit happened, which is the row that is going to have its color changed.
If you run this in the manager there is no event for it to read, hence undefined. You can't debug it either for the same reasons.
Lastly, column K will not always be the reference column, as I mean to
add more columns to the left of it. I assume I'll have to update the
script every time I add columns, but there is a column heading in row
2 that will never change, so if someone can help me devise a bit of
code that will look for a specific string in row 2, then get that
column reference for use in function colorRow(), I would appreciate
it.
Before I give you code help her, I have an alternative suggestion because you are also talking about efficiency and it is often faster to run functions in the spreadsheet than using scripts. You could try having column A as an index columns where ColumnA(Row#) = ColumnK(Row#). If you put the following into cell(A1), ColumnA will be an exact match of Column K.
=ArrayFormula(K:K)
Even better, if you add/remove Columns between A and K, the formula will change its reference without you doing anything. Now just hide columnA and your sheet is back to its originator appearance.
Here is your code help, utilizing some of your own code.
function findSearchColumn () {
var colNo; // This is what we are looking for.
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
// gets the values form the 2nd row in array format
var values = sheet.getRange(2, 1, 1, c).getValues();
// Returns a two-dimensional array of values, indexed by row, then by column.
// we are going to search through values[0][col] as there is only one row
for (var col = 0; col < data[0].length; col++) { // data[0].length should = c
if (data[0][col] == value) {
colNo = col;
break; // we don't need to do any more here.
}
}
return(colNo);
}
If break gives you a problem just delete it and let the look complete or replace it with col = data[0].length;
I can't tell if this script is structured efficiently, but ideally, I
want my spreadsheet to be reactive - I don't want to have to rerun
this script after editing a driving cell, or upon opening; it reads
like it's supposed to do that (were it not buggy), but this is my
first attempt at using Google Apps Script, and I don't feel certain of
anything.
It is ok, the fine tuning of efficiency depends on the spreadsheet. function onEdit(event)
is going to run every time the sheet is edited, there is nothing you can do about that. However the first thing it should do is check that a relevant range has been edited.
The line if (r >= 3) seems to be doing that. You can make this as specific as you need.
My suggestion on a hidden index column was aimed a efficiency as well as being much easier to implement.
I'm not great with scripting,
You are doing ok but could do with some background reading, just look up things like for loops. Unfortunate Python is grammatically different from many other languages. A for loop in google script is the same as VBA, C, JAVA, and many more. So reading about these basic operations is actually teaching you about many languages.
I don't understand what the "++" means in the third argument in the "for" statement
It is why the language C++ gets its name, as a programmer joke.
r++ is the same as saying r = r+1
r-- means r = r-1
r+2 means r = r+2
So
for (var r = startRow; r <= endRow; r++)
means r begins as startRow, which in this case is 3.
the loop will run until r <= endRow, which in this case is sheet.getLastRow()
after each time the loop runs r increments by 1, so if endRow == 10, the loop will run from r = 3 to r = 10 => 8 times
1.The onEdit is a special function that is automatically called when you edit the spreadsheet. If you run it manually, the required arguments won't be available to it.
2.To change the colour of the entire row when column K is 0, you have to make simple modifications to the script . See below
function colorRow(r){
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
var dataRange = sheet.getRange(r, 1, 1, c);
var data = dataRange.getValues();
if(data[0][10].toString() == "0"){ //Important because based on the formatting in the spreadsheet, this can be a String or an integer
dataRange.setBackground("white");
}else{
dataRange.setBackground("yellow");
}
SpreadsheetApp.flush();
}