Apply dynamically defined aggregation function on array values - vba

I want apply a user defined aggregation function (string-value from a certain cell, like "SUM" or "STDEV") on an array of values. Here's a simplified example. However I don't know how to make the aggregation (last line):
Sub test()
Dim values() As Double
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & values & ")") ' <-- This doesn't work, but hopefully it's clear what it should do
End Sub
EDIT
My original code is also creating the values array dynamically from a spreadsheet which uses , as decimal sign. This causes an issue with Scott's answer below.
Const datasht = "Daten"
Const aggregate_cell = "G1"
Sub run()
Dim sht As Worksheet
Dim n_rows As Integer
Dim rw As Integer
Application.DecimalSeparator = "."
Set sht = ActiveWorkbook.Worksheets(datasht)
n_rows = sht.Cells(1, 1).CurrentRegion.Rows.Count ' Get range of data
Dim values() As String
ReDim values(1 To n_rows)
For rw = 1 To n_rows
values(rw) = sht.Cells(rw, 1).Value
Next rw
Debug.Print (aggregate(values))
End Sub
Function aggregate(values() As String)
' Get aggregated value
Dim aggregate_fn As String
aggregate_fn = ActiveWorkbook.Worksheets(datasht).Range(aggregate_cell).Value
aggregate = Evaluate("=" & aggregate_fn & "(" & Join(values, ",") & ")") ' <-- doesn't work as intended
End Function

This should work for any function available via Application.WorksheetFunction, and is less likely to give an error if you pass too many values.
Sub Tester()
Dim arr(0 To 1000), res, x As Long, f as String
'get some values to work with...
For x = 0 To 1000
arr(x) = Rnd() * 10
Next x
f = "SUM"
res = CallByName(Application.WorksheetFunction, f, VbMethod, arr)
Debug.Print res
End Sub

I was able to get this to work by creating a string from your array, then using that instead:
Sub test()
Dim values() As Double
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim mystring As String, i As Long
mystring = values(LBound(values))
For i = LBound(values) + 1 To UBound(values)
mystring = mystring & "," & values(i)
Next i
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & mystring & ")")
Debug.Print result
End Sub

Join works with Strings change the values to String instead of Double:
Sub test()
Dim values() As String
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & Join(values, ",") & ")")
Debug.Print result
End Sub
As per your edit there is no need for the array at all, just pass the range and worksheet to the function and use .Address on the range.
Const datasht = "Daten"
Const aggregate_cell = "G1"
Sub run()
Dim sht As Worksheet
Dim n_rows As Integer
Dim rw As Integer
'Application.DecimalSeparator = "."
Set sht = ActiveWorkbook.Worksheets(datasht)
n_rows = sht.Cells(1, 1).CurrentRegion.Rows.Count ' Get range of data
Dim rng As Range
Set rng = sht.Range(sht.Cells(1, 1), sht.Cells(n_rows, 1))
Debug.Print (aggregate(sht, rng))
End Sub
Function aggregate(sht As Worksheet, rng As Range)
' Get aggregated value
Dim aggregate_fn As String
aggregate_fn = sht.Range(aggregate_cell).Value
aggregate = sht.Evaluate("=" & aggregate_fn & "(" & rng.Address(1, 1) & ")")
End Function

Related

VBA Code for Finding Cells Below which match the key

I have the following requirement I have 2 columns with unique keys called code. In one column below the code, there are one or multiple values present which is the answer. Like in below format
A X
1
2
B Y
9
3
Now the code will have a value populated in next column, while answers wont.
Now I have to find answers for all codes like A, B, C etc. For e.g If I compare with A then answer should be 1,2. I was writing a small subroutine as a beginning but I am facing issues. Can you please correct it
Sub CalculateCellValue()
Dim ValuesBelow As Variant
Dim ValuesRight As String
Dim rows1 As Integer
rows1 = 4
Dim colC As Integer
colC = 2
ValuesRight = ActiveSheet.Cells(rows1 + 1, colC + 1)
While (Not IsEmpty(ValuesRight))
ValuesBelow = ActiveSheet.Cells(rows1 + 1, colC)
rows1 = rows1 + 1
ValuesRight = ActiveSheet.Cells(rows1 + 1, colC + 1)
Wend
MsgBox (ValuesBelow)
End Sub
Purely for an ordered example as shown:
Option Explicit
Sub test()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.Worksheets("Sheet5") 'Change as appropriate
Dim myArr()
myArr = ws.Range("A1:B" & GetLastRow(ws, 1)).Value
Dim i As Long
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
For i = LBound(myArr, 1) To UBound(myArr, 1)
If myArr(i, 2) <> vbNullString Then
If Not dict.exists(myArr(i, 1)) Then
Dim currKey As String
currKey = myArr(i, 1)
dict.Add myArr(i, 1), vbNullString
End If
Else
dict(currKey) = dict(currKey) & ", " & myArr(i, 1)
End If
Next i
Dim key As Variant
For Each key In dict
MsgBox key & " = " & Right$(dict(key), Len(dict(key)) - 1)
Next key
End Sub
Public Function GetLastRow(ByVal ws As Worksheet, Optional ByVal columnNumber As Long = 1) As Long
With ws
GetLastRow = .Cells(.Rows.Count, columnNumber).End(xlUp).Row
End With
End Function
I used below code to match my requirement
Function findBelowAll(rows1 As Long)
Dim ValuesBelow() As Variant
ReDim ValuesBelow(1 To 1) As Variant
Dim ValuesRight As Variant
Dim colC As Long
colC = 1
Dim i As Long
ValuesRight = ""
While (ValuesRight = "")
rows1 = rows1 + 1
' change / adjust the size of array
ReDim Preserve ValuesBelow(1 To UBound(ValuesBelow) + 1) As Variant
' add value on the end of the array
ValuesBelow(UBound(ValuesBelow)) =
Worksheets(ActiveSheet.Name).Cells(rows1, colC).Value
ValuesRight = Worksheets(ActiveSheet.Name).Cells(rows1, 2).Value
Wend
For i = LBound(ValuesBelow) To UBound(ValuesBelow) - 1
findBelowAll = findBelowAll & ValuesBelow(i) & vbNewLine
Next i
End Function

VBA to delete rows based on cell value

I am trying to do the following :
VBA to lookup a value from a particular cell
Match these values in a particular column in specified sheets
Delete all rows from the sheet if the value do not match
I have tried the following - the code doesn't seem to function
Sub Delete()
Dim List As Variant
Dim LR As Long
Dim r As Long
List = Worksheets("Sheet1").Cells(28, "C").Value
LR = Range("E" & Rows.Count).End(xlUp).Row
For r = LR To 1 Step -1
If IsError(Application.Match(Range("E" & r).Value, List, False)) Then
Worksheets("Sheet2").Range("A1:AA36429").Rows(r).Delete
End If
Next r
End Sub
Try this:
Sub Delete()
Dim i As Integer
Dim LR As Long
Dim List As Variant
LR = Range("E" & Rows.Count).End(xlUp).Row
List = Worksheets("Sheet1").Cells(28, "C").Value
For i = 1 To LR
If Cells(i, "E").Value = List Then
Worksheets("Sheet1").Rows(i).Delete
End If
Next i
End Sub
I think you have a few ways of going about this, but the quickest way I know of is to use MATCH to compare values in a range to values in an array. Please note that this has a limit to 4000 or so values to compare before it fails. For your purposes, I think the following will work:
Sub test1()
Dim x As Long
Dim array1() As Variant
Dim array2() As Variant
array1 = Array("ABC", "XYX")
array2 = Range("A1:A2")
If IsNumeric(Application.Match(Range("A1").Value, array1, 0)) Then
x = 1
ElseIf IsNumeric(Application.Match(Range("A1").Value, array2, 0)) Then
x = IsNumeric(Application.Match(Range("A1").Value, array2, 0))
End If
'If x is not found in these arrays, x will be 0.
MsgBox x
End Sub
Another similar way is the following:
Sub test2()
Dim array1() As Variant
Dim FilterArray() As String
Dim x As Variant
x = Range("A1").Value
array1 = Array("ABC", "RANDOM", "VBA")
FilterArray = Filter(SourceArray:=array1, _
Match:=strText, _
Include:=True, _
Compare:=vbTextCompare)
If UBound(FindOutArray) = -1 Then
MsgBox "No, Array doesn't contain this item - " & x
Else
MsgBox "Yes, Array contains this item - " & x
End If
End Sub
So if we were to incorporate that all together (and I tested this btw):
Sub Delete()
Dim i As Integer
Dim LR As Long
Dim List() As Variant
Dim x As Long
LR = Range("E" & Rows.count).End(xlUp).Row
List = Worksheets("Sheet1").Range("A1:A2").Value
For i = 1 To LR
If IsNumeric(Application.Match(Cells(i, "E").Value, List, 0)) Then
Worksheets("Sheet1").Cells(i, "E").Value = ""
End If
Next i
Worksheets("Sheet1").Columns("E").SpecialCells(xlCellTypeBlanks).Cells.Delete
End Sub
This will set the cells that have values that are found in the array to blanks. Once the loop is finished, then the blank cells are deleted. If you want to shift the entire rows up, then use this as the last line instead:
Worksheets("Sheet1").Columns("E").SpecialCells(xlCellTypeBlanks).EntireRow.Delete

VBA Index/Match with multiple criteria (unique value & date)

I have a spreadsheet that has values for more than one month, so I am trying to first find the value based on a value in the wsRevFile worksheet and then ensure that this is the value from last month. When I use the following code, I get a "invalid number of arguments" error.
Sub RevLookup(wsMvFile As Worksheet, wsRevOld As Worksheet, wsNewRev As Worksheet, _
rowCount As Integer, workCol As String, _
srcCol1 As Integer, srcCol2 As Integer)
Dim vrw As Variant, i As Long
For i = 2 To rowCount
vrw = Application.Match(wsRevFile.Range("A" & i), wsNewRev.Columns(2), Format(DateSerial(Year(Date), Month(Date), 0), "mm/dd/yyyy"), wsNewRev.Columns(1), 0)
If IsError(vrw) Then
vrw = Application.Match(wsRevFile.Range("A" & i), wsRevOld.Columns(1), 0)
If Not IsError(vrw) Then _
wsRevFile.Range(workCol & i) = Application.Index(wsRevOld.Columns(srcCol1), vrw)
Else
wsRevFile.Range(workCol & i) = Application.Index(wsNewRev.Columns(srcCol2), vrw, 1)
End If
Next i
End Sub
I am assuming this has to do with the way I assigned the Application Match function, because the formula without this part works for other columns. Any ideas on how I could get this to work?
Thanks for your help!
Try ajusting the variables of the following procedure, as I didn't figure out your input and output data:
Sub Main()
Dim SearchValue As Variant
Dim SearchColumn As Range
Dim ReturnColumn As Range
Dim ResultRows As Collection
Dim LastDate As Variant 'Date?
Dim iRow As Variant
SearchValue = 10 '<-- change to suit
Set SearchColumn = wsNewRev.Range("B1:B10")
Set ReturnColumn = wsNewRev.Range("C1:C10") '<-- change to suit
Set ResultRows = GetLoopRows(SearchColumn, SearchValue)
For Each iRow In ResultRows
If LastDate < ReturnColumn(iRow) Then
LastDate = ReturnColumn(iRow)
End If
Next iRow
Debug.Print LastDate
End Sub
Function GetLoopRows(ParamArray pParameters() As Variant) As Collection
'Obtém limites de laços com levando em conta condições
'[vetor1], [valor1], [vetor2], [valor2], ...
Dim iCondition As Long
Dim i As Variant
Dim iRow As Variant
Dim Result As Collection
Dim NumConditions As Long
Dim SearchCollection As Collection
Dim ArraysCollection As Collection
Dim iArray As Variant
NumConditions = (UBound(pParameters) - LBound(pParameters) + 1) / 2
Set ArraysCollection = New Collection
Set SearchCollection = New Collection
For i = LBound(pParameters) To UBound(pParameters) Step 2
ArraysCollection.Add pParameters(i + 0).Value2
SearchCollection.Add pParameters(i + 1)
Next i
Set Result = New Collection
For iRow = LBound(ArraysCollection(1)) To UBound(ArraysCollection(1))
For iCondition = 1 To NumConditions
If ArraysCollection(iCondition)(iRow, 1) <> SearchCollection(iCondition) Then GoTo Continue
Next iCondition
Result.Add CLng(iRow)
Continue:
Next iRow
Quit:
Set GetLoopRows = Result
End Function

Removing Duplicate values from a string in VBA

In VBA if I have a string of numbers lets say ("1,2,3,4,5,2,2"), how can I remove the duplicate values and only leave the first instance so the string says ("1,2,3,4,5").
Here is a function you can use to dedupe a string as you've described. Note that this won't sort the deduped string, so if yours was something like "4,2,5,1,3,2,2" the result would be "4,2,5,1,3". You didn't specify you needed it sorted, so I didn't include that functionality. Note that the function uses , as the default delimiter if not specified, but you can specify a delimiter if you choose.
Function DeDupeString(ByVal sInput As String, Optional ByVal sDelimiter As String = ",") As String
Dim varSection As Variant
Dim sTemp As String
For Each varSection In Split(sInput, sDelimiter)
If InStr(1, sDelimiter & sTemp & sDelimiter, sDelimiter & varSection & sDelimiter, vbTextCompare) = 0 Then
sTemp = sTemp & sDelimiter & varSection
End If
Next varSection
DeDupeString = Mid(sTemp, Len(sDelimiter) + 1)
End Function
Here's some examples of how you would call it:
Sub tgr()
MsgBox DeDupeString("1,2,3,4,5,2,2") '--> "1,2,3,4,5"
Dim myString As String
myString = DeDupeString("4-2-5-1-3-2-2", "-")
MsgBox myString '--> "4-2-5-1-3"
End Sub
I suggest writing a Join function to combine the unique parts back into a single string (there is one available for arrays, but not for any other collection):
Function Join(Iterable As Variant, Optional Delimiter As String = ",") As String
Dim notFirst As Boolean
Dim item As Variant
For Each item In Iterable
If notFirst Then
Join = Join & Delimiter
Else
notFirst = True
End If
Join = Join & item
Next
End Function
Then, use Split to split a string into an array, and Scripting.Dictionary to enforce uniqueness:
Function RemoveDuplicates(s As String, Optional delimiter As String = ",") As String
Dim parts As String()
parts = Split(s,delimiter)
Dim dict As New Scripting.Dictionary
Dim part As Variant
For Each part In parts
dict(part) = 1 'doesn't matter which value we're putting in here
Next
RemoveDuplicates = Join(dict.Keys, delimiter)
End Function
try this:
Sub test()
Dim S$: S = "1,2,3,4,5,2,2,5,6,6,6"
Dim Dic As Object: Set Dic = CreateObject("Scripting.Dictionary")
Dim Key As Variant
For Each Key In Split(S, ",")
If Not Dic.exists(Trim(Key)) Then Dic.Add Trim(Key), Nothing
Next Key
S = Join(Dic.Keys, ","): MsgBox S
End Sub
Heres my crack at it:
Function Dedupe(MyString As String, MyDelimiter As String)
Dim MyArr As Variant, MyNewArr() As String, X As Long, Y As Long
MyArr = Split(MyString, MyDelimiter)
ReDim MyNewArr(0)
MyNewArr(0) = MyArr(0)
Y = 0
For X = 1 To UBound(MyArr)
If InStr(1, Join(MyNewArr, MyDelimiter), MyDelimiter & MyArr(X)) = 0 Then
Y = Y + 1
ReDim Preserve MyNewArr(Y)
MyNewArr(Y) = MyArr(X)
End If
Next
Dedupe = Join(MyNewArr, MyDelimiter)
End Function
Call it like this in code:
Dedupe(Range("A1").Text,",")
Or like this in the sheet:
=Dedupe(A1,",")
The first parameter is the cell to test and the second is the delimiter you want to use (in your example it is the comma)
vb6,Find Duplicate letter in word when there is no delimiter.
Function RemoveDuplicateLetter(ByVal MyString As String) As String
Dim MyArr As Variant, MyNewArr() As String, X As String,str as String
Dim bValue As Boolean
Dim i As Long, j As Long
For i = 0 To Len(MyString)
str = str & Mid$(MyString, i + 1, 1) & vbNullChar
Next
i = 0
MyArr = Split(str, vbNullChar)
ReDim MyNewArr(0)
MyNewArr(0) = MyArr(0)
For i = LBound(MyArr) To UBound(MyArr)
bValue = True
For j = i + 1 To UBound(MyArr)
If MyArr(i) = MyArr(j) Then
bValue = False
Exit For
End If
Next
If bValue Then X = X & " " & MyArr(i)
Next
RemoveDuplicateLetter = X
End Function

search strings in cell

I have multiple values in cell A1 which are separated by a ';'. Some of the same values may be in cell B1. I need to search the values in cell A1 using those in cell B1. All the values that are not found then need to presented in cell C1.
Eg - Cell A1 ( Apple;Orange;Cherry) cell B1 (Apple;Orange;) cell c1 need to reflect "Cherry" as not found
I tried this code:
Sub Splitvalue()
Dim str, mystr As Variant
Dim tp As Integer
str = Split(Range("A1").Value, ";")
For tp = LBound(str) To UBound(str)
mystr = str(tp)
Next
End Sub
Set up your sheet1 like this
the use this code
Option Explicit
Sub Splitvalue()
Dim lastRow As Long
lastRow = Range("A" & Rows.Count).End(xlUp).Row
Dim c As Range
Dim A As Variant, B As Variant
Dim i As Long, j As Long
Dim x As Boolean
Columns(3).ClearContents
For Each c In Range("A1:A" & lastRow)
A = Split(c, ";")
B = Split(c.Offset(0, 1), ";")
For i = LBound(A) To UBound(A)
For j = LBound(B) To UBound(B)
If A(i) = B(j) Then
x = True
Exit For
Else
x = False
End If
Next j
If Not x Then
If IsEmpty(c.Offset(0, 2)) Then
c.Offset(0, 2) = A(i)
Else
c.Offset(0, 2).Value = c.Offset(0, 2).Value & ";" & A(i)
End If
End If
Next i
Next
End Sub
and your results should look like this
Why not just split the second cell like you split the first cell? Then see if you find each element of A1 in B1, otherwise output to C1?
This is not elegant, but will work:
Sub Splitvalue()
Dim str, mystr As Variant
Dim stri As Variant
Dim tp As Integer
str = Split(Range("A1").Value, ";")
str2 = Split(Range("B1").Value, ";")
For tp = LBound(str) To UBound(str)
mystr = str(tp)
'Debug.Print mystr
Dim found As Boolean
found = False
For Each stri In str2
'Debug.Print stri
If stri = mystr Then
found = True
End If
Next stri
If found = False Then
Debug.Print mystr
End If
Next
End Sub
One way:
dim needle() as string: needle = split(Range("B1").Value, ";")
dim haystack as string: haystack = ";" & Range("A1").Value & ";"
dim i as long
for i = 0 To ubound(needle)
haystack = replace$(haystack, ";" & needle(i) & ";", ";")
next
If len(haystack) = 1 then haystack = ";;"
Range("C1").Value = Mid$(haystack, 2, Len(haystack) - 2)