Is there a better way to write the following VB6 snippet? - vb6

I work at $COMPANY and I'm helping maintain $LEGACY_APPLICATION. It's written in visual basic 6.
I was faced with doing an unpleasantly elaborate nested if statement due to the lack of VB6's ability to perform short circuit evaluations in if statements (which would simplify this a lot). I've tried AndAlso, but to no avail. Must be a feature added after VB6.
Some genius on SO somewhere pointed out that you can trick a select case statement into working like a short-circuiting if statement if you have the patience, so I tried that, and here's what I came up with:
Select Case (True) ' pretend this is an if-else statement
Case (item Is Nothing): Exit Sub ' we got a non-element
Case ((item Is Not Nothing) And (lastSelected Is Nothing)): Set lastSelected = item ' we got our first good element
Case (item = lastSelected): Exit Sub ' we already had what we got
Case (Not item = lastSelected): Set lastSelected = item ' we got something new
End Select
It's definitely a little unusual, and I had to make use of my fantastic whiteboard (which, by the way, is pretty much the most useful programming resource besides a computer) to make sure I had mapped all of the statements correctly.
Here's what's going on there: I have an expensive operation which I would like to avoid repeating if possible. lastSelected is a persistent reference to the value most recently passed to this calculation. item is the parameter that was just received from the GUI. If there has never been a call to the program before, lastSelected starts out as Nothing. item can be Nothing too. Additionally, if both lastSelected and item are the same something, skip the calculation.
If I were writing this in C++, I would write:
if (item == NULL || (lastSelected != NULL && item->operator==(*lastSelected))) return;
else lastSelected = item;
However, I'm not.
Question
How can I rewrite this to look better and make more sense? Upvotes will be awarded to answers that say either "YES and here's why: X, Y, Z" or "NO, and here's why not: X, Y, Z".
Edits
Fixed the C++ statement to match the VB6 one (they were supposed to be equivalent)

This is shorter and 100x more readable.
EDIT Wug edited the code in MarkJ's original answer, into this:
If (item Is Nothing)
Then Exit Sub ' we got a non-element
ElseIf (lastSelected Is Nothing) Then
Set lastSelected = item ' we got our first go
ElseIf (item = lastSelected) Then
Exit Sub ' we already had what we got
End If
Set lastSelected = item ' we got something new
Here's MarkJ's edit in response. One nested if, but only one Set. Seems neater to me.
If (item Is Nothing) Then
Exit Sub ' we got a non-element
ElseIf Not (lastSelected Is Nothing) Then ' not our first go
If (item = lastSelected) Then
Exit Sub ' we already had what we got
End If
End If
Set lastSelected = item ' we got something new
' does stuff here? #Wug is that true?
To compare reference equality in VB6 use item Is LastSelected. Because item = lastSelected will probably evaluate the default properties in the objects and compare those instead!
Since brevity appears to be a goal, consider this. If you Exit Sub when condition X is True, you don't need to check X again later. It is False! Unless it changes its value in between evaluations (e.g. X is a function that checks the system clock). You were checking whether item was lastSelected, then whether it wasn't. And if item Is Nothing is False, do not bother to check whether item Is Not Nothing is True!
VB6 does not short circuit for backwards compatibility with ancient versions of Basic
Stop worrying that VB6 is not some other language and relax!

YES
I translated it from your case statement. I find it easier to read, personally.
If Item Is Nothing Then
Exit Sub ' we got a non-element
ElseIf LastSelected Is Nothing Then
Set LastSelected = Item ' we got our first good element
ElseIf Item = LastSelectedItem Then
Exit Sub ' we already had what we got
Else
Set LastSelected = Item ' we got something new
End If
You asked for explanation. I tried not to have to give much (by re-using your own code comments).
But here it is anyway :-)
Firstly if there is no item, just exit. Easy.
Otherwise if LastSelected Is Nothing then we know, because the first if condition failed, that Item exists, and it's safe to mark that value as having been Last Selected. As you say, we got our first good element. The sub continues on.
However if we have existing values for Item and LastSelected, then either they are equal or not. If they are equal, just quit.
If they aren't equal, then update LastSelected. As you say, we got something new.

You can use a helper function like this:
Private Function pvGetItemData(oItem As ListItem) As Variant
If Not oItem Is Nothing Then
pvGetItemData = oItem.Tag
Else
pvGetItemData = -1
End If
End Function
and then
If pvGetItemData(Item) = pvGetItemData(LastSelected) Then
' cache hit
Else
' do calc
Set LastSelected = Item
End If

YES
I'd make that simpler:
If item Is Nothing Then
Exit Sub ' we got a non-element
Else
Set lastSelected = item ' we got something to assign
End If
Unless there are side effects assigning lastItem (it can be property with invalid assignment code), then code logic is essentially same.
If you are not required to exit the sub (snippet is at the end of sub or something), then next is even simpler:
If Not (item Is Nothing) Then Set lastSelected = item
BTW, your Select Case (True) looks really odd to VB programmer :)

Related

Optimize performance of Removing Hidden Rows in VBA

I am using the following code to remove hidden/filtered lines after applying autofilters to a big sheet in VBA (big means roughly 30,000 rows):
Sub RemoveHiddenRows()
Dim oRow As Range, rng As Range
Dim myRows As Range
With Sheets("Sheet3")
Set myRows = Intersect(.Range("A:A").EntireRow, .UsedRange)
If myRows Is Nothing Then Exit Sub
End With
For Each oRow In myRows.Columns(1).Cells
If oRow.EntireRow.Hidden Then
If rng Is Nothing Then
Set rng = oRow
Else
Set rng = Union(rng, oRow)
End If
End If
Next
If Not rng Is Nothing Then rng.EntireRow.Delete
End Sub
The code comes from here: Delete Hidden/Invisible Rows after Autofilter Excel VBA
Moreover I read this thread: Speeding Up Code that Removes Hidden Rows on a Sheet
The situation: I have applied 5 different filters to a table consisting of 12 columns, therefore a lot of rows are filtered out (hidden) after the process. When I try to delete those, the code above takes a very long time. In my case I don't know if Excel was still working, so I had to force an exit. That leads to the following question:
Is there any other way than looping through all the hidden rows and deleting them?
An idea which came to my mind was to copy only the remaining unfiltered (that is non-hidden) content to a new sheet and afterwards delete the old sheet, which contains the full information. If so, how can that be done?
I don't think you need to involve another worksheet. Simply copy the rows below the existing Range.CurrentRegion property and then remove the filter and delete the original data.
Sub RemoveHiddenRows()
With Sheets("Sheet10")
With .Cells(1, 1).CurrentRegion
With .Offset(1, 0).Resize(.Rows.Count - 1, .Columns.Count)
If CBool(Application.Subtotal(103, .Columns(1))) Then
.Cells.Copy Destination:=.Cells(.Rows.Count + 1, 1)
End If
.AutoFilter
.Cells(1, 1).Resize(.Rows.Count, 1).EntireRow.Delete
End With
End With
End With
End Sub
You may also receive some good, focused help on this subject by posting on Code Review (Excel).
You can improve performance significantly with a function like this:
Option Explicit
Public Sub deleteHiddenRows(ByRef ws As Worksheet)
Dim rngData As Range, rngVisible As Range, rngHidden As Range
With ws
Set rngData = .UsedRange
With rngData
Set rngVisible = .SpecialCells(xlCellTypeVisible)
Set rngHidden = .Columns(1)
End With
End With
If Not (rngVisible Is Nothing) Then
ws.AutoFilterMode = False
' invert hidden / visible
rngHidden.Rows.Hidden = False
rngVisible.Rows.Hidden = True
' delete hidden and show visible
rngData.SpecialCells(xlCellTypeVisible).Delete
rngVisible.Rows.Hidden = False
End If
End Sub
I tested it on a file with 2 filters applied to it
The function was adapted from the code in this suggestion

I have a 'end if without block if' in visual basic when it compiles

I am having a problem when I compile my .exe file is says this is the source of the fault. Can anyone recreate this code for me?
Private Sub tmrCounter_Timer()
cntCounter = cntCounter + 1
tmrLogger.Enabled = False
SendCurrentInfos
cntCounter = 0
tmrCounter.Enabled = False
End If
End Sub
I doubt you need an If. You probably just need to remove the end if.
However you may need an If as you are incrementing a counter at top of the procedure, and later set it to 0.
my guess is that the last 3 lines inside the sub should be:
if cntCounter = 0
tmrCounter.Enabled = False
End If
it all depends on the value of cntCounter though, as it always increases in value it should be negative at the start to arrive at 0 at some time, or be altered somewhere else in your code

Excel 2010 VBA Macro making excel freeze

I have a big block of data >50 across and >1500 down and some of the entries are very large negative numbers like -1000000 or -9820000 and I want all of those to be turned into -100's.
I also want any non-zero numbers that are above -100 to show 2 decimals.
I thought this vba macro would work, but its causing excel to freeze and the excel screen turns all grey and idk whats happening.
I think it might be because there are so many cells, so it takes a long time and over loads something, is there any way to make this code more efficient??
Sub Blah()
For ColNum = 2 To WorksheetFunction.CountA(Range("1:1"))
For RowNum = 2 To WorksheetFunction.CountA(Range("A:A"))
If Cells(RowNum, ColNum) < -101 Then Cells(RowNum, ColNum) = -100
If Cells(RowNum, ColNum) <> 0 And Cells(RowNum, ColNum).Value > -100 Then Cells(RowNum, ColNum).NumberFormat = "0.00"
Next RowNum
Next ColNum
End Sub
Although Tmdean pointed out the solution, I'll post a sample code which might be of help.
Edit1: It seems assigning negative value in cell took a while as well. So apply the same principle. Get the relevant cells first and assign value in one go.
Sub marine()
Dim r As Range, c As Range, nonzero As Range, s As Range
Set r = ActiveSheet.UsedRange
For Each c In r
Select Case True
Case c.Value <= -101
'~~> Identify the cells first and combine all of them, don't assign value
If s Is Nothing Then Set s = c _
Else Set s = Union(s, c)
Case c.Value <> 0 And c.Value > -100
'~~> Identify the cells first and combine all of them, do not format
If nonzero Is Nothing Then Set nonzero = c _
Else Set nonzero = Union(nonzero, c)
End Select
Next
'~~> Once you got all the cells, assign value in one go
If Not s Is Nothing Then s.Value = -100
'~~> Once you got all the cells, format in one go
If Not nonzero Is Nothing Then nonzero.NumberFormat = "0.00"
End Sub
You can replace this with your For Loop, whatever is easier.
You can also be more explicit on setting the Range Object instead or using UsedRange. HTH.
There's nothing in that code that's particularly slow. How many cells are you changing? The only suggestion I have is to set the NumberFormat for the entire range beforehand (to 0.00), then turn set it back to General only for the cells that are 0 or -100 in your loop. Changing the NumberFormat is probably the most expensive operation, so you want to minimize the times you set it for individual cells.
Someone will be here shortly to recommend you turn off Application.ScreenUpdating.

How to clear the contents of an array in vbscript?

I have declared a two dimensional array in the function library and associated it with a test. In action1 of the test, I tried to clear the array using "erase" statement.
My code -
In Function Library,
Dim strVerifyAry(25,6)
In action1,
erase strVerifyAry
Error message
Run Error - Type mismatch: 'Erase'
How to clear the contents of this array?
Works for me in plain VBScript, so it's most likely an issue with whatever engine QTP uses for running VBScript code. You should be able to emulate the behavior of Erase for a 2-dimensional array like this:
Sub EraseArray(ByRef arr)
For i = 0 To UBound(arr, 1)
For j = 0 To UBound(arr, 2)
If IsObject(arr(i, j)) Then
Set arr(i, j) = Nothing
Else
arr(i, j) = Empty
End If
Next
Next
End Sub
Or like this, if you don't want to set fields containing objects to Nothing:
Sub EraseArray(ByRef arr)
For i = 0 To UBound(arr, 1)
For j = 0 To UBound(arr, 2)
arr(i, j) = Empty
Next
Next
End Sub
I do not exactly understand why, but you can create a sub like
Public Sub DoErase (byRef Ary)
Erase Ary
End Sub
in the library, and call it from within the action like this:
DoErase StrVerifyAry
and that works.
Update: No it doesn't. The array is successfully passed to DoErase, and the DoErase call works fine, but the test afterwards still can reference the array elements that Erase was supposed to be erasing.
If the test declares the array, it works fine (Erase erases the elements).
This is very strange and probably has to do with the quirky scopes in function libraries.
Please let us know if you ever find out what's going on here...
This drove me nuts for an entire afternoon so I wanted to post an answer for future reference. I filled an array using the Split command and then needed to Erase it before the script looped back through the process again. Nothing I tried would erase or clear the array and the next use of Split just appended to the previous array elements.
By trying the 'array=Nothing' loop above, I finally managed to generate a "This array is fixed or locked" error which I researched. Turns out I had used the array in a 'For Each..Next' loop which locks the array so it can't be erased or cleared. More info is available HERE:
You can use a Dictionary collection rather than an array in some circumstances. Then use RemoveAll when you want to clear it. That doesn't help when your array was created by a split function, or whatever, but it can help in other use cases.
Set myDict = CreateObject("Scripting.Dictionary")
...
myDict.RemoveAll
Refer to: https://www.w3schools.com/asp/asp_ref_dictionary.asp

VBScript - Putting Array Element into GetElementById

I am writing a VBScript that automatically interacts with some web pages. I am having trouble at the final step where the script needs to click on a link to make a booking. The link for each time will only be available if that time is free. The idea of my code is to simply select the first time available (I originally though I could do this by using Mid() and GetElementId as I know the first 7 chars of each link ID but couldn't get this working). The array contains the IDs for all possible times available in a day. Some will already have been taken so that ID will no longer exist on the form.
I have 2 problems:-
1) Neither getElementBy Id or the Document.All.Item().Click commands will accept an element from the array - I get an Object Required run time error.
2) If getElementId doesn't find a matching ID it simply throws an Object required error. I wasn't expecting this, I thought that my elem variable would be nothing or null and that I could test for this.
Can anyone give me any pointers?
'This is a shortened version of my array- there are lots more times!
Times(0)="bookBtn0810"
Times(1)="bookBtn0818"
Times(2)="bookBtn0826"
Dim TimeAvail
Dim i
Dim elem
TimeAvail = "No"
i = 0
Do While (TimeAvail = "No") or (i<3)
Set elem = IE.Document.GetElementById(Chr(34) & Times(i) & Chr(34)) 'Chr(34) is to add ""
if elem is nothing then
TimeAvail = "No"
i=i+1
else
TimeAvail = "Yes"
IE.Document.All.Item(Chr(34) & Times(i) & Chr(34)).click
end if
Loop
Now, unless I'm being very silly, you won't be able to sit a variable to a non-existent element.
The only thing I can think of is to add:
On Error Resume Next
At the beginning, so it skips the error message. You may need to handle the error separately yourself.

Resources