Minimum of Excel Array Formula with INDEX() - excel

I'm having a little trouble with this array formula in Excel.
I have two ranges (columns of data) that correspond to the row and column number to an overall array that contains multipliers to be applied to a non-related function. I want to find the minimum multiplier that is found from the column/row references.
Let's assume the column number range is A1:A10 and the row number range is A15:A24 and the multiplier array is K4:M23. An inefficient way to do this is to do:
=MIN(INDEX(K4:M23,A15,A1),INDEX(K4:M23,A16,A2),etc...)
...but this will get cumbersome, especially if checking for errors, etc. Not to mention the memory usage if this function is called several thousand times (it just so happens to be).
So I thought about an array function:
{=MIN(INDEX(K4:M23,A15:A24,A1:A10))}
...but this only returns the first element in the array. If this function is entered as a multi-cell array formula, it handles it correctly, but it seems that as is, MIN is applied to each singular element and the function returns the original array size, not the single value of the minimum.
Any way around this?

If I understand your question correctly, the following VBA function should produce what you want.
The function takes three arguments: a reference to the array range; a reference to the row number range; and a reference to the column number range. It returns the minimum of the values in the cells corresponding to the row numbers and column numbers.
Function ArrayMin(MatrixRange As Range, RowRange As Range, ColRange As Range) As Double
Application.Volatile
Dim colNum As Long
Dim rowNum As Long
Dim cellVal As Double
Dim MinVal As Double
Dim i As Long
MinVal = 1000000 'a number >= than max array range value
For i = 0 To ColRange.Rows.Count - 1
rowNum = RowRange(1, 1).Offset(i, 0).Value
colNum = ColRange(1, 1).Offset(i, 0).Value
cellVal = MatrixRange(rowNum, colNum).Value
If cellVal < MinVal Then
MinVal = cellVal
End If
Next
ArrayMin = MinVal
End Function
It can be installed in the standard way by inserting a new standard VBA module in your workbook and pasting the code in.

I have never been able to operate on the output of index as if it were an array. What could be done instead is filter the original table of multipliers. To avoid helper cells this can be done within defined names
rowFilter: = SIGN( MATCH(rowIndex, selectedRows, 0))
columnFilter: = SIGN( MATCH(columnIndex, selectedColumns, 0 ) )
filteredMultipliers:= multipliers * rowFilter * columnFilter
The worksheet formula
= AGGREGATE( 15, 6, filteredMultipliers, 1 )
will identify the minimum value ignoring the intentional #N/A errors.

Thanks to a little inspiration by pbart, here's how I ending up doing it correctly... (a year and a half after I asked the question.)
I basically create a reference array the same dimensions as K4:M23 that has ones at the intersection of the column and row arrays and multiply them together.
=AGGREGATE(15,6,(IF(MMULT(TRANSPOSE(IFERROR(IF(A15:A24=COLUMN(1:20),1,0),0)),IFERROR(IF(A1:A10={1,2,3},1,0),0))=0,#N/A,1)*K4:M23),1)
Not sure it's the most efficient, but not too bad doing an array lookup without using Match or Index.

Related

Excel IRR Formula with non-contiguous ranges on a different sheet

I just realized that you can reference non-contiguous ranges in an IRR excel formula by enclosing a collection of references in parentheses (i.e. =IRR( (C18:C62,$B$1) ). This is a useful feature but, from various attempts, I have concluded that it does not work when a range reference includes another sheet/workbook or when a range is constructed with an offset function. Does anyone know of a workaround for this or another way to approach the problem?
Thank you for all your help!
IRR Function:
Sytax : IRR(values, [guess])
Assuming data is in Range A1:A5 and Range B1 for calculating IRR.
Being obvious =IRR(A1:A5) will give correct result. But =IRR(A1:A5,B1) makes excel assume that B1 is [guess] argument of the formula as syntax states and hence gives result same as for =IRR(A1:A5). Now if you add another range in formula like =IRR(A1:A5,B1,C1), excel will give error stating too many arguments entered. Thus, for non-contiguous ranges, all the ranges needs to be enclosed in parenthesis as =IRR((A1:A5,B1)) (as you mentioned in the question). This makes Excel to interpret it as a single argument consisting of 2 distinct ranges.
Using Offset in IRR.
it does not work when a range is constructed with an offset function
Well, IRR does support OFFSET function.
=IRR(OFFSET(A1,0,0,5)) is same as =IRR(A1:A5)
=IRR((OFFSET(A1,0,0,5),OFFSET(A1,0,1))) is same as =IRR((A1:A5,B1))
Above two formulas using Offset will give you correct results.
Using ranges from multiple sheets in IRR
it does not work when a range reference includes another sheet/workbook
This is TRUE when you are using different worksheets in one formula. However, if you are referencing range from only one worksheet and is different from current worksheet (one in which you are entering formula) then IRR function will work. That means =IRR(Sheet2!A1:A5) will give desired result even if formula is entered in Sheet1. But, IRR does not allows to use ranges from multiple worksheets in one formula. Hence, =IRR((A1:A5,Sheet2!B1)) is incorrect if entered in Sheet1.
So to use multiple ranges from different sheets you can use UDF(User Defined Function) in VBA. Following might help.
Function IRR_DEMO(rng1 As Range, ParamArray rng2() As Variant) As Double
Dim elem As Variant
Dim i As Long, cntr As Long
Dim arr() As Double
ReDim arr(1 To rng1.count) As Double 'set size of arr as per rng1
cntr = 1
For Each elem In rng1 'first range
arr(cntr) = elem.Value 'put range values in array
cntr = cntr + 1
Next elem
For i = LBound(rng2) To UBound(rng2) 'all the ranges apart from first range and in rng2
ReDim Preserve arr(1 To UBound(arr) + rng2(i).count) As Double 'reset size of arr as per rng2(i)
For Each elem In rng2(i)
arr(cntr) = elem.Value 'put range values in array
cntr = cntr + 1
Next elem
Next i
IRR_DEMO = IRR(arr) 'use array in IRR
End Function
You can use above UDF as
=IRR_DEMO(A1:A5)
=IRR_DEMO(A1:A5,B1)
=IRR_DEMO(OFFSET(A1,0,0,5),OFFSET(A1,0,1))
=IRR_DEMO(Sheet2!A1:A5)
=IRR_DEMO(A1:A5,Sheet2!B1)
=IRR_DEMO(Sheet2!A1:A5,Sheet2!B1)
See images for reference.
Sheet1
Sheet2
The best way I found to add multiple ranges in IRR is to add brackets around the multiple ranges. With the brackets it seems to recognize the multiple ranges. Example is shown below:
=IRR( **(** B27:F27,Z27:AB27 **)**, 10%)

Excel, indirect reference to range of sheets

Trying to use INDIRECT to reference a range of sheets, and a range of cells in those sheets, looking for the MAX. Neither of these work:
=MAX(INDIRECT("1:"&last_sheet&"!G"&ROW()&":K"&ROW()))
=MAX(INDIRECT("1:6!G"&ROW()&":K"&ROW()))
However, this does (but only looks at one sheet: 1):
=MAX(INDIRECT("1!G"&ROW()&":K"&ROW()))
And so does this (but doesn't use INDIRECT):
=MAX('1:6'!G6:K6)
It seems to me that INDIRECT simply cannot be used with a range of sheets. Please tell me I'm wrong and why.
It looks like you're probably correct. The following workaround is ugly, but it works.
=MAX(MAX(INDIRECT("'1'!B1:C2")),MAX(INDIRECT("'2'!B1:C2")),MAX(INDIRECT("'3'!B1:C2")))
You can paste the below function into the VBA editor, and it will produce the results you're looking for. It returns the max of whatever range you specify, across all of the sheets in the workbook. Use it just like a regular function, ie =MultiMax(A1). It also accepts an INDIRECT as a parameter.
Function MultiMax(r As Range) As Long
Dim s As Worksheet
Dim a() As Long
Dim m As Long
ReDim a(0 To 0)
For Each s In ThisWorkbook.Sheets
m = Application.WorksheetFunction.Max(s.Range(r.Address).Value)
ReDim Preserve a(0 To UBound(a) + 1)
a(UBound(a)) = m
Next
Dim y As Integer
Dim m1 As Long
For y = 0 To UBound(a)
If a(y) > m1 Then
m1 = a(y)
End If
Next
MultiMax = m1
End Function
Similar to the above solution, you could also try an array formula. However, this will require you to do a MAX function on each sheet (preferably in the same cell on each sheet). For example, on sheet '1', you have MAX(B2:C2) in cell D1, and then the same on sheet '2', sheet '3', etc. Then on your summary sheet, use this array formula:
=MAX(N(INDIRECT(ADDRESS(1,4,,,ROW(INDIRECT("A1:A"&last_sheet))))))
Then be sure to hit Ctrl+Shift+Enter to enter it as an array formula.
This assumes "last_sheet" is some integer value like 6 for example, then makes a range string of it ("A1:A6"), passes this to INDIRECT which passes it to ROW() giving you an array from 1:6. This array is used as the list of sheet names for ADDRESS which creates an array of references at cell D1 on each of the six sheets. The array is passed to INDIRECT which returns #VALUE! errors until you pass the array of errors to N(). Finally, max returns the largest value in the array. You can use "Evaluate Formula" to see how it breaks down step by step, but hopefully this is a good starting point for you!

How to make Excel consider ONLY rows that have a given value

Here is an image, followed by description of data
Description of Columns:
Column A (Key) is strictly increasing sequence of decimals
Column B (Group) represents a group the value in A belongs to.
Column C (Data) is assorted data
Inputs (in column F)
Exact Group number in i.e. {1, 2, 3, 4} in F4
A decimal value (unrestricted), call it DecimalValue, in F5
Task
Find row that belongs to the given Group, where ABS(Key - DecimalValue) value is minimized. Return Data from that row.
Ideally looking for an Excel-only solution, using INDEX, VLOOKUP,
ABS, and the like.
This question is similar to my previous question, but different enough (involves a new Groupcolumn), where via comments it was determined that it is best to ask a new question, rather than try to update / modify the existing question:
Display Row Values based on Nearest Numeric Match of Key
Adding correction for Group column, if it is possible is what I am after, hence the title reflects that concern.
(Incomplete Solution - does not consider Group column)
=INDEX(C4:C33,MATCH(MIN(ABS(A4:A33-F5)),ABS(A4:A33-F5),0))
Give this a go...it seems to work on my test sheet. Be sure to adjust the ranges to suit your situation. Again, this is an array formula and needs to be confirmed with Ctrl+Shift+Enter:
=INDEX(C2:C7,MATCH(MIN(ABS((B2:B7=F4)*A2:A7-F5)),ABS((B2:B7=F4)*A2:A7-F5),0),0)
It works by zeroing out keys that don't match your group assignment (that's the (B2:B7=F4)*A2:A7-F5) part. So only keys w/ valid groups have some number to be used to match to the data column.
Hope that helps explain it. You can also utilize the "Evaluate Formula" function on the Formulas toolbar to see it in action.
Note - it seems to return the 1st data value if 0 is used as the closest value (regardless of group selection). Not sure how to get around that...
Here is sous2817's answer adjusted to avoid the problem of getting the wrong result when entering 0:
=INDEX(Data,MATCH(MIN(IFERROR(ABS(IF(GroupNo=Group,1," ")*Key-DecimalValue),
" ")),IFERROR(ABS(IF(GroupNo=Group,1," ")*Key-DecimalValue)," "),0))
The explanation for how it works is the same. The problem is avoided by replacing zeroes with errors.
Note that Key is the key column, GroupNo is the Group number column, Data is the data column, Group is the designated group for searching, and DecimalNumber is the number entered for searching.
EDIT: As discussed in comments below, this formula can be made much more readable by using a named range (AKA named formula). Set a named range searchRange equal to:
IFERROR(ABS(IF(GroupNo=Group,1," ")*Key-DecimalValue)," ")
Then the formula becomes:
=INDEX(Data,MATCH(MIN(searchRange),searchRange,0))
This has the added benefit of less Excel overhead, since the named formula only gets calculated once (whereas in the other version, it is calculated every time it appears).
You should be able to do as follows
Function getLastRow()
Dim i As Integer
Dim l_row As Integer
For i = 1 To 35
If Sheet1.Cells(Rows.Count, i).End(xlUp).Row > l_row Then
l_row = Sheet1.Cells(Rows.Count, i).End(xlUp).Row
End If
Next i
getLastRow = l_row
End Function
Sub data_lookup
Dim last_row As Integer
Dim lcell as Range
Dim col_a_lookup As Double
Dim col_b_lookup AS Double
Dim row_collection As New Collection
Dim variance AS Double
Dim closest_row AS Integer
col_b_lookup = 0.04
col_a_lookup = 8
variance = 50
last_row = getLastRow
'Find All the Cells that match your lookup value for column B
For Each lcell in Sheet1.Range("$B$2", "$B$" & last_row)
If lcell.value = col_b_lookup Then
row_collection.Add lcell
End If
Next lcell
'Loop through the collection created above to find the closest absolute value to
'your lookup value for Column A
For Each lcell in row_collection
If Abs(Sheet1.Cells(lcell.row,"A") - col_a_lookup) < variance then
variance = Abs(Sheet1.Cells(lcell.row,"A") - col_a_lookup)
closest_row = lcell.row
End If
Next lcell
'Return Results
If closest_row > 0 Then
Msgbox "Closest Data: " & Sheet1.Cells(closest_row,"G")
Else
Msgbox "Cannot Locate"
End If
End Sub
Obviously you will have to set col_a_lookup and col_b_lookup to the values specified and I am sure you want to change the Msgbox. But this should help you on your way.

Excel Lookup return multiple values horizontally while removing duplicates

I would like to do a vertical lookup for a list of lookup values and then have multiple values returned into columns for each lookup value. I actually managed to do this after a long Google search, this is the code:
=INDEX(Data!$H$3:$H$70000, SMALL(IF($B3=Data!$J$3:$J$70000, ROW(Data!$J$3:$J$70000)-MIN(ROW(Data!$J$3:$J$70000))+1, ""), COLUMN(A$2)))
Now, my problem is, as you can see in the formula, my lookup range contains 70,000 rows, which means a lot of return values. But most of these return values are double. This means I have to drag above formula over many columns until all lookup values (roughly 200) return #NUM!.
Is there any possible way, I guess VBA is necessary, to return the values after duplicates have been removed? I'm new at VBA and I am not sure how to go about this. Also it takes forever to calculate having so many cells.
[Edited]
You can do what you want with a revised formula, not sure how efficient it will be with 70,000 rows, though.
Use this formula for the first match
=IFERROR(INDEX(Data!$H3:$H70000,MATCH($B3,Data!$J3:$J70000,0)),"")
Now assuming that formula in in F5 use this formula in G5 confirmed with CTRL+SHIFT+ENTER and copied across
=IFERROR(INDEX(Data!$H3:$H70000,MATCH(1,($B3=Data!$J3:$J70000)*ISNA(MATCH(Data!$H3:$H70000,$F5:F5,0)),0)),"")
changed the bolded part depending on location of formula 1
This will give you a list without repeats.....and when you run out of values you get blanks rather than an error
Not sure if you're still after a VBA answer but this should do the job - takes about 25 seconds to run on my machine - it could probably be accelerated by the guys on this forum:
Sub ReturnValues()
Dim rnSearch As Range, rnLookup As Range, rnTemp As Range Dim varArray
As Variant Dim lnIndex As Long Dim strTemp As String
Set rnSearch = Sheet1.Range("A1:A200") 'Set this to your 200 row value range
Set rnLookup = Sheet2.Range("A1:B70000") 'Set this to your lookup range (assume 2
columns)
varArray = rnLookup
For Each rnTemp In rnSearch
For lnIndex = LBound(varArray, 1) To UBound(varArray, 1)
strTemp = rnTemp.Value
If varArray(lnIndex, 1) = strTemp Then
If WorksheetFunction.CountIf(rnTemp.EntireRow, varArray(lnIndex, 2)) = 0 Then 'Check if value exists already
Sheet1.Cells(rnTemp.Row, rnTemp.EntireRow.Columns.Count).End(xlToLeft).Offset(0, 1).Value =
varArray(lnIndex, 2)
End If
End If
Next Next
End Sub

Transform a worksheet to an array containing only strings

I need to extract the data from an excel worksheet to an array that will be used in an application that uses VBScript as scripting language (Quick Test Professional). We can use the following code for that:
' ws must be an object of type Worksheet
Public Function GetArrayFromWorksheet(byref ws)
GetArrayFromWorksheet = ws.UsedRange.Value
End Function
myArray = GetArrayFromWorksheet(myWorksheet)
MsgBox "The value of cell C2 = " & myArray(2, 3)
All nice and well, but unfortunately the array that gets returned does not only contain the literal text strings, but also primitives of type date, integer, double etc. It happened multiple times that that data got transformed.
[edit] Example: when entering =NOW() in a cell and set the cell formatting to hh:mm makes the displayed value 17:45, the above method retuns a variable of type double and a value like 41194.7400990741
The following solution worked better: I can get the literal text from a cell by using the .Text property, but they only work on one cell and not on a range of cells. I cannot do this at once for an array as I could with the .Value property, so I have to fill the array one cell at a time:
Public Function GetArrayFromWorksheet_2(byref ws)
Dim range, myArr(), row, col
Set range = ws.UsedRange
' build a new array with the row / column count as upperbound
ReDim myArr(range.rows.count, range.columns.count)
For row = 1 to range.rows.count
For col = 1 to range.columns.count
myArr(row, col) = range.cells(row, col).text
Next
Next
GetArrayFromWorksheet_2 = myArr
End Function
But ouch... a nested for loop. And yes, on big worksheets there is a significant performance drop noticable.
Does somebody know a better way to do this?
As we covered in the comments, in order to avoid the issue you will need to loop through the array at some point. However, I am posting this because it may give you a significant speed boost depending on the type of data on your worksheet. With 200 cells half being numeric, this was about 38% faster. With 600 cells with the same ratio the improvement was 41%.
By looping through the array itself, and only retrieving the .Text for values interpreted as doubles (numeric), you can see speed improvement if there is a significant amount of non-double data. This will not check .Text for cells with Text, dates formatted as dates, or blank cells.
Public Function GetArrayFromWorksheet_3(ByRef ws)
Dim range, myArr, row, col
Set range = ws.UsedRange
'Copy the values of the range to temporary array
myArr = range
'Confirm that an array was returned.
'Value will not be an array if the used range is only 1 cells
If IsArray(myArr) Then
For row = 1 To range.Rows.Count
For col = 1 To range.Columns.Count
'Make sure array value is not empty and is numeric
If Not IsEmpty(myArr(row, col)) And _
IsNumeric(myArr(row, col)) Then
'Replace numeric value with a string of the text.
myArr(row, col) = range.Cells(row, col).Text
End If
Next
Next
Else
'Change myArr into an array so you still return an array.
Dim tempArr(1 To 1, 1 To 1)
tempArr(1, 1) = myArr
myArr = tempArr
End If
GetArrayFromWorksheet_3 = myArr
End Function
Copy your worksheet into a new worksheet.
Copy Paste values to remove formulas
Do a text to columns for each column, turning each column into Text
Load your array as you were initially doing
Delete the new worksheet
You cant do this quickly and easily without looping through the worksheet.
If you use the technique above with 2 lines of code it must a variant type array.
I've included a real example from my code that does it in 6 lines because I like to A) work with the worksheet object and B) keep a variable handy with the original last row.
Dim wsKeyword As Worksheet
Set wsKeyword = Sheets("Keywords")
Dim iLastKeywordRow As Long
iLastKeywordRow = wsKeyword.Range("A" & wsKeyword.Rows.Count).End(xlUp).Row
Dim strKeywordsArray As Variant
strKeywordsArray = wsKeyword.Range("A1:N" & iLastKeywordRow).Value
Note your array MUST be a variant to be used this way.
The reason that Variants work like this is that when you create an array of variants, each 'cell' in the array is set to a variant type. Each cell then get's it's variant type set to whatever kind of value is assigned to it. So a variant being assigned a string gets set to variant.string and can now only be used as a string. In your original example it looks like you had time values which were kind of stored as variant.time instead of variant.string.
There are two ways you can approach your original problem
1) loop through and do the process with more control, like the double nested for loop. explained in another answer which gives you complete control
2) store all the data in the array as is and then either re-format it into a second array, or format it as desired text as you use it (both should be faster)

Resources