I tried to use Solver on a working sheet, say sheet A. I also have some cells with "=rand()" on sheet A, or say any formula on the sheet using custom functions with Application.Volatile written inside. I'd like these volatile cells to stop recalculate when I am doing solver, so I used Application.Calculation = xlCalculationManual in my Solver program, but turned out after I ran the Solver, I found that those volatile cells have changed. Where have I done wrong?
There are 2 ways to avoid this:
Do not use solver! If you write an own sub to do the stuff you can use Range.Calculate while the calculation is manual to just calculate this cells (all other cells will be ignored).
Change the formulas and go with iteration options -> formulas -> enable iterative calculation. Now use a helper-cell which holds true/false (or 1/0 or whatever) and the change the formulas you do not want to calculate as follows(helper-cell A1 / formula A2):
=IF(A1,A2,[your old formula])
This way, as long as A1 is true it will output the value from before the calculation.
As far as I know, there is no other way, because solver does a Calculate each time a new cycle is tested.
EDIT:
Having volatile UDF which do not need to be volatile the whole time, you can use a simple trick (again a helper-cell) is needed:
A1: [volatile function like =NOW() or =RAND()]
A2: =MY_UDF(A1)
In Module:
Public Function MY_UDF(a As Variant) As Double
a = a
MY_UDF = Now
End Function
As long as A1 holds something volatile, your UDF will recalculate every time. But if you empty out A1 (or make it at leas non-volatile) there will be no change anymore to the value submitted to your UDF and this way excel/vba assumes that also no change will happen and just skip it for recalculation. This way you also can build up your own RAND-UDF (with different name of course) to just stop ALL volatile functions in your workbook as long as your helper-cell is non-volatile.
As a note: after making A1 (in this example) non-volatile, the first calculation afterwards will still run 1 time like it is volatile. Also changing A1 from one value to another (like 0 to 1) will run one time.
Here is a workaround that might help if you want to have random values in a spreadsheet that can be recalculated at will and whose volatility can be turned on and off.
First, create a 1-cell named range, say "trigger"
Then, put the following code in a standard code module:
Function SemiVolatileRand(x As Variant) As Double
SemiVolatileRand = x - x + Rnd()
End Function
Sub ReSample()
Randomize
Range("trigger").Value = Range("trigger").Value + 0.01
End Sub
Attach the ReSample sub to a button. Replace all occurrences of =RAND() by
=SemiVolatileRand(trigger). They will recalulcate whenever the button is pressed. Also, if you ever want to turn full volatility back on, just put the formula =RAND() in the trigger cell. (Getting full volatility in this last case seemed to require that my code does something with the dummy variable x, hence the somewhat poinless x - x).
Randomize reseeds the random number generator from the system clock. It should be called at least once per session. If you don't, then each time you open an Excel workbook which uses VBA rnd, you will get the same sequence of random values. You can verify this by making a blank workbook and in the ThisWorkbook code module put:
Private Sub Workbook_Open()
MsgBox Rnd()
End Sub
The message block will display the same value each time you open the workbook. On the other hand if you put the line Randomize before the MsgBox then you will get different values each time you open it.
Note that the workbook open event is a natural place to put the statement Randomize if you are planning to use rnd.
The reason I didn't put Randomize in the function itself was both to save CPU cycles and also because of a nagging concern that a certain percentage of the time you will be reseeding the random number generator with exactly the same system time. That might be impossible with modern architectures running recent versions of Excel, but e.g. does sometimes happen if you had Randomize Timer (which you sometimes encounter when reading other's code) since the timer function has 1 millisecond resolution irrespective of the system clock.
The code I have does have the drawback that if you bypass the button and just change the trigger cell then you could miss the reseeding. If this is a concern, 1 possibility would be like this:
Public Initialized as Boolean
Function SemiVolatileRand(x As Variant) As Double
If Not Initialized Then
Randomize
Initialized = True
End If
SemiVolatileRand = x - x + Rnd()
End Function
This will prevent the function from running if rnd isn't properly seeded.
Excel itself takes care of seeding automatically with the worksheet function Rand(), so it is strictly a VBA complication.
Related
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.
I have many uses of the INDIRECT function in my workbook, and it is causing performance issues. I need to replace them with something that will give me the same results. All the INDIRECTS recalculate anytime anything is changed, causing the workbook to lag.
I was wondering if there is a way to code INDIRECT in VBA without actually using the INDIRECT function, and take away the volatility of the function in the code.
=INDIRECT("'" & $AC$9 & "'!" & AC26)
This is an example. I need to remove INDIRECT but get the same results for this cell. Is there a way to accomplish this in VBA?
You can try this.
Place the following routines in a standard code module:
Public Function INDIRECTVBA(ref_text As String)
INDIRECTVBA = Range(ref_text)
End Function
Public Sub FullCalc()
Application.CalculateFull
End Sub
Replace the INDIRECT functions in your formulas with INDIRECTVBA.
These will be static. If the slowness of your workbook is because your INDIRECTs are constantly evaluating, then this will put an end to that.
IMPORTANT: all cells that contain a formula using INDIRECTVBA will be static. Each formula will calculate when you confirm it, but it will not recalculate when precedents change.
You will then need a way to force them to recalculate at a convenient time. You can do that from the Ribbon. Or, you can run FullCalc.
Was going to add this as a comment, but my thought process got too long.
What is the context of the problem you are trying to solve?
I am guessing you are using some kind of data validation drop-down menu in $AC$9 to select a sheet name and then all your INDIRECT formulas are providing a mirror image of a particular section of the user-specified worksheet.
If that is the case then you might consider using INDEX as an alternative. It is written as =INDEX(Range, RowNum, ColNum) E.g. if you put this in H20: =INDEX(Sheet1!A:Z,ROW()+10,COLUMN()-5) then it would reflect whatever is in sheet 1, cell C30 (H - 5 columns, 20 + 10 rows). Of course, you don't have to offset anything if you don't want to, I just wanted to demonstrate that as an option.
Now, the trickier part would still remain - assigning/updating the SheetName variable. This could be done with a UserForm instead of typing in a value in a particular input cell. For example, you could have VBA provide an input box/dropdown menu for the user to select one of the available sheet names, then take that input and use it in a quick find and replace instruction - searching for "=INDEX(*!" and replacing with "=INDEX(" & InputVariable & "!"
I've made a few assumptions about your dataset and what you're trying to achieve, so it might not be the ideal solution, but perhaps something to think about.
The solution to volatility with the Indirect function (typical in multi-version cross platform use and partitioning to run Windows on Mac) can be absorbed by splitting its various functions with a pseudonym for Indirect I have named "Implied":
Public Function Implied(Varient)
' CREDIT: Stephen L. Rush
On Error Resume Next
If IsError(Range(Varient)) Then
If IsError(Find(Varient, "&")) Then
'Not Range, is Indirect. "A" & Match() Style (where Match() = row).
Implied = WorksheetFunction.Indirect(Varient)
Else
'Not a Range, not Indirect. "A" & B99 Reference (where B99 = row).
Implied = Range(Left(Varient, Find(Varient, "&") - 1) & Range(Right(Varient, Len(Varient) - Find(Varient, "&"))))
End If
Else
'Is pure Range
Implied = Range(Varient)
End If
'[On Error GoTo 0] Conflicts with use as formula
End Function
I am trying to make a non-volatile UDF but it seems not possible. So here is a my very simple test-UDF:
Option Explicit
Dim i As Integer
Sub Main()
i = 0
[A1] = "zyy"
MsgBox i
End Sub
Function Test(rng As Range)
Application.Volatile (False)
Test = rng.Value
i = i + 1
End Function
I got a otherwise empty worksheet that uses this function a couple of times, and every time I call Main() and change any cell on the sheet with the UDFs all of them recalculate. How can I make this (any) UDF no-volatile? The Application.Volatile (False) should have that effect but obviously doesn't work.
Edit: If I change a cell manually it works like intended, it only recalculates when I change a cell via VBA. Is this normal behaviour or can I change it?
I am posting a new answer instead of trying to salvage my previous answer, even though I think they point to the same thing, it will be better to start fresh.
Background:
Previously I had tested your code and the results were exactly as I would expect them to be if you simply omit the False from that statement. I have never seen any reason to explicitly do Application.Volatile (False), because that is equivalent to simply omitting the statement entirely.
If omitted, the function is non-volatile and the UDF evaluates only when a reference changes (i.e., not when other, non-referent cells change)
If included as Application.Volatile (or Application.Volatile
(True), the UDF becomes volatile and any change to the worksheet
will force re-evaluation.
Continuing investigation
You commented that you still observed otherwise. So I made some changes to my code and tested again. All of a sudden weird stuff was happening. No matter what I did with the Application.Volatile function, any change to the worksheet was re-evaluating the UDF.
This didn't make sense, so I started googling and doing a little more testing.
In my tests I created three functions.
The first one is explicitly Volatile:
The second one is explicitly not volatile.
The third omits any statement of volatility.
I put one instance of each formula on a worksheet. Each referenced a different range.
I tested each of these by making changes to the worksheet (manually), and through a named subroutine. I used a Print statement and monitored the Immediate window in the VBE to confirm that in all cases, the functions evaluated (or not) only as expected. The first one always evaluates, while 2 and 3 only evaluate if reference range changed.
Function f_appvol(rng As Range)
Application.Volatile
Debug.Print "f_appvol"
f_appvol = rng.Value
End Function
Function f_appNOTvol(rng As Range)
Application.Volatile (False)
Debug.Print "f_appNOTvol"
f_appNOTvol = rng.Value
End Function
Function f_omit(rng As Range)
Debug.Print "f_omit"
f_omit = rng.Value
End Function
Then it got weird...
I started making changes within these functions and they start to behave wonky.
Specifically I got lucky and noticed that if I changed my non-volatile function to a volatile one, then all functions started acting as if they were volatile -- even the f_omit. I believe this may be the condition you are experiencing.
Somehow, we have managed to "confuse" Excel
I saved the workbook and tried again... back to normal!
Then I changed the argument in the volatile statement, and the strange behavior happened again.
This appears to be a bug
I don't see anything in the documentation that suggests this is normal/expected behavior, and it sure as hell is not desirable behavior from a debugging standpoint. This is the sort of thing that makes you pull out your hair in frustration!
I am using Excel 2010, Win 7 64b.
Resolution
The cause of the error seems to be making change to the volatility of a UDF.
In order to restore expected behavior, it seems necessary to save the workbook. Again, I don't think this is normal but it seems to solve your problem (or at least a very similar problem that I was able to replicate while troubleshooting yours).
On a possibly related note
There appears to be at least one bug related to volatility, as mentioned here. I link to it mainly because this writer echos my own sentiment: there is no reason to do Application.Volatile (False) because that is (or should be) the "normal" state of a UDF.
I have to admit that I had never seen the point of using Application.Volatile False since thats supposed to be what you get if you omit the Application.Volatile statement altogether.
I found the solution, and it is indeed a very simple one but also made this hard to debug:
If you make any change to your VBA code all the UDF get flagged for recalculation!
I modified the degug code of David:
Sub main()
'nothing depends on the Value in [A13]
[A13] = ""
[A13] = "hgdg"
[A13] = ""
i = 46
End Sub
Function f_appvol(rng As Range)
Application.Volatile
Debug.Print "f_appvol"
f_appvol = rng.Value
End Function
Function f_appNOTvol(rng As Range)
Application.Volatile (False)
Debug.Print "f_appNOTvol"
f_appNOTvol = rng.Value
End Function
Function f_omit(rng As Range)
Debug.Print "f_omit"
f_omit = rng.Value
End Function
After entering the code and running it for the first time, only f_appvol is recalculated. If you now change i=46 to i=47 and execute it, all the UDF get recalculated. All subsequent runs after that first run after the change give the expected behaviour.
I have the following function(which returns the last row number of any selected column)
Function LastrowCC(SelectedRange As Range)
Dim SelectedColumnNum As Long
SelectedColumnNum = SelectedRange.Column
LastrowCC = ActiveSheet.Cells(Rows.count, SelectedColumnNum).End(xlUp).Row
End Function
the problem is that when the last row of the selected column is deleted the function does not update automatically
can we make VBA custom functions update automatically when inputs change?
At risk of repeating the helpful comments and answers already posted, let me point out that there are several issues involved in your question, summed up nicely in the MSDN article "Excel Recalculation."
A more specific question, including the Excel version and the way calculation is handled on your worksheet, may help narrow things down a bit.
Let me sum up some things you may want to check out, all listed in the article above:
There are several ways a recalculation is triggered, including functions. Studying the way this happens may shed some insight. According to the article, "The calculation of worksheets in Excel can be viewed as a three-stage process:
Construction of a dependency tree,
Construction of a calculation chain,
Recalculation of cells."
Volatile functions are an option, but because of resource consumption should be used sparingly and wisely.
You may also explore Range.Dirty and Range.Calculate methods, which starting in Excel 2002 (that's farther back in the past than some of us realize!) allows "forced recalculation," to again quote the article above.
These are a few options and things to consider.
Make it Volatile :
Function LastrowCC(SelectedRange As Range) As Long
Application.Volatile
Dim SelectedColumnNum As Long
SelectedColumnNum = SelectedRange.Column
LastrowCC = ActiveSheet.Cells(Rows.Count, SelectedColumnNum).End(xlUp).Row
End Function
The key to making a function work within Excel's calculation tree and not be volatile is to include everything you need in the arguments to the function. That is, don't reference any ranges that are not included in the arguments to the function.
In your example, you can send it any single cell and it will return the last row of that same column. But you're looking outside of that single cell so Excel doesn't know that the whole column should be in the calculation tree.
In this rewrite, the function only accepts single, whole column arguments. If you send it less than a whole column, it returns an error. If you send it more than one column, it returns an error. But since the whole column is in the argument, any changes in the column trigger a recalc.
Public Function LastRowCc(ByVal SelectedRange As Range)
If SelectedRange.Address = SelectedRange.EntireColumn.Address And SelectedRange.Columns.Count = 1 Then
LastRowCc = SelectedRange.Find("*", SelectedRange.Cells(1), , , , xlPrevious).Row
Else
LastRowCc = CVErr(xlErrValue)
End If
End Function
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.)