In Excel, is there an early opt out AND/OR functions (Short Circuit Evaluation)? - excel

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.

Related

Can you access a VBA list with an in-cell Excel formula?

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

Trying to insert variable in match function in vba

I am getting an error while running this code, cannot understand what's wrong with this:
i = ActiveCell.End(xlUp).Offset(0, 1).Value
ActiveCell.Offset(0, 1).Value = Application.WorksheetFunction.VLookup(r, source, WorksheetFunction.Match("i", msource, 0), 0)
Application.WorksheetFunction invokes worksheet functions, but in the context of VBA code: normally in VBA, when a function throws an error, it comes in the form of a run-time error, and that's exactly what these functions do.
So you have two options.
One, handle (or swallow) VBA runtime errors:
On Error Resume Next
ActiveCell.Offset(1, 0).Value = Application.WorksheetFunction.VLookup(...)
On Error GoTo 0
Like this, when the right-hand side of the assignment throws an error (i.e. when the lookup fails), the left-hand side won't be affected and the target cell keeps whatever value it had before. Note the On Error GoTo 0, which restores runtime errors. This is critically important; without it On Error Resume Next will have your code running in some unhandled error state, and that is the single best way to hide bugs and make them pretty much impossible to diagnose later.
Two, use the late-bound version.
When invoked directly against Application, worksheet functions are late-bound. You don't get compiler assistance so watch out for any typos and make sure the parameters are good (you don't get a tooltip with a parameters list for late-bound member calls).
ActiveCell.Offset(1, 0).Value = Application.VLookup(...)
It's the same VLookup function, but now it behaves like it would on a worksheet, returning an error value instead of throwing a run-time error like a VBA function would. That makes the target cell value hold a #N/A worksheet error when the lookup fails.
Same applies to the inner Match function; now while nesting worksheet functions is how we do things in a worksheet cell's formula, doing that in code makes everything much harder than necessary - split things up, evaluate the Match separately, validate whether it returned an error value, then pass it to VLookup only if it didn't.
Dim matchResult As Variant
matchResult = Application.Match(i, msource, 0)
If IsError(matchResult) Then Exit Sub
Note that your code is passing the literal string value "i" to the Match function; you probably intend to pass the value of the i variable: you must remove the double quotes around it to do that.

Excel VBA WorksheetFunction.IsError false positive

In VBA, I am trying to determine whether or not a cell contains an error value, e.g. due to an invalid function. My existing code uses the Excel.WorksheetFunction.IsError method, but I recently came across a case which caused a false positive. The cell does not contain an error value, but Excel.WorksheetFunction.IsError returns true. Another method, VBA.Information.IsError, does not exhibit this behavior; it correctly returns false.
I was able to determine that the issue only occurs when the cell contains a value exceeding 255 characters in length. Here is a routine to verify the behavior:
Sub IsErrorBehavior()
Dim i As Long
Dim str As String
Dim r1 As Range, r2 As Range
Dim xlWSFuncCheck1 As Boolean, xlWSFuncCheck2 As Boolean
Dim vbaInfCheck1 As Boolean, vbaInfCheck2 As Boolean
str = WorksheetFunction.Rept("A", 255)
Set r1 = Range("A1")
r1.Value = str
xlWSFuncCheck1 = Excel.WorksheetFunction.IsError(r1)
vbaInfCheck1 = VBA.Information.IsError(r1)
str = str & "A"
Set r2 = Range("A2")
r2.Value = str
xlWSFuncCheck2 = Excel.WorksheetFunction.IsError(r2)
vbaInfCheck2 = VBA.Information.IsError(r2)
End Sub
The above routine was written in Excel 2010 and verified in Excel 2007. The target application is currently running under Excel 2007.
Question 1: Is this a bug or is there a reasonable explanation for the behavior of Excel.WorksheetFunction.IsError in this case?
I will be switching to the VBA.Information.IsError method, but now I'm a bit concerned that I may encounter bugs with it as well. Which leads me to...
Question 2: Is there a more reliable way to check for errors in a specific cell?
Not a complete response for Excel 2007, but might give some ideas for other peers to follow up.
Question 1: Is this a bug or is there a reasonable explanation for the behavior of Excel.WorksheetFunction.IsError in this case?
There used to be an old known bug with the 255 character limit that was supposed to be fixed in newer versions. Your issue is of very similar nature so it could be the same source, although it is not possible to say for sure.
It seems that setting a Value of a Range equal to a String of more than 255 characters corrupts the Range itself, and makes the IsError check on the Range false.
Question 2: Is there a more reliable way to check for errors in a specific cell?
I would do it as in CPearson's website, namely checking r2.Value instead of r2, and using the IsError function (which is the VBA IsError) that has more stable performance. In general, checking a Range object for error might be tricky. In this question a while ago I was getting funny results because of type casting that goes on in the background. I guess this might be the case here as well. In any case, being explicit helps (your example works as expected if r2.value is used instead).
I encountered this error, too, and in our case it was independent of the use or omission of ".value".
Suppose, in cell A1, you enter
=1-BINOM.DIST(35, 76, 0.1, TRUE)
The resulting probability is minuscule, so Excel will return 0.000000 as the cell value.
However, when you then use VBA to test
IsError(ActiveSheet.Range("A1").Value)
VBA will return TRUE. Probably this is due to some internal rounding error checking where Excel realizes that the return value of the formula is to close to zero to be correctly calculated.
This is an unfortunate situation since Excel will display the return value as a cell value without any error or warning, while at the same time VBA will return TRUE on IsError(). I don't know whether this bug is present in other Excel functions as well, such as
=1-NORM.S.VERT(42, TRUE)
but would advise caution when working with IsError().

Excel Select Case?

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.

Stop VBA Evaluate from calling target function twice

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.

Resources