Auto fill a column with formulas when new rows are added - google-apps-script

I have an automation workflow (Zapier) which adds a new row to a Google Sheets whenever a new card is added in Trello. We use it to track statistics for departments etc. I have some extra fields which transform a date field into things like weeknumber, day, month, year... simple stuff.
I need to write a script which looks out for new rows entered into the spreadsheet and then auto-fills the other columns with preset formulas.
Example layout:
Columns populated via the automation:
a,b,d,e
Columns to populate via the script.
f,g,h,i,j,k
I've seen a few scripts that are similar but don't take a formula from the row above as the content to autofill.
Note: The formulas for each column are available in the row above which has been added manually by me (for now). So, in theory, the script could reference the cell above for the correct formula to use.
Note 2: I cannot use the ARRAYFORMULA method because Zapier will see the row as having content and will skip to the next empty row.

I think this will do it.
function fillInFGHIJK()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
for(var i=0;i<vA.length;i++)
{
if(!vA[i][5] && !vA[i][6] && !vA[i][7] && !vA[i][8] && !vA[i][9] && !vA[i][10])
{
sh.getRange(i,6,1,6).copyTo(sh.getRange(i+1,6,1,6));//According to Michelle this will works better because the cell references change appropriately
}
}
}

Related

Is there a Google Sheets method to have coded formulas fill values in the next empty row each day?

I have a Google Sheets document that has 36 formulas across a row in the "Historical Data" tab. When I run the script each morning it populates the next empty row with the formulas in the previous row and then makes the previous row a value (so as to remove the formulas). I have an issue sometimes that the row with formulas loses the values and display "#N/A". I believe this is because the data is pulled from GoogleFinance.
Error in the image below.
I am curious if there is a method to do the following:
Define the formulas for each column in the code.
Run the script each morning and populate the next empty row with the values of the formulas.
The biggest item I need help with is adding the following formulas to the code so they will not be present in the worksheet. Additionally, some of the formulas rely on the value in a cell. For example the first formula is to create the date based on the date in the last row cell (A). The remaining formulas use the date from the new row columnm (a) and inputs from other columns in the new row. So when you see A2736 that is in last row and A2737 is the new row that was create when I last ran the script.
//DATE(A)=workday(A2736,1,'NYSE Holidays'!$A$2:$A$27)
//VIX9D(B)=INDEX(GOOGLEFINANCE("INDEXCBOE:VIX9D","close",$A2737),2,2)
//VIX1D(C)=INDEX(GOOGLEFINANCE("INDEXCBOE:VIX","close",$A2737),2,2)
//VIX3M(D)=index(GOOGLEFINANCE("INDEXCBOE:VIX3M","close",$A2737),2,2)
//VIX6M(E)=index(GOOGLEFINANCE("INDEXCBOE:VIX6M","close",$A2737),2,2)
//VIX1Y(F)=index(GOOGLEFINANCE("INDEXCBOE:VIX1Y","close",$A2737),2,2)
//VVIX(G)=index(GOOGLEFINANCE("INDEXCBOE:VVIX","close",$A2737),2,2)
//SPX(H)=index(GOOGLEFINANCE("INDEXSP:.INX","close",$A2737),2,2)
//VIX9D:VIX(I)=B2737/C2737
//VIX:VIX3M(J)=C2737/D2737
//VIX:VIX6M(K)=C2737/E2737
//VIX:VIX1Y(L)=C2737/F2737
//CORR, SPX , VVIX, 5(M)=correl(H2733:H2737,G2733:G2737)
//CORR, SPX , VIX, 10(N)=correl(H2728:H2737,C2728:C2737)
//Contango VIX2:VIX1(O) No formula yet
//Vix4:Vix7 Contango(P) No formula yet
//Log Ret(Q)=ln(H2737/H2736)
//V10(R)=stdev(Q2728:Q2737)
//V20(S)=stdev(Q2719:Q2737)
//HV10(T)=sqrt(252)*R2737
//HV20(U)=sqrt(252)*S2737
//VIX - HV10(V)=C2737-(T2737*100)
//VIX - hv20(W)=C2737-(U2737*100)
//VIX9D % Rank(X)=PERCENTRANK(B$2:B,B2737)
//VIX1D % Rank(Y)=PERCENTRANK(C$2:C,C2737)
//VIX3M % Rank(Z)=PERCENTRANK(D$2:D,D2737)
//VIX6M % Rank(AA)=PERCENTRANK(E$2:E,E2737)
//VIX1Y % Rank(AB)=PERCENTRANK(F$2:F,F2737)
//VVIX % Rank(AC)=PERCENTRANK(G$2:G,G2737)
//VIX9D Median(AD)=MEDIAN(B$2:B)
//VIX1D Median(AE)=MEDIAN(C$2:C)
//VIX3M Median(AF)=MEDIAN(D$2:D)
//VIX6M Median(AG)=MEDIAN(E$2:E)
//VIX1Y Median(AH)=MEDIAN(F$2:F)
//VVIX Median(AI)=MEDIAN(G$2:G)
//VDelta (VIX-VIX9D)(AJ)=C2737-B2737
Current code is below.
// Add Run button to menu
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu("Auto Trigger")
.addItem("Run","runAuto")
.addToUi();
}
// Define function to run with menu button
function runAuto() {
recordValue()
}
function createTimeDrivenTrigger() {
// Trigger every Weekday at 09:00.
ScriptApp.newTrigger('recordValue')
.timeBased()
.onWeekDay(ScriptApp.WeekDay)
.atHour(9)
.create();
}
// Record history from a cell and append to next available row
function recordValue() {
if (isNotHoliday()){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Historical_Data");
var lastRow = sheet.getLastRow();
var oldDate = sheet.getRange(lastRow,1).getValue();
var rng = sheet.getRange(lastRow,1,1,36);
rng.copyTo(sheet.getRange(lastRow+1,1,1,36));
rng.setValues(rng.getValues());
}
}
function isNotHoliday(){
var yesterday = new Date(new Date().getFullYear(),new Date().getMonth(),new Date().getDate()-1);
var formattedDate = Utilities.formatDate(yesterday, Session.getScriptTimeZone(), "M/d/yy")
var holidays = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('NYSE Holidays').getRange('A2:A').getDisplayValues().join().split(",");
return (! ~holidays.indexOf(formattedDate))
}
If you wish to run the script once every morning you could use programmatically triggers. This way you can easily set up a script run inside your desired time frame. Then you only would need to get the range of the last row and paste the formula there. I see that the script already gets the range, so you only need Range.setFormula() to drop the formula.
There is a much simpler way to do this - which is to flip time the other way.
You leave the google finance formulas in row 2 along with a =TODAY() in cell B2, with headers in row 1.
every night at 11pm, your sheet inserts a row above row 3, copies the values from row 2 into the new row 3 as values only.
That's it. super simple. you never need to copy formulas at all.
The newest prices are always at the top. This is how most people track portfolios.

Google Sheets onEdit - clearing select content in the row above the active row in which the edit occurred

I'm currently building out an onEdit script that clears the content of select cells upon edit. The issue I'm running into is that the cells to be cleared are dependent on the cell that was edited, so I can't use fixed ranges.
Please see Sheet2 of my spreadsheet here: https://docs.google.com/spreadsheets/d/1EoOIQxWyKWOvtlCrmJNI76FAxGhzgXrE4s0F05tw2MY/edit#gid=1388181285
Below is my current attempt at this:
function onEdit(e){
var submitColumn = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange('H:H');
var sh=e.range.getSheet();
if (sh.getName()=='Sheet2' && e.range.columnStart==8 && e.range.rowStart>1 && e.value=="submitResponse") {
var msh=e.source.getSheetByName("Master");
msh.appendRow(sh.getRange(e.range.rowStart,1,1,7).getDisplayValues()[0]);
var rowAbove = sh.getRange(e.range.rowStart,1,1,7).getLastRow()-1;
submitColumn.clearContent();
rowAbove.clearContent();
}
}
I'm trying to clear out two things:
Upon edit clear out the cell in column H that I changed to "submitResponse." Currently my script clears out the full column, which is not exactly what I want but it gets the job done.
Clear out the select cells in the rows above my target row. These rows (ex: 11,16,21,26...) are user-entered and inform the data in my target row (ex: 12,17,22,27...). Upon edit I'm trying to clear out the specific user-entered cells that informed the row I copied to the Master sheet. Currently my script – if it were working as written – would clear out the full row of user-entered cells, which is not what I want. Let me clarify with an example: when I change H12 to submitResponse, I want the following to happen: 1) A12:G12 is copied to the Master sheet; 2) H12 is cleared; 3) B11,D11:F11 is cleared. ... There may be upwards of 50 of these potential "submitResponse" rows so I'm trying to avoid fixed ranges if possible.
Google Sheets onEdit - trying to copy a row to a new sheet by changing the value of a column within that same row
function onEdit(e) {
var sh=e.range.getSheet();
if (sh.getName()=='Sheet2' && e.range.columnStart==8 && e.range.rowStart>1 && e.value=="submitResponse") {
e.range.setValue('');//the edited cell
var msh=e.source.getSheetByName("Master");
msh.appendRow(sh.getRange(e.range.rowStart,1,1,7).getDisplayValues()[0]);//append data no change
e.range.offset(-1,-5).clearContent();//clear cell B in row above
sh.getRange(e.range.rowStart-1,4,1,3).clearContent();//clear D,E & F of row above
}
}

How do I detect the newest row in a specific column?

I am currently having some problems with my Google App Script.
What we have for now is: when the users input two values(time) into the column, the duration between the column will be automatically calculated in another column, which we did this by the functions in the Spreadsheet itself.
When we transfer the input values, we are using the function appendRow, and since things will get messed up in the column we set the value that is supposed to go in the duration column as null, so that it will be filled blank and that the function in the Spreadsheet itself will calculate it. However, the problem we encountered was that by using this function in the Spreadsheet, we get a 00:00 as a default value in every single row that doesn't have a time input, meaning that the appendRow function will detect these rows as filled, and will append the values to the very bottom of the Spreadsheet, creating a big gap between the rows with the values and without the values.
We need a solution to solve this issue, and we are posting to get any possible solutions about this.
So the method we thought of to solve this issue was using getRange(), then setValue. With getRange we thought of using something like make a 1x1 grid that has the row Index as the same row Index as the new cell in the specific column that we want to import the values, and for columns we can just choose the column Index. But we soon realized that this will still fail as appendRow in the row Index will still return the row index of the very last row in the spreadsheet.
The next method we tried was to detect any "0:00:00" in the duration column, then get the corresponding row Index, and use that row Index to setValue at that specific row using getRange(), which again, failed.
for (var i = 0; i < sheet.getMaxRows()+1; i++ ){
if (sheet.getRange(i,4).getValue() === "0:00:00") {
sheet.getRange(i,1).setValue(datas.enterDate);
sheet.getRange(i,2).setValue(datas.enterStartTime);
sheet.getRange(i,3).setValue(datas.enterEndTime);
sheet.getRange(i,6).setValue(datas.enterFullName);
sheet.getRange(i,7).setValue(datas.enterCategory);
sheet.getRange(i,8).setValue(datas.enterActivity);
sheet.getRange(i,9).setValue(datas.enterLink);
break;
}
}
Expected results: As the function goes down the column it will search for any cells containing "0:00:00", and when it does it will set the values with the users data.
Output: Nothing shown.
Try this:
col is the column number. The default is the column of the active cell
sh is a Sheet Obj and the the default is current active sheet
ss is a Spreadsheet Object and the default is the current active spreadsheet.
function newestRow(col,sh,ss){
var ss=ss || SpreadsheetApp.getActive();
var sh=sh || ss.getActiveSheet();
var col=col || sh.getActiveCell().getColumn();
var rg=sh.getRange(1,col,sh.getLastRow(),1);
var vA=rg.getValues();
while(vA[vA.length-1][0].length==0){
vA.splice(vA.length-1,1);
}
return vA.length+1;
}

google sheets script to remove rows based on dates in multiple cells

I am working on an app to remove old content from a spreadsheet. I have found several posts that hide or delete sheets based on specific data, but it is always in a single cell. I need something that checks dates in three cells per row.
The sheet is populated by a form that asks users to enter three dates they want a bulletin to run in our video and print bulletin at the HS where I teach. I just want to remove old data, and found countless (well, maybe 6 or 8) examples of scripts that did something similar, so I grabbed one and tried to expand the logic statement to check columns C, D, and E (using index, of course).
It works, but removes any row with a date that is not today rather than dates in all three columns. I also need to have it count cells that are empty as qualifying as old.
So I tried the script suggested below by Cooper, which is a big advancement. It is still skipping rows with empty values, and in one case removes a row that I don't want to remove
In the image shown a form accepts input from teachers who submit bulletin content for a maximum of three dates. I've formatted the sheet to show old dates in red, today in green, and future dates in yellow. I want to delete rows that have all red or empty cells. A bandaid, however, would be to force them to pick a date for all three rather than allow non entries. But it would be nice to have the code recognize blank cells as qualifying.
[![Data Sheet for daily bulletin][1]][1]
Here's what eventually worked. I'm sure there are some smoother ways of doing this, but I actually grew a bit in my programming ability by working things out through the log first, then moving on once I had evidence that what I was doing worked.
/*
Here is the thinking:
I need to have a script that is run from Add-ons that checks each cell of a range
for dates. It can be done through conditional formatting bg colors, but that is
redundant.
The first thing is to create the functions that find and return row numbers that have all
old dates. In the initial build these can be returned to the log (Logger.log()) inside
of a for(loop).
'*/
function readRows() {
var sh=SpreadsheetApp.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rowsDeleted=0;
var today=new Date().valueOf();
//cycle through rows. In each row check column values for date. if date is old, add 1 to oldCount
for(var i=vA.length-1;i>=0;i--) {
var oldCount=0;
if(new Date(vA[i][2]).valueOf()<today) {
oldCount++;
}
if(new Date(vA[i][3]).valueOf()<today) {
oldCount++;
}
if(new Date(vA[i][4]).valueOf()<today) {
oldCount++;
}
if(oldCount>2) {
sh.deleteRow(i+1);
rowsDeleted++;
}
}
Logger.log(rowsDeleted);
}
Next I want to figure out how to make something that will output the daily bulletin to a Doc or pdf.
Try this:
function readRows() {
var sh=SpreadsheetApp.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rowsDeleted=0;
var today=new Date().valueOf();
for(var i=vA.length-1;i>=0;i--) {
if ((new Date(vA[i][2]).valueOf()<today)||(new Date(vA[i][3]).valueOf()<today)||(new Date(vA[i][4]).valueOf()<today) ){
sh.deleteRow(i+1);
rowsDeleted++;
}
}
Logger.log(rowsDeleted);
}
I'm not completely sure what you want but this might help. When your deleting rows you should start from the bottom.

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