VBA Multi-Criteria Parent-Child SumProduct - excel

Please help.
I'm trying to create a user defined function that will use an array as multiple criteria (Pedigree) to check for corresponding parents (Parent) and then sum their respective ranges (Sumrange).
I've managed to create code that will check if a parent is in the pedigree range which will then return a result of 1 or 0. This will not return true values if blanks verify blanks. I intend to create an array with these 1's and 0's to then SumProduct it with my Sumrange. My problem is that I am unable create an array of these 1's and 0's and SumProduct them with the Sumrange without returning a #value.
This below code doesn't include the SumProduct portion but just returns 1 or 0 based on the criteria.
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Long
Application.Volatile
Dim i As Variant
Dim j As Variant
Dim result As Integer
result = 0
For Each i In Parent
For Each j In Pedigree
If i.Value = "" Or j.Value = "" Then
result = result
ElseIf i.Value = j.Value Then
result = 1: GoTo NextIteration
End If
Next j
NextIteration:
Next i
ProdIf = result
End Function
Thanks for you help.

Thanks to Super Symmetry for getting this 99% of the way there.
Since the original code returned a 1 or 0, I changed the code to provide a sumproduct.
Also I've made the PedigreeRange loop through columns instead of rows to fit the way my Pedigree data is.
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Variant
Application.Volatile
Dim i As Long
Dim j As Long
Dim result() As Variant
ReDim result(1, 1 To Parent.Rows.Count)
Dim x As Long
For i = 1 To Parent.Rows.Count
x = 0
result(1, i) = x
For j = 1 To Pedigree.Columns.Count
If Parent.Cells(i, 1).Value <> "" And Pedigree.Cells(1, j) <> "" And Parent.Cells(i, 1) = Pedigree.Cells(1, j) Then
x = 1
Exit For
End If
Next j
result(1, i) = x * Sumrange(i, 1).Value
Next i
ProdIfs = WorksheetFunction.Sum(result)
End Function
Thanks again. If there are any improvements that can be made to this please let me know.

Answer changed following comments
If you want to return an array, you actually have to create and populate an array in your function and make sure the return type is Variant.
Try this
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Variant
Application.Volatile
Dim i As Long
Dim j As Long
Dim result() As Integer ' The return value must be an array
ReDim result(1 To Parent.Rows.Count, 1 To 1) ' Assuming Parent is 1 column
For i = 1 To Parent.Rows.Count
result(i, 1) = 0 ' set to 0 by default but always good to do it explicitly
For j = 1 To Pedigree.Rows.Count
If Parent.Cells(i, 1).Value <> "" And Parent.Cells(i, 1) = Pedigree.Cells(j, 1) Then
result(i, 1) = 1
Exit For
End If
Next j
Next i
ProdIfs = result
End Function
Edit: following your answer
You just need to keep a running sum.
To make your code run faster you should read values of these ranges and process them in memory. (It is much faster than asking excel for values in cells).
The return value should be a Double
This feels like a SumIfs ranther than a ProdIfs
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Double
Application.Volatile
Dim i As Long
Dim v As Variant
Dim vParent As Variant: vParent = Parent.Value
Dim vPedigree As Variant: vPedigree = Pedigree.Value
Dim vSumRange As Variant: vSumRange = Sumrange.Value
ProdIfs = 0
For i = 1 To UBound(vParent, 1)
For Each v In vPedigree
If len(v) > 0 And v = vParent(i, 1) Then
ProdIfs = ProdIfs + vSumRange(i, 1)
Exit For
End If
Next v
Next i
End Function

Related

Application.Match doesn work - types incompatible

I have a table with strings, and I want to check whether those strings are already stored as elements in a certain array. If not, they're supposed to be added as the last element of the respective array. For some reason, I receive an error stating that the types are incompatible in the line mtch = Application.Match(srch, arr, 0).
Also, I want to work with this approach and not a different one since this is supposed to be the basis for further checks.
Sub Test_4()
Dim i, j, k As Long
Dim arr As Variant
Dim srch, mtch As String
With Worksheets("table1")
For i = 1 To .Range("A1").End(xlDown).Row
srch = .Range("A" & i).Value
mtch = Application.Match(srch, arr, 0)
If Not IsNumeric(mtch) Then
ReDim Preserve arr(UBound(arr) + 1)
arr(UBound(arr)) = mtch
End If
Next i
End With
End Sub
Your base fault is - as Mate wrote - that arr isn't initialized in the first run
You can use this code - it uses the VBA Filter function to test wether a value is already part of an array or not.
Public Function getUniqueValuesFromRange(rg As Range) As Variant
Dim arrResult As Variant
ReDim arrResult(0 To rg.Cells.Count - 1) 'dim arrResult to the max
Dim iCell As Long, iResult As Long
Dim value As Variant
For iCell = 1 To rg.Cells.Count
value = rg.Cells(iCell)
If UBound(Filter(arrResult, value)) = -1 Then 'value is not part of arrResult
arrResult(iResult) = value
iResult = iResult + 1
End If
Next
'it is "cheaper" to redim the array once at the end of the function
ReDim Preserve arrResult(iResult - 1)
getUniqueValuesFromRange = arrResult
End Function
You can call this function like this:
arr = getUniqueValuesFromRange(Worksheets("table1").UsedRange.Columns("A"))
EDIT: you can use
If Not IsNumeric(Application.Match(value, arrResult, 0)) Then
instead of
If UBound(Filter(arrResult, value)) = -1 Then
If you have Excel 365 you can use the UNIQUE function as well
Public Function getUniqueValuesFromRange(rg As Range) As Variant
With Application.WorksheetFunction
getUniqueValuesFromRange= .Transpose(.Unique(rg))
End With
End Function
Be aware: there is no check, that you pass only one column ...

Develop a more efficient ordered-union function in VBA

I would like to make a user-defined function union in VBA, such that:
it could take variable parameters
each parameter is a one-column range like A1, A2:A10; we don't need to consider passing constant values to parameters
we could consider, within one input range, there are no duplicates; but it is very possible to have duplicates among input ranges.
union combines the input ranges, and keeps the order of the elements. For instance, =union(A1:A5, C1:C2, E1:E3) has the following expected output in Column I:
I wrote the following code which works. However, the problem is that it is slow. A union over a list of 4000 rows and a list of 20 rows takes already several seconds. First, I don't know the way I coded arrays could be improved. Second, the algorithm just consists in comparing each new element against the accumulating result list; there is no sort, no other techniques. Third, I don't know if there are any existing functions we could use in other objects of VBA (eg, VBA FILTER function, Collection, ArrayLists, Scripting.Dictionary).
Could anyone propose a more efficient code?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
Function exists(v As Variant, arr As Variant, resCount As Long) As Boolean
If resCount = 0 Then
exists = False
Else
exists = False
i = LBound(arr, 1)
Do While (i <= resCount) And (Not exists)
If arr(i) = v Then
exists = True
End If
i = i + 1
Loop
End If
End Function
' assumption: every input is a range (eg, A1, A1:A2)
' assumption: each input range has only one column
Function union(ParamArray arr() As Variant) As Variant
Dim res As Variant
ReDim res(1 To 100000)
Dim resCount As Long
resCount = 0
For k = LBound(arr) To UBound(arr)
Dim arrk As Variant
Dim v
arrk = arr(k).Value2
If getDimension(arrk) = 0 Then 'case of A1, B1
v = arrk
If Not exists(v, res, resCount) Then
resCount = resCount + 1
res(resCount) = v
End If
ElseIf getDimension(arrk) = 2 Then 'case of A1:A10, B1:B10
For i = LBound(arrk, 1) To UBound(arrk, 1)
v = arrk(i, 1)
If Not exists(v, res, resCount) Then
resCount = resCount + 1
res(resCount) = v
End If
Next i
End If
Next k
ReDim Preserve res(1 To resCount)
union = Application.WorksheetFunction.Transpose(res)
End Function
Something like this should work, using a Dictionary to eliminate duplicates.
Function UniqueValues(ParamArray arr() As Variant)
Dim r, c As Range, v, dict
Set dict = CreateObject("scripting.dictionary")
For Each r In arr
For Each c In r.Cells
v = c.Value
If Len(v) > 0 Then dict(v) = 1
Next c
Next r
UniqueValues = ToColumn(dict.keys)
End Function
Function ToColumn(arr)
Dim arrOut, i As Long
ReDim arrOut(1 To UBound(arr) + 1, 1 To 1)
For i = 1 To UBound(arr) + 1
arrOut(i, 1) = arr(i - 1)
Next i
ToColumn= arrOut
End Function

VBA: Search column for first instance of string and assign to new array or string

I have a column that contains mixed strings and I need to find all the unique strings and declare to either a string or array variable. The last row of the column will vary so I cannot use a definite range. I was thinking of using some form of string comparison of the preceding cell and current cell, but like I said the data is mixed so when redundant data comes up this complicates the problem. Here is a picture to try and explain it better.
EDIT: The string concatenation I will worry about that later.
Using Excel 365.
With data in A1 through A11, in another cell enter:
=UNIQUE(A1:A11)
to get:
or:
=TEXTJOIN(",",TRUE,UNIQUE(A1:A11))
to get a comma-separated list:
EDIT#1:
With VBA, try this UDF:
Public Function unikue(rng As Range)
Dim arr, c As Collection, r As Range
Dim nCall As Long, nColl As Long
Dim i As Long
Set c = New Collection
nCall = Application.Caller.Count
On Error Resume Next
For Each r In rng
c.Add r.Text, CStr(r.Text)
Next r
On Error GoTo 0
nColl = c.Count
If nCall > nColl Then
ReDim arr(1 To nCall, 1 To 1)
For i = 1 To nCall
arr(i, 1) = ""
Next i
Else
ReDim arr(1 To nColl, 1 To 1)
End If
For i = 1 To nColl
arr(i, 1) = c.Item(i)
Next i
unikue = arr
End Function

How to sort an array to a Range

I have an array of numbers in an excel spreadsheet which I am trying to sort (all numbers >60) using a user defined vba function and i want to return the result as a range in the same excel sheet.
I am getting a value error when i run this function in excel.I am not too sure where this error is coming from as I a new to VBA.I would really appreciate some guidance in resolving this issue.
Array Excel
Column A
200
50
23
789
Function trial(number As Range)
Dim cell As Range
Dim savearray() As Variant
Dim d As Long
For Each cell In Range("a3:a6").Cells
If cell.Value > 60 Then
d = d + 1
ReDim Preserve savearray(1 To d)
savearray(1, d) = cell.Value
trial = savearray
End If
Next cell
End Function
There is a bit of work to do on your Sub. However, to help you, below is a way to dynamically build an array:
d = 0
For Each cell In Range("A3:A1000")
If cell.Value > 60 Then
If d = 0 Then
ReDim savearray(0 To 0)
Else
ReDim Preserve savearray(0 To UBound(savearray) + 1)
End If
savearray(d) = cell.Value
d = d + 1
End If
Next cell
I feel like you might want to rather return a sorted array and only then, cast results to a Range
First we create a Function to sort our array
Private Function BubbleSort(ByRef from() As Variant) As Variant()
Dim i As Integer, j As Integer
Dim temp As Variant
For i = LBound(from) To UBound(from) - 1
For j = i + 1 To UBound(from)
If from(i) < from(j) Then
temp = from(j)
from(j) = from(i)
from(i) = temp
End If
Next j
Next i
BubbleSort = from ' returns sorted array
End Function
Then we create a simple "Range replacer" procedure
Private Sub replace_with_sorted(ByVal which As Range)
Dim arr() As Variant
arr = Application.Transpose(which)
arr = BubbleSort(arr)
which = Application.Transpose(arr)
End Sub
So the invokation would look the following way:
Private Sub test()
replace_with_sorted Range("A1:A4")
End Sub
This of course produces the expected result:
EDIT: Just noticed you want to sort only values larger than 60.
In that case, simply fill an array with values larger than 60 and use the same application.
Private Sub test()
Dim arr() as Variant: arr = Application.Transpose(Range("A1:A4"))
Dim above60() as Variant
Dim i as Integer, j as Integer: j = 0
For i = LBound(arr) To UBound(arr)
If arr(i) > 60 Then
ReDim Preserve above60(j)
above60(j) = arr(i)
j = j + 1
End If
Next i
ReDim arr()
arr = BubbleSort(above60)
' returns sorted array, do whatever u want with it _
(place it in whatever range u want, not defined in ur question)
End Sub

Array() = range().value

I saw array() = range().value in an example and I'm trying to understand how it works.
Sub test()
Dim arr() As Variant
arr() = Range("E5:E7").Value
For i = 0 To UBound(arr)
Debug.Print arr(i)
Next i
End Sub
First, above code is giving me subscript out of range error. How come ? Second, what part of the documentation would let me know how array() = range().value would play out without testing it ? My hypothesis is that it will go through the cells from the top left to the bottom right and add them to the array. How can I confirm that though ?
I see two issues with your code. The first is that you start i at 0, but arrays in Excel begin at index 1. Instead of For i = 0 you can use For i = LBound(arr) like you use UBound(arr) or just start it at 1.
Second, and more importantly, an array of cells has both columns and rows. When you read a range into a variant array, you get a two-dimensional result (rows and columns) and not a one-dimensional result as you seem to be expecting.
Try this:
Sub test()
Dim arr() As Variant
Dim i As Long, j As Long
arr() = Range("E5:E7").Value
For i = 1 To UBound(arr, 1)
For j = 1 To UBound(arr, 2)
Debug.Print arr(i, j)
Next j
Next i
End Sub
If you want to get just the values of the cells into a one dimensional array, you can do that by using the Transpose function, like this:
arr() = Application.WorksheetFunction.Transpose(Range("E5:E7").Value)
If you do this, the array is now one-dimensional and you can iterate through it like you were trying to.
arr() = Application.WorksheetFunction.Transpose(Range("E5:E7").Value)
For i = 1 To UBound(arr)
Debug.Print arr(i)
Next i
This is a good read for you: http://www.cpearson.com/excel/ArraysAndRanges.aspx
The reason you're getting "out of range" is because it returns a 2 dimensional array.
Your line of code For i = 0 To UBound(arr) should be For i = 1 To UBound(arr,1)
Also, the array starts at 1, so don't use the 0 For i = 1 to UBound(arr, 1)
Your corrected code would be:
Sub Test()
Dim arr() as Variant
arr = Range("E5:E7")
For i = 1 To UBound(arr, 1)
MsgBox (arr(i, 1))
Next i
End Sub
It's basically loading the cell values of E5 - E7 into an array. But it is going to be two dimensional. So you will need Debug.Print arr(i, 1)
Sub test()
Dim arr() As Variant
arr() = Range("E5:E7").Value
For i = 1 To UBound(arr)
Debug.Print arr(i, 1)
Next i
End Sub

Resources