i want to create the "cases" formula for excel to simulate Select case behavior (with multiple arguments and else optional).
If A1 and A2 are excel cells, this is the goal:
A1 Case: A2 Formula: A2 Result
5 cases({A1>5,"greather than 5"}, {A1<5, "less than 5"},{else,"equal to 5"}) equal to 5
Hi cases({A1="","there is nothing"},{else,A1}) Hi
1024 cases({5<A1<=10,10},{11<=A1<100,100},{A1>100,1000}) 1000
12 cases({A1=1 to 9, "digit"}, {A1=11|22|33|44|55|66|77|88|99, "11 multiple"}) (empty)
60 cases({A1=1 to 49|51 to 99,"not 50"}) not 50
If it could, It must accept excel formulas or vba code, to make an operation over the cell before take a case, i.g.
cases({len(A1)<7, "too short"},{else,"good length"})
If it could, it must accept to or more cells to evaluate, i.g.
if A2=A3=A4=A5=1 and A1=2, A6="one", A7="two"
cases(A1!=A2|A3|A4|A5, A6}, {else,A7}) will produce "two"
By the way, | means or, != means different
Any help?
I'm grateful.
What I could write was this:
Public Function arr(ParamArray args()) 'Your function, thanks
arr = args
End Function
Public Function cases(arg, arg2) 'I don't know how to do it better
With Application.WorksheetFunction
cases = .Choose(.Match(True, arg, 0), arg2)
End With
End Function
I call the function in this way
=cases(arr(A1>5, A1<5, A1=5),arr( "gt 5", "lt 5", "eq 5"))
And i can't get the goal, it just works for the first condition, A1>5.
I fixed it using a for, but i think it's not elegant like your suggestion:
Function selectCases(cases, actions)
For i = 1 To UBound(cases)
If cases(i) = True Then
selectCases = actions(i)
Exit Function
End If
Next
End Function
When i call the function:
=selectCases(arr(A1>5, A1<5, A1=5),arr( "gt 5", "lt 5", "eq 5"))
It works.
Thanks for all.
After work a little, finally i get a excel select case, closer what i want at first.
Function cases(ParamArray casesList())
'Check all arguments in list by pairs (case, action),
'case is 2n element
'action is 2n+1 element
'if 2n element is not a test or case, then it's like the "otherwise action"
For i = 0 To UBound(casesList) Step 2
'if case checks
If casesList(i) = True Then
'then take action
cases = casesList(i + 1)
Exit Function
ElseIf casesList(i) <> False Then
'when the element is not a case (a boolean value),
'then take the element.
'It works like else sentence
cases = casesList(i)
Exit Function
End If
Next
End Function
When A1=5 and I call:
=cases(A1>5, "gt 5",A1<5, "lt 5","eq 5")
It can be read in this way: When A1 greater than 5, then choose "gt 5", but when A1 less than 5, then choose "lt 5", otherwise choose "eq 5". After run it, It matches with "eq 5"
Thank you, it was exciting and truly educative!
O.K., there's no way at all to do exactly what you want. You can't use anything other than Excel syntax within a formula, so stuff like 'A1 = 1 to 9' is just impossible.
You could write a pretty elaborate VBA routine that took strings or something and parsed them, but that really amounts to designing and implementing a complete little language. And your "code" wouldn't play well with Excel. For example, if you called something like
=cases("{A1="""",""there is nothing""},{else,A1}")
(note the escaped quotes), Excel wouldn't update your A1 reference when it moved or the formula got copied. So let's discard the whole "syntax" option.
However, it turns out you can get much of the behavior I think you actually want with regular Excel formulas plus one tiny VBA UDF. First the UDF:
Public Function arr(ParamArray args())
arr = args
End Function
This lets us create an array from a set of arguments. Since the arguments can be expressions instead of just constants, we can call it from a formula like this:
=arr(A1=42, A1=99)
and get back an array of boolean values.
With that small UDF, you can now use regular formulas to "select cases". They would look like this:
=CHOOSE(MATCH(TRUE, arr(A1>5, A1<5, A1=5), 0), "gt 5", "lt 5", "eq 5")
What's going on is that 'arr' returns a boolean array, 'MATCH' finds the position of the first TRUE, and 'CHOOSE' returns the corresponding "case".
You can emulate an "else" clause by wrapping the whole thing in 'IFERROR':
=IFERROR(CHOOSE(MATCH(TRUE, arr(A1>5, A1<5), 0), "gt 5", "lt 5"), "eq 5")
If that is too verbose for you, you can always write another VBA UDF that would bring the MATCH, CHOOSE, etc. inside, and call it like this:
=cases(arr(A1>5, A1<5, A1=5), "gt 5", "lt 5", "eq 5")
That's not far off from your proposed syntax, and much, much simpler.
EDIT:
I see you've already come up with a (good) solution that is closer to what you really want, but I thought I'd add this anyway, since my statement above about bringing MATCH, CHOOSE, etc. inside the UDF made it look easier thatn it really is.
So, here is a 'cases' UDF:
Public Function cases(caseCondResults, ParamArray caseValues())
On Error GoTo EH
Dim resOfMatch
resOfMatch = Application.Match(True, caseCondResults, 0)
If IsError(resOfMatch) Then
cases = resOfMatch
Else
Call assign(cases, caseValues(LBound(caseValues) + resOfMatch - 1))
End If
Exit Function
EH:
cases = CVErr(xlValue)
End Function
It uses a little helper routine, 'assign':
Public Sub assign(ByRef lhs, rhs)
If IsObject(rhs) Then
Set lhs = rhs
Else
lhs = rhs
End If
End Sub
The 'assign' routine just makes it easier to deal with the fact that users can call UDFs with either values or range references. Since we want our 'cases' UDF to work like Excel's 'CHOOSE', we'd like to return back references when necessary.
Basically, within the new 'cases' UDF, we do the "choose" part ourselves by indexing into the param array of case values. I slapped an error handler on there so basic stuff like a mismatch between case condition results and case values will result in a return value of #VALUE!. You would probably add more checks in a real function, like making sure the condition results were booleans, etc.
I'm glad you reached an even better solution for yourself, though! This has been interesting.
MORE ABOUT 'assign':
In response to your comment, here is more about why that is part of my answer. VBA uses a different syntax for assigning an object to a variable than it does for assigning a plain value. Look at the VBA help or see this stackoverflow question and others like it: What does the keyword Set actually do in VBA?
This matters because, when you call a VBA function from an Excel formula, the parameters can be objects of type Range, in addition to numbers, strings, booleans, errors, and arrays. (See Can an Excel VBA UDF called from the worksheet ever be passed an instance of any Excel VBA object model class other than 'Range'?)
Range references are what you describe using Excel syntax like A1:Q42. When you pass one to an Excel UDF as a parameter, it shows up as a Range object. If you want to return a Range object from the UDF, you have to do it explicitly with the VBA 'Set' keyword. If you don't use 'Set', Excel will instead take the value contained within the Range and return that. Most of the time this doesn't matter, but sometimes you want the actual range, like when you've got a named formula that must evaluate to a range because it's used as the source for a validation list.
Related
I wrote a VBA script/macro which runs when a change is detected in a specific range (n x m) of cells. Then, it changes the values in another range (n x 1) based on what is detected in the first range.
This bit works perfectly ... but then comes the age old erased undo stack problem. Unfortunately, the ability for the user to undo their last ~10 or so actions is required.
My understanding is that the undo stack is only cleared when VBA directly edits something on the sheet - but it is preserved if the VBA is just running in the back without editing the sheet.
So my question is: Is it possible to use an in cell formula (something like below) to pull values from a VBA array?
'sample of in-cell function in cell A3
=function_to_get_value_from_vba_array(vba_array, index_of_desired_value)
Essentially, VBA would store a 1D array of strings with the values needed for the range. And by using a formula to grab the value from the array: I might be able to get around the issue of the undo stack being erased.
Thanks!
Solution
You need to do something like the following: your argument for the function should be calling the array bulding; I created one dummy function that creates some sample arrays to demonstrate it. In your case, likely you will need to store the changes on the worksheet event in a global array variable instead, and as you stated, do nothing on the worksheet (whenever a change happens, just redim or appended it on your global array as needed). However, a new problem may arise and that is when you close/reopen, or by some reason the array is lost, so you need to keep track of it, I would suggest to catch before close event and then convert the formulas to static values.
Function vba_array(TxtCase As String)
Dim ArrDummy(1) As Variant
Select Case TxtCase
Case "Txt": ArrDummy(0) = "Hi": ArrDummy(1) = "Hey"
Case "Long": ArrDummy(0) = 0: ArrDummy(1) = 1
Case "Boolean": ArrDummy(0) = True: ArrDummy(1) = False
End Select
vba_array = ArrDummy
End Function
In your calling function, do the following
Function get_value_from_vba_array(vba_array() As Variant, index_of_desired_value As Long) As Variant
'when parsing, even with option base 0 it starts at 1, so we need to add 1 up
get_value_from_vba_array = vba_array(index_of_desired_value + 1)
End Function
In your book, your formula should be something like:
=get_value_from_vba_array(vba_array("Txt"),1)
Demo
I did some actions before, so you are able to see that the "undo" works
In Excel 2007, how do I add a description and parameter hints to a user-defined function? When I start typing a function invocation for a built-in function, Excel shows a description and parameter list--a tooltip. I'd like to do the same for the functions I define.
Not just for the formula insert wizard, but in the formula box, so if I key "=myFun(", at the "(" the tooltip pops up just like it does for "=average("
There's no help in VBA Help, none on MSDN and none on any of the Excel and VBA dedicated forums I can find, so this is clearly a long shot.
Not a tooltip solution but an adequate workaround:
Start typing the UDF =MyUDF( then press CTRL + Shift + A and your function parameters will be displayed. So long as those parameters have meaningful names you at-least have a viable prompt
For example, this:
=MyUDF( + CTRL + Shift + A
Turns into this:
=MyUDF(sPath, sFileName)
Professional Excel Development by
Stephen Bullen describes how to
register UDFs, which allows a
description to appear in the Function
Arguments dialog:
Function IFERROR(ByRef ToEvaluate As Variant, ByRef Default As Variant) As Variant
If IsError(ToEvaluate) Then
IFERROR = Default
Else
IFERROR = ToEvaluate
End If
End Function
Sub RegisterUDF()
Dim s As String
s = "Provides a shortcut replacement for the common worksheet construct" & vbLf _
& "IF(ISERROR(<expression>), <default>, <expression>)"
Application.MacroOptions macro:="IFERROR", Description:=s, Category:=9
End Sub
Sub UnregisterUDF()
Application.MacroOptions Macro:="IFERROR", Description:=Empty, Category:=Empty
End Sub
From: http://www.ozgrid.com/forum/showthread.php?t=78123&page=1
To show the Function Arguments dialog, type the function name and press CtrlA. Alternatively, click the "fx" symbol in the formula bar:
I know you've accepted an answer for this, but there's now a solution that lets you get an intellisense style completion box pop up like for the other excel functions, via an Excel-DNA add in, or by registering an intellisense server inside your own add in. See here.
Now, i prefer the C# way of doing it - it's much simpler, as inside Excel-DNA, any class that implements IExcelAddin is picked up by the addin framework and has AutoOpen() and AutoClose() run when you open/close the add in. So you just need this:
namespace MyNameSpace {
public class Intellisense : IExcelAddIn {
public void AutoClose() {
}
public void AutoOpen() {
IntelliSenseServer.Register();
}
}
}
and then (and this is just taken from the github page), you just need to use the ExcelDNA annotations on your functions:
[ExcelFunction(Description = "A useful test function that adds two numbers, and returns the sum.")]
public static double AddThem(
[ExcelArgument(Name = "Augend", Description = "is the first number, to which will be added")]
double v1,
[ExcelArgument(Name = "Addend", Description = "is the second number that will be added")]
double v2)
{
return v1 + v2;
}
which are annotated using the ExcelDNA annotations, the intellisense server will pick up the argument names and descriptions.
There are examples for using it with just VBA too, but i'm not too into my VBA, so i don't use those parts.
Also you can use, this Macro to assign Descriptions to arguments and the UDF:
Private Sub RegisterMyFunction()
Application.MacroOptions _
Macro:="SampleFunction", _ '' Your UDF name
Description:="calculates a result based on provided inputs", _
Category:="My UDF Category", _ '' Or use numbers, a list in the link below
ArgumentDescriptions:=Array( _ '' One by each argument
"is the first argument. tell the user what it does", _
"is the second argument. tell the user what it does")
End Sub
Credits to Kendall and the original post here.
For the UDF Categories
I just create a "help" version of the function. Shows up right below the function in autocomplete - the user can select it instead in an adjacent cell for instructions.
Public Function Foo(param1 as range, param2 as string) As String
Foo = "Hello world"
End Function
Public Function Foo_Help() as String
Foo_Help = "The Foo function was designed to return the Foo value for a specified range a cells given a specified constant." & CHR(10) & "Parameters:" & CHR(10)
& " param1 as Range : Specifies the range of cells the Foo function should operate on." & CHR(10)
&" param2 as String : Specifies the constant the function should use to calculate Foo"
&" contact the Foo master at master#foo.com for more information."
END FUNCTION
The carriage returns improve readability with wordwrap on. 2 birds with one stone, now the function has some documentation.
#will's method is the best. Just add few lines about the details for the people didn't use ExcelDNA before like me.
Download Excel-DNA IntelliSense from https://github.com/Excel-DNA/IntelliSense/releases
There are two version, one is for 64, check your Excel version. For my case, I'm using 64 version.
Open Excel/Developer/Add-Ins/Browse and select ExcelDna.IntelliSense64.xll.
Insert a new sheet, change name to "IntelliSense", add function description, as https://github.com/Excel-DNA/IntelliSense/wiki/Getting-Started
Then enjoy! :)
Unfortunately there is no way to add Tooltips for UDF Arguments.
To extend Remou's reply you can find a fuller but more complex approach to descriptions for the Function Wizard at
http://www.jkp-ads.com/Articles/RegisterUDF00.asp
I tried #ScottK's approach, first as a side feature of my functional UDF, then as a standalone _Help suffix version when I ran into trouble (see below). In hindsight, the latter approach is better anyway--more obvious to a user attentive enough to see a tool tip, and it doesn't clutter up the functional code.
I figured if an inattentive user just typed the function name and closed the parentheses while he thought it over, help would appear and he would be on his way. But dumping a bunch of text into a single cell that I cannot format didn't seem like a good idea. Instead, When the function is entered in a cell with no arguments i.e.
= interpolateLinear()
or
= interpolateLinear_Help()
a msgBox opens with the help text. A msgBox is limited to ~1000 characters, maybe it's 1024. But that's enough (barely 8^/) for my overly tricked out interpolation function. If it's not, you can always open a user form and go to town.
The first time the message box opened, it looked like success. But there are a couple of problems. First of course, the user has to know to enter the function with no arguments (+1 for the _Help suffix UDF).
The big problem is, the msgBox reopens several times in succession, spontaneously while working in unrelated parts of the workbook. Needless to say, it's very annoying. Sometimes it goes on until I get a circular reference warning. Go figure. If a UDF could change the cell formula, I would have done that to shut it up.
I don't know why Excel feels the need recalculate the formula over and over; neither the _Help standalone, nor the full up version (in help mode) has precedents or dependents. There's not an application.volatile statement anywhere. Of course the function returns a value to the calling cell. Maybe that triggers the recalc? But that's what UDFs do. I don't think you can not return a value.
Since you can't modify a worksheet formula from a UDF, I tried to return a specific string --a value --to the calling cell (the only one you can change the value of from a UDF), figuring I would inspect the cell value using application.caller on the next cycle, spot my string, and know not to re-display the help message. Seemed like a good idea at the time--didn't work. Maybe I did something stupid in my sleep-deprived state. I still like the idea. I'll update this when (if) I fix the problem. My quick fix was to add a line on the help box: "Seek help only in an emergency. Delete the offending formula to end the misery.
In the meantime, I tried the Application.MacroOptions approach. Pretty easy, and it looks professional. Just one problem to work out. I'll post a separate answer on that approach later.
A lot of dancing around the answer. You can add the UDF context help, but you have to export the Module and edit the contents in a text editor, then re-import it to VBA. Here's the example from Chip Pearson: Adding Code Attributes
In Excel, is there an early opt out AND function (also known as short-circuit evaluation)?
For example:
=AND(FALSE, #N/A)
Returns #N/A. If the function was an "early opt out", it would return FALSE as soon as the first FALSE was found, as no additional value could make the function ever return true.
Does such a function exist in excel?
What you're calling "early opt out" is more commonly called "short-circuit evaluation," and is generally a feature of languages in the C/C++/C#/Java family (but, notably, not Visual BASIC).
For Excel formulas, some logical functions practice short-circuit evaluation but some do not. AND does not, as you've found. Neither does OR--if the first argument in an OR expression is true, Excel will still try to evaluate the subsequent arguments.
The solution is to use nested IFs; evaluation of IFs goes step-by-step from outer to inner, branching as necessary, and stopping when there is no further nested formula to be evaluated. This produces the correct short-circuit behavior. So you can write your code above as
=IF(FALSE, IF(ISNA(<address of cell to test for the #N/A state>), TRUE), FALSE)
Another example that may be more clear:
Cell A1 contains the value 1 and cell A2 contains the formula "=1/0",
causing a #DIV/0 error.
Put this formula in A3:
=IF(A1 = 0, IF(A2 = 5, "Never get here"), "Short-circuit OK: A1 <> 0")
The function you're looking for does not exist in native Excel.
You could however, imitate it, e.g. using IFERROR:
=AND(FALSE,IFERROR(A1,FALSE))
(Work ins 2007 and beyond. In 2003, you need to use =IF(ISERROR(A1),FALSE,A1) instead of IFERROR(A1,FALSE).)
Alternatively, you could build a User Define Function:
Public Function EarlyAnd(var1 As Variant, ParamArray vars() As Variant) As Boolean
On Error GoTo Finalize
Dim blnTemp As Boolean
Dim varNext As Variant
If Not CBool(var1) Then GoTo Finalize
For Each varNext In vars
If Not CBool(varNext) Then GoTo Finalize
Next
blnTemp = True
Finalize:
EarlyAnd = blnTemp
End Function
Place this function in a module in the Visual Basic Editor. Now you can use =EarlyAnd(False,A1) in your Excel.
In Excel 2007, how do I add a description and parameter hints to a user-defined function? When I start typing a function invocation for a built-in function, Excel shows a description and parameter list--a tooltip. I'd like to do the same for the functions I define.
Not just for the formula insert wizard, but in the formula box, so if I key "=myFun(", at the "(" the tooltip pops up just like it does for "=average("
There's no help in VBA Help, none on MSDN and none on any of the Excel and VBA dedicated forums I can find, so this is clearly a long shot.
Not a tooltip solution but an adequate workaround:
Start typing the UDF =MyUDF( then press CTRL + Shift + A and your function parameters will be displayed. So long as those parameters have meaningful names you at-least have a viable prompt
For example, this:
=MyUDF( + CTRL + Shift + A
Turns into this:
=MyUDF(sPath, sFileName)
Professional Excel Development by
Stephen Bullen describes how to
register UDFs, which allows a
description to appear in the Function
Arguments dialog:
Function IFERROR(ByRef ToEvaluate As Variant, ByRef Default As Variant) As Variant
If IsError(ToEvaluate) Then
IFERROR = Default
Else
IFERROR = ToEvaluate
End If
End Function
Sub RegisterUDF()
Dim s As String
s = "Provides a shortcut replacement for the common worksheet construct" & vbLf _
& "IF(ISERROR(<expression>), <default>, <expression>)"
Application.MacroOptions macro:="IFERROR", Description:=s, Category:=9
End Sub
Sub UnregisterUDF()
Application.MacroOptions Macro:="IFERROR", Description:=Empty, Category:=Empty
End Sub
From: http://www.ozgrid.com/forum/showthread.php?t=78123&page=1
To show the Function Arguments dialog, type the function name and press CtrlA. Alternatively, click the "fx" symbol in the formula bar:
I know you've accepted an answer for this, but there's now a solution that lets you get an intellisense style completion box pop up like for the other excel functions, via an Excel-DNA add in, or by registering an intellisense server inside your own add in. See here.
Now, i prefer the C# way of doing it - it's much simpler, as inside Excel-DNA, any class that implements IExcelAddin is picked up by the addin framework and has AutoOpen() and AutoClose() run when you open/close the add in. So you just need this:
namespace MyNameSpace {
public class Intellisense : IExcelAddIn {
public void AutoClose() {
}
public void AutoOpen() {
IntelliSenseServer.Register();
}
}
}
and then (and this is just taken from the github page), you just need to use the ExcelDNA annotations on your functions:
[ExcelFunction(Description = "A useful test function that adds two numbers, and returns the sum.")]
public static double AddThem(
[ExcelArgument(Name = "Augend", Description = "is the first number, to which will be added")]
double v1,
[ExcelArgument(Name = "Addend", Description = "is the second number that will be added")]
double v2)
{
return v1 + v2;
}
which are annotated using the ExcelDNA annotations, the intellisense server will pick up the argument names and descriptions.
There are examples for using it with just VBA too, but i'm not too into my VBA, so i don't use those parts.
Also you can use, this Macro to assign Descriptions to arguments and the UDF:
Private Sub RegisterMyFunction()
Application.MacroOptions _
Macro:="SampleFunction", _ '' Your UDF name
Description:="calculates a result based on provided inputs", _
Category:="My UDF Category", _ '' Or use numbers, a list in the link below
ArgumentDescriptions:=Array( _ '' One by each argument
"is the first argument. tell the user what it does", _
"is the second argument. tell the user what it does")
End Sub
Credits to Kendall and the original post here.
For the UDF Categories
I just create a "help" version of the function. Shows up right below the function in autocomplete - the user can select it instead in an adjacent cell for instructions.
Public Function Foo(param1 as range, param2 as string) As String
Foo = "Hello world"
End Function
Public Function Foo_Help() as String
Foo_Help = "The Foo function was designed to return the Foo value for a specified range a cells given a specified constant." & CHR(10) & "Parameters:" & CHR(10)
& " param1 as Range : Specifies the range of cells the Foo function should operate on." & CHR(10)
&" param2 as String : Specifies the constant the function should use to calculate Foo"
&" contact the Foo master at master#foo.com for more information."
END FUNCTION
The carriage returns improve readability with wordwrap on. 2 birds with one stone, now the function has some documentation.
#will's method is the best. Just add few lines about the details for the people didn't use ExcelDNA before like me.
Download Excel-DNA IntelliSense from https://github.com/Excel-DNA/IntelliSense/releases
There are two version, one is for 64, check your Excel version. For my case, I'm using 64 version.
Open Excel/Developer/Add-Ins/Browse and select ExcelDna.IntelliSense64.xll.
Insert a new sheet, change name to "IntelliSense", add function description, as https://github.com/Excel-DNA/IntelliSense/wiki/Getting-Started
Then enjoy! :)
Unfortunately there is no way to add Tooltips for UDF Arguments.
To extend Remou's reply you can find a fuller but more complex approach to descriptions for the Function Wizard at
http://www.jkp-ads.com/Articles/RegisterUDF00.asp
I tried #ScottK's approach, first as a side feature of my functional UDF, then as a standalone _Help suffix version when I ran into trouble (see below). In hindsight, the latter approach is better anyway--more obvious to a user attentive enough to see a tool tip, and it doesn't clutter up the functional code.
I figured if an inattentive user just typed the function name and closed the parentheses while he thought it over, help would appear and he would be on his way. But dumping a bunch of text into a single cell that I cannot format didn't seem like a good idea. Instead, When the function is entered in a cell with no arguments i.e.
= interpolateLinear()
or
= interpolateLinear_Help()
a msgBox opens with the help text. A msgBox is limited to ~1000 characters, maybe it's 1024. But that's enough (barely 8^/) for my overly tricked out interpolation function. If it's not, you can always open a user form and go to town.
The first time the message box opened, it looked like success. But there are a couple of problems. First of course, the user has to know to enter the function with no arguments (+1 for the _Help suffix UDF).
The big problem is, the msgBox reopens several times in succession, spontaneously while working in unrelated parts of the workbook. Needless to say, it's very annoying. Sometimes it goes on until I get a circular reference warning. Go figure. If a UDF could change the cell formula, I would have done that to shut it up.
I don't know why Excel feels the need recalculate the formula over and over; neither the _Help standalone, nor the full up version (in help mode) has precedents or dependents. There's not an application.volatile statement anywhere. Of course the function returns a value to the calling cell. Maybe that triggers the recalc? But that's what UDFs do. I don't think you can not return a value.
Since you can't modify a worksheet formula from a UDF, I tried to return a specific string --a value --to the calling cell (the only one you can change the value of from a UDF), figuring I would inspect the cell value using application.caller on the next cycle, spot my string, and know not to re-display the help message. Seemed like a good idea at the time--didn't work. Maybe I did something stupid in my sleep-deprived state. I still like the idea. I'll update this when (if) I fix the problem. My quick fix was to add a line on the help box: "Seek help only in an emergency. Delete the offending formula to end the misery.
In the meantime, I tried the Application.MacroOptions approach. Pretty easy, and it looks professional. Just one problem to work out. I'll post a separate answer on that approach later.
A lot of dancing around the answer. You can add the UDF context help, but you have to export the Module and edit the contents in a text editor, then re-import it to VBA. Here's the example from Chip Pearson: Adding Code Attributes
I am having trouble getting VBA's Evaluate() function to only execute once; it seems to always run twice. For instance, consider the trivial example below. If we run the RunEval() subroutine, it will call the EvalTest() function twice. This can be seen by the two different random numbers that get printed in the immediate window. The behavior would be the same if we were calling another subroutine with Evaluate instead of a function. Can someone explain how I can get Evaluate to execute the target function once instead of twice? Thank you.
Sub RunEval()
Evaluate "EvalTest()"
End Sub
Public Function EvalTest()
Debug.Print Rnd()
End Function
This bug only seems to happen with UDFs, not with built-in functions.
You can bypass it by adding an expression:
Sub RunEval()
ActiveSheet.Evaluate "0+EvalTest()"
End Sub
But there are also a number of other limitations with Evaluate, documented here
http://www.decisionmodels.com/calcsecretsh.htm
I don't know of a way to stop it, but you can at least recognize when it is happening most of the time. That could be useful if your computation is time consuming or has side effects that you don't want to have happen twice and you want to short circuit it.
(EDIT: Charles Williams actually has an answer to your specific quesion. My answer could still be useful when you don't know what data type you might be getting back, or when you expect to get something like an array or a range.)
If you use the Application.Caller property within a routine called as a result of a call to Application.Evaluate, you'll see that one of the calls appears to come from the upper left cell of of the actual range the Evaluate call is made from, and one from cell $A$1 of the sheet that range is on. If you call Application.Evaluate from the immediate window, like you would call your example Sub, one call appears to come from the upper left cell of the currently selected range and one from cell $A$1 of the current worksheet. I'm pretty sure it's the first call that's the $A$1 in both cases. (I'd test that if it matters.)
However, only one value will ever be returned from Application.Evaluate. I'm pretty sure it's the one from the second eval. (I'd test that too.)
Obviously, this won't work with calls made from the actual cell $A$1.
(As for me, I would love to know why the double evaluation happens. I would also love to know why the evaluator is exposed at all. Anyone?)
EDIT: I asked on StackOverflow here: Why is Excel's 'Evaluate' method a general expression evaluator?
I hope this helps, although it doesn't directly answer your question.
I did a quick search and found that others have reported similar behavior and other odd bugs with Application.Evaluate (see KB823604 and this). This is probably not high on Microsoft's list to fix since it has been seen at least since Excel 2002. That knowledge base article gives a workaround that may work in your case too - put the expression to evaluate in a worksheet and then get the value from that, like this:
Sub RunEval()
Dim d As Double
Range("A1").Formula = "=EvalTest()"
d = Range("A1").Value
Range("A1").Clear
Debug.Print d
End Sub
Public Function EvalTest() As Double
Dim d As Double
d = Rnd()
Debug.Print d
EvalTest = d + 1
End Function
I modified your example to also return the random value from the function. This prints the value a second time but with the one added so the second print comes from the first subroutine. You could write a support routine to do this for any expression.
I face the same problem, after investigation i found the function called twice because i have drop down list and the value used in a user defined function.
working around by the code bellow, put the code in ThisWorkbook
Private Sub Workbook_Open()
'set the calculation to manual to stop calculation when dropdownlist updeated and again calculate for the UDF
Application.Calculation = xlCalculationManual
End Sub
Private Sub Workbook_SheetChange(ByVal Sh As Object, _
ByVal Source As Range)
'calculte only when the sheet changed
Calculate
End Sub
It looks like Application.Evaluate evaluates always twice, while ActiveSheet.Evaluate evaluates once if it is an expression.
When the object is not specified Evaluate is equivalent to Application.Evaluate.
Typing [expression] is equivalent to Application.Evaluate("expression").
So the solution is to add ActiveSheet and to make that an expression by adding zero:
ActiveSheet.Evaluate("EvalTest+0")
After seeing there is no proper way to work around this problem, I solved it by the following:
Dim RunEval as boolean
Sub RunEval()
RunEval = True
Evaluate "EvalTest()"
End Sub
Public Function EvalTest()
if RunEval = true then
Debug.Print Rnd()
RunEval = False
end if
End Function
problem solved everyone.