UDF returns the same value everywhere - vba

I am trying to code in moving average in vba but the following returns the same value everywhere.
Function trial1(a As Integer) As Variant
Application.Volatile
Dim rng As Range
Set rng = Range(Cells(ActiveCell.Row, 2), Cells(ActiveCell.Row - a + 1, 2))
trial1 = (Application.Sum(rng)) * (1 / a)
End Function

The ActiveCell property does not belong in a UDF because it changes. Sometimes, it is not even on the same worksheet.
If you need to refer to the cell in which the custom UDF function resides on the worksheet, use the Application.Caller method. The Range.Parent property can be used to explicitly identify the worksheet (and avoid further confusion) in a With ... End With statement.
Function trial1(a As Integer) As Variant
Application.Volatile
Dim rng As Range
with Application.Caller.Parent
Set rng = .Range(.Cells(Application.Caller.Row, 2), _
.Cells(Application.Caller.Row - a + 1, 2))
trial1 = (Application.Sum(rng)) * (1 / a)
end with
End Function
You've applied the Application.Volatile¹ method but allowed the range to be averaged to default to the ActiveSheet property by not explcitly specifying the parent worksheet.
The average is computed with the Excel Application object returning a SUM function's result and some maths. The same could have been returned in one command with the worksheet's AVERAGE function but blank cells would be handled differently.
trial1 = Application.Average(rng)
¹ Volatile functions recalculate whenever anything in the entire workbook changes, not just when something that affects their outcome changes.

It's kind of strange to me for a UDF to calculate moving average given a number. If this UDF is to be used within the Worksheet, I believe you would put it next to existing data and if you want to change the size of the range for average amount, you update them manually?
Assuming you can name a Range "MovingAverageSize" to store the size of the range to calculate the average, and the average amount on the right of the existing data, consider below:
Range C2 is named MovingAverageSize
Data stored from B3 and downwards
Moving Average result is stored 1 column on the right of the data
If the data is less than MovingAverageSize, the SUM function adjusts accordingly
Any calculation error occurs with result in zero
Every time MovingAverageSize changes value, it triggers a Sub to update the formulas (Codes are placed in the Worksheet object rather than normal Module)
Alternatively, you can change the code to place the MovingAverage to same column of the MovingAverageSize, so you can have a few different size comparing next to each other.
Code in Worksheet Object:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Count = 1 Then
If Target.Address = ThisWorkbook.Names("MovingAverageSize").RefersToRange.Address Then UpdateMovingAverage Target
End If
End Sub
Private Sub UpdateMovingAverage(ByRef Target As Range)
Dim oRngData As Range, oRng As Range, lSize As Long, lStartRow As Long
Debug.Print "UpdateMovingAverage(" & Target.Address & ")"
If IsNumeric(Target) Then
lSize = CLng(Target.Value)
If lSize <= 0 Then
MsgBox "Moving Average Window Size cannot be zero or less!", vbExclamation + vbOKOnly
Else
' Top Data range is "B3"
Set oRngData = Target.Parent.Cells(3, "B") ' <-- Change to match your top data cell
lStartRow = oRngData.Row
' Set the Range to last row on the same column
Set oRngData = Range(oRngData, Cells(Rows.Count, oRngData.Column).End(xlUp))
Application.EnableEvents = False
For Each oRng In oRngData
If (oRng.Row - lSize) < lStartRow Then
oRng.Offset(0, 1).FormulaR1C1 = "=iferror(sum(R[" & lStartRow - oRng.Row & "]C[-1]:RC[-1])/MovingAverageSize,0)"
Else
oRng.Offset(0, 1).FormulaR1C1 = "=iferror(sum(R[" & 1 - lSize & "]C[-1]:RC[-1])/MovingAverageSize,0)"
End If
Next
Application.EnableEvents = True
Set oRngData = Nothing
End If
End If
End Sub
Sample data and screenshots

I believe that Application.ActiveCell is not what you should be using here.
Application.ThisCell would be more appropriate assuming that "a" is the size of the subset and that the dataset is 1 column on the right.
Moreover, I would simply use "WorksheetFunction.Average" instead of "Application.Sum" and I would add "Application.Volatile" so the average is recalculated whenever an update occurs on the worksheet.
So one solution to your issue would be:
Public Function Trial1(a As Integer) As Variant
Application.Volatile
Trial1 = WorksheetFunction.Average(Application.ThisCell(1, 2).Resize(a))
End Function
Another solution here would be to use an array formula entered with Control/Shift/Enter:
Public Function MovAvg(dataset As Range, subsetSize As Integer)
Dim result(), subset As Range, i As Long
ReDim result(1 To dataset.Rows.count, 1 To 1)
Set subset = dataset.Resize(subsetSize)
For i = 1 To dataset.Rows.count
result(i, 1) = WorksheetFunction.Average(subset.offset(i - 1))
Next
MovAvg = result
End Function
And to use this array function:
Select the range where all the results will be written (should be the size of your dataset)
Type "=MovAvg(A1:A100, 2)" where A1:A100 is the source of the data and 2 the size of the subset
Press Ctrl+Shift+Enter

A UDF should only access a range when it is passed as a parameter.
Also, you should eliminate Application.Volatile because (1) your calculation is deterministic and not volatile, (2) Excel will re-calculate automatically your UDF whenever any cell in the input range changes, and (3) because the 'volatile' attribute in a UDF can make a model very slow so it should avoided when not necessary.
So, for a moving average, the correct formula is:
Public Function SpecialMovingAverage(Rng as Excel.Range) As Double
Dim denominator as Integer
denominator = Rng.Cells.Count
if Denominator = 0 then SpecialMovingAverage = 0: exit function
' write your special moving average logic below
SpecialMovingAverage = WorksheetFunction.Average(Rng)
End Function
Note: I changed the answer following two comments because I initially did not see that the question was after a moving average (maybe the question was changed after my answer, or I initially missed the UDF's stated objective).

I believe
Your trial1() function is in one or more cells, as a part of a formula or by itself
You want those cells to be recalculated whenever the user changes any cell on the worksheet
For this, you'd need to identify the cell where the change happened. This cell is not given by
A. ActiveCell - because that is the cell the cursor is on when the calculation starts; it could be anywhere but not on the cell that was changed
B. Application.ThisCell - because that returns the cell in which the user-defined function is being called from, not the cell that changed
The cell where the change happened is passed to the Worksheet's Change event. That event is triggered with an argument of type Range - the range that changed. You can use that argument to identify the cell(s) that changed and pass that to trial1(), possibly through a global variable (yeah, I know).
I tried this in a worksheet and it works, so let me know your results.

Related

How to sum with arguments in vba?

I have the following code:
Option Explicit
Function SumAboveV(column As Range)
Worksheets("Sheet16").Activate
Dim r As Range, rAbove As Range
Dim wf As WorksheetFunction
Set wf = Application.WorksheetFunction
Set r = zelle.Offset(0, -2)
Set rAbove = Range(r.Offset(-1, 0), Cells(2, r.column))
column.Value = wf.Sum(rAbove)
End Function
All I want to have is to sum up all numbers I have in row B until the cell where I place the function so e.g. as you see in the attached screenshot I want to sum up all values from B2 until that cell and place the total value in row D.
I don't understand my mistake. Any ideas?
Like
Public Function SumUntil(ByRef rng As Range) As Double
SumUntil = Application.WorksheetFunction.Sum(Range(Cells(2, rng.Column), rng))
End Function
Usage:
You add an optional start row for summing with
Public Function SumUntil(ByRef rng As Range, Optional ByVal startRow As Long = 3) As Double
SumUntil = Application.WorksheetFunction.Sum(Range(Cells(startRow, rng.Column), rng))
End Function
I might be missing something, but why not have just use sum in column D? if you use "=Sum($B2:B3)" Then drag that formula down to wherever. Then every cell in D would have a sum up until that row. It may be simpler than writing a whole macro for it.
You could use the formula:
=SUM($B$1:INDEX($B:$B,ROW()-1))
Placed in cell B367 it returns the sum of B1:B366 and doesn't give a circular reference warning.
Instead of creating Range variables and all those manipulations, you can rely on R1C1 notation. Using R1C1 makes reference dynamic - no matter where you insert this formula in column D, it will get correct address. You just won't have to do extra manipulations.
zelle.FormulaR1C1 = "=SUM(RC[-2]:R2C[-2])"
All I want to have is to sum up all numbers I have in row B until the
cell where I place the function
then go this way:
Public Function SumAboveV() As Double
SumAboveV = Application.WorksheetFunction.Sum(Range("B2", Cells(Application.Caller.Row, "B")))
End Function
and you don't need to pass any parameter to your function, since Application.Caller.Row will get the row index of the "calling" cell
if you want it to update at any sheet recalculation then add Application.Volatile statement
Public Function SumAboveV() As Double
Application.Volatile
SumAboveV = Application.WorksheetFunction.Sum(Range("B2", Cells(Application.Caller.Row, "B")))
End Function

Making a CountRows function in Excel

I am trying to make a simple countRows function that will count the number of cells I have in a dynamic range. Basically if I have values in cells, say B2:B500, the count would return 499. However next time around values are in cell B2:B501, the count would return 500. But you wouldn't have to do anything to the cell in which you typed in the formula.
I thought if I reference the cell as a Variant, then any value could be accepted. Then find the Address of that cell and return the Count of a Range. But I get a #Value error.
Public Function countRows(startRange As Variant)
Dim rng As Range
Set rng = startRange.Address
If IsEmpty(Range(rng, rng.End(xlDown))) = True Then
countRows = 1
Else
countRows = Range(rng, rng.End(xlDown)).Rows.Count
End If
End Function
This is the code I have used for many years successfully under many different worksheets. It handles many cells, singular cells or empty cells.
Public Function CountRows(ByRef r As Range) As Long
If IsEmpty(r) Then
CountRows = 0
ElseIf IsEmpty(r.Offset(1, 0)) Then
CountRows = 1
Else
CountRows = r.Worksheet.Range(r, r.End(xlDown)).Rows.count
End If
End Function
Public Function CountCols(ByRef r As Range) As Long
If IsEmpty(r) Then
CountCols = 0
ElseIf IsEmpty(r.Offset(0, 1)) Then
CountCols = 1
Else
CountCols = r.Worksheet.Range(r, r.End(xlToRight)).Columns.count
End If
End Function
It's not entirely clear what you are looking for, when you mentioned there are values in cells "B2:B500" and the count should return 499, as there could be a few possible scenarios:
You simply want to count the rows in the range "B2:B500". The code will be:
Range("B2:B500").Rows.Count
You want to count the non-blank cells in the range "B2:B500". In that case, as suggested in the comments:
WorksheetFunction.CountA(Range("B2:B500"))
As indicated in your code rng.End(xlDown), you probably want to the count continuous non-blank cells starting with the range "B2" in the overall range "B2:B500". You may create a function like this:
Public Function countRows(rng As Range) As Long
Dim rw As Range
For Each rw In rng
If IsEmpty(rw) Then Exit For
countRows = countRows + 1
Next
End Function
Clarification:
Based on subsequent comments, I thought it's worth explaining why the variable "countRows" wasn't initialized by adding a line countRows = 0.
Certain programming languages like assembly language, C, C++ require explicit initialization. This was intentionally so designed due to the philosophy in which conflicts between performance and safety were generally resolved in favor of performance.
However, such is not the case with other programming languages like VBA or Java.
Speaking about VBA, during macro run, all the variables are initialized to a value. A numeric variable is initialized to zero, a variable length string is initialized to a zero-length string (""), and a fixed length string is filled with the ASCII code 0. Variant variables are initialized to Empty. An Empty variable is represented by a zero in a numeric context and a zero-length string ("") in a string context.
Therefore a separate line of code countRows = 0 wasn't added in the above code block.
While coding, one need to keep this in perspective as the same might not be true for other languages.

Passing an Array or Range through a function in VBA

So I want to make a basic function that takes an average of values that I highlight in Excel. I am well aware there is already a built-in function in Excel for this but I am trying to make one as practice.
My problem is I am not sure how to pass a range and then call on specific elements in the Range.
Below is the pseudo code I've been playing around with. I understand it may be horribly written. I am a beginner and I just want to get some practice.
Function averagetest(range As Range) '<------(Is this how I pass a Range into a function?)
Dim N as Integer
Dim i as Integer
Dim average as Double
average = 0
N = LengthofRange '<--------- (Is there a way to get the length of the
range like UBound or LBound for an array?)
Do Until i = LengthofRange
average = average + Range(i, i+1) '<--------(Is this how you call a
specific element in the range? I'm just adding every element in the
Range)
i = i + 1
Loop
average = average/N
End Function
You can't assume a Range is going to be contiguous, nor can you assume a Range is going to be horizontal, nor vertical.
A Range is a collection of objects, so you iterate it with a For Each loop for optimal performance.
Assuming the function is meant to be used as a UDF worksheet function, and therefore is defined in a standard module (.bas):
Public Function AverageTest(ByVal target As Range) As Variant
Dim total As Double
Dim count As Double
Dim cell As Range
For Each cell In target
If IsNumeric(cell.Value) Then
total = total + cell.Value
count = count + 1
'Else
' AverageTest = CVErr(xlErrValue)
' Exit Function
End If
Next
If count = 0 Then
AverageTest = CVErr(xlErrDiv0)
Else
AverageTest = total / count
End If
End Function
Note:
Parameter is passed ByVal, and isn't named after an existing type (Range); we don't need a reference to the range pointer, a copy of it is good enough.
Function is explicitly Public, and has an explicit return type (Variant).
Function returns a Variant, so as to return a Double result in the "happy path", or an appropriate Error value (#Div/0!) when applicable.
Function is only counting numeric cells, which means it works even if the target range contains error values. The commented-out code would bail out and return a #VALUE! error if a non-numeric value is encountered.
How you "pass the range" is the caller's problem. There are many ways you can do this - from an Excel formula:
=AverageTest(A1:A10)
=AverageTest(A1:B12,F4:L4)
You can also use it in VBA code:
foo = Module1.AverageTest(ActiveSheet.Range("A1:D10"))
Do not use range as a variable.
Then you can use rows.Count or Columns.Count to get the extent
Function averagetest(rng As Range)
Dim N as Integer
Dim i as Integer
Dim average as Double
average = 0
N = rng.rows.count
For i = 1 to N 'use For loop
average = average + rng.cells(i,1)'Cells will work here
Next i
averagetest= average/N
End Function
Or you can do this -- there's not really any need to iterate over the count of cells, when you can just iterate over Each cell in the rng.Cells collection. I would also change the variable name from average (which is misleading) to something a bit more descriptive, like total:
Option Explicit
Function averagetest(rng As Range)
Dim cl As Range
Dim total As Double
For Each cl In rng.Cells
total = total + cl.Value
Next
averagetest = total / rng.Cells.Count
End Function
As a bonus, this latter method would work on a 2-dimensional range as well.
Note that this will treat empty cells as 0-values (the AVERAGE worksheet function ignores empty cells, so your results may vary) and it will raise an error if there are non-numeric values in the range.

Simple moving average range in Excel-VBA

This code is just to calculate simple moving average. Opened an excel, created dummy array in C row from 1 to 20. I want to create a function for eg: SMA(C7,3) = which should give average of C5:C7.
Coming back to VBA after long time, not able to figure whats the error in the below code.
Function sma1(rng As Range, N As Integer)
Set rng = rng.Resize(-N + 1, 0)
sma1 = Application.WorksheetFunction.average(rng)
End Function
avoid using a cell name as a function
fixed the RESIZE()
used an internal range variable
Function smal(rng As Range, N As Integer) As Variant
Dim rng2 As Range
Set rng2 = rng.Resize(N, 1)
smal = Application.WorksheetFunction.Average(rng2)
End Function
EDIT#1:
Based on Scott's comment:
Function smal(rng As Range, N As Integer) As Variant
Dim rng2 As Range
Set rng2 = rng.Offset(1 - N, 0).Resize(N, 1)
smal = Application.WorksheetFunction.Average(rng2)
End Function
I assume you want the column along side it to give you're SMA (as shown below?):
If so, the below will do it and drag it autocomplete it to the bottom of you column C array:
Sub SMA3()
Range("D7").FormulaR1C1 = "=AVERAGE(R[-2]C[-1]:RC[-1])" 'This is a relative reference (left one cell and up two cells) - This give your three inputs
Range("D7").AutoFill Destination:=Range("D7:D" & Range("C1048576").End(xlUp).Row) 'Autofills the SMA
End Sub
Just an FYI this can be done with existing formula:
=IF(ROW(C1)<$E$1,"",AVERAGE(INDEX(C:C,ROW(C1)-$E$1+1):C1))
E1 contains the number of rows to include.

How do I get the cell value from a formula in Excel using VBA?

I have a formula in a range of cells in a worksheet which evaluate to numerical values. How do I get the numerical values in VBA from a range passed into a function?
Let's say the first 10 rows of column A in a worksheet contain rand() and I am passing that as an argument to my function...
public Function X(data as Range) as double
for c in data.Cells
c.Value 'This is always Empty
c.Value2 'This is always Empty
c.Formula 'This contains RAND()
next
end Function
I call the function from a cell...
=X(a1:a10)
How do I get at the cell value, e.g. 0.62933645?
Excel 2003, VB6
The following code works for me when running from VBA (Excel 2003):
Public Function X(data As Range) As Double
For Each c In data.Cells
a = c.Value 'This works
b = c.Value2 'This works too (same value)
f = c.Formula 'This contains =RAND()
Next
End Function
a and b are the same and equal what I'm passing in (which is a range of cells with Rand() in them). I'm not sure what else is going on here.
Aha! You need to set X, no? I'm not sure what exactly you expect this function to do, but you need to set X (the name of the function) to the value you want returned. Add this line:
X = a
I can't replicate a problem using the layout you posted. I noticed a few syntax errors in your posted code (ie: "for" should be "for each"). But when I put =RAND() in A1:A10 and =X(A1:A10) I got a return just fine with this:
Public Function X(data As Range) As Double
Dim c As Excel.Range
Dim sum As Double
For Each c In data.Cells
sum = sum + c.Value
Next
X = sum
End Function
However, just to a expand a little more on a few of the other questions you brushed up against. You can evaluate a formula for a result like so:
Public Function X(data As Range) As Double
Dim c As Excel.Range
Dim sum As Double
For Each c In data.Cells
sum = sum + Excel.Evaluate(c.Formula)
Next
X = sum
End Function
But generally speaking you won't want to, as this is basically calculating the same value twice.
Make sure you do a calculate before requesting the value.
To Speed up macros something like the following is often preformed..
'Set Reasonable default
Application.CutCopyMode = False
Application.ScreenUpdating = False
Application.Interactive = False
Application.Calculation = xlCalculationManual
in this state you must force calculation before the value will be available.
Public Function X(data As Range) As Double
'You may need the following as well
'Application.Calculate
Dim c As Range
For Each c In data.Cells
c.Calculate
c.Value 'This is now has a value
c.Value2 'This is now has a value
c.Formula 'This contains RAND()
Next
End Function