How to write an or statement without a duplicating code (VBA) - vba

I know in most other languages you can write an expression if (ptr == NULL || ptr->foo()) {something bad happened} to test both the validity of a variable and a test you want to perform. VBA does not let you do that. So I am wracking my brain to figure out a way to write the following code without 1) using error catching as means of conditional branching and 2) not duplicating code.
Type Vector
vData() as Variant
vSize as Long
End Type
Sub add(v as Vector, elem as Variant)
Dim oldSize as Long
if v.vSize = 0 Or UBound(v.vData) >= v.vSize then
oldSize = v.vSize
ReDim Preserve v.vData(0 to (oldSize * 2 + 1))
v.vData(oldSize) = elem
v.vSize = v.vSize + 1
else
v.vData(v.vSize) = elem
v.vSize = v.vSize + 1
end if
End Sub
Now that code will crash on the UBound line regardless if vSize is 0 or not (if vData was never Dim'd). The only other way i can see to do it is do an additional elseif statement to check UBound, but that would duplicate the code of the doubling the vector size.
In case you think this is a duplicate: VBA Short-Circuit `And` Alternatives . This talks about alternatives to AND statements (not or). Nested ifs (aka AND statements) doesn't duplicate code like OR would.

If I understand correctly you need to check if the array has been allocated or not.
One such option is to do this (however weird it may look):
If (Not Not MyArray) <> 0 Then 'Means it is allocated
Answer taken from this thread - see for more ideas.

Using SnowGroomer's answer I am posting the complete solution:
This is a class named Vector
Private data() As Variant
Private size As Long
Property Get vSize() As Long
vSize = size
End Property
Property Let vData(ByVal index As Long, elem As Variant)
If index < 0 Then Exit Property
If index < size Then
data(index) = elem
Else
Me.add elem, index
End If
End Property
Property Get vData(ByVal index As Long) As Variant
If index < 0 Or (Not Not data) = 0 Then
vData = Nothing
Exit Property
End If
vData = data(index)
End Property
Public Sub add(elem As Variant, Optional index As Long = -1)
If index > -2 Then
If index = -1 Then
If size = 0 Or (Not Not data) = 0 Then
ReDim data(0)
data(size) = elem
size = size + 1
Exit Sub
Else 'size <> 0
ReDim Preserve data(0 To size * 2 + 1)
data(size) = elem
size = size + 1
End If
Else 'index <> -1
If index >= size Then
ReDim Preserve data(0 To index)
data(index) = elem
size = index + 1
Else 'index < vSize
data(index) = elem
End If 'index >= vSize
End If 'index = -1
End If 'index > -2
End Sub 'add

Related

gas behavior in VBA

my project is to predict non-ideal gas movement, so i wrote this code to give every molecule a specific number, but it keeps repeating numbers (i used randbetween)
how do i chnge it so it wont repeat the same number?
Sub Rand_Number()
'áåçø 20 àçåæ ùì îñôøé äîåì÷åìåú
Dim RandNum As Long
Dim k As Long
Dim Mone As Integer
Mone = 0
Num_molecules = Sheets("Data").Range("A14").Value
RandNum = WorksheetFunction.RandBetween(1, Num_molecules)
For j = 1 To Num_molecules * 0.2
If IsEmpty(Sheets("rand").Cells(1, 1)) = True Then
Sheets("rand").Cells(1, 1) = RandNum
Else
i = 1
'RandNum = WorksheetFunction.RandBetween(1, Num_molecules)
Do 'Until IsEmpty(Sheets("rand").Cells(i, 1)) = True
If Sheets("rand").Cells(i, 1) = RandNum Then
RandNum = WorksheetFunction.RandBetween(1, Num_molecules)
Do Until RandNum = Cells(i, 1) Or IsEmpty(Cells(i, 1)) = True
If RandNum = Sheets("rand").Cells(i, 1) Then
RandNum = WorksheetFunction.RandBetween(1, Num_molecules)
Else
i = i + 1
End If
Loop
ElseIf IsEmpty(Sheets("rand").Cells(i, 1)) = False Then
i = i + 1
Else
Sheets("rand").Cells(i, 1) = RandNum
Exit Do
End If
Loop
End If
Next j
End Sub
Generation of numbers until all numbers from the range are generated. It is inefficient as towards the end of the algorithm most random numbers become a "miss", but it is still more efficient than collection's remove method below.
Sub uniqRndMissedHits()
Dim lb As Long: lb = 1 ' lower bound
Dim ub As Long: ub = 1000 ' upper bound
' populate collection with numbers starting from lb to ub
Dim i As Long
Dim c As New Collection
' iterate while we haven't generated all the random numbers
' in the specified range
While c.Count < ub - lb + 1
i = Int((ub - lb + 1) * Rnd + lb)
If Not contains(c, CStr(i)) Then
c.Add i, CStr(i)
Debug.Print i ' this is your unique random number from the
' remaining in the collection
End If
Wend
End Sub
Function contains(col As Collection, key As String) As Boolean
On Error Resume Next
col.Item key
contains = (Err.Number = 0)
On Error GoTo 0
End Function
This example generates a guaranteed unique (i.e. previously not generated) values, but Remove method of the Collection makes it inefficient for large number of simulations.
Sub uniqRnd()
Dim lb As Long: lb = 1 ' lower bound
Dim ub As Long: ub = 1000 ' upper bound
' populate collection with numbers starting from lb to ub
Dim i As Long
Dim c As New Collection
For i = lb To ub: c.Add i: Next
' randomly pick the number and (!) remove it from the
' collection at the same time so it won't be repeated
While c.Count > 0
lb = 1
ub = c.Count
i = Int((ub - lb + 1) * Rnd + lb)
Debug.Print c(i) ' this is your unique random number from the
' remaining in the collection
c.Remove i
Wend
End Sub
Comparison of performance of all the methods in this answer can be found in this GitHub Gist Excel VBA: Generate complete set of unique random numbers
I'd recommend using a dictionary to keep track of the random numbers that have been generated so far. If the number doesn't exist in the dictionary you can proceed with the simulation, otherwise you could generate a new random number (this would be the Else condition)
Using a dictionary is very fast for doing the lookup.
Here's a code sample of how to work with a dictionary.
Public Sub DictionaryExample()
Dim myDict As Object: Set myDict = CreateObject("Scripting.Dictionary")
Dim myRand As Long
Dim i As Long
For i = 1 To 10000
myRand = WorksheetFunction.RandBetween(1, 10000)
If myDict.exists(myRand) = False Then ' The random number doesn't exist in the previous items added
'If it doesn't exist, add it to the dictionary
myDict.Add myRand, myRand 'First parameter is the key, or the unique value
'The second parameter is the value associated with the key, the lookup value
Else
'Do something here when it does exist
End If
Next i
End Sub

If an INDEX exist in Listbox in vb.net

I am running a Do Until Loop and its giving the error Index out of range. I am using this code:
If Not imgList.Item(i).ToString = Nothing Then
but its not working..
Actually this loop (in a private sub) is called before addition of any value in the Listbox..
here is the complete loop..
Dim i As Integer = 0
Do Until i = pagesRange
If Not imgList.Item(i).ToString = Nothing Then
'other code
i += 1
Else
End If
Loop
for the given code to avoid Index out of range exception try below
If imgList.Count < i AndAlso Not (imgList.Item(i).ToString Is Nothing) Then
End If
Remember about Zero Based ..
Dim i As Integer = 0
Do Until i = pagesRange -1
If Not imgList.Item(i).ToString = Nothing Then
'other code
i += 1
Else
End If
'why i += 1 not here ?
Loop

FILTER Function for integers - VBA

I searched the website but was not succesfful and tried doing some research on this but facing with " Type Mismatch" error.
I declared an array as integer type but the FILTER function seems to work only with STRING's. Can you please let me know how I can use the FILTER function for integers?
If UBound(Filter(CntArr(), count)) > 0 Then
msgbox "found"
End If
as i understand you need to know if specified count present in array. You can use for loop for it:
Dim found as Boolean
found = False
For i = 0 To UBound (CntArr())
If CntArr(i) = count Then
found = True
Exit For
End If
Next i
If found Then msgbox "found" End If
Below I have created IsIntegerInArray() function that returns boolean. Follow the two Subs for an example of integer array declaration. Declaring array as Integer should also prevent some unnecessary bugs caused by implicit data conversion.
Sub test_int_array()
Dim a() As Integer
ReDim a(3)
a(0) = 2
a(1) = 15
a(2) = 16
a(3) = 8
''' expected result: 1 row for each integer in the array
Call test_printing_array(a)
End Sub
Sub test_printing_array(arr() As Integer)
Dim i As Integer
For i = 1 To 20
If IsIntegerInArray(i, arr) Then
Debug.Print i & " is in array."
End If
Next i
End Sub
Function IsIntegerInArray(integerToBeFound As Integer, arr() As Integer) As Boolean
Dim i As Integer
''' incorrect approach:
''' IsIntegerInArray = (UBound(Filter(arr, integerToBeFound)) > -1) ' this approach searches for string, e.g. it matches "1" in "12"
''' correct approach:
IsIntegerInArray = False
For i = LBound(arr) To UBound(arr)
If arr(i) = integerToBeFound Then
IsIntegerInArray = True
Exit Function
End If
Next i
End Function

How can I list all the combinations that meet certain criteria using Excel VBA?

Which are the combinations that the sum of each digit is equal to 8 or less, from 1 to 88,888,888?
For example,
70000001 = 7+0+0+0+0+0+0+1 = 8 Should be on the list
00000021 = 0+0+0+0+0+0+2+1 = 3 Should be on the list.
20005002 = 2+0+0+0+5+0+0+2 = 9 Should not be on the list.
Sub Comb()
Dim r As Integer 'Row (to store the number)
Dim i As Integer 'Range
r = 1
For i = 0 To 88888888
If i = 8
'How can I get the sum of the digits on vba?
ActiveSheet.Cells(r, 1) = i
r = r + 1
End If
Else
End Sub
... Is this what you're looking for?
Function AddDigits(sNum As String) As Integer
Dim i As Integer
AddDigits = 0
For i = 1 To Len(sNum)
AddDigits = AddDigits + CInt(Mid(sNum, i, 1))
Next i
End Function
(Just remember to use CStr() on the number you pass into the function.
If not, can you explain what it is you want in a bit more detail.
Hope this helps
The method you suggest is pretty much brute force. On my machine, it ran 6.5min to calculate all numbers. so far a challenge I tried to find a more efficient algorithm.
This one takes about 0.5s:
Private Const cIntNumberOfDigits As Integer = 9
Private mStrNum As String
Private mRng As Range
Private Sub GetNumbers()
Dim dblStart As Double
Set mRng = Range("a1")
dblStart = Timer
mStrNum = Replace(Space(cIntNumberOfDigits), " ", "0")
subGetNumbers 8
Debug.Print (Timer - dblStart) / 10000000, (Timer - dblStart)
End Sub
Private Sub subGetNumbers(intMaxSum As Integer, Optional intStartPos As Integer = 1)
Dim i As Integer
If intStartPos = cIntNumberOfDigits Then
Mid(mStrNum, intStartPos, 1) = intMaxSum
mRng.Value = Val(mStrNum)
Set mRng = mRng.Offset(1)
Mid(mStrNum, intStartPos, 1) = 0
Exit Sub
End If
For i = 0 To intMaxSum
Mid(mStrNum, intStartPos, 1) = CStr(i)
subGetNumbers intMaxSum - i, intStartPos + 1
Next i
Mid(mStrNum, intStartPos, 1) = 0
End Sub
It can be sped up further by about factor 10 by using arrays instead of writing directly to the range and offsetting it, but that should suffice for now! :-)
As an alternative, You can use a function like this:
Function isInnerLowr8(x As Long) As Boolean
Dim strX As String, inSum As Long
isInnerLowr8 = False
strX = Replace(CStr(x), "0", "")
For i = 1 To Len(strX)
Sum = Sum + Val(Mid(strX, i, 1))
If Sum > 8 Then Exit Function
Next i
isInnerLowr8 = True
End Function
Now change If i = 8 to If isInnerLowr8(i) Then.

Need help with VB.NET List Logic

Hey guys, so I am creating a List(Of String), always of size 9.
This list contains True/False values. I need to go through this list and find the 3 values that are True (will never be more than 3, but could be less) and then set 3 string values in my code to the 3 index's of those values + 1.
Here is my current code:
Private Sub SetDenialReasons(ByVal LoanData As DataRow)
Dim reasons As New List(Of String)
With reasons
.Add(LoanData.Item("IsDenialReasonDTI").ToString)
.Add(LoanData.Item("IsDenialReasonEmploymentHistory").ToString)
.Add(LoanData.Item("IsDenialReasonCreditHistory").ToString)
.Add(LoanData.Item("IsDenialReasonCollateral").ToString)
.Add(LoanData.Item("IsDenialReasonCash").ToString)
.Add(LoanData.Item("IsDenialReasonInverifiableInfo").ToString)
.Add(LoanData.Item("IsDenialReasonIncomplete").ToString)
.Add(LoanData.Item("IsDenialReasonMortgageInsuranceDenied").ToString)
.Add(LoanData.Item("IsDenialReasonOther").ToString)
End With
Dim count As Integer = 0
For Each item As String In reasons
If item = "True" Then
count += 1
End If
Next
If count = 1 Then
DenialReason1 = (reasons.IndexOf("True") + 1).ToString
ElseIf count = 2 Then
DenialReason1 = (reasons.IndexOf("True") + 1).ToString
DenialReason2 = (reasons.LastIndexOf("True") + 1).ToString
ElseIf count >= 3 Then
Dim tempIndex As Integer = reasons.IndexOf("True")
DenialReason1 = (reasons.IndexOf("True") + 1).ToString
DenialReason2 = (reasons.IndexOf("True", tempIndex, reasons.Count - 1) + 1).ToString
DenialReason3 = (reasons.LastIndexOf("True") + 1).ToString
End If
End Sub
I had 3 True's next to each other in the array and the code failed with an exception saying count must be positive or something.
Now if there are less than 3 True's, it should set the remaining DenialReason's that haven't been set yet as blank (however they are set as blank in the constructor already to account for this).
Any ideas?
Perhaps you could modify your For Each code to handle the assignment of the DenialReasons. This still feels like a hack, but I think it may be cleaner that what you have. If you use this code, you don't need the code that begins with If count = 1...:
Dim count As Integer = 0
Dim index As Integer = 1
For Each item As String In reasons
If item = "True" Then
count += 1
Select Case count
Case 1
DenialReason1 = index.ToString()
Case 2
DenialReason2 = index.ToString()
Case 3
DenialReason3 = index.ToString()
End Select
End If
index += 1
Next
The index variable above assumes a 1-based index. I think this is cleaner than using IndexOf().
I think a better solution might be to have a list of DenialReasons and add to that list as items are true:
Dim count As Integer = 0
Dim index As Integer = 1
Dim denialReasons As New List(Of String)()
For Each item As String In reasons
If item = "True" Then
denialReasons.Add(index)
End If
index += 1
Next
Then you can simply iterate through your list of denialReasons. This is flexible so that if, for whatever reason, you have more than three DenialReasons, you don't have to add another hard-coded variable.