I have come across a situation which required me to average the result of an array of Vlookups. I had no idea how to achieve this with formulas and it seemed like nobody else on StackOverflow had any idea either.
So I decided to write a function to do the job for me. Unfortunately it returns the "#VALUE!" error and I have no idea why! The function works fine when tested with a msgbox. I have annotated my code below:
Option Explicit
Public Function AvgVlookup(Target_Array As String, Lookup_Array As String, Column_Index As Long) As Double
Dim Result As Double
Dim Total As Double
Dim Counter As Long
Dim TargetRange As Range
Dim LookupRange As Range
Dim Cell As Range
' Remove Absolute Indicator
Target_Array = Replace(Target_Array, "$", "")
Lookup_Array = Replace(Lookup_Array, "$", "")
' Convert String to Range
Set TargetRange = Range(Left(Target_Array, InStr(1, Target_Array, ":") - 1), Mid(Target_Array, InStr(1, Target_Array, ":") + 1))
Set LookupRange = Range(Left(Lookup_Array, InStr(1, Lookup_Array, ":") - 1), Mid(Lookup_Array, InStr(1, Lookup_Array, ":") + 1))
' Set Variables to 0
Counter = 0
Total = 0
' For each cell in defined array
For Each Cell In TargetRange
' Vlookup the cell and save lookup value to Result variable
Result = Application.WorksheetFunction.vlookup(Cell, LookupRange, Column_Index, "False")
' Update variables used to calculate average
Total = Total + Result
Counter = Counter + 1
Next Cell
' Perform calculation
AvgVlookup = Total / Counter
End Function
Sub test()
MsgBox AvgVlookup("A5:A8", "G5:H8", 2)
End Sub
Any ideas?
Thanks!
Two things:
First, the way you are setting your ranges are a little long, it can be truncated to simply:
Set TargetRange = Range(Target_Array)
No need to parse the strings after removing the $.
Second, you need to put in an error check in case one of the values in the target range is not in the lookup range.
The whole code:
Public Function AvgVlookup(Target_Array As String, Lookup_Array As String, Column_Index As Long) As Double
Dim Total As Double
Dim Counter As Long
Dim TargetRange As Range
Dim LookupRange As Range
Dim Cell As Range
' Remove Absolute Indicator
Target_Array = Replace(Target_Array, "$", "")
Lookup_Array = Replace(Lookup_Array, "$", "")
' Convert String to Range
Set TargetRange = Range(Target_Array)
Set LookupRange = Range(Lookup_Array)
' Set Variables to 0
Counter = 0
Total = 0
' For each cell in defined array
For Each Cell In TargetRange
' Vlookup the cell and save lookup value to Result variable
Dim Result
Result = Application.VLookup(Cell, LookupRange, Column_Index, "False")
If IsNumeric(Result) Then
Total = Total + Result
Counter = Counter + 1
End If
Next Cell
' Perform calculation
AvgVlookup = Total / Counter
End Function
With the above function to call from the worksheet you would need to call it like this: =AvgVlookup("A5:A8", "G5:H8", 2)
But that is not very helpful. If you change your inputs to ranges:
Public Function AvgVlookup(TargetRange As Range, LookupRange As Range, Column_Index As Long) As Double
Dim Result As Double
Dim Total As Double
Dim Counter As Long
Dim Cell As Range
' Set Variables to 0
Counter = 0
Total = 0
' For each cell in defined array
For Each Cell In TargetRange
' Vlookup the cell and save lookup value to Result variable
Dim t
t = Application.VLookup(Cell, LookupRange, Column_Index, "False")
If IsNumeric(t) Then
Total = Total + t
Counter = Counter + 1
End If
Next Cell
' Perform calculation
AvgVlookup = Total / Counter
End Function
Then you would call it simply, =AvgVlookup($A$5:$A$8,$G$5:$H$8,2). This way you can just highlight the correct ranges and it will work. Also less typing trying to convert a string to a range when what you want to enter is a range.
Related
I want to use a VBA function ScopeSum() in an Excel table, such function is checking the "1" values on the same row & then sum relevant header's values.
"SopeH" is named header range.
I've to use this function on the same column (column "P" for the below example) for 100's of rows.
If I copy the function cell & fill all rows the result is as the first cell, but if I edit it, it works fine.
Function ScopeSum() As String
Dim i As Integer
Dim j As Long
Dim rng As Range
Dim cur_rng As Range
Dim ScopeText As String
Dim cell As Variant
Set rng = Range("ScopeH")
j = Range(ActiveCell.Address).Row
Set cur_rng = Range("ScopeH").Offset(j - 2, 0)
i = 0
ScopeText = ""
For Each cell In cur_rng.Cells
i = i + 1
If UCase(cell.Value) = 1 Then ScopeText = ScopeText & ", " & Application.WorksheetFunction.Index(rng, 1, i)
Next
ScopeSum = ScopeText
End Function
Excel Table
After refreshing the page
Make sure you submit the data and header ranges as parameters, so the UDF (user defined function) works for any data range and depends on the data range. Otherwise your formula would not update automatically if the data changes.
Option Explicit
Public Function ScopeSum(ByVal DataRange As Range, ByVal HeaderRange As Range) As String
Dim Data() As Variant ' read data into array
Data = DataRange.Value
Dim Header() As Variant ' read header into array
Header = HeaderRange.Value
Dim Result As String ' collect results for output here
Dim iCol As Long
For iCol = 1 To UBound(Data, 2) ' loop through data and concatenate headers
If Data(1, iCol) = 1 Then
Result = Result & IIf(Result <> vbNullString, ", ", vbNullString) & Header(1, iCol)
End If
Next iCol
ScopeSum = Result ' output results
End Function
Then use the following formula in cell P3:
=ScopeSum(Q3:Z3,$Q$2:$Z$2)
Make sure the header is fixed with the $ signs in the formula. And copy it down:
This has the advantage that you never need to change the code, even if the ranges change. Also you could easily add an Item 11 without changing the code by just adjusting the ranges in the formula.
I have this Excel VBA function:
Function Positives(Rng As Range) As Range
Dim cell As Range, out As Range
For Each cell In Rng
If cell > 0 Then
If Not out Is Nothing Then
Set out = Union(out, cell)
Else
Set out = cell
End If
End If
Next cell
Set Positives = out
End Function
Why doesn't it work well when there are non-sequential positive numbers in Rng range? for example with values 5, 6, 7, -3, 4, 5 but values 5, 6, 7, -3, -4, -5 it works.
Thank you for your cooperation.
There is a limit for excel to display not consecutive range value using array function, see the comparison below
With consecutive range:
When it is not consecutive range
In your VBA code, when the result is in consecutive range, you will have no issue to display in array formula
When the result is not consecutive range, it return #Value as you mention, to solve this problem, one of the method is to change the result to display in string, hope you find it useful :)
Function Positives1(Rng As Range) As String
Dim cell As Range, out As Range, outCell As Range
Dim result As String
For Each cell In Rng.Cells
If cell.Value > 0 Then
If Not out Is Nothing Then
Set out = Union(out, cell)
Else
Set out = cell
End If
End If
Next cell
result = ""
For Each outCell In out.Cells
If result <> "" Then
result = result & "," & outCell.Value
Else
result = outCell.Value
End If
Next
Positives1 = result
End Function
if you want to return multiple values to multiple cells from a UDF to a sheet, you can do this using an array. Note that the best result will be when the dimensions of the call area and the return array match.
Option Explicit
Function Positives(Rng As Range) As Variant
Dim cell As Range, out As Variant, i As Long
ReDim out(1 To Rng.Cells.Count, 1 To 1) ' form the vertical array
For i = 1 To UBound(out, 1)
out(i, 1) = "no value" ' initial fill of the array
Next
i = 0
For Each cell In Rng
If cell > 0 Then
i = i + 1
out(i, 1) = cell
End If
Next cell
Positives = out
End Function
I'm trying to resize a table located in the middle of my sheet. Code snippet - most integers you're seeing are actually variables, but there's no sense in having a huge amount of extra code.
Sub StackOverFlowTest()
destSheet = Thisworkbook.Sheets("Test")
Set DestTb = destSheet.ListObjects("CTROutputTable")
DestTb.Resize DestTb.Range.Resize(100+1,5)
End Sub
I have a table, DestTb, of N rows and 5 columns. I'd like it to turn into 100 rows + a header column. It's located at an unknown (Read: Dynamic) location in my sheet. Moving data into it directly isn't auto-expanding the table, so I need to resize the table first.
How can I easily resize the number of rows in a table?
Full code so far, if you're really interested:
https://gist.github.com/OlivierHJ/5b039a8c5da05d137f5c8d00f6108309
This is an updated answer because OP has a dynamic table. That means the table won't be always in same range, so we need to get the address of ListObject everytime.
For this code, I needed two extra functions to locate the where is the table every time. 1 UDF to extract the text part of an address and 1 UDF to extract the number part of Address.
Sub RESIZZE_TABLE()
Dim DestTb As ListObject
Set DestTb = Sheets("Hoja1").ListObjects("Tabla1")
Dim TotalRows As Long
Dim MyFirstCell As String
Dim MyLastCol As String
Dim MyLastRow As String
Dim ColCount As Integer
ColCount = 5 'number of columns in your table
MyFirstCell = Range(DestTb).Cells(0, 1).Address
TotalRows = (DestTb.DataBodyRange.Count / ColCount) 'how many rows got table
MyLastCol = TextOnly(Range(DestTb).Cells(TotalRows, ColCount).Address) 'we get letter of last column of table
MyLastRow = onlyDigits(Range(DestTb).Cells(TotalRows, ColCount).Address) 'we get number of last row of table
DestTb.Resize Range(MyFirstCell & ":" & MyLastCol & (MyLastRow + 2)) 'change 2 by number of rows you want to increase
End Sub
Private Function TextOnly(ByVal xValue As String) As String
'source: https://www.extendoffice.com/documents/excel/1625-excel-extract-text-from-alphanumeric-string.html
'adapted for SO
Dim OutValue As String
Dim xIndex As Byte
For xIndex = 1 To Len(xValue)
If Not IsNumeric(Mid(xValue, xIndex, 1)) Then
OutValue = OutValue & Mid(xValue, xIndex, 1)
End If
Next
TextOnly = OutValue
End Function
Private Function onlyDigits(s As String) As String
' Variables needed (remember to use "option explicit"). '
Dim retval As String ' This is the return string. '
Dim i As Integer ' Counter for character position. '
' Initialise return string to empty '
retval = ""
' For every character in input string, copy digits to '
' return string. '
For i = 1 To Len(s)
If Mid(s, i, 1) >= "0" And Mid(s, i, 1) <= "9" Then
retval = retval + Mid(s, i, 1)
End If
Next
' Then return the return string. '
onlyDigits = retval
End Function
Tested with a table moving around a worksheet. It did not matter where it was the table, the code always resized the table with no problems.
Hope this works now!
I would imagine that if you are reducing the number of rows you will wish to clear the cells that no longer belong to the table.
Dim iTBLrws As Long, rng As Range, rngOLDBDY As Range
iTBLrws = 100
With Thisworkbook.Sheets("Test").ListObjects("CTROutputTable")
Set rngOLDBDY = .DataBodyRange
.Resize .Range.Cells(1, 1).Resize(iTBLrws, .DataBodyRange.Columns.Count)
If rngOLDBDY.Rows.Count > .DataBodyRange.Rows.Count Then
For Each rng In rngOLDBDY
If Intersect(rng, .DataBodyRange) Is Nothing Then
rng.Clear
End If
Next rng
End If
End With
I'm looking to create a COUNTIF which takes into account the text value and font color of a criteria string. This is what I've come up with so far. It's currently returning a #VALUE! error.
Function test(range As range, criteria As range) As Double
Dim cell As range
Dim criteriaLength As Integer
Dim counter As Integer
counter = 0
For Each cell In range
If InStr(cell.value, criteria.value) > 0 And
cell.Characters(InStr(cell.value, criteria.value),
criteriaLength).Font.Color = criteria.Font.Color Then
counter = counter + 1
End If
Next cell
test = counter
End Function
The cells in the search range each contain characters of different font colors--that's why I'm using InStr and Characters. Any ideas what the issue might be?
You never assign a value to criteriaLength
You need to add:
criteriaLength = Len(criteria)
before the loop.
And the If will try both sides of the And in the if and if the criteria is not in the string the second part will error. You need to nest the if so the second only fires if the first returns True.
Function test(range As range, criteria As range) As Double
Dim cell As range
Dim criteriaLength As Integer
Dim counter As Integer
counter = 0
criteriaLength = Len(criteria)
For Each cell In range
If InStr(cell.Value, criteria.Value) > 0 Then
If cell.Characters(InStr(cell.Value, criteria.Value), criteriaLength).Font.color = criteria.Font.color Then
counter = counter + 1
End If
End If
Next cell
test = counter
End Function
Whenever I try to call the function from a cell by typing =MAE() it won't run, always returns a 0. Can someone help guide me? The function below works fine as a sub procedure. It needs to loop through an entire column and calculate the absolute average
Function MAE() As Double
Dim cell As Object
Dim nRows As Integer
Dim total As Double
Dim i As Integer
Dim mean As Double
total = 0
' Count the rows
With wsData.Range("A2")
nRows = Range(.Offset(0, 0), .End(xlDown)).Rows.Count
End With
'loop through rows, add to total, select next cell
Range("A2").Select
For i = 0 To nRows
total = total + Abs(ActiveCell.Value)
ActiveCell.Offset(1, 0).Select
Next i
MAE = total / (nRows)
End Function
You rarely have to select or activate cells. Never select or activate from a UDF.
Use Application.Volatile so that your function will recalculate as values change.
Function MAE() As Double
Dim cell As Object
Dim rCells As Range
Application.Volatile
With wsData.Range("A2")
Set rCells = Range(.Offset(0, 0), .End(xlDown))
For Each cell In rCells
total = total + Abs(cell.value)
Next cell
End With
MAE = total / rCells.Count
End Function
This might be more useful.
MAE(A2:A7)
Function MAE(rCells As Range) As Double
Dim cell As Object
Application.Volatile
For Each cell In rCells
total = total + Abs(cell.value)
Next cell
MAE = total / rCells.Count
End Function
I would avoid selecting the cells when looping through them.
With the following code it works for me:
...
' Count the rows
With ActiveSheet.Range("A2")
nRows = Range(.Offset(0, 0), .End(xlDown)).Rows.Count
End With
'loop through rows, add to total, select next cell
For i = 0 To nRows
total = total + Abs(ActiveSheet.Range("A2").Offset(i, 0).Value)
Next i
...