Excel SUMPRODUCT with VBA based conditions - vba

Does anyone know how to use a VBA function within a worksheet based call to SUMPRODUCT?
This works fine, summing the values in column N where column L contains "Y" and col A contains a different value to col K...
=SUMPRODUCT(--(Input!L1:L100="Y"), --(Input!A1:A100<>Input!K1:K100), Input!N1:N100)
But I want to be able to apply more logic than just A<>K in my second criteria, something like this...
=SUMPRODUCT(--(Input!L1:L100="Y"), --(MatchNames(Input!A1:A100,Input!K1:K100)), Input!N1:N100)
I have a function called MatchNames in my VBA, but I can't work out what it needs to return in order to work. I've tried returning an array of boolean, integer (set to 0 or 1), I've tried transposing the results before returning them, but nothing is working. I've debugged through the MatchNames function, and it does return something "useful" (i.e. an array of booleans), so it's not that the function is bombing out part way through, but I get #VALUE! when I try to use it in a SUMPRODUCT.
This is my VBA function...
Public Function MatchNames(ByVal rng1 As Range, rng2 As Range) As Boolean()
Dim blnOut() As Boolean
Dim k As Long
ReDim blnOut(rng1.Rows.Count - 1)
For k = 1 To rng1.Rows.Count
If rng1.Cells(k, 1).Value <> "" Then
If rng1.Cells(k, 1).Value <> rng2.Cells(k, 1).Value Then
blnOut(k - 1) = True
End If
End If
Next
MatchNames = blnOut
End Function

I think your MatchNames array needs to be transposed as you suggest (because it appears to be returning the equivalent of a row of values - which doesn't work with the other columns of values in SUMPRODUCT).
I don't know how you'd transpose that in VBA but if you can't do that then transpose in SUMPRODUCT like
=SUMPRODUCT(--(input!L1:L100="Y"),--TRANSPOSE(MatchNames(input!A1:A100,input!K1:K100)), input!N1:N100)
but use of TRANSPOSE means that formula now needs to be "array-entered" with CTRL+SHIFT+ENTER
...or you can use MMULT which will multiply a 1x100 range by a 100x1, i.e.
=MMULT(MatchNames(input!A1:A100,input!K1:K100)+0,input!N1:N100*(input!L1:L100="Y"))
for that latter formula to work the sum range - input!N1:N100 - must be all numeric

Related

Passing arrays to UDF; UBound

It's quite frustrating that I already seem to be doing something wrong in the first line of my first VBA project. In essence my project has already been stalled because I can't figure out why this works:
Function TestF(Values As Variant, Dates As Variant)
TestF = Values(2)
End Function
But this does:
Function TestF(Values As Variant, Dates As Variant)
TestF = UBound(Values)
End Function
(I just want to see that UBound works. So, I removed everything from the project that is not related.)
Where Values and Dates are each supposed to be a 1-dimensional array. (Or ranges? Or is that the issue?)
How do I fix the first bit of code?
I Googled and I Googled, but I haven't found my answer.
The (Excel) 'error' is get is #VALUE! from =testf(A2:A10,B2:B10), where the ranges are like:
-1000 31-Dec-13
-10 31-Dec-14
30 13-Mar-15
1200 17-Mar-15
-40 30-Jun-15
1300 30-Sep-16
1200 31-Oct-17
1250 30-Nov-18
1500 31-Dec-18
The problem in your passing of array to UDF is your understanding about array and ranges. In general, ranges are presented as a two dimensional array even when they are with single row or column.
To make your code work, try the "Double Transpose Trick" which is pretty much the following:
Public Sub TestMe()
Dim k As Variant
k = Range("A2:F2")
Debug.Print k(1)
With WorksheetFunction
k = .Transpose(.Transpose(k))
Debug.Print k(1)
End With
End Sub
The code above will hopefully result as an error in the Debug.Print k(1) part. This is because k is a two-dimensional array, although it is on one row only:
Comment this part or change it to Debug.Print k(1, 1) and continue with the .Transpose It will change your array to a single dimension and it will be working.
Thus, if you want your non-working formula to work, this is a possible way to do it:
Public Function TestFormula(myValues As Variant) As String
Dim myArr As Variant
With WorksheetFunction
myArr = .Transpose(.Transpose(myValues))
TestFormula = myArr(UBound(myArr))
End With
End Function
Some important notes: the "Double Transpose Trick" works on one row ranges, when these should be transfered to array. If you are having a one column range, use "Single Transpose":
myArr = .Transpose(myValues)
With more than 1 column or 1 row, do not use transpose as it cannot be mapped to a 1 dimensional array.
Furthermore, if you pass a range of once cell, it will not work. But you will find some way to go around it.
As mentioned by #cyboashu in the comments, the myArr() array would take values up to column 65536, because the Transpose() is made this way, probably for compatibility reasons with Excel 2003. To make a bigger array - Put entire column (each value in column) in an array?
You need to pass the right parameters to your UDF.
Both parameters are Range.
Function TestF(Rng1 As Range, DatesRng As Range) As Long
Dim C As Range
Dim x As Long
For Each C In Rng1
If Trim(C.Value) <> "" Then ' make sure there isn't a blank cell in the middle of your Range
x = x + 1
End If
Next C
TestF = x - 1
End Function
If you don't have empty cells in the middle of your Range, then the following code will be sufficient:
Function TestF(Rng1 As Range, DatesRng As Range) As Long
Dim C As Range
TestF = Rng1.Cells.Count - 1
End Function
You probably wanted:
TestF=Values(Ubound(Values))
also, you should declare what your function is suppose to return, like
Function TestF(Values As Variant, Dates As Variant) as String

How to highlight cells if they match neighboring cells

I regularly work with data spanning multiple columns and need a convenient way to highlight multiple rows that contain the same value in a specific column, but I need to alternate between highlighted and non-highlighted.
For example, I'll have several rows with data in Column A like:
700105862
700105862
700105862
700103235
700103235
700108783
700108783
700108783
And what I'd want to do is highlight the first three rows (700105862), then not highlight 700103235, then again, highlight 700108783.
I was wondering if there was a conditional formatting formula that'd make this possible.
Any help would be greatly appreciated!
Thank you,
if your numbers are divided into chunks of always different numbers repetitions, then you could use this VBA code:
Sub main()
Dim item As Variant
Dim startRow As Long
Dim okHighlight As Boolean
With Range("A1", Cells(Rows.count, 1).End(xlUp))
For Each item In GetUniqueValues(.Cells).Items
If okHighlight Then .Range(.Cells(startRow, 1), .Cells(item, 1)).Interior.ColorIndex = 48
startRow = item + 1
okHighlight = Not okHighlight
Next
End With
End Sub
Function GetUniqueValues(rng As Range) As Dictionary
Dim cell As Range
Dim dict As Dictionary
Set dict = New Dictionary
With dict
For Each cell In rng
.item(cell.Value) = cell.row - rng.Rows(1).row + 1
Next
End With
Set GetUniqueValues = dict
End Function
a Conditional formatting approach is possible on with a helper column
assuming:
your data are in column A and begin from row 2
column B is free
then:
write the following formula in helper column B cells:
=IF(A2<>A1,B1+1,0)
apply conditional formatting to column A with the following formula:
=INT(B2/2)=B2/2
and choosing the format you like to highlight cells
Sure, if you know what ranges you want to highlight you'd simply set the conditional formatting to be between x and y values. Comment on this question with what you dont get and I'll amend the answer accordingly.

Range ID's changing due to cell selection

32bit Excel 2013 / Win 7 64 bit
UDF asks user for two range inputs from the same table and a lookup value ie:
Public Function FindBfromA(A as Range,B as Range, IDValue as Integer)
For IDCheck = 1 to A.Count
IF A(IDCheck) = IDValue then
IDNum = IDCheck
Exit For
End if
Next IDCheck
FindBfromA = B(IDNum)
End Function
Formula is added into another column of the table, for example
=FindBfromA([A],[B],[#C])
'Where C is calculated via something
My issues is Ranges A & B become disjointed. Where A(IDCheck) and B(IDCheck) should belong to corresponding columns in the same table row, based on where my cursor is when calculating begins Range [B] will re-key
This then causes the formula to return the wrong value from the FindBfromA=B(IDNum) as A(IDNum){Row} <> B(IDNum){Row}
I couldn't reproduce the error in the workbook I created with the false data - in my company (private) workbook the function operates essentially the same way, but captures two 'B' values given two IDs and passes them to another function.
It's difficult to be sure without seeing your range selections, but the unreliable element of your code is the cell references. By using a single integer index, you are basically selecting the nth cell in the range rather than cell on row n. My suspicion is that range B is offset from range A by a number of rows. Let's say your two selections were A = "A1:A10" and B = "B2:B11" then A(3), for example, would be on row 3 but B(3) would be on row 4. The same would apply if Range A had more than one column.
To eliminate that risk, refer to the ranges by the row and column indexes, as in the code below. You'll note I've also change the data type of the IDValue to a variant as this prevent an error being thrown in your IDValue should ever be something like a String or Long. I've also looped through range A with a For Each loop on each cell to cater for the case that range A has more than one column.
Public Function FindBfromA(A As Range, B As Range, IDValue As Variant) As Variant
Dim cell As Range
For Each cell In A.Cells
If cell.Value2 = IDValue Then
FindBfromA = B.Cells(cell.Row, 1).Value2
Exit Function
End If
Next
End Function

Formula using Cells in the spreadsheet

I am looking for a way to set a variable equal to the number of non-empty cells in Column A using Excel VBA.
So pseudo code
Dim j As Integer
j = CountA(A:A)
This however does not work. Neither does j = "=CountA(A:A)"
Something like this will do the trick.
The VBA functions don't work exactly the same as in the spreadsheets themselves. You need to first select the range of the active worksheet and then call the counta function.
Dim j = Application.WorksheetFunction.counta(activeworksheet.range("A:A"))
Paste this into a module in Excel VBA.
Function CountNonEmptyCells(ColId As Integer) As Integer
Dim r As Range
Dim Count As Integer
Set r = Sheet1.Columns(ColId)
For Each cell In r.Cells
If cell.Value <> "" Then
Count = Count + 1
End If
Next
CountNonEmptyCells = Count
End Function
My end result:
You can also use Evaluate Function like this:
Dim j As Long
j = [CountA(A:A)] 'brackets are shortcut for Evaluate
or explicitly like this:
j = Evaluate("CountA(A:A)")
Essentially, you can either Evaluate the formula as it would appear on the worksheet or you can adapt the syntax to use as an adopted VBA command. Here are a couple variations of each. Note that I am explicitly including a reference to the parent worksheet. This is particularly important for the first two evaluate methods and desirable for all four variations in order that you are not counting column A from the wrong worksheet.
Dim j As Long
j = [COUNTA(Sheet1!A:A)]
Debug.Print j
j = Evaluate("COUNTA(Sheet1!A:A)")
Debug.Print j
j = Application.CountA(Sheets("Sheet1").Columns(1))
Debug.Print j
j = Application.CountA(Range("Sheet1!A:A"))
Debug.Print j
The first simply uses [ and ] as wrappers around the COUNTA formula as it would appear on the worksheet. This forces evaluation of the formula to a result. The second is another evaluation of the formula but using the .Evaluate command allows you the option to construct the formula as a string using concatenation, replacement and other text parsing methods. You can include an equals sign (e.g. =) as a prefix if that makes more sense to you, (e.g. j = [=COUNTA(Sheet1!A:A)]) but it is not necessary.
In the last two, VBA adopts the native worksheet COUNTA function by prefacing it with either Application.Worksheetfunction. or (as above) just Application.. The cell range also moves from worksheet cell notation to VBA style cell notation.

VBA Evaluate function and array formula returning range of values

Suppose I want to use Evaluate function to evaluate an array formula which returns a range of values. For example I try to get index (using Excel function MATCH) in an ordered list in Sheet2!A:A for each values in Sheet1!A:A. And I want to put the indices in column B.
Dim sh as Worksheet
Set sh = Sheets("Sheet1")
sh.Range("B1:B10").Value = sh.Evaluate("=MATCH(A1:A10,Sheet2!A:A)")
Whan I run the code, I get a column of repeated values - the values are equal to the index of the first element. This is not correct.
When I try the same by putting array formula in the worksheet {=MATCH(A1:A10,Sheet2!A:A)}, it works without problems and returns the correct index for every element.
So my question: how to use Evaluate function returning a whole range of values?
Interesting issue. I was not able to get the MATCH function to return an array using VBA Evaluate. However, the following modification seems to work, using the zero (0) for the row argument in the index function returns all of the rows. Note also that I added the match_type argument to the Match function.
sh.Range("B1:B10").Value = sh.Evaluate("=INDEX(MATCH(A1:A10,Sheet2!A:A,0),0,1)")
If Evaluate() does not make you happy, then:
Sub marine()
Dim sh As Worksheet, r As Range
Set sh = Sheets("Sheet1")
Set r = sh.Range("B1:B10")
r.FormulaArray = "=MATCH(A$1:A$10,Sheet2!A:A,0)"
r.Copy
r.PasteSpecial xlPasteValues
End Sub