Google Sheets OnEdit script: copy value from specific columns - google-apps-script

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

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'.

Google Sheets (Google Finance) - Get stock price once and track

I would like to, upon data being entered into a new row (from external sheet - date, time, stock ticker), use google finance to get the current price of the stock into a cell and then no longer update the price in that cell. In the cell next to it, I want to track the price and in the cell next to that, I want to track the high, since creation (the highest value of the 'current price' cell).
Here is my sheet https://docs.google.com/spreadsheets/d/1EUU1bZnIfBatNI8H9pPk202PCD1g8wWXt5M0wx6S-jM/edit?usp=sharing
All I've got so far is a high value tracker that runs on the first row only for the right cells. Embarrasingly enough I can't figure out how to apply it to the entire columns.
So in summary, date time and stock will be entered into column A B and C. When that happens, I want to get the current price of the stock in C, and have that number no longer update. In D I want the price to be tracked, like Google Finance normally functions, and in E, the highest value of the D, in the same row.
That's the goal. Any help would be very much appreciated :)
All I've got so far is a high value tracker that runs on the first row
only for the right cells. Embarrasingly enough I can't figure out how
to apply it to the entire columns.
The code bound to your spreadsheet is
function onEdit() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("D2:E2");
var values = range.getValues()[0];
range.setValues([[values[0], Math.max(values[0], values[1])]]);
}
What does this code do?
It sets the range to work with always to D2:E2 - no matter what the actual edited range is
It works only with the first row of the range (range.getValues()[0])
It compares the 0 and 1 columns of the range (e.g. columns D and E) and assigns the value of column D back to column D (is it necessary?) and the higher of the two values to column E
How to modify your code?
It is not quite clear from your description how column D is populated and what you want to do with column F, but to give your general advice:
If you want your function to run on all rows:
Modify your range and expand it until the last row. Subsequently loop
through all rows:
function onEdit() {
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
var range = sheet.getRange("D2:E"+lastRow);
for (var i = 0; i < lastRow-1; i++){
var values = range.getValues()[i];
range.setValues([[values[0], Math.max(values[0], values[1])]]);
}
}
If you want you code to run only on the currently edited row:
Use the event object e.range to find out which is the edited row and work on this row:
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSheet();
var range = e.range;
var row = range.getRow();
var values = sheet.getRange(row, 4, 1, 2).getValues()[0];
range.setValues([[values[0], Math.max(values[0], values[1])]]);
}
Note: getRange(row, 4, 1, 2) is the notation to get the range starting with the defined row and column 4 (D), 1 row long and two columns wide, see here.
IMPORTANT: If your sheet is being populated automatically from an
external sheet - the onEdit trigger will not work for you (it only
fires on manual, human-made edits). In this case you will need a
workaround as described here.

Google app script: find first empty cell in a row

Im learning Google app script while building a dashboard. I'm collecting data from several sheets. My goal is to see by how many rows each sheet grows every week. This gives me insight in how my business is doing.
I can get the length of all the sheets I want to check, however I cant find any code which helps me to find the first empty cell in a specific row. I want to place the length of each sheet there (in my dashboard datacollection sheet) to create a graphs later on.
What I have is:
var range = ss.getRange(2, 1, 1, 1000);
var waarden = range.getValues();
Logger.log(waarden);
var counter = 0
for (var j = 0; j < ss.getLastColumn(); j++) {
Logger.log(waarden[0][j]);
if (waarden[0][j] == ""){
break
} else {
counter++;
}
Logger.log(counter);
}
This works but I can't image this being the best solution (or quickest solution). Any tips in case my length goes beyond 1000 without me noticing it (although it would take a couple of years to do so in this case ;) )?! Why does getLastColumn() behave so much different than getLastRow()?
Thanks for helping me learn :)
*** edited I figured out I have to use if (waarden[0][j] === ""){ with three = otherwise if my sheet in the row that I use as a check has a length of 0 than this is also counted as empty with two =operators.
Try indexOf()
function firstEmptyCell () {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var range = ss.getRange(2, 1, 1, ss.getMaxColumns());
var waarden = range.getValues();
// Get the index of the first empty cell from the waarden array of values
var empty_cell = waarden[0].indexOf("");
Logger.log("The index of the first empty cell is: %s", empty_cell);
}
This will give you the column position of the empty cell starting from a 0 index. So if the returned index is 4, the column is "E".
edit: As for the getLastColumn() question; you could use getMaxColumns() instead. Updated code to get all columns in the sheet.

Autofill google forms based on user input

Alright stack friends,
I'm working on my first projects using google scripts and it's been pretty fun so far. My project is to create a form for data entry that can either accept an ID number and fill in the rest of the fields, or let the user fill out the entire form. Basically my method to fill in the other fields is just to have a lookup table on the second sheet. When the user submits a form, the script runs, looks for the ID of the last row, scans the reference table for the ID, and then fills in the details.
I think the problem I'm having is the assumption that the data from the form is already in the sheet when the script runs. The problem I noticed is that the script sometimes fails to fill in the gaps. I tried creating form submissions in a loop with the same ID and they function somewhat erratically but it seems like the last sumbission always works which would make sense if the script executions are not matching up with the form submissions. Here's the script for reference:
function fillGaps() {
// First take in the appropriate spreadsheet objects and get the sheets from it
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheets()[0];
var refSheet = ss.getSheets()[1];
// Here's the last rows' index
var lastRow = sheet.getLastRow();
var lastRowRef = refSheet.getLastRow();
// now this is an array of values for the last row and the student ID entered
var response = sheet.getRange(lastRow, 1, 1, 7).getValues();
var enteredID = response[0][1];
// Next we're going to try to load up the lookup table and scan for the ID
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
var row = 0;
while(enteredID != stuIDs[row] && row <= lastRowRef){
row++;
}
// Okay at this point the row variable is actually -2 from what the sheet index
// is that I'm thinking of. This is because we didn't load the first row (names)
// and the way arrays are indexed starts with 0.
row++;
row++;
// now assuming that it found a match we'll fill in the values
if(row < refSheet.getLastRow()){
// Alright now we need to wrangle that row and format the data
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
}
So I'm wondering:
Does this seem like the right diagnosis?
If so, what would be the best way to remedy? I thought of adding a little delay into the script as well as trying to capture the submissions timestamp (not sure how to do that)
Thank you much!
The following code gives a 2D array:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
Also,refSheet.getLastRow gives the last row, lets say it is 10 in this case. The syntax for getRange is getRange(row, column, numRows) and the last argument is the number of rows, not the last column. So in the above code the selected range would be row 2 - 11 rather than 2- 10. Unless that is what you intended, modify the code like so:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()-1).getValues();
To access the values in stuIDs you should use stuIDs[row][0] (2D array) to check for matching ID. Assuming your ID was to be matched was in column 1.
Secondly, in the loop you are using the following to check for the last index in array row <= lastRowRef which will cause it go out of range(because array starts at 0 and sheet row at 1) instead use this row < stuIDs.length
Finally, in case you don't find a match you will end up with the last row and your code will end you taking the last row as the matched index. This can be prevented by using a boolean variable to check for a match.
var foundId = false
var row = 0;
var i = 0;
for (i in stuIDs){
if(stuIDs[i][0] == enteredID)
foundID = true
break
}
}
row = i + 2
if (foundID){
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
PS: You can also use event objects to get the values of response (eventObj.values). As mentioned here: https://developers.google.com/apps-script/guides/triggers/events
Hope that helps!

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