Get the last non-empty cell in a column in Google Sheets - google-sheets

I use the following function
=DAYS360(A2, A35)
to calculate the difference between two dates in my column. However, the column is ever expanding and I currently have to manually change 'A35' as I update my spreadsheet.
Is there a way (in Google Sheets) to find the last non-empty cell in this column and then dynamically set that parameter in the above function?

There may be a more eloquent way, but this is the way I came up with:
The function to find the last populated cell in a column is:
=INDEX( FILTER( A:A ; NOT( ISBLANK( A:A ) ) ) ; ROWS( FILTER( A:A ; NOT( ISBLANK( A:A ) ) ) ) )
So if you combine it with your current function it would look like this:
=DAYS360(A2,INDEX( FILTER( A:A ; NOT( ISBLANK( A:A ) ) ) ; ROWS( FILTER( A:A ; NOT( ISBLANK( A:A ) ) ) ) ))

To find the last non-empty cell you can use INDEX and MATCH functions like this:
=DAYS360(A2; INDEX(A:A; MATCH(99^99;A:A; 1)))
I think this is a little bit faster and easier.

If A2:A contains dates contiguously then INDEX(A2:A,COUNT(A2:A)) will return the last date. The final formula is
=DAYS360(A2,INDEX(A2:A,COUNT(A2:A)))

Although the question is already answered, there is an eloquent way to do it.
Use just the column name to denote last non-empty row of that column.
For example:
If your data is in A1:A100 and you want to be able to add some more data to column A, say it can be A1:A105 or even A1:A1234 later, you can use this range:
A1:A
So to get last non-empty value in a range, we will use 2 functions:
COUNTA
INDEX
The answer is =INDEX(B3:B,COUNTA(B3:B)).
Here is the explanation:
COUNTA(range): Returns number of values in a range, we can use this to get the count of rows.
INDEX(range, row, col): Returns the content of a cell, specified by row and column offset. If the column is omitted then the whole row is returned.
Examples:
INDEX(A1:C5,1,1) = A1
INDEX(A1:C5,1) = A1,B1,C1 # Whole row since the column is not specified
INDEX(A1:C5,1,2) = B1
INDEX(A1:C5,1,3) = C1
INDEX(A1:C5,2,1) = A2
INDEX(A1:C5,2,2) = B2
INDEX(A1:C5,2,3) = C2
INDEX(A1:C5,3,1) = A3
INDEX(A1:C5,3,2) = B3
INDEX(A1:C5,3,3) = C3
For the picture above, our range will be B3:B. So we will count how many values are there in range B3:B by COUNTA(B3:B) first. In the left side, it will produce 8 since there are 8 values while it will produce 9 in the right side. We also know that the last value is in the 1st column of the range B3:B so the col parameter of INDEX must be 1 and the row parameter should be COUNTA(B3:B).
PS: please upvote #bloodymurderlive's answer since he wrote it first, I'm just explaining it here.

My favorite is:
=INDEX(A2:A,COUNTA(A2:A),1)
So, for the OP's need:
=DAYS360(A2,INDEX(A2:A,COUNTA(A2:A),1))

If the column expanded only by contiguously added dates
as in my case - I used just MAX function to get last date.
The final formula will be:
=DAYS360(A2; MAX(A2:A))

Here's another one:
=indirect("A"&max(arrayformula(if(A:A<>"",row(A:A),""))))
With the final equation being this:
=DAYS360(A2,indirect("A"&max(arrayformula(if(A:A<>"",row(A:A),"")))))
The other equations on here work, but I like this one because it makes getting the row number easy, which I find I need to do more often. Just the row number would be like this:
=max(arrayformula(if(A:A<>"",row(A:A),"")))
I originally tried to find just this to solve a spreadsheet issue, but couldn't find anything useful that just gave the row number of the last entry, so hopefully this is helpful for someone.
Also, this has the added advantage that it works for any type of data in any order, and you can have blank rows in between rows with content, and it doesn't count cells with formulas that evaluate to "". It can also handle repeated values. All in all it's very similar to the equation that uses max((G:G<>"")*row(G:G)) on here, but makes pulling out the row number a little easier if that's what you're after.
Alternatively, if you want to put a script on your sheet you can make it easy on yourself if you plan on doing this a lot. Here's that scirpt:
function lastRow(sheet,column) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (column == null) {
if (sheet != null) {
var sheet = ss.getSheetByName(sheet);
} else {
var sheet = ss.getActiveSheet();
}
return sheet.getLastRow();
} else {
var sheet = ss.getSheetByName(sheet);
var lastRow = sheet.getLastRow();
var array = sheet.getRange(column + 1 + ':' + column + lastRow).getValues();
for (i=0;i<array.length;i++) {
if (array[i] != '') {
var final = i + 1;
}
}
if (final != null) {
return final;
} else {
return 0;
}
}
}
Here you can just type in the following if you want the last row on the same of the sheet that you're currently editing:
=LASTROW()
or if you want the last row of a particular column from that sheet, or of a particular column from another sheet you can do the following:
=LASTROW("Sheet1","A")
And for the last row of a particular sheet in general:
=LASTROW("Sheet1")
Then to get the actual data you can either use indirect:
=INDIRECT("A"&LASTROW())
or you can modify the above script at the last two return lines (the last two since you would have to put both the sheet and the column to get the actual value from an actual column), and replace the variable with the following:
return sheet.getRange(column + final).getValue();
and
return sheet.getRange(column + lastRow).getValue();
One benefit of this script is that you can choose if you want to include equations that evaluate to "". If no arguments are added equations evaluating to "" will be counted, but if you specify a sheet and column they will now be counted. Also, there's a lot of flexibility if you're willing to use variations of the script.
Probably overkill, but all possible.

This works for me. Get last value of the column A in Google sheet:
=index(A:A,max(row(A:A)*(A:A<>"")))
(It also skips blank rows in between if any)

This seems like the simplest solution that I've found to retrieve the last value in an ever-expanding column:
=INDEX(A:A,COUNTA(A:A),1)

For strictly finding the last non-empty cell in a column, this should work...
=LOOKUP(2^99, A2:A)

What about this formula for getting the last value:
=index(G:G;max((G:G<>"")*row(G:G)))
And this would be a final formula for your original task:
=DAYS360(G10;index(G:G;max((G:G<>"")*row(G:G))))
Suppose that your initial date is in G10.

I went a different route. Since I know I'll be adding something into a row/column one by one, I find out the last row by first counting the fields that have data. I'll demonstrate this with a column:
=COUNT(A5:A34)
So, let's say that returned 21. A5 is 4 rows down, so I need to get the 21st position from the 4th row down. I can do this using inderect, like so:
=INDIRECT("A"&COUNT(A5:A34)+4)
It's finding the amount of rows with data, and returning me a number I'm using as an index modifier.

for a row:
=ARRAYFORMULA(INDIRECT("A"&MAX(IF(A:A<>"", ROW(A:A), ))))
for a column:
=ARRAYFORMULA(INDIRECT(ADDRESS(1, MAX(IF(1:1<>"", COLUMN(1:1), )), 4)))

This will give the contents of the last cell:
=indirect("A"&max(ARRAYFORMULA(row(a:a)*--(a:a<>""))))
This will give the address of the last cell:
="A"&max(ARRAYFORMULA(row(a:a)*--(a:a<>"")))
This will give the row of the last cell:
=max(ARRAYFORMULA(row(a:a)*--(a:a<>"")))
Maybe you'd prefer a script. This script is way shorter than the huge one posted above by someone else:
Go to script editor and save this script:
function getLastRow(range){
while(range.length>0 && range[range.length-1][0]=='') range.pop();
return range.length;
}
One this is done you just need to enter this in a cell:
=getLastRow(A:A)

Calculate the difference between latest date in column A with the date in cell A2.
=MAX(A2:A)-A2

To find last nonempty row number (allowing blanks between them) I used below to search column A.
=ArrayFormula(IFNA(match(2,1/(A:A<>""))))

The way an amateur does it is "=CONCATENATE("A",COUNTUNIQUE(A1:A9999))", where A1 is the first cell in the column, and A9999 is farther down that column than I ever expect to have any entries. This resultant A# can be used with the INDIRECT function as needed.

Ben Collins is a Google sheets guru, he has many tips on his site for free and also offers courses. He has a free article on dynamic range names and I have used this as the basis for many of my projects.
https://www.benlcollins.com/formula-examples/dynamic-named-ranges/
Disclaimer, I have nothing to gain by referring Ben's site.
Here is a screenshot of one of my projects using dynamic ranges:
Cell D3 has this formula which was shown above except this is as an array formula:
=ArrayFormula(MAX(IF(L2s!A2:A1009<>"",ROW(2:1011))))
Cell D4 has this formula:
="L2s!A2:E"&D3

This may work:
=DAYS360(A2,INDEX(A2:A,COUNTA(A2:A)))

To pick the last in a column of arbitrary, non-empty values ignoring the header cell (A1):
=INDEX(A2:A,COUNT(A2:A))

With the introduction of LAMBDA and REDUCE functions we can now compute the row number in a single pass through the cells (Several of the solutions above filter the range twice.) and without relying on magic text or numeric values.
=lambda(rng,
REDUCE(0, rng, lambda(maxrow, cell, if(isblank(cell),maxrow,row(cell)) ) )
)(A:A)
which can be nicely packaged into a Named Function for usage like
=LAST_ROWNUM(A:A)
It works on columns with interspersed blanks, and multi-column ranges (because REDUCE iterates over the range in row-first), and partial columns (like A20:A), still returning the actual row number (not the offset within the range).
This can then be combined with Index to return the value
=DAYS360(A2, Index(A1, LAST_ROWNUM(A:A)))
(In truth, though, I suspect that the OPs date values are monotonic (even if with blanks in between), and that he could get away with
=DAYS360(A2, MAX(A2:A))
This solution is identified above as relying on the dates being "contiguous" - whether that means "no blanks" or "no missing dates" I'm not certain - but either stipulation is not necessary.)

Related

Multiple search criteria in google sheets

I am trying to make an automated attendance sheet
I have 2 google sheets,
the first one is the responses from a google form that has the name of the students and the date they attended, so it will have duplicated name and duplicated dates.
The second sheet have the names of the students on the left and the dates on the top.
I am trying to automate the second sheet to put "P" under the date that the student was present and "A" when his name is not in the first sheet with that date.
Best i could do was adding an extra column with the letter "p" in the first sheet and using dget to search for the name and date and output the "p" from the extra column, which only worked for one of them for some reason.
=DGET('ATTENDANCE DATE !B:D, "AT", {"NAME", "DATE"; $H$4,12})
I tried to use query also but no luck.
=QUERY('ATTENDANCE DATE'!B7:D,"
SELECT D
WHERE B MATCHES'"&$H9&"' AND C MATCHES '"&I$2&"'
")
Sorry if my question was confusing.
A good solution is to use 4 formulas to do exactly what you like. Each formula has a function:
B1 formula: generates the headers for all the dates with data.
B2 formula: generates the sub-header with the day of the week for each date.
A3 formula: gets all the names.
B3 formula: gets the attendance values for all users. This is the most complexs one.
Here is how it looks:
A
B
1
< fromula 1 >
2
name
< formula 2 >
3
< formula 3 >
< formula 4 >
Before starting there a few things to note
Questions and more information
Please, if at any point you don't understand something, let me know (I'd like this to be a nice resource on how to do formulas).
Also, at the end I left links to all the formulas I use, so you can see what they exactly do.
Locale
I'm using the English locale. this means that I'm using commas , to separate arguments (instead of ;) and array literals (instead of \). if you have function formatting errors, look into it, as this could be the issue.
Sheet names
I've changed the Sheet's names as they are very long and made the formulas harder to follow. Fell free to replace the names on the formulas back to the original name. Here is how I named them:
ATTENDANCE RESPONSES FROM GOOGLE FORM ⟶ Att
LATE/ABSENT RESPONSES FROM GOOGLE FORM ⟶ Late
Formula format
Almost all formulas require "ARRAYFORMULA" to show their full effects. I won't be adding it everywhere as it could get confusing. If you'd like to see what a formula part (doesn't have an equal sign =) does, go to a sheet and do:
=arrayformula(
<paste formula part>
)
Also, parts that are in <some name> are not literal, and represent the code named in between the brackets.
Formula 1
It can be split into 2 formulas:
Get the ordered unique dates
Add a Reason column for each date
Get the dates
The first thing you can use is UNIQUE to get only the unique ones and SORT to sort them. You also need to get them from both sheets as there could be a day that everyone is absent or another where everyone came. SORT(UNIQUE({Att!B2:B; Late!B2:B})) does most of the trick but you also get empty cells. because of that we add a filter. So together:
SORT(UNIQUE(FILTER({Att!B2:B; Late!B2:B}, {Att!B2:B; Late!B2:B}<>"")))
Adding Reason
The problem of it is that the number of column is not fixed (it grows over time). A good workaround is to concatenate the date with a separator and Reason and then split it again. This only works for columns and generates a 2 column, result. Then it can be moved into a single column by using FLATTEN.
FLATTEN(SPLIT(
<previous part>&"␟Reason",
"␟"
))
I'm using ␟ (Symbol For Unit Separator) as the separator as it indicates exactly what it is and is very-very unlikely to be included in the sheet.
If you use that you'll see that the date is shown in numbers. To Change that we'll format the date before concatenating and splitting:
FLATTEN(SPLIT(
TEXT(
<previous part>,
"dd/mmm/yyyy"
)&"␟Reason",
"␟"
))
Now we need to make it a row instead of a column. There is a function that does that: TRANSPOSE.
Complete formula 1
=ARRAYFORMULA(
TRANSPOSE(FLATTEN(SPLIT(
TEXT(
SORT(UNIQUE(FILTER({Att!B2:B; Late!B2:B}, {Att!B2:B; Late!B2:B}<>""))),
"dd/mmm/yyyy"
)&"␟Reason",
"␟"
)))
)
Formula 2
To add the day of the week we need to format the date with TEST and the format ddd, which is the short-version of the name of the day of the week (Mon, Tue, etc).
TEXT(B1:1, "ddd")
Note though that if the value formatted is already text, it will pass it. Because of that, we need to only do this for the columns with dates. One way that I found is to get the even-numbered columns. TO do that, it's a combination of COLUMN (get the number of the column), MOD (get the module), and IF:
IF(MOD(COLUMN(B1:1),2)=0, <formatted text>, "")
This does what we want but we now get "Sun" on columns that there is nothing. The reason is that empty cells are being interpreted as zeros. Because of that we need to add another condition: the cell is not empty.
IF((MOD(COLUMN(B1:1),2)=0)*(B1:1<>""), <formatted text>, "")
To do the logical and I'm using the product because the formula AND would return a single value ("eats" the passed array).
Complete
=ARRAYFORMULA(
IF(
(MOD(COLUMN(B1:1),2)=0)*(B1:1<>""),
TEXT(B1:1, "dddd"),
""
)
)
Formula 3
The third formula is the simplest and should be self-explanatory:
=ARRAYFORMULA(
SORT(UNIQUE(Att!A2:A))
)
Formula 4
This is the final formula. This formula is based on using VLOOKUP to know the value for each person and date.
Making a table to VLOOKUP into
The way of doing that is to generate a key by joining both values into a single text value and set the other values on the other columns. To prevent problems we add a separator to make sure that there are no combination that will be equal. Here is how the table to lookup into looks like:
< name >␟< date >
< status >
< reason >
< name >␟< date >
< status >
< reason >
< name >␟< date >
< status >
< reason >
⋮
⋮
⋮
The key for the first sheet is:
Att!$A$2:$A&"␟"&Att!$B$2:$B
To add the other 2 columns (Present and an empty one) we use a similar trick to Formula 1: we add a separator to split it later on. Because we already are using ␟ for the key, we need another one. In the same block there is another meant for this cases: ␞ (Symbol For Record Separator).
Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞"&"Present"&"␞"&""
or joining the literal text:
Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞Present␞"
This need to be filtered, as there are empty values, which we don't want. We'll use FILTER to do exactly that:
FILTER(Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞Present␞", Att!$A$2:$A<>"", Att!$B$2:$B<>"")
For the second sheet, we do something similar but including the other columns:
FILTER(Late!$A$2:$A&"␟"&Late!$B$2:$B&"␞"&Late!$C$2:$C&"␞"&Late!$D$2:$D, Late!$A$2:$A<>"", Late!$B$2:$B<>"", Late!$C$2:$C<>"")
Note that I've added more conditionals.
This needs to be vertically joined. This can be done with an array literal:
{
<Attr formula>;
<Late formula>
}
Then we need to split the rows to expand into the multiple columns:
SPLIT(
{
FILTER(Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞Present␞", Att!$A$2:$A<>"", Att!$B$2:$B<>"");
FILTER(Late!$A$2:$A&"␟"&Late!$B$2:$B&"␞"&Late!$C$2:$C&"␞"&Late!$D$2:$D, Late!$A$2:$A<>"", Late!$B$2:$B<>"", Late!$C$2:$C<>"")
},
"␞"
)
Using VLOOKUP
Now that we have where to lookup into, we can do it like so:
VLOOKUP(
A3:A&"␟"&C1:1,
<lookup table>,
2,
false
)
Note that the key that we are looking up is the one we generate. Also, this will get the values only below the dates (will fail otherwise).
Adding the reason
Since we know that the cells which are for the reason fail (since <name>␟Reason shouldn't exist), we can use IFERROR to detect it:
IFERROR(
<vlookup status>,
<vlookup reason>
)
The formula for reason is almost identical to the one for status. The only changes are that we lookup into the third column (instead of the second) and we look one to the left:
VLOOKUP(
A3:A&"␟"&OFFSET(C1:1, 0, -1),
<lookup table>,
3,
false
),
Using OFFSET instead of a range ensures that they have the same size.
Final error management
This formula fails when the key doesn't have name or date (which is outside the table or there that entry missing). For that case we add another IFERROR:
IFERROR(
<formula>,
""
)
Complete formula
=ARRAYFORMULA(
IFERROR(
IFERROR(
VLOOKUP(
A3:A&"␟"&C1:1,
SPLIT(
{
FILTER(Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞Present␞", Att!$A$2:$A<>"", Att!$B$2:$B<>"");
FILTER(Late!$A$2:$A&"␟"&Late!$B$2:$B&"␞"&Late!$C$2:$C&"␞"&Late!$D$2:$D, Late!$A$2:$A<>"", Late!$B$2:$B<>"", Late!$C$2:$C<>"")
},
"␞"
),
2,
false
),
VLOOKUP(
A3:A&"␟"&OFFSET(C1:1, 0, -1),
SPLIT(
{
FILTER(Att!$A$2:$A&"␟"&Att!$B$2:$B&"␞Present␞", Att!$A$2:$A<>"", Att!$B$2:$B<>"");
FILTER(Late!$A$2:$A&"␟"&Late!$B$2:$B&"␞"&Late!$C$2:$C&"␞"&Late!$D$2:$D, Late!$A$2:$A<>"", Late!$B$2:$B<>"", Late!$C$2:$C<>"")
},
"␞"
),
3,
false
)
),
""
)
)
Final touches
The final result is something like this.
After that you can simply add formats to your taste. You can also add conditionals ones to more easily see the result.
References
MOD (Google Editors Help)
SPLIT (Google Editors Help)
TEXT (Google Editors Help)
IF (Google Editors Help)
IFERROR (Google Editors Help)
FILTER (Google Editors Help)
UNIQUE (Google Editors Help)
SORT (Google Editors Help)
TRANSPOSE (Google Editors Help)
FLATTEN (Google Editors Help)
ARRAYFORMULA (Google Editors Help)
VLOOKUP (Google Editors Help)
OFFSET (Google Editors Help)
I've completed a not-so-neat solution for you, starting on Row10 in the 'AUTO ATTENDANCE' sheet. It's divided into 4 parts:
The formula in cell D10 auto-populates Row10 with dates and empty cells in between:
=SPLIT(JOIN("|REASON|",SORT(UNIQUE({'ATTENDANCE RESPONSES FROM GOOGLE FORM'!$B$2:$B;'LATE/ABSENT RESPONSES FROM GOOGLE FORM'!$B$2:$B}))),"|")
Row 11 gets the day of the week from row 10 (if the cell above it contains a date:
=IF(ISDATE(D10),TEXT(D10,"dddd"),)
Cell C12 gets all unique names from both response sheets (auto-populates the name column):
=SORT(UNIQUE({'ATTENDANCE RESPONSES FROM GOOGLE FORM'!$A$2:$A;'LATE/ABSENT RESPONSES FROM GOOGLE FORM'!$A$11:$A}))
Cell D12 onwards gets the form responses and does the auto-attendance:
=IF($C12<>"", IF(ISDATE(D$1), IF(IFERROR(QUERY('ATTENDANCE RESPONSES FROM GOOGLE FORM'!$A$2:$B,"select A where A = '"&$C12&"' AND B = datetime '"&TEXT(D$1, "yyyy-mm-dd hh:mm:ss")&"'"), "noresult")=$C12, "PRESENT", IFERROR(QUERY('LATE/ABSENT RESPONSES FROM GOOGLE FORM'!$A$2:$D, "select C where A = '"&$C12&"' and B = datetime '"&TEXT(D$1, "yyyy-mm-d hh:mm:ss")&"'"), "NO RESPONSE")), IFERROR(QUERY('LATE/ABSENT RESPONSES FROM GOOGLE FORM'!$A$2:$D, "select D where A = '"&$C12&"' and B = datetime '"&TEXT(C$1, "yyyy-mm-d hh:mm:ss")&"'"), )), )
The cells with yellow background contain formulae, the ones with green background do not contain formulae but will be auto-populated as the forms get more responses. The single cell with red background (C11), you'll have to write manually ;) Hope this solves your issue!

Using arrayformula with countif only up to a certain row

I have a sheet with a column of data where the entry is one of two strings, for simplicity's sake we'll say "A" and "B". I want another column of calculated data which is the differential between the number of "A"s and the number of "B"s up to that point, so just a countif()-countif() with the range increasing by one row. I can do this using =countif(A$2:A2, "A") - countif(A$2:A2, "B"), but it means I have to keep filling that equation down to cover any newly entered data, so I figured an array formula would be the best option to not have to do that. However when I try to use =arrayformula(countif(A$2:A, "A") - countif(A$2:A, "B")), it only populates a single cell with the difference counting the entire column. Is there a way to use an array formula so that it increases the range by one row for every row it populates?
=ARRAYFORMULA("A2:A"&ROW(A2:A))
This will give a dynamic string for the right range, which in theory, you should be able to wrap with INDIRECT, to plug into COUNTIF.
But COUNTIF only works in some circumstances with ARRAY FORMULA
Unfortunately, AFAIK, only if you use COUNTIF like this:
=ARRAYFORMULA(COUNTIF(A1:A100,A1:A100))
will it work as an array formula. Note how both the ranges in the arguments are the same.
MMULT
There may be a way along these lines with MMULT:
=ARRAYFORMULA(
MMULT(
(A2:A100 = TRANSPOSE(A2:A100)) * (ROW(A2:A100) >= TRANSPOSE(ROW(A2:A100))),
SIGN(ROW(A2:A100))
)
)
But I couldn't get that to work because I am not 100% sure on how to use MMULT in this way, but I have seen that type of solution elsewhere.
An Apps Script Workaround
As you have seen, what you want to achieve with sheet functions quickly gets very complicated. If you wanted to use more letters or different rules, it would get tough to maintain. Apps Script can make these things much simpler.
So here is a custom Apps Script function:
function myFunction() {
// Initializing
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
var lastRow = sheet.getLastRow();
// This is the range of the As and Bs
let range = sheet.getRange(2,1, parseInt(lastRow) - 1,1)
let rows = range.getValues();
// Creating an object to keep track of the count
let tracker = {}
// This will be the output column
let newCol = []
rows.forEach(row => {
tracker[row[0]] += 1
// Adding a row to the output
newCol.push([tracker.A - tracker.B])
})
return newCol
}
Paste that into the script editor and you can use it like this:
Demo:
You can change the name of the function in the script editor according to what you need it for, just remember to call it with that name from the sheet.
References
Main Page
Sheets Guide
Tutorials
Sheets Reference
INDIRECT
MMULT
try:
=ARRAYFORMULA(
COUNTIFS(A2:A, A2:A, A2:A, "A", ROW(A2:A), "<="&ROW(A2:A))-
COUNTIFS(A2:A, A2:A, A2:A, "B", ROW(A2:A), "<="&ROW(A2:A)))
Alternatively, you may this IF statement:
=ARRAYFORMULA(IF(K2:K<>"",K2:K*B294,""))
Results (Above formula is applied on L2 row onwards):
Source: https://blog.sheetgo.com/google-sheets-formulas/arrayformula-google-sheets/

ARRAY formula to find last row to contain value in Google Sheets

I have a Google Sheet that is populated automatically via Zapier integration. For each new row added, I need to evaluate a given cell (Shipper Name) to find last instance of Shipper Name in prior rows, and if so, return Row# for the last entry.
Example Data Sheet
I am trying to create a formula that simply looks at name in new row and returns the number of the most recent row with that name.
Formula needs to run as an Array formula so that the data auto populates with each new row added to the Sheet.
I have tried to use this formula, but when refactored as Array formula, it doesn't populate new values for new rows, it just repeats the first value for all rows.
From Row J:
=sumproduct(max(row(A$1:A3)*(F4=F$1:F3)))
I need this formula refactored to be an Array formula that auto populates all the cells below it.
I have tried this version, but it doesn't work:
=ArrayFormula(IF(ISBLANK($A2:$A),"",sumproduct(max(row(A$1:A3)*($F4:$F=F$1:F3))))
A script (custom function maybe?) would be better.
Solution 1
Below is a formula you can place into the header (put in in J1, remove everything below).
It works much faster than the second solution and has no N² size restriction. Also it works with empty shippers (& "♥" is for those empty ones): as long as A:A column has some value it will not be ignored.
={
"Row of Last Entry";
ARRAYFORMULA(
IF(
A2:A = "",
"",
VLOOKUP(
ROW(F2:F)
+ VLOOKUP(
F2:F & "♥",
{
UNIQUE(F2:F & "♥"),
SEQUENCE(ROWS(UNIQUE(F2:F)))
* POWER(10, INT(LOG10(ROWS(F:F))) + 1)
},
2,
0
),
SORT(
{
ROW(F2:F) + 1
+ VLOOKUP(
F2:F & "♥",
{
UNIQUE(F2:F & "♥"),
SEQUENCE(ROWS(UNIQUE(F2:F)))
* POWER(10, INT(LOG10(ROWS(F:F))) + 1)
},
2,
0
),
ROW(F2:F);
{
SEQUENCE(ROWS(UNIQUE(F2:F)))
* POWER(10, INT(LOG10(ROWS(F:F))) + 1),
SEQUENCE(ROWS(UNIQUE(F2:F)), 1, 0, 0)
}
},
1,
1
),
2,
1
)
)
)
}
Details on how it works
For every row we use VLOOKUP to search for a special number in a sorted virtual range to get the row number of the previous entry matching current.
A special number for a row is constructed like this: we get a sequential number for the current entry among unique entries and append to it current row number.
The right part (row number) of the resulting special numbers must be aligned between them. If the entry has sequential number 13 and the row number is 1234 and there are 100500 rows, then the number must be 13001234. 001234 is the aligned right part.
Alignment is done by multiplying a sequential number by 10 to the power of (log10(total number of rows) + 1), gives us 13000000 (from the example above). This approach is used to avoid using LEN and TEXT - working with numbers is faster then working with strings.
Virtual range has almost the same special numbers in the first column and original row numbers in the second.
Almost the same special numbers: they just increased by 1, so VLOOKUP will stop at most one step before the number corresponding to the current string.
Also virtual range has some special rows (added at the bottom before sorting) which have all 0's as the right part of their special numbers (1st column) and 0 for the row number (2nd column). That is done so VLOOKUP will find it for the first occurrence of the entry.
Virtual range is sorted, so we could use is_sorted parameter of the outer VLOOKUP set to 1: that will result in the last match that is less or equal to the number being looked for.
& "♥" are appended to the entries, so that empty entries also will be found by VLOOKUP.
Solution 2 - slow and has restrictions
But for some small enough number of rows this formula works (put in in J1, remove everything below):
={
"Row of Last Entry";
ARRAYFORMULA(
REGEXEXTRACT(
TRANSPOSE(QUERY(TRANSPOSE(
IF(
(FILTER(ROW(F2:F), F2:F <> "") > TRANSPOSE(FILTER(ROW(F2:F), F2:F <> "")))
* (FILTER(F2:F, F2:F <> "") = TRANSPOSE(FILTER(F2:F, F2:F <> ""))),
TRANSPOSE(FILTER(ROW(F2:F), F2:F <> "")),
""
)
), "", ROWS(FILTER(F2:F, F2:F <> "")))),
"(\d*)\s*$"
)
)
}
But there is a problem. The virtual range inside of the formula is of size N², where N is the number of rows. For current 1253 rows it works. But there is a limit after which it will throw an error of a range being too large.
That is the reason to use FILTER(...) and not just F2:F.
Here is a significantly simpler way to get at the information you're interested in. (I think.) I'm mostly guessing about what you want because your question wasn't really about what you want, but rather about how to get something that you think would help you get what you want. This is an example of an XY problem. I attempted to guess based on experience at what you're really after.
This editable sheet contains just 3 formulas. 2 on the raw data sheet and one in a new tab called "analysis."
The first formula on the Raw data tab extracts a properly formatted timestamp using a combination of MMULT and SPLIT functions and looks like this:
=ARRAYFORMulA({"Good Timestamp";IF(A2:A="",,MMULT(N(IFERROR(SPLIT(A2:A,"T"))),{1;1}))})
The second formula finds the amount of time since the previous timestamp for that Shipper. and subtracts it from the current timestamp thereby giving you the time between timestamps. However, it only does this if the time is less than 200 minutes. IF it is more than 200 minutes, it assumes that was a different shift for that shipper. It looks like this and uses a combination of LOOKUP() and SUBSTITUTE() to make sure it's pulling the correct timestamps. Obviously, you can find and change the 200 value to something more appropriate if it makes sense.
=ARRAYFORMULA({"Minutes/Order";IF(A2:A="",,IF(IFERROR((G2:G-1*SUBSTITUTE(LOOKUP(F2:F&G2:G-0.00001,SORT(F2:F&G2:G)),F2:F,""))*24*60)>200,,IFERROR((G2:G-1*SUBSTITUTE(LOOKUP(F2:F&G2:G-0.00001,SORT(F2:F&G2:G)),F2:F,""))*(24*60))))})
The third formula, on the tab called analysis uses query to show the average minutes per order and the number of orders per hour that each shipper is processing. It looks like this:
=QUERY({'Sample Data'!F:I},"Select Col1,AVG(Col3),COUNT(Col3)/(SUM(Col3)/60) where Col3 is not null group by Col1 label COUNT(Col3)/(SUM(Col3)/60)'Orders/ hour',AVG(Col3)'Minutes/ Order'")
Hopefully I've guessed correctly at your real goals. Always do your best to explain what they are rather than asking for only a small portion that you think will help you get to the answer. You can end up overcomplicating your process without realizing it.

Is there a function that can create an expanding array formula that conditionally sums a column based on multiple criteria?

To start, this is my first time posting and so please let me know if I can fix my post in any way to make it easier to answer.
I am trying to create an auto-expanding array formula
I have a sheet with my investment asset mix that including amounts of shares owned of each particular stock, and a sheet that tracks when I receive dividends. My goal is to write an automatically expanding array formula that will sum up the amount of shares that own of a stock on the date a dividend is received and return that value. I have written three different formulas that all accomplish this but none of them will auto-expand as an array.
I'm sure there are a lot of solutions I've overlooked. To boil it down, I need an expanding array formula that will sum the "Shares" column of my asset mix sheet ('Asset Mix'!D2:D, or 'AssetMixShares') conditionally. The name of the stock entered in 'Dividends'!C2:C needs to match the name of the stock in 'Asset Mix'!A2:A (or the named range 'AssetMixStocks'). It then needs to check the dates in 'Asset Mix'!C2:C (or 'AssetMixDates') against the dates in 'Dividends'!A2:A and sum all share amounts where the purchase date is less than (earlier than) the Ex-Dividend Date.
I could probably write some sort of vlookup array on the "Running Total" column -- 'Asset Mix'!E:E -- that would solve the issue, but I'm hoping to eliminate that column. I feel very strongly that what I'm trying to do should be possible without the help of a running total column -- I just don't have the knowledge.
I have tried countless functions and formulas, but the four that I currently have in my example worksheet are SUM, SUMPRODUCT, DSUM, and QUERY.
Attempt 1
SUM and IF
=ArrayFormula(SUM(IF('Asset Mix'!A:A=C2,IF('Asset Mix'!C:C<A2,'Asset Mix'!D:D))))
Attempt 2
SUMPRODUCT
=({arrayformula(SUMPRODUCT(--((AssetMixStock=(indirect("C"&ROW())))*(AssetMixDate<(indirect("A"&ROW())))),AssetMixShares))})
Attempt 3
DSUM
=DSUM('Asset Mix'!A:E,"Shares",{"Date","Stock";"<"&A2,C2})
Attempt 4
QUERY
=arrayformula(query(AssetMix,"Select sum(D) where A = '"&C2:C&"' and C < date'"&(text(year(A2:A),"0000") & "-" & text(month(A2:A),"00") & "-" & text(day(A2:A),"00"))&"' label sum(D) ''",0))
These will all work, as long as I manually drag the formula down, but I want to write some sort of formula that will auto-expand to the bottom of the Dividends sheet.
I have tried to create a Dummy sheet that has both of the relevant sheets. Please let me know if you can access it -- the link is below.
https://docs.google.com/spreadsheets/d/1wlKffma0NJ0KrlWxyX_N20y62azsGpFp3enhmjzJK1Q/edit?usp=sharing
Thanks so much for getting this far and any help you can provide!
We can focus in the first formula to understand a way to make it "self-expandable". As we see it contains references to the cells A2 and C2 in "Dividends" sheet:
=ArrayFormula(SUM(IF('Asset Mix'!A:A=C2,IF('Asset Mix'!C:C<A2,'Asset Mix'!D:D))))
Every time some data appears in these columns (A and C), the formula should work. We can control the presence of the formula by onEdit trigger, if editing is manual. Consider the code:
function onEdit(e) {
var sheet = SpreadsheetApp.getActive().getActiveSheet();
if (sheet.getName() == 'Dividends') {
var row = e.range.getRow();
for (var offset = 0; offset < e.range.getHeight(); offset++) {
sheet.getRange(3, 10).copyTo(sheet.getRange(row + offset, 10));
}
}
}
It checks any modification on the sheet "Dividends" and copies required formula to the modified row(s). This way the formula is expanded for other rows in use.
Well, it's solved! I'll leave this up in case anyone else has the same question.
A kind soul explained the magic of MMULT() to me, and wrote this solution.
=ARRAYFORMULA(MMULT((C2:C=TRANSPOSE('Asset Mix'!A2:A))*(A2:A>TRANSPOSE('Asset Mix'!C2:C)),N('Asset Mix'!D2:D))

How to find the first cell in a row where value is not empty and check if the number is less or equal the number in other cell

I've got the following Google spreadsheet:
item have ready need1 need2 need3
A 1 2 1
B 1 2 1 1
C 2 2
etc
I want to fill ready column as follows:
find the first column in need1, ..., needN range which has a non-empty value
if the value found is less or equals the value in have column, set ready column to something cheerful (e.g. yes)
if the value found is larger than the value in have column, don't do anything
So above input, when processed should look like this:
item have ready need1 need2 need3
A 1 2 1
B 1 2 1 1
C 2 yes 2
For the first step I found a suggested solution, which did not work for me:
=INDEX( SORT( FILTER( D10:H10 , LEN( D10:H10 ) ) ,
FILTER( COLUMN( D10:H10 ) , LEN( D10:H10 ) ) , 0 ) , 1 )
(it returns #REF!) Not sure what's wrong with it or how to proceed to the next step.
Thanks in advance!
If you know how many need columns you have, or even just how many columns are on the sheet, this is quite straightforward. If not and you need to look at the entire row, you might have to redesign a bit to avoid a circular reference from the cell with the formula being part of that row.
Your second two steps are fairly simple either way - you want one of two results based on a condition, so you're going to want to use =IF. Your condition is that the 'need' number is less than or equal to the 'have' number, and you want it to say 'yes' if that's true, and nothing if it isn't. So, that gives us:
=IF(need<=have, "Yes", "")
The examples below assume your table above starts from cell A1 in the top left, and that the last column in your sheet is Z
Next we need to find 'need' and 'have'. Finding 'have' is pretty easy - it's just the number in column B.
Finding 'need' is slightly more complicated. You've got the right idea using INDEX and FILTER, but your formula seems a little overcomplicated. Basically we can use FILTER to filter out the blank values, and INDEX to find the first one that is left. First, FILTER:
The range you want to filter from is everything in the same row from column D to column Z (or whatever the final column is), and the condition you want to filter for is that those same cells are not blank. For the formula you're typing into cell C2, that gives us:
=FILTER(D2:Z2, D2:Z2<>"")
Next, INDEX: If you give INDEX an array, a row number, and a column number, it will tell you what is at that the cell where that row and column meet. As we've filtered out the blanks, we just want whatever is left in the first column of our filtered array, which gives us:
=INDEX(FILTER(D2:Z2, D2:Z2<>""), 1, 1)
Or, as we only have one row in our array, and INDEX is pretty smart, simply:
=INDEX(FILTER(D2:Z2, D2:Z2<>""), 1)
So to bring it all together, our final formula for cell C2 is:
=IF(INDEX(FILTER(D2:Z2, D2:Z2<>""), 1)<=B2, "Yes", "")
Then just drag the formula down for as many rows as you need. If your sheet is or becomes wider, just change Z to whatever your last column is.
When you don't know the size of a range, use functions row, column, rows, columns.
Simple formula
Here's an example of what you are looking:
=if(INDEX(FILTER(OFFSET(D2,,,1,COLUMNS(1:1)-column(D2)+1),OFFSET(D2,,,1,COLUMNS(1:1)-column(D2)+1)<>""),1)<=B2,"yes","")
this part of formula:
OFFSET(D2,,,1,COLUMNS(1:1)-column(D2)+1)
returns the range starting from given cell (D2) to the end of Sheet (COLUMNS(1:1)-column(D2)+1)
ArrayFormula
I suggest using ArrayFormula, it'll expand automatically:
=ARRAYFORMULA(if(REGEXEXTRACT(SUBSTITUTE(trim(transpose(query(transpose(OFFSET(D2,,,COUNTA(A2:A),COLUMNS(1:1)-column(D2)+1)),,COLUMNS(OFFSET(D2,,,COUNTA(A2:A),COLUMNS(1:1)-column(D2)+1)))))," ",", "),"\d+")*1<=OFFSET(B2,,,COUNTA(A2:A)),"yes",""))
It assumes that 'Item' column has no blank values.
The solution from #Max Makhrov works, and has the advantage of using a single formula for the whole column.
However, it assumes that all of your columns at the right from your ready column (D) will be need_ columns.
The solution from #dmusgrave also works, provided you remove the extra "=" before INDEX:
=IF(INDEX(FILTER(D2:Z2,D2:Z2<>""),1)<=B2,"Yes","").
However, it makes the same assumption, and also limits at column Z.
Such assumptions seem reasonable, but if they are limiting you, here's how you can have any number of need_ columns starting right of your ready column:
=IF(INDEX(FILTER(INDIRECT( "D"&ROW()&":"&CHAR(67+COLUMNS(FILTER($1:$1,LEFT($1:$1, 4)="need")))&row() ), INDIRECT( "D"&ROW()&":"&CHAR(67+COLUMNS(FILTER($1:$1,LEFT($1:$1,4)="need")))&row() )<>""),1)<=B2,"Yes","")
The idea is simply to replace D2:Z2 (in #dmusgrave's solution) by :
INDIRECT( "D"&ROW()&":"&CHAR(67+COLUMNS(FILTER($1:$1,LEFT($1:$1, 4)="need")))&row() )
Explanation: You start from D at current row, and you go until the last need_ column on the same current row.
CHAR(68) is D, to which you add the number of columns titled need.*, minus one (hence the 67).
Using the same logic, you can easily make your formula more robust/generic, such as not having the need_ columns starting right form the ready column, etc.

Resources