View Full Version : Solved: VBA Evaluate function not working in arrays - Need alternate
wezred
07-02-2007, 09:06 PM
My column has 10000 combination in each cell of fruits like apple, banana, orange, kiwi. In the next column there are corresponding numbers. For example:
Apple/Banana 10
Apple/Kiwi 12
Kiwi/Orange 30
Orange/Banana 20
Banana/Apple 19
This formula works and sums up all corresponding columns which cells contain the text "apple":
=+SUM(IF(ISNUMBER(SEARCH("Apple",$A$8:$A$10000)),$B$8:$B$10000))
Answer is 41
I tried creating an evaluation function in VBA as I'm planning to use more than 10 criterias, the function looks like this:
Function Txt2F(InputString As String)
Application.Volatile
Txt2F = Evaluate("=(" & InputString & ")")
End Function
However, the following function doesn't work at all:
=+SUM(IF(Txt2F($A$1),Executive!$B$8:$B$10000))
Where cell $A$1 has the text:
ISNUMBER(SEARCH("Apple",$A$8:$A$10000))
I'd like to create an array such that Cell $A$1 has all the criterias, for example:
ISNUMBER(SEARCH("apple",$a$8:$a$10000))*ISNUMBER(SEARCH("banana",$a$8:$a$10000))+ISNUMBER(SEARCH("kiwi",$a$8:$a$10000))
So the function
=+SUM(IF(Txt2F($A$1),Executive!$B$8:$B$10000)) is 71
Anyone here has any ideas or alternatives? I need an evaluation function that works in complex formulas within an array. Furthermore, this should also cover all sort of statistical functions like Percentile and Average.
Thanks.
RichardSchollar
07-03-2007, 01:02 AM
Hi
I must admit I'm failing to see why you want to use a UDF for this - especially one you've declared as Volatile. Is there some reason you don't simply want to use a worksheet formula?
Best regards
Richard
Bob Phillips
07-03-2007, 01:13 AM
Because he wants to dynamically change the formula. It has been asked many times before and I don't think this type of flexibilty is easily achieved. If limits are set, you can write code, but the limits become self-defeating.
wezred
07-03-2007, 03:11 AM
I would use a simple worksheet formula but it'd be too long and not dynamic enough.
Something like a DAverage formula would be good but I can't seem to find anything similar to a DPercentile.
Anyone has UDF for DPercentile?
Bob Phillips
07-03-2007, 04:27 AM
Not sure how good it is, but here is one.
target is the range to get the percentile of, percentile is ... well it is the percentile value, data is the data range including the headings, and criteria is the criteria range including headings
Public Function DPERCENTILE(target As Range, percentile As Double, data As Range, criteria As Range)
Dim aryRanges
Dim sFormula As String
Dim rng As Range
Dim i As Long, j As Long
Dim icol As Long, j1 As Long
ReDim aryRanges(1 To criteria.Columns.Count)
For i = 1 To criteria.Columns.Count
icol = Application.Match(criteria.Cells(1, i), data.Rows(1).Cells, 0)
Set rng = data.Cells(2, icol).Resize(data.Rows.Count - 1)
aryRanges(i) = rng.Address
Next i
ReDim aryCriteria(1 To criteria.Rows.Count - 1)
sFormula = "=PERCENTILE(IF("
For i = 1 To criteria.Rows.Count - 1
sFormula = sFormula & "((" & aryRanges(1) & "=""" & criteria.Cells(i + 1, 1).Value & """)"
For j = 1 To criteria.Columns.Count - 1
If criteria.Cells(i + 1, j + 1).Value <> "" Then
If ((Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "<" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = ">" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 1))) Or _
((Left$(criteria.Cells(i + 1, j + 1).Value, 2) = "<=" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 2) = ">=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 2))) Then
sFormula = sFormula & "*(" & aryRanges(j + 1) & criteria.Cells(i + 1, j + 1).Value & ")"
Else
sFormula = sFormula & "*(" & aryRanges(j + 1) & "=""" & criteria.Cells(i + 1, j + 1).Value & """)"
End If
End If
Next j
sFormula = sFormula & ")+"
Next i
sFormula = Left$(sFormula, Len(sFormula) - 1) & "," & target.Address & ")," & percentile & ")"
DPERCENTILE = Evaluate(sFormula)
End Function
Bob Phillips
07-03-2007, 04:37 AM
This may be better, more consistent with the DBASE functions.
You would call it like so
=DPERCENTILE(A4:E10,"Height",30%,A1:F3)
where I have used the help example
Public Function DPERCENTILE(data As Range, field As String, percentile As Double, criteria As Range)
Dim aryRanges
Dim target As Range
Dim sFormula As String
Dim rng As Range
Dim i As Long, j As Long
Dim icol As Long, j1 As Long
'get the target field range
icol = Application.Match(field, data.Rows(1).Cells, 0)
Set target = data.Cells(2, icol).Resize(data.Rows.Count - 1)
'build an array of ranges for each criteria
ReDim aryRanges(1 To criteria.Columns.Count)
For i = 1 To criteria.Columns.Count
icol = Application.Match(criteria.Cells(1, i), data.Rows(1).Cells, 0)
Set rng = data.Cells(2, icol).Resize(data.Rows.Count - 1)
aryRanges(i) = rng.Address
Next i
'build a conditional formula from the criteria
ReDim aryCriteria(1 To criteria.Rows.Count - 1)
sFormula = "=PERCENTILE(IF("
For i = 1 To criteria.Rows.Count - 1
sFormula = sFormula & "((" & aryRanges(1) & "=""" & criteria.Cells(i + 1, 1).Value & """)"
For j = 1 To criteria.Columns.Count - 1
If criteria.Cells(i + 1, j + 1).Value <> "" Then
If ((Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "<" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = ">" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 1))) Or _
((Left$(criteria.Cells(i + 1, j + 1).Value, 2) = "<=" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 2) = ">=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 2))) Then
sFormula = sFormula & "*(" & aryRanges(j + 1) & criteria.Cells(i + 1, j + 1).Value & ")"
Else
sFormula = sFormula & "*(" & aryRanges(j + 1) & "=""" & criteria.Cells(i + 1, j + 1).Value & """)"
End If
End If
Next j
sFormula = sFormula & ")+"
Next i
sFormula = Left$(sFormula, Len(sFormula) - 1) & "," & target.Address & ")," & percentile & ")"
DPERCENTILE = Evaluate(sFormula)
End Function
wezred
07-03-2007, 08:33 AM
Thanks xld for this UDF.
This works fine if the UDF is used in the same worksheet. However, I get a #NUM! when i use this UDF in another worksheet.
I suspect it is the referencing to "data As Range". Note that the reference to "criteria As Range" works even on a different worksheet.
Can't seem to figure out the culprit. Please help!!
Bob Phillips
07-03-2007, 12:50 PM
I had thought about that when testing it, but forget to test for it.
Public Function DPERCENTILE(data As Range, field As String, percentile As Double, criteria As Range)
Dim aryRanges
Dim Target As Range
Dim sFormula As String
Dim rng As Range
Dim i As Long, j As Long
Dim icol As Long, j1 As Long
'get the target field range
icol = Application.Match(field, data.Rows(1).Cells, 0)
Set Target = data.Cells(2, icol).Resize(data.Rows.Count - 1)
'build an array of ranges for each criteria
ReDim aryRanges(1 To criteria.Columns.Count)
For i = 1 To criteria.Columns.Count
icol = Application.Match(criteria.Cells(1, i), data.Rows(1).Cells, 0)
Set rng = data.Cells(2, icol).Resize(data.Rows.Count - 1)
aryRanges(i) = rng.Address(, , , True)
Next i
'build a conditional formula from the criteria
ReDim aryCriteria(1 To criteria.Rows.Count - 1)
sFormula = "=PERCENTILE(IF("
For i = 1 To criteria.Rows.Count - 1
sFormula = sFormula & "((" & aryRanges(1) & "=""" & criteria.Cells(i + 1, 1).Value & """)"
For j = 1 To criteria.Columns.Count - 1
If criteria.Cells(i + 1, j + 1).Value <> "" Then
If ((Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "<" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = ">" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 1) = "=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 1))) Or _
((Left$(criteria.Cells(i + 1, j + 1).Value, 2) = "<=" Or _
Left$(criteria.Cells(i + 1, j + 1).Value, 2) = ">=") And _
IsNumeric(Right$(criteria.Cells(i + 1, j + 1).Value, Len(criteria.Cells(i + 1, j + 1).Value) - 2))) Then
sFormula = sFormula & "*(" & aryRanges(j + 1) & criteria.Cells(i + 1, j + 1).Value & ")"
Else
sFormula = sFormula & "*(" & aryRanges(j + 1) & "=""" & criteria.Cells(i + 1, j + 1).Value & """)"
End If
End If
Next j
sFormula = sFormula & ")+"
Next i
sFormula = Left$(sFormula, Len(sFormula) - 1) & "," & Target.Address(, , , True) & ")," & percentile & ")"
DPERCENTILE = Evaluate(sFormula)
End Function
wezred
07-03-2007, 09:41 PM
Can you take a look at the attached worksheet?
The criteria I'm using doesn't seem to work with this UDF when:
1. Using "<>" and "*" in the criteria to exclude items that contains certain text
2. Having criteria also applied to the field array i.e. "only get the percentile of the count if count is more than 10" does not work
3. Criterias with more than 2 rows are not working
4. Criterias with more than 1 row and 1 column are not working
My guess might be that there are some missing quotation marks and missing if and then statements for "<>".
I was hoping that DPercentile would replicate DAverage but using Percentile.
Thanks for the help so far.
Bob Phillips
07-04-2007, 03:58 AM
This was quickly knocked up and not thoroughly tested (I have no use for such a UDF personally), so I am sure it is anything but complete. This addresses this immediate issue, but don't be surprised if there are more later.
wezred
07-04-2007, 10:47 PM
Thanks. I think i've got the gist of the formula and am able to customize it. However, i think the limitations now lie on the string of the evaluation function. Apparently, VBA is limited to evaluate upto a certain (255?) number of characters. Anything beyond that would give you a #Value.
You've been a great help in this and it's much appreciated.
Here's my VBA code for all the rest to see:
Public Function DPERCENTILE(data As Range, field As String, percentile As Double, criteria As Range)
Dim aryRanges
Dim Target As Range
Dim sFormula As String
Dim rng As Range
Dim i As Long, j As Long
Dim icol As Long, j1 As Long
'get the target field range
icol = Application.Match(field, data.Rows(1).Cells, 0)
Set Target = data.Cells(2, icol).Resize(data.Rows.Count - 1)
'build an array of ranges for each criteria
ReDim aryRanges(1 To criteria.Columns.Count)
For i = 1 To criteria.Columns.Count
icol = Application.Match(criteria.Cells(1, i), data.Rows(1).Cells, 0)
Set rng = data.Cells(2, icol).Resize(data.Rows.Count - 1)
aryRanges(i) = rng.Address(, , , True)
Next i
'build a conditional formula from the criteria
ReDim aryCriteria(1 To criteria.Rows.Count - 1)
sFormula = "=PERCENTILE(IF("
For i = 1 To criteria.Rows.Count - 1
sFormula = sFormula & "(" & EvaluateCriteria(criteria.Cells(i + 1, 1).Value, aryRanges(1))
For j = 2 To criteria.Columns.Count
If criteria.Cells(i + 1, j).Value <> "" Then
sFormula = sFormula & "*" & EvaluateCriteria(criteria.Cells(i + 1, j).Value, aryRanges(j))
End If
Next j
sFormula = sFormula & ")+"
Next i
sFormula = Left$(sFormula, Len(sFormula) - 1) & "," & Target.Address(, , , True) & ")," & percentile & ")"
DPERCENTILE = Evaluate(Replace(sFormula, "[" & ActiveWorkbook.Name & "]", ""))
End Function
Function EvaluateCriteria(ByVal criteria, ByVal rngAddress) As String
'Number type criteria
If ((Left$(criteria, 1) = "<" Or Left$(criteria, 1) = ">" Or Left$(criteria, _
1) = "=") And IsNumeric(Right$(criteria, Len(criteria) - 1))) Or _
((Left$(criteria, 2) = "<=" Or Left$(criteria, 2) = ">=") And _
IsNumeric(Right$(criteria, Len(criteria) - 2))) Or ((Left$(criteria, 2) = _
"<>") And IsNumeric(Right$(criteria, Len(criteria) - 2))) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
'Character type criteria - EXCLUDE
ElseIf ((Left$(criteria, 2) = "<>") And Not IsNumeric(Right$(criteria, _
Len(criteria) - 2))) Then
'Fuzzy Matching - EXCLUDE
If Mid$(criteria, 3, 1) = "*" And Right$(criteria, 1) = "*" Then
EvaluateCriteria = "(NOT(ISNUMBER(SEARCH(""" & Mid$(criteria, 4, _
Len(criteria) - 4) & """," & rngAddress & "))))"
Else
'Exact Matching - EXCLUDE
EvaluateCriteria = "(" & rngAddress & "<>""" & Right$(criteria, _
Len(criteria) - 2) & """)"
End If
'Character type criteria - INCLUDE
'Fuzzy Matching - INCLUDE
ElseIf ((Left$(criteria, 1) = "*") And (Right$(criteria, 1) = "*") And Not _
IsNumeric(Right$(criteria, Len(criteria) - 1))) Then
EvaluateCriteria = "(ISNUMBER(SEARCH(""" & Mid$(criteria, 2, _
Len(criteria) - 2) & """," & rngAddress & ")))"
Else
EvaluateCriteria = "(" & rngAddress & "=""" & criteria & """)"
End If
End Function
Bob Phillips
07-05-2007, 01:06 AM
Probably a better way would be to evaluate all of the criteria building an array of cell references, and then pass that array to the PERCENTILE function.
If I get some time, I might play with that (is that last version of code functionally any different from my last posting?).
wezred
07-05-2007, 04:26 AM
I've made changes EvaluateCriteria to include different criteria types.
Would be interesting to see your result in this regard. Thanks.
Bob Phillips
07-05-2007, 03:42 PM
Here is a first cut of a more resilient version.
It needs tidying up and finalisuing, but I thought I would post it to show you where I was going
Public Function DPERCENTILE(data As Range, field As String, percentile As Double, criteria As Range)
Dim aryRanges
Dim Target As Range
Dim sFormula As String
Dim sTemp As String
Dim rng As Range
Dim i As Long, j As Long
Dim rtn As Long
Dim aryFieldValues
Dim aryRows
Dim iRow As Long
ReDim aryRows(1 To criteria.Rows.Count - 1)
iRow = 0
'get the target field range
icol = Application.Match(field, data.Rows(1).Cells, 0)
Set Target = data.Cells(2, icol).Resize(data.Rows.Count - 1)
'build an array of ranges for each criteria
ReDim aryRanges(1 To criteria.Columns.Count)
For i = 1 To criteria.Columns.Count
icol = Application.Match(criteria.Cells(1, i), data.Rows(1).Cells, 0)
Set rng = data.Cells(2, icol).Resize(data.Rows.Count - 1)
aryRanges(i) = rng.Address(, , , True)
Next i
'build a conditional formula from the criteria
ReDim aryCriteria(1 To criteria.Rows.Count - 1)
For i = 1 To criteria.Rows.Count - 1
sFormula = ""
For j = 1 To criteria.Columns.Count
If criteria.Cells(i + 1, j).Value <> "" Then
sTemp = EvaluateCriteria(criteria.Cells(i + 1, j).Value, aryRanges(j), rtn)
If rtn = -1 Then
DPERCENTILE = CVErr(xlErrRef)
Exit Function
Else
sFormula = sFormula & "*" & sTemp
End If
End If
Next j
sFormula = "=INDEX(" & Right$(sFormula, Len(sFormula) - 1) & _
"*(ROW(" & Target.Address(, , , True) & _
")-ROW(" & Target.Cells(1, 1).Address(, , , True) & ")+1),0)"
sFormula = Replace(sFormula, "[" & ActiveWorkbook.Name & "]", "")
aryFieldValues = Evaluate(sFormula)
iRow = iRow + 1
aryRows(iRow) = aryFieldValues
Next i
DPERCENTILE = Evaluate("=PERCENTILE(" & ExtractFields(Target, aryRows) & "," & percentile & ")")
End Function
Function EvaluateCriteria(ByVal criteria, ByVal rngAddress, rtn As Long) As String
Dim sChar1 As String
Dim sChar2 As String
rtn = 0 'no error is default
sChar1 = Left$(criteria, 1)
sChar2 = Mid$(criteria, 2, 1)
Select Case sChar1
Case "<"
Select Case sChar2
Case "="
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
Case ">"
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
If Mid$(criteria, 3, 1) = "*" And Right$(criteria, 1) = "*" Then
EvaluateCriteria = "(NOT(ISNUMBER(SEARCH(""" & Mid$(criteria, 4, _
Len(criteria) - 4) & """," & rngAddress & "))))"
Else
EvaluateCriteria = "(" & rngAddress & "<>""" & Right$(criteria, _
Len(criteria) - 2) & """)"
End If
End If
Case Else
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
End Select
Case ">"
Select Case sChar2
Case "="
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
Case Else
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
End Select
Case "="
Select Case sChar2
Case ">"
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
Case "<"
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
rtn = -1
End If
Case Else
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
If Mid$(criteria, 2, 1) = "*" And Right$(criteria, 1) = "*" Then
EvaluateCriteria = "(ISNUMBER(SEARCH(""" & Mid$(criteria, 3, _
Len(criteria) - 3) & """," & rngAddress & ")))"
Else
EvaluateCriteria = "(" & rngAddress & "=""" & Right$(criteria, _
Len(criteria) - 1) & """)"
End If
End If
End Select
Case Else
If IsNumeric(Right$(criteria, Len(criteria) - 2)) Then
EvaluateCriteria = "(" & rngAddress & criteria & ")"
Else
EvaluateCriteria = "(" & rngAddress & "=""" & criteria & """)"
End If
End Select
End Function
Private Function ExtractFields(ByVal Target As Range, ByVal RowNums) As String
Dim oCollRows As Collection
Dim aryRows
Dim iRow As Long
Dim iRowNum As Long
Dim item
Dim sArray As String
Set oCollRows = New Collection
On Error Resume Next
For iRow = LBound(RowNums) To UBound(RowNums)
aryRows = RowNums(iRow)
For iRowNum = LBound(aryRows, 1) To UBound(aryRows, 1)
If aryRows(iRowNum, 1) <> 0 Then _
oCollRows.Add CStr(aryRows(iRowNum, 1)), CStr(aryRows(iRowNum, 1))
Next iRowNum
Next iRow
On Error GoTo 0
ReDim ary(1 To 1)
iRow = 1
sArray = "{"
For Each item In oCollRows
ReDim Preserve ary(1 To iRow)
sArray = sArray & Target.Cells(oCollRows.item(iRow), 1) & ","
iRow = iRow + 1
Next item
sArray = Left$(sArray, Len(sArray) - 1) & "}"
Set oCollRows = Nothing
ExtractFields = sArray
End Function
Powered by vBulletin® Version 4.2.5 Copyright © 2024 vBulletin Solutions Inc. All rights reserved.