Say I have a spreadsheet with 18 rows, and I can hard code the range of the last 7 values in column D and average their values like so:
=AVERAGE(D12:D18)
How then could I do the same without hard coding them, so it'll work as I add more rows?
If you want to calculate the average or sum, there's no need for a script. You can accomplish this with an array filter as well. The following formula calculates the average over the last 7 rows in the A column:
=AVERAGE(FILTER(A:A;ARRAYFORMULA(ROW(A:A)>COUNT(A:A)-7+1)))
This assumes that the data starts at row 1. Otherwise you have to change the latter constant in the formula to the row number where the data starts.
You can also use OFFSET() as this Webapps.SO answer does for aggregating over the last X cells of a row.
For the opposite---aggregating the last X cells of a column---the OFFSET params just need moving around. Here's a command should should work for your example (this assumes your data starts at D2 and goes to D18):
=iferror(average(offset($D$2, max(0, count($D$2:$D18)-7), 0, 7, 1)), 0)
Turns out you can use your own code. Messy, but it works:
function lastValues(column, num) {
var lastRow = SpreadsheetApp.getActiveSheet().getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + lastRow).getValues();
for (; values[lastRow - 1] == "" && lastRow > 0; lastRow--) {};
arr = [];
for(i=0; i < num+1; i++){
arr.push(values[lastRow - i]);
};
return arr;
};
Related
I am a beginner in google sheets and I couldn't get around this formula. I have range of cells and I want to subtract last non empty cell to first cell (Z-A), here is the image:
As the values are updated in columns C, D, E and so on. I want to get the last non empty cell (from right) and subtract the values by moving backward (left). Like this:
sub = 10(Column G)-0(Column F)-10(Column E)-0(Column D)-10(Column C)
Can we devise a formula which will get the last non empty cell and subtract values until the first value? Here is the link to the sample sheet Thank you
try:
=LOOKUP(1, INDEX(1/(C2:F2<>"")), C2:F2)-(SUM(C2:F2)-
LOOKUP(1, INDEX(1/(C2:F2<>"")), C2:F2))
Suggestion: Use a custom function
You may use the following script as a custom function to get the difference between the value of the last cell and the sum of the other cells:
function SUBTRACTFROMLASTNUMBER(data) { //you can rename the custom function name
var sum = 0;
var data2 = data[0].filter(x => {
return (x != "") ? x : null;
}); //filtered data
var lastNumber = data2.pop(); //last number
data2.map(x => sum += x); //sums the remaining values
return (lastNumber - sum); //returns the output
}
This custom function extracts the selected data from the sheet and then separates the value of the last cell using pop() and then filters and sums the remaining data using filter() and map() and then subtracts the sum from the value of the last cell.
Usage
You may use this function as:
=SUBTRACTFROMLASTNUMBER(<range>)
Reference:
How to Manipulate Arrays in JavaScript
I have a google sheet that I want to add a conditional formatting, but the merged cells kinda of get in the way.
In my spreadsheet, I have the columns B,C,D and E where i'm using a formula in column B for when columns C,D and E are "done" column B gets a green background. The problem is, for some rows, columns D and E are merged.
This is the formula I'm using in column B
=and(C:C="done",D:D="done")
My desired result is: When the columns D and E are not merged, column B only gets the green background if columns C,D and E are "done", or else, it stays blank.
When D and E are Merged : B only gets green background if C and DE are "done", or else, stays blank.
Thanks in advance!!
If E always has a non-empty value when D and E aren't merged, you can use that to check for when E is blank using ISBLANK(E:E):
=AND(C:C="done", D:D="done", OR(E:E="done", ISBLANK(E:E)))
Otherwise, there isn't a way to test if a cell is part of a merged range without using a custom function, which is far less efficient, but could technically work with the assistance of a new helper column (e.g. column Z):
/**
* #customfunction
*/
function ISMERGED(cellAddress) {
var cell = SpreadsheetApp.getActiveSheet().getRange(cellAddress);
return cell.isPartOfMerge();
}
Where column Z (or whatever you choose for your helper) has the following in every relevant row (assuming data starts in row 1):
=ISMERGED(ADDRESS(ROW(E1), COLUMN(E1)))
=ISMERGED(ADDRESS(ROW(E2), COLUMN(E2)))
=ISMERGED(ADDRESS(ROW(E3), COLUMN(E3)))
...and so on.
And then for formatting B, use:
=AND(C:C="done", D:D="done", OR(E:E="done", $Z:$Z))
WARNING: If you toggle between merging and unmerging, Z wont contain the correct values immediately, though, because custom functions only re-compute when input changes (and the address of the cell wont in this case).
Update
Here's how you could compute your helper for the whole column downward, by row, by just putting the value in the topmost cell (ex: =ISMERGED("E:E") in cell Z1):
/**
* #customfunction
*/
function ISMERGED(rangeAddress) {
var range = SpreadsheetApp.getActiveSheet().getRange(rangeAddress);
var numCols = range.getNumColumns();
var numRows = range.getNumRows();
var result = [];
for (var i=0; i < numRows; i++) {
var rowRange = range.offset(i, 0, 1, numCols);
result.push(rowRange.isPartOfMerge());
}
return result;
}
best you can get is:
=OR(AND(C:C="done", D:D="done", E:E="done"),
AND(C:C="done", D:D="done", ISBLANK(E:E)))
which will work unless 4th row:
but you could pre-populate the whole E column with =CHAR(1) and then:
//Sample sheet here
Hi,
I am using formulas to calculate an array N:R. Once calculated, I want to determine the last row of the array with a non-empty cell (the empty cells are not blank).
What I can do so far:
Return the last non-empty cell of a column
=INDEX(FILTER(O:O,O:O<>""), ROWS(FILTER(O:O,O:O<>"")))
or the row of the filter selection (in my case 25 in the filter selection vs 38 in the sheet)
=ROWS(FILTER(O:O,O:O<>""))
What I haven't figured out is how to:
Do this search for the whole array and not just one row at a time
Return the row of the last non-empty cell in the array
Cheers
For a formulaic approach, you can try
=max(filter(row(N2:N), MMULT(N(N2:R<>""), transpose(column(N2:R2)^0))>0))
This custom function will do it. Sometimes scripts are way easier than some of the bizarre formulas that arise (IMHO). It just loops through the data row by row and notes the row number if it finds data ie cell.value() != ""
function findHighestNonEmptyRow(dummyRange){
var sheet = SpreadsheetApp.getActive();
var range = sheet.getRange("N:R");
var valuesRC = range.getValues();
var numRows = range.getNumRows();
var numCols = range.getNumColumns();
var highestNonEmptyRow = 0;
for (var row = 0; row < numRows; row++) {
for (var col = 0; col < numCols; col++) {
if (valuesRC[row][col] != ""){
highestNonEmptyRow = row+1; // +1 to offset loop variable
}
}
}
Logger.log(highestNonEmptyRow);
return highestNonEmptyRow;
}
Log show correct value of 38. You can delete the Logger.log(highestNonEmptyRow); line when you have tested.
I put the formula in W44 in your test sheet....
EDIT: Due to feedback that all was not as expected...
There was a typo in the first script: This line var range =
sheet.getRange("N:D"); should have been var range =
sheet.getRange("N:R");
I found out that Google scripts caches the result of custom
formulas, and just returns the cached value, even if things on the
sheet have changed. This is bizarre behavior, but is intended to
reduce CPU time. The workaround is to pass in a range that is likely
to change, and this causes the function to recalculate. I updated
the formula and the called the function like this:
=findHighestNonEmptyRow(N2:R42)
and hey it all works!
Stick to the formula... however, we both learned a lot from your
question I think, so thanks for that!
How my sheet works
I'm making a spreadsheet to show how much parts I have. By using a dropdown, am I able to show that I created a product. With conditional formatting I am showing that having 0 items isn't an issue when the product is created. Created products with 0 items change from red to purple. Purple means it doesn't matter to have 0 items from this product.
My issue
My issue starts with my dropdown. If I merge cells, The value will go into the upperleft cell. This means other cells inside the merged cell are blank. This gives me a problem with conditional formatting.
My conditional formatting code example:
=if($D2=0;$E2="Created")
I have to change this code for every cell because of the condition combined with a dropdown. Having more than 250 rows would be inhumanly hard to do by hand.
My questions
Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
Is there a better way to make my conditional formatting code applyable to merged cells?
This is my sheet
Product items collected sheet link (Shows the problem and solution!)
Product items collected sheet image (Version 1)
Product items collected sheet image (Version 2)
At the heart of this question is the operation of merged cells. When a cell is merged, say over several rows, only the cell at the top left of the merged cell can contain data, respond to conditional formatting, and so on. In a manner of speaking the other cells cease to exist and values CANNOT be assign to them.
The questioner asks:
Q: Are there ways to give all cells of a merged cell the value of the combined cell in an efficient way?
A: No. Not just in an "efficient" way; it's just not possible.
Q: Is there a better way to make my conditional formatting code applicable to merged cells?
A: No and yes ;)
No. In so far as a merged cell is concerned, everything is driven by the value in the top cell of the merged range. There are no other options for the "rest" of the merged cell.
Yes. I'd create a "helper" cells in Column F as in this screenshot
The code to achieve this is dynamic - it will automatically adapt to adding more products, more items, etc.
The logic is fairly simple: Start in F2, test whether E2 has a value (that is, is it the top of the merged cell?). If yes, then assign the value of E2 to F2 AND put that value in a variable for the following cells. If no, the cell in Column E must be part of a merged cell, so assign the value for Column F to the variable that was saved earlier.
function so5270705902() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = ECell.getValue();
//Logger.log("offsetrange#"+i+" range value = "+ECellVal);
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBUG
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
if (ECell.isBlank()) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); ??DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 22 October 2018
For development purposes, I used a small range of only 14 rows in Column E. However the questioner's data covers over 250 rows, so I expanded development testing to cover 336 rows (yeah, I know, but I was copy/pasting and I ended up with 336 and was too lazy to delete any rows. OK?). I found that the code took over 81 seconds to process. Not good.
The primary reason (about 80 seconds worth) for the long processing time is that there is a getValue statement within the loop - var ECellVal = ECell.getValue();. This costs about 0.2 seconds per instance. Including getValue in a loop is a classic performance mistake. My bad. So I modified the code to get the values of Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();.
I was surprised when the execution time stayed around the same mark. The reason was that the isBlank evaluation - if (ECell.isBlank()) { which previously took no time at all, was now consuming #0.2 second per instance. Not good++. So after searching Stack Overflow, I modified this line as follows:
if (!Evals[(i-dataStart)][0]) {.
Including setValues in a loop is also asking for trouble. An option would have been to write the values to an array and then, after the loop, update the Column E values with the array. However in this case, the execution time doesn't seem to have suffered and I'm leaving the setValues inside the loop.
With these two changes, total execution time is now 1.158 seconds. That's a percentage reduction of , um, a LOT.
function so5270705903() {
// basic declarations
var ss = SpreadsheetApp.getActiveSpreadsheet();
// note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[2];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
// Row 1 is a header row, so data commences in Row 2 - this can be edited
var dataStart = 2;
// Logger.log("the last row in column D = "+Blast);// DEBUG
// set up to loop through the rows of Column F
var mergedcellvalue = "";
// get the values for Column E BEFORE the loop
var Evals = sheet.getRange("e2:E").getValues();
for (i = dataStart; i < (Blast + 1); i++) {
// set the range for the row
var range = sheet.getRange(i, 6);
//Logger.log("row#"+i+" = "+range.getA1Notation()); DEBUG
// get the value in column E
var ECell = range.offset(0, -1);
var ECellVal = Evals[(i - dataStart)][0];
//Logger.log("Column E, row#"+i+", value = "+ECell.getA1Notation()+" range value = "+ECellVal);//DEBU
// when a row is merged, on the top row contains any data
// so we'll evaluate to see whether there is any value in this row in Column E
// instead is isblank, which was talking 0.2 seconds to evaluate, this if is more simple
if (!Evals[(i - dataStart)][0]) {
//Logger.log("ECell is blank. We're in the middle of the Merged Cell"); //DEBUG
// Set the value to the lastes value of "mergedcellvalue"
range.setValue(mergedcellvalue);
} else {
//Logger.log("ECell has a value. We're at the top of the merged cell");//DEBUG
// paste the ECellVal into this range
range.setValue(ECellVal);
// Update the "mergedcellvalue" variable so that it can be applied against lower cells of this merged cell
mergedcellvalue = ECellVal;
} // end of the if isblank
} // end of the loop through column F
}
UPDATE 3 March 2019
The questioner made his final changes to the code. This code is the final solution.
function reloadCreatedCells() {
// Basic declarations.
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Note this is going to work on the second sheet in the spreadsheet - this can be edited.
var sheet = ss.getSheets()[1];
// Column B contains no merged cells, and always contains data (it is the BOM for the Products).
// so we'll use it to established the last row of data.
var D_vals = sheet.getRange("D1:D").getValues();
var D_last = D_vals.filter(String).length;
// First row with data.
var dataStart = 2;
// Set up to loop through the rows of Column H - K.
var mergedcellvalue = "";
// Get the values for Column H - K BEFORE the loop.
var H_K_vals = sheet.getRange("H2:K").getValues();
// How many people we have.
var people = 4;
// The first vertical row.
var rowStart = 12;
// Horizontal rows.
for (var h = 0; h < people; h++) {
// Vertical rows.
for (var v = dataStart; v < D_last; v++) {
// Set the range for the row.
var range = sheet.getRange(v, rowStart + h);
// Logger.log(range.getA1Notation()); //DEBUG
// Get the value in column H - K.
var H_K_Cell = range.offset(0, -people);
// Adding Created and not created values inside L - O.
var H_K_CellVal = H_K_vals[(v - dataStart)][h];
// Logger.log(H_K_Cell.getA1Notation() + ': ' + H_K_CellVal); //DEBUG
// When a row is merged, the value is only inside the top row.
// Therefore, you need to check if the value is empty or not.
// If the value is empty. Place the top value of the merged cell inside the empty cell.
if (!H_K_vals[(v - dataStart)][h]) {
// Logger.log(H_K_Cell.getA1Notation() + ": is blank. We're below the top cell of the merged cell."); //DEBUG
// Set the value to the top cell of the merged cell with "mergedcellvalue".
range.setValue(mergedcellvalue);
} else {
// Logger.log(H_K_Cell.getA1Notation() + ": has a value. We're at the top of the merged cell."); //DEBUG
// Paste the H_K_CellVal into this range.
range.setValue(H_K_CellVal);
// Update the "mergedcellvalue" variable, so that it can be applied against lower cells of this merged cell.
mergedcellvalue = H_K_CellVal;
} // end of the if isblank.
} // End of the vertical row loop.
} // End of the horizontal row loop.
}
Spreadsheet data looks like this
function myFunction()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
sh.insertRowBefore(15551)
}
As i have large range of rows that could be work on. If the value of the range matches with "Apr 9" then insert row before to that. Could anyone help me to get that.
A 'for loop' to cycle through your rows from the bottom would almost do the trick. The loop inserts a row after each row specified by i. Keep in mind you'll need a different solution if your Apr 9 column is formatted as a date. This works for plain text only. You can select the column and change to plain text with "Format > Number > Plain Text" on the menu.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Active Listeners');
//var shlen = sh.getDataRange().getLastRow(); //returns integer last row
var shlen = Browser.inputBox("Enter Last Row of Preferred Range", Browser.Buttons.OK_CANCEL);
var ecell = sh.getActiveCell().getA1Notation();
You may need a different dataRange (below), I've just grabbed the parameters of data in your whole sheet (above), then grabbed a range specified in A1 notation of "A1:B[number reference of bottom row]" The modification may be that you need "B1:C" + [shlen] or whichever other range.
if (shlen >= 1) {
var dataRange = sh.getRange("A1:A" + shlen).getValues();
for (var i = shlen; i > 0; i--) {
var row = dataRange[i-1];
if (row[0] == "Apr 9") {
sh.insertRowAfter(i-1)
}
}}
}
Someone more knowledgeable than me can pitch in if they have a better answer, but my only solution (which should be ok if it's a one-of) would be to just repeat the script a few times, starting at the row of your choice each time. Select cell A1 and then press "control (or command) + down arrow". It will take you to the first gap, which should be where the previous script ended. Remember the row number you're up to and plug that in the input box when you run the script again. Might take a few iterations but you'll get there.
If this process is going to be done repeatedly then best of luck in finding a solution :)