Functions drastically slowing down calculation time - vba

I have an excel table where I'm using my custom made functions, made in VBA. I have to mention that I am using these functions inside cells, not in a macro. Everything works just fine, however, I am noticing a dramatic slowdown in calculation time. To be honest, I am using my function in about 200 cells, but still I don't see how it can reason such a memory hungry spreadsheet. Also my 2 functions are very basic, one is a plain vanilla isExist(value) search in a 5 cells range (no binary or other fast search algos). The other function is a modified dateDiff, so I can use it as a cell function (for some reason dateDiff does not show as a cell formula). Anyways, can someone tell me why a vba function would be so memory hungry when used in cell as a formula, and how can I optimize this?
Function existsInArray(array_to_search As range, value_to_exist As String) As Boolean
For Each value In array_to_search
If value_to_exist = value Then
existsInArray = True
Exit Function
End If
Next
existsInArray = False
End Function
Function dayOfTheYear(begining_of_year_date As Date, to_date As Date) As Integer
dayOfTheYear = CInt(DateDiff("d", begining_of_year_date, to_date)) + 1
End Function

I can't analyze completely without seeing how the UDFs are being used, but having code loop over ranges can be very slow. For example:
Public Function MyUdf(rng As Range) As Variant
Dim r As Range
For Each r In rng
' do something that calculates MyUdf
MyUdf = 1
Next r
End Function
will examine and process each cell in rng. If the user puts something like:
=MyUdf(A:A)
in a cell, it will process every cell in the entire column.
To limit the extent of the looping, you can use something like:
Public Function MyUdf2(rng As Range) As Variant
Dim r As Range, RNG2 As Range
Set RNG2 = Intersect(rng, rng.Parent.UsedRange)
For Each r In RNG2
' do something that calculates MyUdf
MyUdf2 = 1
Next r
End Function
That way you may end up processing thousands of cells rather than millions.
Anther possible speedup technique is to create VBA arrays to process the data rather than use cells directly.

Well, by default, UDF's (User Defined Functions) in Excel VBA are not volatile. They are only recalculated when any of the function's arguments change. A volatile function will be recalculated whenever calculation occurs in any cells on the worksheet.
Turn Off Automatic Calculation
vba performanceSet the Calculation mode to xlCalculationManual so that no calculations are carried out within the Excel Workbook until the Calculation mode is changed back to xlCalculationAutomatic or by running Application.Calculate:
Application.Calculation = xlCalculationManual
Turn Off Screen Updating
vba performancePrevent Excel from updating the Excel screen until this option is changed to True:
Application.ScreenUpdating = False
The XLSB format vs. XLSM
vba performanceA known way to improve Excel VBA speed and efficiency, especially fore large Excel files, is to save your Workbooks in binary XLSB format.

Related

Excel VBA: Insheet function code can not access other cells on sheet

I'm having some issues with an insheet function that I am writing in VBA for Excel. What I eventually am trying to achieve is an excel function which is called from within a cell on your worksheet, that outputs a range of data points underneath the cell from which it is called (like the excel function =BDP() of financial data provider Bloomberg). I cannot specify the output range beforehand because I don't know how many data points it is going to output.
The issue seems to be that excel does not allow you to edit cells on a sheet from within a function, apart from the cell from which the function is called.
I have created a simple program to isolate the problem, for the sake of this question.
The following function, when called from within an excel sheet via =test(10), should produce a list of integers from 1 to 10 underneath the cell from which it is called.
Function test(number As Integer)
For i = 1 To number
Application.Caller.Offset(i, 0) = i
Next i
End Function
The code is very simple, yet nothing happens on the worksheet from which this formula is called (except a #Value error sometimes). I have tried several other specifications of the code, like for instance:
Function test(number As Integer)
Dim tempRange As Range
Set tempRange = Worksheets("Sheet1").Range(Application.Caller.Address)
For i = 1 To number
tempRange.Offset(i, 0) = i
Next i
End Function
Strangely enough, in this last piece of code, the command "debug.print tempRange.address" does print out the address from which the function is called.
The problem seems to be updating values on the worksheet from within an insheet function. Could anybody please give some guidance as to whether it is possible to achieve this via a different method?
Thanks a lot, J
User defined functions are only allowed to alter the values of the cells they are entered into, because Excel's calculation method is built on that assumption.
Methods of bypassing this limitation usually involve scary things like caching the results and locations you want to change and then rewriting them in an after calculate event, whilst taking care of any possible circularity or infinite loops.
The simplest solution is to enter a multi-cell array formula into more cells than you will ever need.
But if you really need to do this I would recommend looking at Govert's Excel DNA which has some array resizer function.
Resizing Excel UDF results
Consider:
Public Function test(number As Integer)
Dim i As Long, ary()
ReDim ary(1 To number, 1 To 1)
For i = 1 To number
ary(i, 1) = i
Next i
test = ary
End Function
Select a block of cells (in this case from C1 through C10), and array enter:
=test(10)
Array formulas must be entered with Ctrl + Shift + Enter rather than just the Enter key.

Excel still recalculates when hiding a row despite CalculationManual

Excuse me if duplicating, but couldn't find anything in regards to my problem on the internet.
So I have:
Private Sub Worksheet_Change(ByVal Target As Range)
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Then somewhere down the script there is
For Each sheet In sheets
If sheet.Cells(i, ColBrand).Rows.Hidden Then
sheet.Cells(i, ColBrand).EntireRow.Hidden = False
End If
Next
, where ColBrand is just a column number defined as a constant integer in the beginning.
And so it goes through an array of worksheets hiding rows where necessarity doing its job just fine.
Problem is, despite CalculationManual, "calculating: xx%" still pops up in a status bar occasionally. Which, alongside with the speed of execution, leads me to thinking Excel recalculates the entire Worksheet (possibly Workbook) after hiding each row. Which is sad.
Any ideas?
Thank you!
Now I'm thinking, could it be because I have a couple of VBA written Application.Volatile functions on one of the Worksheets?
I do have this function, which gets called under a condition, which triggers WorkSheet_change event. If this is of any help
Function get_rand(floor As Long, ceiling As Long, exceptions() As Long)
Dim rand As Long, _
position As Variant
If UBound(exceptions) - LBound(exceptions) + 1 >= 999999 Then
get_rand = "error"
Else
Do Until IsError(position)
rand = CLng(((ceiling - floor + 1) * Rnd() + floor))
position = Application.Match(rand, exceptions, False)
get_rand = rand
Loop
End If
End Function
Oh btw the code above interacts with sheets which have WorkSheet_Change routines themselves. Target.Adress 'es of these do partly intersect but get neither changed nor updated by the code above. Could it be the cause?
This MS Document discusses this problem.
It states "The user can trigger recalculation in Microsoft Excel in several ways, for example:" ... "Deleting or inserting a row or column."
I think you've stumbled upon a documented "feature".

Excel & VBA: Skip calculating formula in Macro

I'm having a rather simple issue with Macro. In it, I assign a formula to a cell. I would like it not to calculate it at first and do calculation only after some other part of Macro is finished. I thought I would do something like this:
Application.Calculation = xlCalculationManual
Cells(StartRow, 4 + i).Formula = "FORMULA"
...
Application.Calculation = xlCalculationAutomatic
But that doesn't work. It stops automatic calculations, but not for that cell - it still performs the calculation right after I assign the formula. Is there a way to skip it?
To clarify the exact prupose of this: in my actual code I'm assigning a formula to a group of cells in a cycle. Everytime I assign it to one cell - it calculates it. I figured if I will first assign them all and then do the calculation - it would be faster. As a matter of fact it is. So instead of assigning it in a cycle, I assign it to the first cell and then do autofill. Autofilled formulas wait until I enable automatic calculation and I get a much faster macro. However, the initial assignemnt is still calculated which makes macro almost twice as slow.
place the formula in the cell with a prefix character
continue the macro
"activate" the formula
for example:
Sub dural()
With Range("A1")
.Value = "'=1+2"
MsgBox " "
.Value = .Value
End With
End Sub
#Alex, you can delay the calculation as #Gary answer. However, you was asking the question because you need "SPEED to CYCLE through cells" while assign a formula, right?
If yes, from my point of view, if you are NOT using the formulas until ALL the formulas are assigned in the excel sheet, you will gain a lot of speed by writing all the formulas at once using an array (a single step in VBA).
The procedure is: first put all the formulas to an VBA array of strings, and later on to use for example Range("B1:B100").Formula = ArrayWithFormulas. In that example you are assigning 100 formulas at once, without recalculation in between.
You will see a large improve in SPEED if use an array to write all the cells at ones instead of writing cell by cell! (Don't loop using cells(r,c+i) if you have a lot of cells to go through). Here one example:
Sub CreateBunchOfFormulas()
Dim i As Long
Dim ARRAY_OF_FORMULAS() As Variant 'Note: Don't replace Variant by String!
ReDim ARRAY_OF_FORMULAS(1 To 100, 1 To 1)
' For Vertical use: (1 to NumRows,1 to 1)
' for Horizontal: (1 to 1,1 to NumCols)
' for 2D use: (1 to NumRows,1 to NumCols)
'Create the formulas...
For i = 1 To 100
ARRAY_OF_FORMULAS(i, 1) = "=1+3+" & i ' Or any other formula...
Next i
' <-- your extra code here...
' (New formulas won't be calculated. They are not in the Excel sheet yet!
' If you want that no other old formula to recalculate use the old trick:
' Application.Calculation = xlCalculationManual )
'At the very end, write the formulas in the excel at once...
Range("B1:B100").Formula = ARRAY_OF_FORMULAS
End Sub
If you want an extra delay in the new formula, then you can use #Gary trick, but applied to a range, not to a single cell. For that start the formulas with a ' like '=1+2 and add the following code at the end:
'... previous code, but now formulas starting with (')
Range("B1:B100").Formula = ARRAY_OF_FORMULAS
'Formulas not calculated yet, until next line is executed
Range("B1:B100").Value = Range("B1:B100").Value ' <~~ #Gary's trick
End Sub
Last, a small snipped: if your formulas are in a horizontal arrangement (Means one formula for column A, other for column B, etc) and just a small number of columns, then you can keep in mind the a shorter version of previous code:
Dim a as Variant 'Note that no () needed
a = Array("=1+3","=4+8","=5*A1","=sum(A1:C1)")
Range("A1:D1").Formula = ARRAY_OF_FORMULA ' Just a single row
' or...
Range("A1:D100").Formula = ARRAY_OF_FORMULA ' If you want to repeat formulas
' in several rows.
Finally, You can use the method .FormulaR1C1 instead of .Formula in all the previous code examples, if you want an easy way to use relative references in your formula...
Hope this helps!

Excel VBA Autofilters inside functions

Sub TurnAutoFilterOn()
'check for filter, turn on if none exists
If Not ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
End Sub
Works well and turns on the AutoFilter.
Function Req(ByVal MCode As String) As Integer
TurnAutoFilterOn
End Function
Doesn't work.
Function Req(ByVal MCode As String) As Integer
'check for filter, turn on if none exists
If Not ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
End Function
Doesn't work.
Is excel vba autofilters supposed to be working only under SUBs and not in Functions?
The above commenters are right regarding updating the workbook (i.e. any cells) from a function invoked by a cell - it is not allowed/supported.
Excel provides a workbook reclaculation model in which it can precompute inter-cell dependencies based on the cell formulas. This allows for a (relatively) efficient propagation of changes from their original sources to the cells that depend upon them. It propagates changes repeatedly (i.e. recursively) until they've been propagated to cells that are not referenced in other formulas, when the workbook relaculation is completed. It does NOT allow cell formulas to modify any cells in the workbook; if it were to support that it would effectively invalidate (or at least dramatically weaken) the pre-computed formula-based dependency analysis, and require another calculation model (that would likely much less efficient). (Circular cell references (direct or indirectly) are also problematic for this method, which makes history functions a bit tricky.)
However, what you can do is record some data in a VBA data structure to save for later use (in the example below, the very simple public gx, but such data structure can by almost of any complexity). You can then use that recorded data after a workbook recalculation using events. The Worksheet Change Event is way to run some code after the calculation (you write subroutine Worksheet_Calculate and put it in the worksheet), at a time when it will be OK to modify the cells. There is also a Workbook_SheetCalculation which goes in the code for ThisWorkbook, which might be of interest.
In "ThisWorkbook":
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
MsgBox "gx=" & gx
Application.Worksheets("Sheet1").Range("A1") = gx
End Sub
In "Module1":
Public gx As Long
Function MyFormula(x As Long) As String
gx = x
MyFormula = "hello"
End Function
In Sheet1, cell A5:
=MyFormula(A4)
You'll get a pop up in a context where gx was set to the number in A4 (showing the storing of data from the run of the formula), and, modifying a worksheet as well. Now, modify A4 with another number and you'll see the results because changing A4 triggers a recalculation.
(Note that you may also be interested in the Workbook_SheetChange event as an alternative to the SheetCalculate event.)

How can I write a UDF in Excel VBA to filter a range of cells?

I've gotten into the habit of marking outlying data by changing cell styles. I'd like to write a UDF in excel to take a Range of cells as input, and return the subset of that range that is not marked as an outlier.
This is what I have tried:
Function ValidCells(rCells As Range) As Range
Dim c As Range
For Each c In rCells
If c.Style <> "Bad" Then
Set ValidCells = Range(c, ValidCells)
End If
Next
End Function
My intent is to be able to do =Sum(ValidCells(A1:D1)), and have it only sum the non-styled data.
However, ValidCells seems to return an empty range every time. What am I doing wrong?
Are you sure it's returning an empty range? When I try running this, VBA raises an error on your 'Set' line. If you're calling the routine as a UDF from the worksheet you won't see the VBA error, but the UDF should stop executing and return #VALUE!.
In any case, you can do what you want, but there is one big caveat. First, the code:
Function ValidCells(rCells As Range) As Range
Dim valid As Range
Dim c As Range
For Each c In rCells
If c.Style <> "Bad" Then
If valid Is Nothing Then
Set valid = c
Else
Set valid = Union(valid, c)
End If
End If
Next
Set ValidCells = valid
End Function
The idea is to build up a multi-area range using VBA's 'Union' method. So, for example, if I put a bad cell in C8, and call ValidCells(B7:D9), this returns the multi-area range $B$7:$D$7,$D$8,$B$8:$B$9,$C$9:$D$9. You can then use the result with SUM just fine.
The caveat is that changing cell styles won't trigger this UDF to recalculate. Normally, you'd be able to add a line like this:
Call Application.Volatile(True)
to your UDF and it would recalc on every change to the workbook. However, it seems like changing a cell style doesn't qualify as a "change" for volatility purposes. So, you can get what you want out of the UDF, but there appears to be no real way to make it work like a "normal" one as far as recalculation goes, even if you mark it as volatile. You'll have to remain aware of that if you use it.