Speed up simple user defined functions - vba

User defined functions writen in Excel VBA seem to run much slower than functions simply written in the worksheet cells. Is there a way to run them faster? As an example I have a very simple user defined function:
Function myweekday(mydate As Double)
myweekday = Weekday(mydate)
End Function
Basically it does the same thing as the built in =weekday() function. However running this on 50,000 cells takes about 5 seconds to calculate, whereas simply using the built in function takes a fraction of a second.
What can I do to make user defined functions like this run faster?

You can instead pass in the values as a range and have it return all the values at once in an array. I just tried this on 50,000 rows and it returned all the values instantly. You will need to enter the UDF with CTRL + SHIFT + ENTER.
Function myweekday(mydate As Range) As Variant
Dim vMydate As Variant
Dim vMyWeekDay As Variant
Dim i As Long
vMydate = mydate.Value2
ReDim vMyWeekDay(1 To UBound(vMydate), 1 To 1)
For i = 1 To UBound(vMydate)
vMyWeekDay(i, 1) = Weekday(vMydate(i, 1))
Next i
myweekday = vMyWeekDay
End Function

Related

Considerations when using Function name as a variable

In VBA, you can treat a UDF name just like a Dimmed variable. For example
Function ADD(a, b) As Long
ADD = a
ADD = ADD + b
End Function
Where ADD stores the intermediate value as well as the end result. My question is; in terms of what's going on behind the scenes, is there any difference between storing a number/piece of data in a standard variable vs a function name variable.
I worry that perhaps the routine that called the function gets an update whenever the variable changes if you use the function name, rather than only when End Function is executed. IE. if you have some code
answer = ADD(1, 2)
then in memory answer gets written to twice, once when ADD = a, once when ADD = ADD + b. Of course we don't see this, because answer is left with whatever the final ADD value is
I ask because I often find I build up a function answer in stages using an intermediate variable, then pass that to the function name itself, where instead I could just write directly to the function name.
E.g.
Function ADD(a, b) As Long
Dim tot As Long
tot = a
tot = tot + b
ADD = tot
End Function
vs the first example. They acheive the same thing, in the second example tot represents the formula result, and so we need a final ADD = tot line at the end. For speed I would like to reduce any writes that are made, so is there any drawback in terms of speed, not readability to using the first method as opposed to declaring intermediates?
NB, to clarify, that's not all intermediates, I just mean the single intermediate that represents the function result, and could be replaced by the function name in the code.
In speed the first method should be slightly faster - you declare one variable less (but I doubt that someone would be able to notice it).
In general, using the first method can bring you to a recursion, if you are not careful (or if you are a VBA beginner):
Option Explicit
Dim lng10 As Long
Public Sub Test()
lng10 = 0
Debug.Print ADD2(1, 1)
End Sub
Function ADD2(a As Long, b As Long) As Long
lng10 = lng10 + 1
ADD2 = a + b
If lng10 < 10 Then
ADD2 = ADD2 + ADD2(1, 1)
End If
End Function
And if the recursion does not have a bottom, it would go to an overflow error.
With other words, this would be an runtime error:
Option Explicit
Dim lng10 As Long
Public Sub Test()
lng10 = 0
Debug.Print ADD2(1, 1)
End Sub
Function ADD2(a As Long, b As Long) As Long
lng10 = lng10 + 1
ADD2 = ADD2(a, b)
End Function

Count characters between two empty space to dashes() in vba

How do I get the length of character between beginning with space and ending with * Here is the image. Column B shows the total len before dasher(-) and my code
Sub xn()
Dim x As Integer
x = 1
If Worksheet("Sheet1").Range("A"& x).len(Right," ") Or _
Worksheet("Sheet1").Range("A"&x)len(Left,"-") Then
len(totallen)
End If
x = x + 1
End Sub
The code posted has multiple issues:
Worksheet is not a valid object - you need to use Worksheets.
.len is not a property of a Range object.
Even in .len was a property of a Range, you would need a
de-reference operator (aka '.') in here: Range("A"&x)len(Left,"-")
If you intend to use the function Len(), it only takes one argument.
You apparently are trying to loop, but you need to use either a For
or For Each loop - it won't loop automatically when you increment x
at the bottom of the sub.
Right is a function, but you're calling it without arguments and they are not optional.
Similarly, Left is a function, but you're also calling it without
the required arguments.
totallen is not declared anywhere, so Len(totallen) will assume
that totallen is a Variant (default for undeclared variables), then
cast it to a String, and then always return 0 because it has never
been given a value.
Anything else I may have missed.
The solution is to use the InStr function. It returns the location in a string of a given sub-string.
Sub xn()
Dim x As Long
Dim sheet As Worksheet
Set sheet = ActiveWorkbook.Worksheets("Sheet1")
For x = 1 To sheet.Range("A" & sheet.Rows.Count).End(xlUp).Row
sheet.Cells(x, 2) = InStr(1, sheet.Cells(x, 1), "-") - 1
Next x
End Sub
I'd also recommend taking a look at the MSDN article on Looping Through a Range of Cells (2003 vintage, but still valid), and Error Finding Last Used cell In VBA.

Macro to run through 3 conditions and provide value

This is my first time using VBA for Excel (I usually code Java and C++), and I was hoping to get some tips to start out.
I want to write a macro for a large data set that will proceed through the following list of conditions to provide a dollar result:
Collect unit size from column A (Possible values 0-8)
Determine whether single or family unit from Column B (Single- 1, Family- 0)
Collect utility code from Column C (code for type of product being assessed)
From this information, a new value will be placed in the row which determines utility costs by taking into account unit size, type of unit, and the product in question. I have thought about using nested Select Case or nested conditionals in a loop, but overall I am pretty lost.
It seems like a worksheet formula might do the trick, but it's hard to tell without knowing what the calculation is. Below is a user-defined function (UDF) that you would put in a standard module. You would call it from a cell like:
=computecosts(A2,B2,C2)
Obviously the code would change depending on how your data is laid out and what your calculation is.
Public Function ComputeCosts(rSize As Range, rFamily As Range, rCode As Range) As Double
Dim lSizeFactor As Long
Dim lFamilyFactor As Long
Dim dCodeFactor As Double
Dim rFound As Range
Const lFAMILY As Long = 0
'Size factor is a function of 0-8, namely adding 1
lSizeFactor = rSize.Value + 1
'Family factor is computed in code
If rFamily.Value = lFAMILY Then
lFamilyFactor = 3
Else
lFamilyFactor = 2
End If
'Code factor is looked up in a different sheet
Set rFound = Worksheets("Sheet2").Columns(1).Cells.Find(rCode.Value, , xlValues, xlWhole)
If Not rFound Is Nothing Then
dCodeFactor = rFound.Offset(0, 1).Value
End If
'do the math
ComputeCosts = lSizeFactor * lFamilyFactor * dCodeFactor
End Function
Thanks for the responses, they were helpful in understanding VBA for Excel. I just ended up putting possible values in a table and then using Match functions within an Index function to pick out the right value.

Functions not actualizing

I execute a VBA code that takes a database, treats it and export it into a sheet. This is working fine. However, I have a sheet that produces graphs depending on the data in the particular sheet. The datas does not actualize. I have to enter the cell and click enter to actualize it. I'm pretty sure there is an easier way to do this. Calculation is set to automatic but that doesn't seem to change anything.
In my cell, I have my own vba function that needs to be updated once the report is done. When I click the cell and then enter, the result is updated but I would like this to be done automatically. I hope this is clearer !
Thanks in advance,
Etienne NOEL
HEre is the code of my function
Public Function number_of_appearances(term As String, sheet As String, column As Integer) As Integer
Application.Volatile
Dim number_of_rows As Integer
Dim appearances As Integer
Dim row As Integer
appearances = 0
row = 1
number_of_rows = Worksheets(sheet).UsedRange.Rows.Count
Do While row <= number_of_rows
If Worksheets(sheet).Cells(row, column).Value = term Then
appearances = appearances + 1
End If
row = row + 1
Loop
number_of_appearances = appearances
End Function
A cell example of a user of the function
=number_of_appearances('test';'sheet1'; 3)
Sounds like your UDF might not depend on any cells that change value when your DB is processed.
See This MSDN Link
Post your UDF (or just its header if you prefer) and an example of its use...
EDIT:
Yes, none of the parameters to the UDF are cell references, therefore the UDF is not triggered to recalculate when data on the shet changes.
You have two choices:
1. rewrite your UDF to include parameter(s) that reference cells that change value when the DB is processed
2. make your UDF volitile (include Application.Volatile in the UDF code) WARNING: this can be very inefficient, depending on how many time the UDF is used and how intensive its calculation is
EDIT 2:
Heres a refactor of your udf using the first option mentioned:
Public Function number_of_appearances(term As String, rng As Range) As Integer
Dim v As Variant
Dim i As Long, j As Long
Dim appearances As Long
v = Intersect(rng, rng.Worksheet.UsedRange)
For j = LBound(v, 2) To UBound(v, 2)
For i = LBound(v, 1) To UBound(v, 1)
If v(i, j) = term Then
appearances = appearances + 1
End If
Next i, j
number_of_appearances = appearances
End Function
use like
=number_of_appearances("test";Sheet1!C:C)
EDIT 3:
If all you are doing is counting number of occurances of a string in a range, consider using
=COUNTIF(Sheet1!C:C;"test")

Counting Rows/Columns of Selected Range Error

I am trying to determine if a selected range is within a set area... This toggles Copy/Paste restrictions in the spreadsheet. I have figured it out, I think, but I'm getting a run-time error 6 (Overflow) if you select an entire row or column. This is what I've got..
Function BETWEENROWS(ByVal Selected As Range, ByVal Min As Double, ByVal Max As Double) As Boolean
Dim LastRow As Integer
LastRow = Selected.Row + Selected.Rows.Count - 1
If BETWEEN(Min, Selected.Row, Max) = True And BETWEEN(Min, LastRow, Max) = True Then
BETWEENROWS = True
Else
BETWEENROWS = False
End If
End Function
There is one for columns BETWEENCOLUMNS as well and the function BETWEEN just returns True/False if a given number is between a min and max value.
This is working great, however, if an entire row/column is selected it's throwing an error and I'm not too familiar with VBA and the only way that I know of bypassing the error is with On Error Resume Next but that seems like I'm putting a bandaid on it and would like to figure out how to fix it another way.
Your LastRow variable is not the correct type for a number as large as the max columns/rows of the spreadsheet. Change the type to Long:
Dim LastRow As Long
You are getting an overflow error because you have made the LastRow variable an integer. Since there are more rows in an entire column then can fit in an integer variable, it triggers the overflow. You could fix this by changing the LastRow variable to be type Long
However, rather then comparing row values you may want to look into the Intersect() function. Given two (or more) ranges it will return the range object that represents the intersection of the two ranges. You could then check that intersection. If they don't intersect the range object will be Nothing. There is a good tutorial for this function at ozgrid.com
UPDATE
Here is the code to ensure range intersects fully using the Intersect() function
'// Run a test here to make sure Intersect does not return Nothing
If (TestRNG.Count <= ISectRNG.Count) And (Intersect(TestRNG, ISectRNG).Count = TestRNG.Count) Then
'// All of TestRNG falls within ISectRNG
End If