AndAlso/OrElse in VBA - vb.net

I'm trying to get a lazy evaluation with 'And' in my Excel macro by doing the following:
If Not myObject Is Nothing *And* myObject.test() Then
'do something'
Else
'do something else'
End If
I know lazy evaluation exists in VB.NET as AndAlso and OrElse but cannot find anything similar in VBA. If lazy evaluation does not exist in VBA, what's the best way to structure the code so that it will evaluate the way I expect?

The only short circuiting (of a sort) is within Case expression evaluation, so the following ungainly statement does what I think you're asking;
Select Case True
Case (myObject Is Nothing), Not myObject.test()
MsgBox "no instance or test == false"
Case Else
MsgBox "got instance & test == true"
End Select
End Sub

This is an old question, but this issue is still alive and well. One workaround I've used:
Dim success As Boolean ' False by default.
If myObj Is Nothing Then ' Object is nothing, success = False already, do nothing.
ElseIf Not myObj.test() Then ' Test failed, success = False already, do nothing.
Else: success = True ' Object is not nothing and test passed.
End If
If success Then
' Do stuff...
Else
' Do other stuff...
End If
This basically inverts the logic in the original question, but you get the same result. I think it's a cleaner solution than the others here that only use If statements. The solution using a Select statement is clever, but if you want an alternative using only If statements, I think this is the one to use.

Or you could create a function that takes your object as a parameter and returns boolean for either case. That's what I usually to.
i.e.
if Proceed(objMyAwesomeObject) then
'do some really neat stuff here
else
'do something else, eh
end if
...
end sub
private function Proceed(objMyAwesomeObject as Object)
if not objMyAweseomeObject is nothing then
Proceed = true
elseif objMyAwesomeObject.SomeProperty = SomeValue then
Proceed = true
else
Proceed = false
endif
end function

Improving on this answer to a different question about the same basic problem, here is what I chose to do:
dim conditionsValid as boolean
conditionsValid = myObject Is Nothing
if conditionsValid then conditionsValid = myObject.test()
if conditionsValid then conditionsValid = myObject.anotherTest()
if conditionsValid then
'do something'
else
'do something else'
end if
I think this code is clearer than the other answers that have been suggested, and you (usually) don't need a different variable for each validation, which is the improvement over the original answer to the other question. By the way, each new condition you need adds just one more line of code.

If Not myObject Is Nothing Then
If myObject.test() Then
'do something'
End If
Else
'do something else'
End If
I think that's the way you have to do it.
Edit
Maybe like this
Dim bTestsFailed as Boolean
bTestsFailed = False
If Not myObject Is Nothing Then
If myObject.test() Then
'do something'
Else
bTestsFailed = True
End If
Else
bTestsFailed = True
End If
If bTestsFailed Then
'do something else
End If
Isn't VBA great?

A trick around missing values may help:
Dim passed, wrongMaxPercent, wrongPercent, rightMinPercent, rightPercent
wrongPercent = 33
rightPercent = 55
'rightMinPercent = 56
wrongMaxPercent = 40
passed = (Len(wrongMaxPercent) = 0 Or wrongPercent < wrongMaxPercent) And _
(Len(rightMinPercent) = 0 Or rightPercent >= rightMinPercent)

Since the following syntax works
If myObject.test() Then do something
Then the one liner syntax could be used to short circuit the evaluation. Below, the first If statement ensures that myObject is something. Otherwise, it won't even try to evaluate the second If.
If Not myObject Is Nothing Then If myObject.test() Then
'do something'
Else
'do something else'
End If
Of course, if you want 'do something else' if myObject Is Nothing, then this may not work.
Update 2020/06/30
After a comment pointed out that this answer didn't work, I have verified that the syntax does not appear to work in modern VBA. Leaving original answer for legacy purposes.

One can switch the logical condition, working with the Or operator and switch off error messages like this:
Err.Clear
On Error Resume Next
If myObject Is Nothing Or Not myObject.test() Then
Err.Clear
'do something else'
Else
'do something'
End If
On Error Goto 0 ' or On Error Goto ErrorHandler (depending on previous setting in the code)
The test for Nothing is not necessary - it only serves to clarify what is meant.

I don't know any equivalent for OrElse, but there is a limited but useful solution for AndAlso. The following two are equivalent:
vb.net: If Condition1 AndAlso Condition2 Then DoSomething
vba: If Condition1 Then If Condition2 Then DoSomething
The are two limitations:
It only works as a one liner, cannot be used to start an if-block
The Else block is executed if the first condition is true and the second is false, not when the first condition is false
Even considering these two fairly crippling limitations, I often use this little trick when I don't have an Else block.
Here is an example:
Sub Test()
Dim C As Collection
' This is what I often use
If Not C Is Nothing Then If C.Count Then DoSomethingWith C
' Here are other usages I stay away from, because of bad readability
If Not C Is Nothing Then If C.Count Then Debug.Print "not empty" Else Debug.Print "empty or nothing"
Set C = New Collection
If Not C Is Nothing Then If C.Count Then Debug.Print "not empty" Else Debug.Print "empty or nothing"
C.Add 1
If Not C Is Nothing Then If C.Count Then Debug.Print "not empty" Else Debug.Print "empty or nothing"
End Sub```

We might want to say:
If XX < 7 Then
XX = 19
but XX might be Null, so we have to test.
We can do this:
If Switch ( IsNull( XX ) , True, True, XX < 7 ) Then
XX = 19
So, if XX is Null, we drop through to do the assignment, otherwise only do it if the test, XX < 7, is True.

Related

Differentiate between entering 0 and pressing cancel in application.inputbox

I have a macro which takes a value from Application.InputBox and then tries to ensure that a numerical value has been put into it, and that cancel has not been pressed.
Sub test()
Dim testvalue As Variant
testvalue = Application.InputBox(Prompt:="Whatever", Title:="Whatever", Default:=10, Type:=1)
If testvalue <> False Then
MsgBox ("Test != False")
Else
MsgBox ("Test = False")
End If
If VarType(testvalue) = vbInteger Or VarType(testvalue) = vbLong Then
MsgBox ("Numerical value")
Else
MsgBox ("Nonnumerical value")
End If
End Sub
However, it seems to have trouble differentiating between having 0 put into the InputBox and cancel being pressed. In both cases, the if-statements will return that Test = False and that Test is a nonnumerical value. To be fair, the second if-statements does the same for other integers as well, but I thought there might be some small chance of differentiating between integers stored in the variant and a boolean value stored in it using the VarType method.
Is there any way to differentiate between 0 and cancel being the return-value for the inputbox, or will I have to make my own custom form?
Trying to use VBA's InputBox as suggested in the comments returns "String", no matter what value is entered into it, or what button is pressed:
Sub test2()
Dim testvalue As Variant
testvalue = InputBox("Test")
Select Case VarType(testvalue)
Case vbBoolean:
Debug.Print "Boolean"
Case vbInteger:
Debug.Print "Integer"
Case vbLong:
Debug.Print "Long"
Case vbString:
Debug.Print "String"
Case vbVariant:
Debug.Print "Variant"
End Select
End Sub
Apparently double is a numerical type as well >_> Chalk this one up to me being stupid, checking for the wrong thing, and making the wrong assumptions. Thanks to rory in the comments for getting me to check further on what types the various functions returned.
To be more explicit, when a numerical value is entered into Application.InputBox, it returns a Double, so that is what I should check for when wanting to ensure a numerical value.
Instead of VarType you can use StrPtr which will only give 0 if the input box was cancelled, although I'm not sure how it reacts to Application.InputBox(). If you are using just the standard InputBox() however it works perfectly, therefore to detect a cancelled InputBox you would add this into your code:
If StrPtr(testvalue) = 0 Then
Call MsgBox("Input cancelled.", "Cancelled")
End If
*Something to be aware of when using this method is that it relies on buggy behaviour in InputBox. Microsoft aren't going to fix it as it would break a lot of people's code but it is something to take note of.
You could adjust your type to 1+4 and check for the Vartype
Sub test()
Dim testvalue As Variant
testvalue = Application.InputBox(Prompt:="Whatever", Title:="Whatever", Default:=10, Type:=5)
If VarType(testvalue) <> vbBoolean Then
MsgBox ("Test != False")
Else
MsgBox ("Test = False")
End If
End Sub

My VBA for loop in MS word 2016 is not working

I having trouble with the following code in MS word VBA
For i = 6 To ActiveDocument.Tables(2).Rows.Count
z = Len(ActiveDocument.Tables(2).Cell(i, 2).Range.Text) - 2
x = Len(ActiveDocument.Tables(2).Cell(i, 3).Range.Text) - 2
b = ActiveDocument.Tables(2).Cell(i, 5).Range.ContentControls.Item(1).ShowingPlaceholderText
If (z = 0 And x = 0) Then
If b = True Then
MsgBox "Please do error 1!"
If vbOK Then
Exit Sub
End If
Else
MsgBox "Please do error 2!"
If vbOK Then
Exit Sub
End If
End If
Else
If b = True Then
MsgBox "Please do error 3!"
If vbOK Then
Exit Sub
End If
Else
Confirm = MsgBox("Are you sure to submit?", vbYesNo, "Confirmation")
If Confirm = vbNo Then
Exit Sub
End If
End If
End If
Next i
The for loop won't go into the second line to check if z or x is having value or not
I doubt moving Next i would have solved anything. This code is riddled with badness.
My impression is that your code is intended to check three columns in a table (from rows 6 downwards) - this appears to be a consistency check.
Naming. z, x and b are not very descriptive. Using names like lengthCol2, lengthCol3 and hasPlaceHolderText will help you follow your logic more closely.
Use Option Explicit. Always.
You use a standard MsgBox call, which by default only has a single button ("OK"). The MsgBox is a blocking code element, so the macro will not progress until the user has clicked "OK".
vbOK is an enumerated value (value = 1). So If vbOK then always comes out true. Always. You appear to be seeking some sort of user input, but you are not clear on what that input is.
Address these simple steps gives us:
For i = 6 To ActiveDocument.Tables(2).Rows.Count
lengthCol2 = Len(ActiveDocument.Tables(2).Cell(i, 2).Range.Text) - 2
lengthCol3 = Len(ActiveDocument.Tables(2).Cell(i, 3).Range.Text) - 2
hasPlaceHolderText = ActiveDocument.Tables(2).Cell(i, 5).Range.ContentControls.Item(1).ShowingPlaceholderText
If (lengthCol2 = 0 And lengthCol3 = 0) Then
If hasPlaceHolderText = True Then
MsgBox "Please do error 1!"
Exit Sub
Else
MsgBox "Please do error 2!"
Exit Sub
End If
Else
If hasPlaceHolderText = True Then
MsgBox "Please do error 3!"
Exit Sub
Else
Confirm = MsgBox("Are you sure to submit?", vbYesNo, "Confirmation")
If Confirm = vbNo Then
Exit Sub
End If
End If
End If
Next i
Your logic is negative-biased - that is, intending to find reasons not to do something than to do something. Positive-biased logic is usually easier to understand and maintain - the coder's intent is clearer.
Rewording the logic gives us:
For i = 6 To ActiveDocument.Tables(2).Rows.Count
lengthCol2 = Len(ActiveDocument.Tables(2).Cell(i, 2).Range.Text) - 2
lengthCol3 = Len(ActiveDocument.Tables(2).Cell(i, 3).Range.Text) - 2
hasPlaceHolderText = ActiveDocument.Tables(2).Cell(i, 5).Range.ContentControls.Item(1).ShowingPlaceholderText
If (lengthCol2 > 0 OR lengthCol3 > 0) AND hasPlaceHolderText Then
Confirm = MsgBox("Are you sure to submit?", vbYesNo, "Confirmation")
If Confirm = vbYes Then
'Do submission code here - or call the submission procedure
End If ' Just do nothing if they say "No" - this is what your current code does.
Else
' The next line could be used instead of the nested IF-the-else statements following.
'MsgBox " Table contents are not valid, please ensure columns 2,3 and 5 are completed"
If hasPlaceHolderText then
If (lengthCol2 = 0 And lengthCol3 = 0) Then
MsgBox "Please do error 1!"
Else
MsgBox "Please do error 2!"
EndIF
Else
MsgBox "Please do error 3!"
End If
End If
Next i
Note that in your logic, either Column 2 or Column 3 can be empty and (as long as placeholder text not being shown) you document is ready for submission. Perhaps you meant AND instead of OR (i.e. all columns should be filled).
There is still one problem. Your loop. As currently written, you loop over the logic, thus you ask the user to either check errors or submit the document x number of times based on error checking in each row. But, just moving the Next i does not solve the problem because the only results that are retained are those in the last row. In other words, all the previous rows could be bad/invalid, but you would still be able to submit.
We can fix this last bit by creating cumulative logic. In other words, we track the errors in a short loop, then go into the main logic. This seems to be a little more complex but it really is relative straight forwards. But, we do need more Booleans to make it work.
Dim rowsOK as Boolean
'explicit initialisation - I am working on a positive bias here.
rowsOK = True
For i = 6 To ActiveDocument.Tables(2).Rows.Count
Dim lengthCol2OK as Boolean ' Use these just to make the logic clearer and the code cleaner
Dim lengthCol3OK as Boolean
Dim hasPlaceHolderTextOK as Boolean
lengthCol2OK = Len(ActiveDocument.Tables(2).Cell(i, 2).Range.Text) > 2
lengthCol3OK = Len(ActiveDocument.Tables(2).Cell(i, 3).Range.Text) > 2
hasPlaceHolderTextOK = ActiveDocument.Tables(2).Cell(i, 5).Range.ContentControls.Item(1).ShowingPlaceholderText
rowsOK = rowsOK And ((lengthCol2OK Or lengthCol3OK) And hasPlaceHolderTextOK) ' Note: Using "Or" here as per original code logic
' Extra logic could go here to message the user if any of the above are false.
Next i
If rowsOK Then
Confirm = MsgBox("Are you sure to submit?", vbYesNo, "Confirmation")
If Confirm = vbYes Then
'Do submission code here - or call the submission procedure
End If ' Just do nothing if they say "No" - this is what your current code does.
Else
MsgBox " Table contents are not valid, please ensure columns 2,3 and 5 are completed"
End If
However, this logic works on all the rows, so identifying individual row errors in not possible in the main loop. You could work extra logic in the For-Next loop to message the user for errors.
Now the code is maintainable, and more likely does what you want.
Key points:
Use Option Explicit. This prevents typos and ensures that you are using variables in they way you intend.
Use meaningful variable names. Makes it easier to follow what you want to do.
Don't confuse enumerated values with returns from functions. Don't confuse constants with variables.
Take some time to review your logic chains to ensure they do what you want to do, rather than not do what you don't want to do. The latter has greater chance of missing a non-valid path.

Do nothing in vba

Is there an equivalent to python "pass" in VBA to simply do nothing in the code?
for example:
For Each ws In ThisWorkbook.Sheets
If ws.Name = "Navy Reqs" Then
ws.Select
nReqs = get_num_rows
Cells(1, 1).Select
If ActiveSheet.AutoFilterMode Then Cells.AutoFilter
Selection.AutoFilter
ElseIf ws.Name = "temp" Then
pass
Else
ws.Select
nShips = get_num_rows
End If
Next
I get an error here that pass is not defined. Thanks.
just remove pass and re run the code. VBA will be happy to accept that I believe
Don't include any statements:
Sub qwerty()
If 1 = 3 Then
Else
MsgBox "1 does not equal 3"
End If
End Sub
Just leave it blank. You can also use a Select statement, it's easier to read.
For Each ws In ThisWorkbook.Sheets
Select Case ws.Name
Case "Navy Reqs":
'...
Case "temp":
'do nothing
Case Else:
'...
End Select
Next
Write code that does what it says, and says what it does.
For Each ws In ThisWorkbook.Sheets
If ws.Name = "Navy Reqs" Then
ws.Select
nReqs = get_num_rows
Cells(1, 1).Select
If ActiveSheet.AutoFilterMode Then Cells.AutoFilter
Selection.AutoFilter
Else If ws.Name <> "temp" Then
ws.Select
nShips = get_num_rows
End If
Next
That's all you need. An instruction that means "here's some useless code" does not exist in VBA.
You want comments that say why, not what - a comment that says 'do nothing is the exact opposite of that. Don't write no-op code, it's pure noise.
Assuming Python's pass works like C#'s continue statement and skips to the next iteration, then the VBA equivalent is the one and only legitimate use of a GoTo jump:
For ...
If ... Then GoTo Skip
...
Skip:
Next
This code shows an IF test that keeps searching unless it gets a match.
Function EXCAT(Desc)
Dim txt() As String
' Split the string at the space characters.
txt() = Split(Desc)
For i = 0 To UBound(txt)
EXCAT = Application.VLookup(txt(i), Worksheets("Sheet1").Range("Dept"), 2, False)
If IsError(EXCAT) Then Else Exit Function
Next
' watch this space for composite word seach
EXCAT = "- - tba - -"
End Function
I coded in COBOL for many years and the equivalent 'do nothing' statement is NEXT SENTENCE.
In VBA, I find myself creating a dummy variable (sometimes a global) dim dummy as integer and then when I need that 'do nothing' action in an If..Then..Else I put in a line of code: dummy = 0.
This is actually a legit question. I want to run a debug routine that stops when a certain critical value is, say, 8, i.e., set break point at x = 8 and then step through line by line. So the following construct is useful:
Select Case x
Case 21
'do nothing
Case 8
'do nothing
Case 14
'do nothing
Case 9
'do nothing
End Select
Since you can't put a break point on a comment, an actual statement is needed.
You also can't put a break point on the Case statements because that gets executed every time.
Obviously anything here is OK e.g., x=x, but it would be nice to have something formal like pass.
But I've been using x=x.
Most languages have a "null" or "empty" statement like Python's pass. These statements have evolved because they are practically useful, and in some grammars, necessary. For example, as others have suggested, null statements can serve as side-effect free anchors for a debugger.
I use:
Debug.Assert True
Note that if your use case is to set a conditional breakpoint, you may find Stop more useful, as in:
If targetShape.Type = msoAutoShape Then
Stop
End

Resume next index in a for If a condition is true VBA

I want to resume the next index in a for if a condition is granted
The code i'm trying to use looks like this:
For i = 0 to 10
If condition is true then
Next i
else
'code
end if
Next i
Any help is welcome
Just:
For i = 0 to 10
If condition is False then
'code
end if
Next i
If the condition is True then you will just skip to the next i automatically
If the situation is more complicated, because of VBA's lack of a Continue construct, you would need to use a GoTo:
For i = 0 to 10
'code
If condition is true then
'more code
GoTo LoopBottom
else
'still more code
end if
'even more code
LoopBottom:
Next i
Why not use Not in that if statement?
For i = 0 to 10
If Not condition is true Then
'code
End If
Next i
Though John Coleman's answer is better if you wish to have a more complicated statement. His answer would also be more readable.

Include code based on ToC existing?

This is more of a general question on how to implement my code, than an issue with the code itself.
I have some code that scans through a Word Document, looks for some words, and puts them in an index. This document may or may not have a Table of Contents.
If there's a ToC, I want my macro to skip the text in that range, so I have a line like this:
Sub find_things()
Dim myDoc as Word.Document
Dim otherVariables here
[some preliminary code]
' the next line checks to see if the current range is in a ToC
If rngXE.InRange(myDoc.TablesOfContents(1).Range) = False Then
[code here will find my text, and do some things]
End If
End Sub
So, hopefully you can see that if the rngXE range is NOT in a table of contents, run some code.
However, since sometimes there's no ToC, I get an error raised at the If statement line. ("The requested member of the collection does not exist").
Question: What's the best way to handle this? Should I do On Error Resume Next before that? So if there's an error, it will continue on?
Or, is there some way to count the Tables of Contents, and if it's 0, then don't include the If statement lines?
Sub find_things()
Dim myDoc as Word.Document
Dim otherVariables here
[some preliminary code]
' the next line checks to see if the current range is in a ToC
If myDoc.TablesofContents.Count > 0 Then
If rngXE.InRange(myDoc.TablesOfContents(1).Range) = False Then
[code here will find my text, and do some things]
End If
End If
End Sub
...except if I use this, it's always going to skip my code when there's no TOC. I'd rather not use On Error Goto since I've had it drilled into my head that GoTo is generally a bad idea...but that may be the best option, no?
I hope this is clear at all - kinda hard to verbalize the issue but please let me know if I can clarify anything!
edit: The way I'm handling this now, is if I get that Error, I just Comment out the If rngXE.InRange(myDoc...) = False and corresponding End If lines, and it runs perfectly. I am looking for an automatic way to do this, rather than my personal intervention.
Two fairly simple patterns you could use without resorting to either "On Error Goto" or "Goto" are:
If myDoc.TablesofContents.Count = 0 Then
call asubtodothework(rngXE) ' you may need to pass more parameters...
elseif not rngXE.InRange(myDoc.TablesOfContents(1).Range)
call asubtodothework(rngXE) ' ditto
end if
Dim bDoIt As Boolean
bDoIt = True
If myDoc.TablesofContents.Count > 0 Then
If rngXE.InRange(myDoc.TablesOfContents(1).Range) Then
bDoIt = False
End If
End If
If bDoIt Then
' do the work
End If
(As they stand, both make the same assumption as your existing code, i.e. that you only need to check for TableOfContents(1). My guess is that if you need to check for more ToCs, somewhere you will end up with some code rather like the second pattern anyway).
or even the following...
Dim bDoIt As Boolean
bDoIt = True
If myDoc.TablesofContents.Count > 0 Then
bDoIt = rngXE.InRange(myDoc.TablesOfContents(1).Range)
End If
If bDoIt Then
' do the work
End If
...or perhaps a single-liner would work (I haven't actually checked the logic, and I have a personal preference for using the full If...End If construct anyway)...
Dim bDoIt As Boolean
bDoIt = True
If myDoc.TablesofContents.Count > 0 Then bDoIt = rngXE.InRange(myDoc.TablesOfContents(1).Range)
If bDoIt Then
' do the work
End If