I am trying to simulate a multiple deck drawing using an array. After the first card is dealt, how can I remove this random number ("p1") from the deck array, so I can have an array with 51 elements without the first one to be selected?
That`s how I am doing it so far
Dim deck(1 To 52) As Variant
Dim p1 As Integer
For i = 1 To 52
deck(i) = i
Next
p1 = Int((UBound(deck) * Rnd) + 1)
I could solve it with the code below but I am still wondering if there is an easier way
Sub preflop()
Dim deck() As Integer
Dim p1 As Integer
For i = 1 To 52
ReDim Preserve deck(1 To i) As Integer
deck(i) = i
Next
p1 = Int((UBound(deck) * Rnd) + 1)
For i = LBound(deck) To UBound(deck)
If deck(i) = p1 Then
For j = i To UBound(deck) - 1
deck(j) = deck(j + 1)
Next
End If
Next
ReDim Preserve deck(1 To UBound(deck) - 1) As Integer
End Sub
As per my comment, here an example on how you can utilize an ArrayList for this purpose:
Sub preflop()
Dim arr As Object: Set arr = CreateObject("System.Collections.ArrayList")
Dim item As Variant, ResultArr As Variant
Dim i As Long, p1 As Long
With arr
'Load all variables
For i = 1 To 52
.Add i
Next i
'Get random number
p1 = ((.Count - 1) - 1) * Rnd
'Remove the random card from the deck
.Remove (p1)
'To use an array in further code somewhere
ResultArr = .Toarray
End With
End Sub
AFAIK the use of ArrayList over the more native Collection will open ways to use methods like Toarray to export the arrayList to an array without an expensive Redim loop.
If you don't need to end up with an array you might as well use the Collection approach.
Related
I would like to make a user-defined function union in VBA, such that:
it could take variable parameters
each parameter is a one-column range like A1, A2:A10; we don't need to consider passing constant values to parameters
we could consider, within one input range, there are no duplicates; but it is very possible to have duplicates among input ranges.
union combines the input ranges, and keeps the order of the elements. For instance, =union(A1:A5, C1:C2, E1:E3) has the following expected output in Column I:
I wrote the following code which works. However, the problem is that it is slow. A union over a list of 4000 rows and a list of 20 rows takes already several seconds. First, I don't know the way I coded arrays could be improved. Second, the algorithm just consists in comparing each new element against the accumulating result list; there is no sort, no other techniques. Third, I don't know if there are any existing functions we could use in other objects of VBA (eg, VBA FILTER function, Collection, ArrayLists, Scripting.Dictionary).
Could anyone propose a more efficient code?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
Function exists(v As Variant, arr As Variant, resCount As Long) As Boolean
If resCount = 0 Then
exists = False
Else
exists = False
i = LBound(arr, 1)
Do While (i <= resCount) And (Not exists)
If arr(i) = v Then
exists = True
End If
i = i + 1
Loop
End If
End Function
' assumption: every input is a range (eg, A1, A1:A2)
' assumption: each input range has only one column
Function union(ParamArray arr() As Variant) As Variant
Dim res As Variant
ReDim res(1 To 100000)
Dim resCount As Long
resCount = 0
For k = LBound(arr) To UBound(arr)
Dim arrk As Variant
Dim v
arrk = arr(k).Value2
If getDimension(arrk) = 0 Then 'case of A1, B1
v = arrk
If Not exists(v, res, resCount) Then
resCount = resCount + 1
res(resCount) = v
End If
ElseIf getDimension(arrk) = 2 Then 'case of A1:A10, B1:B10
For i = LBound(arrk, 1) To UBound(arrk, 1)
v = arrk(i, 1)
If Not exists(v, res, resCount) Then
resCount = resCount + 1
res(resCount) = v
End If
Next i
End If
Next k
ReDim Preserve res(1 To resCount)
union = Application.WorksheetFunction.Transpose(res)
End Function
Something like this should work, using a Dictionary to eliminate duplicates.
Function UniqueValues(ParamArray arr() As Variant)
Dim r, c As Range, v, dict
Set dict = CreateObject("scripting.dictionary")
For Each r In arr
For Each c In r.Cells
v = c.Value
If Len(v) > 0 Then dict(v) = 1
Next c
Next r
UniqueValues = ToColumn(dict.keys)
End Function
Function ToColumn(arr)
Dim arrOut, i As Long
ReDim arrOut(1 To UBound(arr) + 1, 1 To 1)
For i = 1 To UBound(arr) + 1
arrOut(i, 1) = arr(i - 1)
Next i
ToColumn= arrOut
End Function
I'm stuck with a problem when I'm working with a dictionary in VBA. The reason why I want to work with a dictionary and a do-while loop is because I have variables with different length, that I want to loop through.
First I want to give the dic keys and and items.
The reason why I skip one col for each loop is because each series has a col with dates and then a col with prices. If it is possible I want to capture the dates that match the prices in the same dictionary.
Sub opg1(dicSPX As Object)
Dim i As Integer, m As Integer
Dim varColLeng As Variant
Set dicSPX = CreateObject("Scripting.Dictionary")
m = 10
Sheets("Data").Activate
ReDim intCol(1 To nCol)
'opretter dictionary
ReDim n(1 To m)
Do While n <> ""
For i = 1 To mn
' redim preserve IntColLen
dicSPX.Add Cells(1, 2 + ((i - 1) * 2)).Value, Range(Cells(9, 2 + ((i - 1) * 2)), Cells(n, 2 + ((i - 1) * 2))).Value
Next i
Loop
End Sub
then I want to execute a procedure for all keys in my dic. I want to compute returns in different time series.
However, when I call the dic, to the sub Returns() I get an error (Compile error: Variable not defined). I'm new to dictionaries and I probably missed a small detail.
Sub Returns()
Call opg1(dicSPX)
Dim dicSPX As New Scripting.Dictionary
Dim varKey As Variant, varArr As Variant
For Each varKey In dicSPX
varArr = dicSPX(varKey)
For i = LBound(varArr, 1) To UBound(varArr, 1)
For j = LBound(varArr, 2) To UBound(varArr, 2)
' varReturns(i,j) = compute the return here
Next
Next
Next
Any suggestions? I hope the question is clear.
Thank you
It should be clear which variable is not defined by the line which is highlighted after the error is thrown. At a quick glance I can tell that you didn't define i or j:
Sub Returns()
Dim dicSPX As New Scripting.Dictionary
opg1(dicSPX)
Dim varKey As Variant
For Each varKey In dicSPX
Dim varArr As Variant
varArr = dicSPX(varKey)
Dim i As Long
For i = LBound(varArr, 1) To UBound(varArr, 1)
Dim j As Long
For j = LBound(varArr, 2) To UBound(varArr, 2)
varReturns(i,j) = compute the return here
Next
Next
Next
End Sub
You should always explicitly define each variable, one line at a time, right before they are used. This way the declaration is always near the usage so you can see it on the same page.
You should also make opg1 a function that returns a dictionary. That would clarify your intent. Passing a variable by reference makes it harder to tell how the program works to the reader.
I'm new in VBA. I want to make a random pick cycle like that:
Let's say I have seven elements in an array(1,2,3,4,5,6,7), each time when I pick one element from the array, the total number of elements will decrease by 1. After picking every element, the array will be reset to what I initially defined (1,2,3,4,5,6,7) and do the random pick cycle again.
The result of every cycle should be different.
Is it possible to do that in VBA?
Here's a stateful function that does what you described each time it is called.
Option Base 1
Dim digits, NLeft
Function RemoveDigit() as Integer
Dim Element as Integer
If IsEmpty(digits) or NLeft = 0 Then
digits = array(1,2,3,4,5,6,7)
NLeft = 7
End If
Element = WorksheetFunction.RandBetween(1,NLeft)
RemoveDigit = digits(Element)
digits(Element) = digits(NLeft)
digits(NLeft) = RemoveDigit
NLeft = NLeft - 1
End Function
It uses a well known algorithm to arrange digits in a random order. Basically you choose to swap a random element number with the last element. Then you repeat it on an n - 1 sized array, making it a tail-recursive algorithm (although this implementation of it is not recursive).
Delete this if you want to, but here is a suggestion for a test sub:
Sub TestRemoveDigit()
NLeft = 0
For i = 1 To 7
d = RemoveDigit()
Debug.Print (d)
Next i
End Sub
I think this should do what you're asking for:
Option Explicit
Global vCurrentArray As Variant
Sub ResetArray()
vCurrentArray = Array(1, 2, 3, 4, 5, 6, 7)
End Sub
Sub RemoveElementWithIndex(lIndex As Long)
Dim vTemp() As Variant '* Change the type as needed
Dim lLBound As Long: lLBound = LBound(vCurrentArray)
Dim lUBound As Long: lUBound = UBound(vCurrentArray)
Dim i As Long, v As Variant
Dim blSkipped As Boolean
If lLBound = lUBound Then '* only 1 element
Call ResetArray
Else
ReDim vTemp(lLBound To lUBound - 1)
i = lLBound
For Each v In vCurrentArray
If i <> lIndex Or blSkipped Then
vTemp(i) = v
i = i + 1
Else
blSkipped = True
End If
Next v
vCurrentArray = vTemp
End If
End Sub
Function GetRandomElement() As Variant '* Change the type as needed
Dim lRandomIndex As Long
lRandomIndex = WorksheetFunction.RandBetween(LBound(vCurrentArray), UBound(vCurrentArray))
GetRandomElement = vCurrentArray(lRandomIndex)
RemoveElementWithIndex lRandomIndex
End Function
Sub TestCycles()
Dim lCycle As Long
Dim i As Long
ResetArray
For lCycle = 1 To 3
Debug.Print
For i = 1 To 7
Debug.Print "Cycle: " & lCycle, "i: " & i, "Random Elem: " & GetRandomElement
Next i
Next lCycle
End Sub
Note: There're many ways of achieving the end result. The above is almost a literal translation of your post.
We can not remove a random element from an array. We can redim array to remove last element(s). If you want to remove random element, you can use collection instead like ..
Option Explicit
Sub RemoveRandom()
Dim coll As Collection, cl As Variant, i As Long, j As Long
Set coll = New Collection
For Each cl In Range("A1:A7")
coll.Add cl.Value
Next cl
For j = 1 To coll.Count
i = WorksheetFunction.RandBetween(1, coll.Count)
Debug.Print coll(i)
coll.Remove (i)
Next j
End Sub
Write a subroutine in VBA to generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40.
In order to have a small simulation animation, range("A1:E8") should contain the numbers 1 to 40 and the subroutine should then cycle through these numbers using a colored cell and then momentarily pause 2 seconds on a selected winning number. The list of winning numbers drawn should then be printed in the range("G2:G7"). In case a number drawn has already been drawn previously in the list, then a new number should be redrawn.
I have only been able to do as follows.
Option Explicit
Sub test1()
Sheet1.Cells.Clear
Dim i As Integer
For i = 1 To 40
Cells(i, 1) = i
Next
End Sub
'-----------------------------
Option Explicit
Option Base 1
Function arraydemo(r As Range)
Dim cell As Range, i As Integer, x(40, 1) As Double
i = 1
For Each cell In r
x(i, 1) = cell.Value
i = i + 1
Next cell
arraydemo = x
End Function
Sub test3()
Dim x() As String
chose = Int(Rnd * UBound(x))
End Sub
I got stuck elsewhere, the sub test3(), does not seem appropriate here. I need some suggestions. Also, I appologise for my poor formatting, I am new to this.
Populating your range like this:
range("A1:E8") should contain the numbers 1 to 40
Sheet1.Cells.Clear
Dim i As Integer
Dim rng as Range
Set rng = Range("A1:E8")
For i = 1 To 40
rng
Next
generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40
Using a dictionary object to keep track of which items have been picked (and prevent duplicate) in a While loop (until there are 6 numbers chosen):
Dim picked as Object
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
Using the Application.Wait method to do the "pause", you can set up a procedure like so:
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(picked(val)).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(picked(val)).Interior.ColorIndex = xlNone
Next
The list of winning numbers drawn should then be printed in the range("G2:G7").
Print the keys from the picked dictionary:
Range("G2:G7").Value = Application.Transpose(picked.Keys())
Putting it all together:
Sub Lotto()
Dim i As Integer, num As Integer
Dim rng As Range
Dim picked As Object 'Scripting.Dictionary
Dim val As Variant
'Populate the sheet with values 1:40 in range A1:E8
Set rng = Range("A1:E8")
For i = 1 To 40
rng.Cells(i) = i
Next
'Store which numbers have been already chosen
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(val).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(val).Interior.ColorIndex = xlNone
Next
'Display the winning series of numbers in G2:G7
Range("G2:G7").Value = Application.Transpose(picked.Keys())
End Sub
NOTE This absolutely will not work on Excel for Mac, you would need to use a Collection instead of a Dictionary, as the Scripting.Runtime library is not available on Mac OS.
In addition to the excellent answer given by member David Zemens, following is the universal function written in "pure" Excel VBA, which does not contain any Excel Worksheet Functions, neither Dictionary Object (re: CreateObject("Scripting.Dictionary").
Option Explicit
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer) As Variant
Dim I As Integer
Dim arrRandom() As Integer
Dim colRandom As New Collection
Dim colItem As Variant
Dim tempInt As Integer
Dim tempExists As Boolean
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
While colRandom.Count < N
Randomize
' get random number in interval
tempInt = Int((UB - LB + 1) * Rnd + LB)
'check if number exists in collection
tempExists = False
For Each colItem In colRandom
If (tempInt = colItem) Then
tempExists = True
Exit For
End If
Next colItem
' add to collection if not exists
If Not tempExists Then
colRandom.Add tempInt
End If
Wend
'convert collection to array
ReDim arrRandom(N - 1)
For I = 0 To N - 1
arrRandom(I) = colRandom(I + 1)
Next I
'return array of random numbers
RandomNumbers = arrRandom
Else
RandomNumbers = Nothing
End If
End Function
'get 5 Random numbers in the ranger 1...10 and populate Worksheet
Sub GetRandomArray()
Dim arr() As Integer
'get array of 5 Random numbers in the ranger 1...10
arr = RandomNumbers(1, 10, 5)
'populate Worksheet Range with 5 random numbers from array
If (IsArray(arr)) Then
Range("A1:A5").Value = Application.Transpose(arr)
End If
End Sub
The function
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer)
returns array of N random numbers in the range LB...UB inclusively without repetition.
Sample Sub GetRandomArray() demonstrates how to get 5 random numbers in the range 1...10 and populate the Worksheet Range: it could be customized for any particular requirements (e.g. 8 from 1...40 in PO requirements).
APPENDIX A (Courtesy of David Ziemens)
Alternatively, you can do similar without relying on Collection object at all. Build a delimited string, and then use the Split function to cast the string to an array, and return that to the calling procedure.
This actually returns the numbers as String, but that shouldn't matter for this particular use-case, and if it does, can easily be modified.
Option Explicit
Sub foo()
Dim arr As Variant
arr = RandomNumbersNoCollection(1, 40, 6)
End Sub
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbersNoCollection(LB As Integer, UB As Integer, N As Integer)
Dim I As Integer
Dim numbers As String ' delimited string
Dim tempInt As Integer
Const dlmt As String = "|"
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
' get random number in interval
Do
Randomize
tempInt = Int((UB - LB + 1) * Rnd + LB)
If Len(numbers) = 0 Then
numbers = tempInt & dlmt
ElseIf InStr(1, numbers, tempInt & dlmt) = 0 Then
numbers = numbers & tempInt & dlmt
End If
Loop Until UBound(Split(numbers, dlmt)) = 6
numbers = Left(numbers, Len(numbers) - 1)
End If
RandomNumbersNoCollection = Split(numbers, dlmt)
End Function
The purpose of this code is to be able to track multiple properties of undefined "IDs," like name, total time, and occurrences while going through ton of data. As of right now I am using a jagged array to achieve this.
idArray = array of all gathered ID's in data
idProp = array of properties of each ID
I had everything working perfectly till I added the nested array.
I need to be able to check while going through the data if the ID is a new or not. with the name of each ID being held in the nested array i can't seem to be able to do this. Here is what i have so far:
For Each ID In idArray()(1) '<-- does not target nested array correctly
If ID = wbData.Sheets(1).Range("C" & Count) Then
idPrs = True
End If
Next ID
'adding ID if not present
If idPrs = False Then
idCount = idCount + 1
MsgBox "new ID: " & wbData.Sheets(1).Range("C" & Count) & " on row " & Count
ReDim Preserve idArray(0 To idCount)
ReDim idProp(0 To 2)
'adding starting values
idProp(0) = wbData.Sheets(1).Range("C" & Count)
idProp(1) = wbData.Sheets(1).Range("h" & Count)
idProp(2) = 1
idList(gidCount) = idProp
'double array second "()" referrs to second array
'MsgBox idArray(1)(0)
End If
any ideas are welcome, even if they are done a completely different way.
It's not entirely clear how you are declaring or popuplating your idArray.
This small demo of jagged arrays might give you some hints:
Sub Demo()
Dim i As Long, j As Long
Dim arr1 As Variant, arr2 As Variant
Dim v As Variant
' Declare and populate a jagged array
ReDim arr1(0 To 9)
For i = LBound(arr1) To UBound(arr1)
ReDim arr2(0 To i)
For j = LBound(arr2) To UBound(arr2)
arr2(j) = j
Next
arr1(i) = arr2
Next
' Option 1 to iterate the array
For i = LBound(arr1) To UBound(arr1)
For j = LBound(arr1(i)) To UBound(arr1(i))
Debug.Print arr1(i)(j)
Next
arr2 = arr1(i)
For j = LBound(arr2) To UBound(arr2)
Debug.Print arr2(j)
Next
Next
' Option 2 to iterate the array
For Each arr2 In arr1
For Each v In arr2
Debug.Print v
Next
Next
End Sub
Dictionaries could better tend to your problem. Here is a solution with a dictionary of dictionaries:
Dim probsById As New Scripting.Dictionary
Sub processId(id As String)
If Not probsById.Exists(id) Then
Dim props As New Scripting.Dictionary
Call props.Add("id", id)
Call props.Add("name", "Miller")
Call props.Add("status", 1)
Call probsById.Add(id, props)
End If
End Sub
Sub test()
Call processId("bar")
Debug.Print probsById("bar").Item("name")
End Sub