This question already has answers here:
Repeating random variables in VBA
(2 answers)
Closed 7 years ago.
What would be the VBA code in excel to generate ONE random number between 1 to 100 that is displayed in a given cell (say A1) upon clicking a button, and then when the button is clicked again, it generates another random number between 1 to 100, THAT IS NOT A REPETITION. Ideally, this should allow me to click the button a 100 times and get all the numbers between 1-100 exactly once each ?
Technically there is no such thing as random numbers with no repetition. What you are asking for is actually a random permutation of a set of values, like the ordering of a shuffled deck of cards or lottery ball picks. Random permutation of a range of vlaues can be achieved in Excel VBA succinctly.
Assign your button's macro to RangeValue():
Public Sub RangeValue()
Dim i As Long
Static n As Long, s As String
Const MIN = 1, MAX = 100, OUT = "A1", DEL = "."
Randomize
Do
i = Rnd * (MAX - MIN) + MIN
If 0 = InStr(s, i & DEL) Then
n = n + 1: s = s & i & DEL
Range(OUT) = i
If n > MAX - MIN Then n = 0: s = ""
Exit Do
End If: DoEvents
Loop
End Sub
That's it. The above code is all that is required to answer your question as posed.
You can use the Const line near the top to edit the MIN and MAX range of values that will be spun through randomly. You can also adjust the OUTput cell.
Once all of the values have been output (i.e. 100 button clicks), the code resets and spins through the range again in a new, random order. This continues forever. You can disable multiple spins-through by deleting this line: If n > MAX - MIN Then n = 0: s = ""
How does this work?
The routine maintains a string of previously output values. Each time the procedure is run, it selects a new random value from the range and checks if that value is already logged in the string. If it is it picks a new value and looks again. This continues in a loop until a value not currently logged in the string is randomly selected; that value is logged and output to the cell.
EDIT #1
To address your new question about how to set this up so that it works in more than one cell with different value ranges, assign your button's macro to ButtonClick():
Public Sub ButtonClick()
Static n1 As Long, s1 As String, n2 As Long, s2 As String
RangeValue 1, 100, "A1", n1, s1
RangeValue 1, 150, "B1", n2, s2
End Sub
Private Sub RangeValue(MIN As Long, MAX As Long, OUT As String, n As Long, s As String)
Dim i As Long
Const DEL = "."
Randomize
Do
i = Rnd * (MAX - MIN) + MIN
If 0 = InStr(s, i & DEL) Then
n = n + 1: s = s & i & DEL
Range(OUT) = i
If n > MAX - MIN Then n = 0: s = ""
Exit Do
End If: DoEvents
Loop
End Sub
EDIT #2
While the above methods are concise, we can be more efficient by permuting the set of values in an array, and by avoiding the selection of values that have already been output. Here is a version that uses Durstenfeld's implementation of the Fisher–Yates shuffle algorithm:
Public Sub ButtonClick()
Static n As Long, a
Const MIN = 1, MAX = 100, OUT = "A1"
If n = 0 Then a = Evaluate("transpose(row(" & MIN & ":" & MAX & "))"): n = UBound(a)
PermuteArray a, n: Range(OUT) = a(n): n = n - 1
End Sub
Private Sub PermuteArray(a, n As Long)
Dim j As Long, t
Randomize
j = Rnd * (n - 1) + 1
If j <> n Then t = a(j): a(j) = a(n): a(n) = t
End Sub
Fisher–Yates has the advantage that it can be stopped and started as needed and so I am using it on the fly to permute the next value to display on each button click.
And to round this out with a version to use with your scenario of two output cells that use different value ranges:
Public Sub ButtonClick()
Static n1 As Long, n2 As Long, a1, a2
Const MIN1 = 1, MAX1 = 100, OUT1 = "A1"
Const MIN2 = 1, MAX2 = 150, OUT2 = "B1"
If n1 = 0 Then Reset a1, n1, MIN1, MAX1
If n2 = 0 Then Reset a2, n2, MIN2, MAX2
PermuteArray a1, n1: Range(OUT1) = a1(n1): n1 = n1 - 1
PermuteArray a2, n2: Range(OUT2) = a2(n2): n2 = n2 - 1
End Sub
Private Sub PermuteArray(a, n As Long)
Dim j As Long, t
Randomize
j = Rnd * (n - 1) + 1
If j <> n Then t = a(j): a(j) = a(n): a(n) = t
End Sub
Private Sub Reset(a, n As Long, MIN As Long, MAX As Long)
a = Evaluate("transpose(row(" & MIN & ":" & MAX & "))"): n = UBound(a)
End Sub
EDIT #3
I decided to create a version of this that utilizes the "inside-out" variation of Fisher–Yates. This allows us to specify the array of range values and shuffle it at the same time, an elegant and even more efficient enhancement:
Public Sub ButtonClick()
Const MIN = 1, MAX = 100, OUT = "A1"
Static a, n&
If n = 0 Then Reset a, n, MIN, MAX
Range(OUT) = a(n): n = n - 1
End Sub
Private Sub Reset(a, n&, MIN&, MAX&)
Dim i&, j&
Randomize: n = MAX - MIN + 1: ReDim a(1 To n)
For i = 1 To n
j = Rnd * (i - 1) + 1: a(i) = a(j): a(j) = i - 1 + MIN
Next
End Sub
And to expand on your requirement of two different output cells that each use different value ranges, I decided to craft a generalized solution that can be used for an arbitrary number of independent output cells each tied to its own value range:
Public Sub ButtonClick()
Dim MIN, MAX, OUT, i
Static a, n, z
MIN = Array(1, 11, 200): MAX = Array(100, 20, 205): OUT = Array("A1", "B2", "C3")
z = UBound(MIN)
If Not IsArray(n) Then ReDim a(z): ReDim n(z)
For i = 0 To z
If n(i) = 0 Then Reset a(i), n(i), MIN(i), MAX(i)
Range(OUT(i)) = a(i)(n(i)): n(i) = n(i) - 1
Next
End Sub
Private Sub Reset(a, n, MIN, MAX)
Dim i, j
Randomize: n = MAX - MIN + 1: ReDim a(1 To n)
For i = 1 To n
j = Rnd * (i - 1) + 1: a(i) = a(j): a(j) = i - 1 + MIN
Next
End Sub
While the above is setup for three outputs, simply adjust the MIN, MAX, and OUT arrays near the top to suit your needs.
Here's a button click handler that uses static variables to hold an array containing a random sequence of numbers from 1 to 100, as well as the current position/index within that array. The array is created by populating a collection with numbers from 1 to 100, then transferring each number to the array in a random order.
Sub Button1_Click()
Static NumberArray As Variant
Static intIndex As Long
If Not IsArray(NumberArray) Then NumberArray = GetRandomArray()
' If we haven't reached the end of our sequence, get another number...
If intIndex < 100 Then
Sheets("Sheet1").Range("A1") = NumberArray(intIndex)
intIndex = intIndex + 1
End If
End Sub
Function GetRandomArray() As Variant
Dim c As New Collection
Dim a(99) As Long
' Seed the RNG...
Randomize
' Add each number to our collection...
Dim i As Long
For i = 1 To 100
c.Add i
Next
' Transfer the numbers (1-100) to an array in a random sequence...
Dim r As Long
For i = 0 To UBound(a)
r = Int(c.Count * Rnd) + 1 ' Get a random INDEX into the collection
a(i) = c(r) ' Transfer the number at that index
c.Remove r ' Remove the item from the collection
Next
GetRandomArray = a
End Function
Try this:
Dim Picks(1 To 100) As Variant
Dim which As Long
Sub Lah()
Dim A As Range
Set A = Range("A1")
If A.Value = "" Then
which = 1
For i = 1 To 100
Picks(i) = i
Next i
Call Shuffle(Picks)
Else
which = which + 1
If which = 101 Then which = 1
End If
A.Value = Picks(which)
End Sub
Sub Shuffle(InOut() As Variant)
Dim HowMany As Long, i As Long, J As Long
Dim tempF As Double, temp As Variant
Hi = UBound(InOut)
Low = LBound(InOut)
ReDim Helper(Low To Hi) As Double
Randomize
For i = Low To Hi
Helper(i) = Rnd
Next i
J = (Hi - Low + 1) \ 2
Do While J > 0
For i = Low To Hi - J
If Helper(i) > Helper(i + J) Then
tempF = Helper(i)
Helper(i) = Helper(i + J)
Helper(i + J) = tempF
temp = InOut(i)
InOut(i) = InOut(i + J)
InOut(i + J) = temp
End If
Next i
For i = Hi - J To Low Step -1
If Helper(i) > Helper(i + J) Then
tempF = Helper(i)
Helper(i) = Helper(i + J)
Helper(i + J) = tempF
temp = InOut(i)
InOut(i) = InOut(i + J)
InOut(i + J) = temp
End If
Next i
J = J \ 2
Loop
End Sub
EDIT#1
The code begins by examining the destination cell, A1. If the cell is empty the code:
creates an array of 100 values
randomizes that array
initializes a sequential counter
places the first element of the randomized array in A1
If the cell is not empty, the code just places the next element of the randomized array in A1.
If you want to restart the process, clear A1. This will re-shuffle the array.
Here is an approach that maintains a global collection of available numbers and places #N/A in cells below A100. The button's click() sub makes sure that the collection is initialized when it needs to be. In a standard code module (insert -> module) enter:
Public Available As Collection
Public Initialized As Boolean
Sub Initialize()
Dim i As Long, n As Long
Dim used(1 To 100) As Boolean
Set Available = New Collection
If Not Range("A1").Value < 1 Then
n = Cells(Rows.Count, 1).End(xlUp).Row()
For i = 1 To n
used(Cells(i, 1).Value) = True
Next i
End If
For i = 1 To 100
If Not used(i) Then Available.Add i
Next i
Initialized = True
End Sub
Function NextRand()
'assumes that Initialize() has been called
Dim i As Long, num As Long
i = Application.WorksheetFunction.RandBetween(1, Available.Count)
num = Available.Item(i)
Available.Remove i
NextRand = num
End Function
Add a button, then in its event handler add the code to make it look something like:
(the actual name depends on the button and if it is an Active-X button, a forms button or just a shape)
Private Sub CommandButton1_Click()
If (Not Initialized) Or Range("A1").Value < 1 Then Initialize
Dim i As Long, n As Long
If Range("A1").Value < 1 Then
Range("A1").Value = NextRand()
Exit Sub
End If
n = 1 + Cells(Rows.Count, 1).End(xlUp).Row()
If n > 100 Then
Cells(n, 1).Value = CVErr(xlErrNA)
Else
Cells(n, 1).Value = NextRand()
End If
End Sub
Consider sorting a list of 100 random numbers and keeping their initial index. I have two buttons (or labels), one to initialize the list and the other to show the next random value
with code like this:
Const RandomCount As Long = 100
Private m_seq() As Variant ' Keep in memory the random numbers
Private m_current As Long ' Keep in memory the last shown number
Private Sub initializeLabel_Click()
Dim wk As Worksheet
Set wk = Worksheets.Add(Type:=xlWorksheet) 'add a worksheet
ReDim m_seq(1 To RandomCount, 1 To 2) 'Initialize a 2D array
Dim i As Long
For i = 1 To RandomCount
m_seq(i, 1) = i 'add values 1..100 to first column
m_seq(i, 2) = Rnd() 'add random numbers to second column
Next i
'Output the array into the new worksheet
wk.Range("A1").Resize(RandomCount, 2).Value2 = m_seq
' Sort the worksheet
wk.Range("A1").Resize(RandomCount, 2).Sort wk.Range("B1")
'Input the sorted values back into the array
m_seq = wk.Range("A1").Resize(RandomCount, 2).Value2
' Delete the worksheet quietly
Application.DisplayAlerts = False
wk.Range("A1").Resize(RandomCount, 2).ClearContents
wk.Delete
Application.DisplayAlerts = True
'Reset the UI
m_current = 0
[A1].ClearContents
End Sub
Private Sub randomLabel_Click()
m_current = m_current + 1
If m_current > RandomCount Then m_current = 1
[A1].Value2 = m_seq(m_current, 1)
End Sub
The values in the temporary worksheet look like this
and after the sort
of which the first column is used
Related
So the problem is more in depth than a simple comparison. Essentially im trying to model this dice roll known as the roll and keep system. Example would be 5k3. Where I would roll 5 dice and keep the 3 highest then add them together.
I've gotten my little macro program to roll the dice. Then I put them in an array in my example that would be an array with 5 indices. Now I need to take those 5 dice, and only keep the largest 3 of them.
The code is here A2 gives me the number of sides on the dice, B2 gives me how many I roll, and C2 gives me how many I keep. This rolls 10 dice, but then I transfer 5 of them into my actual dicepool. I know I could probably skip that, but I can deal with that later.
Private Sub CommandButton1_Click()
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim RandNum As Integer
Dim RollArray() As Integer
Dim KeptArray() As Integer
Dim RollArrayDummy() As Integer
Dim NumRoll As Integer
Dim Kept As Integer
Dim Largest As Integer
NumRoll = Range("B2").Value
ReDim RollArray(NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(Kept)
For i = 5 To 15
Randomize
RandNum = 1 + Rnd() * (Range("A2").Value - 1)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
k = 1
i = 1
m = 1
Largest = 1
For k = 1 To Kept
m = 1
KeptArray(k) = Largest
If m <= NumRoll Then
If Largest >= RollArray(m) And Largest >= KeptArray(k) Then
Largest = KeptArray(k)
Else
KeptArray(k) = Largest
Largest = RollArray(m)
End If
m = m + 1
End If
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
I've tried so many things, like creating a dummy array, and comparing the variable Largest with it. And a ton of other things. My big problem is that I can't reuse any of the numbers.
If I roll 5 and keep 3. Say I roll [4,2,3,3,6] . I keep the [6,4,3]. Im sure this is incredibly simple and im overlooking it but its driving me absolutely insane.
Today I was watching some MonteCarlo simulations, so I have decided to do the whole question from the beginning. Thus, imagine that this is the input:
After the first roll, this is what you get:
The values in yellow are the top 3, which are kept. This is the result from the second roll:
And here is the whole code:
Public Sub RollMe()
Dim numberOfSides As Long: numberOfSides = Range("A2")
Dim timesToRoll As Long: timesToRoll = Range("B2")
Dim howManyToKeep As Long: howManyToKeep = Range("C2")
Dim cnt As Long
Dim rngCurrent As Range
Cells.Interior.Color = vbWhite
Set rngCurrent = Range(Cells(1, 6), Cells(1, 6 + timesToRoll - 1))
For cnt = 1 To timesToRoll
rngCurrent.Cells(1, cnt) = makeRandom(1, numberOfSides)
Next cnt
Dim myArr As Variant
With Application
myArr = .Transpose(.Transpose(rngCurrent))
End With
WriteTopN howManyToKeep, myArr, Cells(2, lastCol(rowToCheck:=2))
End Sub
Public Sub WriteTopN(N As Long, myArr As Variant, lastCell As Range)
Dim cnt As Long
For cnt = 1 To N
Set lastCell = lastCell.Offset(0, 1)
lastCell = WorksheetFunction.Large(myArr, cnt)
lastCell.Interior.Color = vbYellow
Next cnt
End Sub
The makeRandom and lastCol functions are some functions that I use for other projects as well:
Public Function makeRandom(down As Long, up As Long) As Long
makeRandom = CLng((up - down + 1) * Rnd + down)
If makeRandom > up Then makeRandom = up
If makeRandom < down Then makeRandom = down
End Function
Function lastCol(Optional strSheet As String, Optional rowToCheck As Long = 1) As Long
Dim shSheet As Worksheet
If strSheet = vbNullString Then
Set shSheet = ActiveSheet
Else
Set shSheet = Worksheets(strSheet)
End If
lastCol = shSheet.Cells(rowToCheck, shSheet.Columns.Count).End(xlToLeft).Column
End Function
Instead of looping through the array "manually", the WorksheetFunction.Large() nicely returns the Nth-largest value.
And if you are willing to color the "dice", which were used to take the top score, you may add this piece:
Public Sub ColorTopCells(howManyToKeep As Long, rngCurrent As Range, myArr As Variant)
Dim colorCell As Range
Dim myCell As Range
Dim cnt As Long
Dim lookForValue As Long
Dim cellFound As Boolean
For cnt = 1 To howManyToKeep
lookForValue = WorksheetFunction.Large(myArr, cnt)
cellFound = False
For Each myCell In rngCurrent
If Not cellFound And myCell = lookForValue Then
cellFound = True
myCell.Interior.Color = vbMagenta
End If
Next myCell
Next cnt
End Sub
It produces this, coloring the top cells in Magenta:
Edit: I have even wrote an article using the code above in my blog here:
vitoshacademy.com/vba-simulation-of-rolling-dices
Try this, changed a few things:
Edited the random bit too
Private Sub CommandButton1_Click()
Dim i As Long, j As Long, k As Long
Dim RandNum As Long
Dim RollArray() As Long
Dim KeptArray() As Long
Dim NumRoll As Long
Dim Kept As Long
NumRoll = Range("B2").Value
ReDim RollArray(1 To NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(1 To Kept)
For i = 5 To 15
Randomize
'RandNum = 1 + Rnd() * (Range("A2").Value - 1)
RandNum = 1 + Int(Rnd() * Range("A2").Value)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
For k = 1 To Kept
KeptArray(k) = Application.WorksheetFunction.Large(RollArray, k)
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
Makes use of the Excel large function
Here is my attempt to fix this problem. I left the reading cell values and writing results to the OP as I am focused on the logic of the process.
There are three main functions. DiceRollSim(), RollDie() and GetNLargestIndex() as well as a function to test the code, named Test().
DiceRollSim() runs the particular simulation given the number of sides, and number of die and the number to keep. It prints the results in the output window. DollDie() fills in an array of random values simulating the rolling of the die. Caution is needed to make sure the interval probabilities are maintained as VBA does round values when converting the result of Rnd() into integers. Finally, GetNLargestIndex() is the meat of the answer, as it takes the die roll results, creates an array of index values (the 1st, 2nd, 3rd .. ) and then sorts the array based on the values of the die rolls.
Option Explicit
Public Sub Test()
DiceRollSim 6, 15, 3
' Example, 15k3:
' Rolling 15 die.
' x(1) = 5 *
' x(2) = 4
' x(3) = 4
' x(4) = 2
' x(5) = 4
' x(6) = 5 **
' x(7) = 6 ***
' x(8) = 1
' x(9) = 4
' x(10) = 3
' x(11) = 1
' x(12) = 3
' x(13) = 5
' x(14) = 3
' x(15) = 3
' Sorting die values.
' x(7) = 6
' x(6) = 5
' x(1) = 5
' Sum of 3 largest=16
End Sub
Public Sub DiceRollSim(ByVal n_sides As Long, ByVal n_dice As Long, ByVal n_keep As Long)
Dim die() As Long, i As Long
ReDim die(1 To n_dice)
Debug.Print "Rolling " & n_dice & " die."
Call RollDie(n_sides, n_dice, die)
For i = 1 To n_dice
Debug.Print "x(" & i & ")=" & die(i)
Next i
Dim largest() As Long
Debug.Print "Sorting die values."
Call GetNLargestIndex(die, n_keep, largest)
Dim x_sum As Long
x_sum = 0
For i = 1 To n_keep
x_sum = x_sum + die(largest(i))
Debug.Print "x(" & largest(i) & ")=" & die(largest(i))
Next i
Debug.Print "Sum of " & n_keep & " largest=" & x_sum
End Sub
Public Sub RollDie(ByVal n_sides As Long, ByVal n_dice As Long, ByRef result() As Long)
ReDim result(1 To n_dice)
Dim i As Long
For i = 1 To n_dice
' Rnd() resurns a number [0..1)
' So `Rnd()*n_sides` returns a floating point number zero or greater, but less then n_sides.
' The integer conversion `CLng(x)` rounds the number `x`, and thus will not keep equal
' probabilities for each side of the die.
' Use `CLng(Floor(x))` to return an integer between 0 and n_sides-1
result(i) = 1 + CLng(WorksheetFunction.Floor_Math(Rnd() * n_sides))
Next i
End Sub
Public Sub GetNLargestIndex(ByRef die() As Long, ByVal n_keep As Long, ByRef index() As Long)
Dim n_dice As Long, i As Long, j As Long, t As Long
n_dice = UBound(die, 1)
' Instead of sorting the die roll results `die`, we sort
' an array of index values, starting from 1..n
ReDim index(1 To n_dice)
For i = 1 To n_dice
index(i) = i
Next i
' Bubble sort the results and keep the top 'n' values
For i = 1 To n_dice - 1
For j = i + 1 To n_dice
' If a later value is larger than the current then
' swap positions to place the largest values early in the list
If die(index(j)) > die(index(i)) Then
'Swap index(i) and index(j)
t = index(i)
index(i) = index(j)
index(j) = t
End If
Next j
Next i
'Trim sorted index list to n_keep
ReDim Preserve index(1 To n_keep)
End Sub
I am receiving a run-time error, but that may be the least of my problems. The logic makes sense in my head but I may not be using the correct syntax or functions. My code is below with comments and "hopes":
Sub Random_Points()
Dim i As Integer
Dim j As Integer
Dim Max As Integer
Dim RandomNumber As Integer
Max = 100 '(Max is being multiplied by the Rnd function to provide a random number between 0-100)
For i = 2 To 100 Step 1
RandomNumber = Int(Rnd * Max)
ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = RandomNumber
'(The for loop above with start assigned cells values starting with Cells(2,2) to Cells(100,2))
'(I DO NOT WANT DUPLICATE VALUES...therefore after the value is assigned above I want the code to compare the newly assigned cell to all the cells above it.)
For j = 1 To 98 Step 1
'(...and IF the cell values are the same...)
If ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = ThisWorkbook.Sheets("VBA").Cells(i - j, 2).Value Then
'(...A new random number will be assigned...)
RandomNumber = Int(Rnd * Max)
ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = RandomNumber
End If
'(...and then re-checked vs all the others)
Next j
'(Next cell is assigned...loop restarts)
Next i
End Sub
Your problem is in your nested loop. As j increments, it approaches and finally equals i. Subsequently, when you use the two values in .Cells(i - j, 2).Value, there is no Range.Cells property with a row number less than 1.
The solution is to change your nested For ... Next statement so that j never reaches i.
'was ...
For j = 1 To 98 Step 1
'should be ...
For j = 1 To (i - 1) Step 1
You only need to check the values up to i in any event.
fwiw, a WorksheetFunction object's use of MATCH function and VBA's IsError function would be faster.
Sub randomPoints_part_deux()
Dim i As Long, mx As Long, randNum As Long
mx = 100 '(mx is being multiplied by the Rnd function to provide a random number between 0-100)
With ThisWorkbook.Sheets("VBA")
'seed the column of numbers so you have something to check against
randNum = Int(Rnd * mx)
.Cells(2, 2) = randNum
For i = 3 To 100 Step 1
Do While Not IsError(Application.Match(randNum, .Range(.Cells(2, 2), .Cells(i - 1, 2)), 0))
randNum = Int(Rnd * mx)
Loop
.Cells(i, 2) = randNum
Next i
'optional formula to count unique in C2
.Cells(2, 3).Formula = "=SUMPRODUCT(1/COUNTIF(B2:B100, B2:B100))"
End With
End Sub
since you don't want duplicates you can either generate random numbers and then repeatedly check if they are already used or you can generate your list first and then pull from it randomly. The second option is easier.
Sub Random100()
Dim i As Integer
Dim j As Integer
Dim Max As Integer
Dim RandomNumber As Integer
Dim cNum As New Collection
Max = 100 '(Max is being multiplied by the Rnd function to provide a random number between 0-100)
For i = 0 To Max 'fill collection with 0-100 in order
cNum.Add i
Next i
k = cNum.Count - 1
For j = 0 To k
RandomNumber = Int(Rnd * (k - j)) + 1
ThisWorkbook.Sheets("VBA").Cells(j + 2, 2).Value = cNum(RandomNumber)
cNum.Remove (RandomNumber)
Next j
End Sub
If your purpose is to get a range of unique values, then a better approach would be to shuffle a serie:
Const MIN = 1
Const MAX = 98
Dim values(MIN To MAX, 0 To 0) As Double, i&, irand&
' generate all the values
For i = MIN To MAX
values(i, 0) = i
Next
' shuffle the values
For i = MIN To MAX
irand = MIN + Math.Round(Rnd * (MAX - MIN))
value = values(i, 0)
values(i, 0) = values(irand, 0)
values(irand, 0) = value
Next
' copy the values to the sheet
ThisWorkbook.Sheets("VBA").Range("A2").Resize(MAX - MIN + 1, 1) = values
What I am trying to do is develop a model that takes a cell that is greater than 1 then to take the sum of the area to the first row using a cone shape, so for example cell D4, sum the area C3:C5 + B2:B6 + A1:A7.
At the moment I have this but it obviously is not working.
Dim I As Double
Dim J As Double
Dim Size As Integer
Dim x As Integer
Dim y As Integer
Dim z As Integer
'Dim Range As Integer
Dim PV1 As Integer
'MCArray = Worksheets("Data")
I = WorksheetFunction.CountA(Worksheets("Data").Rows(1))
J = WorksheetFunction.CountA(Worksheets("Data").Columns(1))
'Loop to Move down the rows
For x = 1 To J
'Loop to move acoss the columns
For y = 1 To I
'IfElse to determine if cell value is greater or equal to zero
If Cells(J, I).Value >= 0 Then
'Loop to sum the cells above
For z = 1 To J
PV1 = (ActiveCell.Value) + Worksheet.Sum(Range([J - z], [I-z:I+z]))
'IfElse to determine if final sum is greater than zero
If PV1 > 0 Then
Worksheets("MC").Range("B4").Value = PV1
Range([J - z], [I-z:I+z]).Interior.ColourIndex = 1
End If
Next z
End If
Next y
Next x
Here is a function you can use either as a UDF or from another routine. Just pass it the single cell you want to start from (D4 in your example) and this function will calculate the sum of the cone as you described.
Public Function SUMCONE(r As Range) As Double
Application.Volatile
SUMCONE = Application.Sum(r, r(-0, -0).Resize(, 3), r(-1, -1).Resize(, 5), r(-2, -2).Resize(, 7))
End Function
Here is an example of how to use the above function from your VBA routine:
Public Sub Demo()
Dim j&
For j = 5 To 10
If Cells(5, j) > 0 Then
Debug.Print SUMCONE(Cells(5, j))
End If
Next
End Sub
UPDATE
Based on your feedback I have updated the function and the demo routine to form an upward cone summation from the initial cell.
UPDATE #2
The above is for a fixed-size cone, extending upwards, that can be initiated from any cell in the worksheet.
But if you would prefer for the cone to always extend all the way up to row 1 regardless of which cell it originates in, then the following is what you are after:
Public Sub Demo()
Dim i&, j&
For j = 1 To Application.CountA(Worksheets("Data").Rows(1))
For i = 1 To Application.CountA(Worksheets("Data").Columns(1))
If Cells(i, j) > 0 Then
Debug.Print Cells(i, j).Address, SumAndColorCone(Cells(i, j))
End If
Next
Next
End Sub
Public Function SumAndColorCone(r As Range) As Double
Dim i&, k&, c As Range
Set c = r
For i = r.Row - 1 To 1 Step -1
Set c = Union(c, r(-k, -k).Resize(, (k + 1) * 2 + 1))
k = k + 1
Next
c.Interior.Color = vbRed
SumAndColorCone = Application.Sum(c)
End Function
UPDATE #3
As I suspected there was a problem if the cone was initiated too close to the left edge of the worksheet. I've added code to handle that now. Also your method for accessing the large matrix (which I had used in the Demo routine) did not work properly. I fixed that as well:
Public Sub Demo()
Dim i&, j&
For j = 1 To Cells(1, Columns.Count).End(xlToLeft).Column
For i = 1 To Cells(Rows.Count, "A").End(xlUp).Row
If Val(Cells(i, j)) > 0 Then
Debug.Print Cells(i, j).Address, SumAndColorCone(Cells(i, j))
End If
Next
Next
End Sub
Public Function SumAndColorCone(r As Range) As Double
Dim i&, k&, c As Range
Set c = r
For i = r.Row - 1 To 1 Step -1
If r.Column - k < 2 Then Exit For
Set c = Union(c, r(-k, -k).Resize(, (k + 1) * 2 + 1))
k = k + 1
Next
c.Interior.Color = vbRed
SumAndColorCone = Application.Sum(c)
End Function
I'm looking for code in VBA to generate all subsets of the items in a passed array.
Below is simple code to select all N choose 2 subsets of array size N.
Looking to augment this for N choose (N-1)... all the way down to N choose 1.
Option Base 1
Sub nchoose2()
iarray = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
n = UBound(iarray)
x = 1
t = 0
r = 0
Do While (n - x) >= 1
For i = 1 To (n - x)
Cells((i + t), 1) = iarray(x)
Cells((i + t), 2) = iarray(i + x)
Next i
x = x + 1
t = t + (n - (1 + r))
r = r + 1
Loop
End Sub
In addition to the Gray-code algorithm, you can also exploit the correspondence between subsets of an n-element set and binary vectors of length n. The following code illustrates this approach:
Sub AddOne(binaryVector As Variant)
'adds one to an array consisting of 0s and 1s
'thought of as a binary number in little-endian
'the vector is modified in place
'all 1's wraps around to all 0's
Dim bit As Long, carry As Long, i As Long, n As Long
carry = 1
n = UBound(binaryVector)
i = LBound(binaryVector)
Do While carry = 1 And i <= n
bit = (binaryVector(i) + carry) Mod 2
binaryVector(i) = bit
i = i + 1
carry = IIf(bit = 0, 1, 0)
Loop
End Sub
Function listSubsets(items As Variant) As Variant
'returns a variant array of collections
Dim lb As Long, ub As Long, i As Long, j As Long, numSets As Long
Dim vect As Variant 'binary vector
Dim subsets As Variant
lb = LBound(items)
ub = UBound(items)
ReDim vect(lb To ub)
numSets = 2 ^ (1 + ub - lb)
ReDim subsets(1 To numSets)
For i = 1 To numSets
Set subsets(i) = New Collection
For j = lb To ub
If vect(j) = 1 Then subsets(i).Add items(j)
Next j
AddOne vect
Next i
listSubsets = subsets
End Function
Function showCollection(c As Variant) As String
Dim v As Variant
Dim i As Long, n As Long
n = c.Count
If n = 0 Then
showCollection = "{}"
Exit Function
End If
ReDim v(1 To n)
For i = 1 To n
v(i) = c(i)
Next i
showCollection = "{" & Join(v, ", ") & "}"
End Function
Sub test()
Dim stooges As Variant
Dim stoogeSets As Variant
Dim i As Long
stooges = Array("Larry", "Curly", "Moe")
stoogeSets = listSubsets(stooges)
For i = LBound(stoogeSets) To UBound(stoogeSets)
Debug.Print showCollection(stoogeSets(i))
Next i
End Sub
Running the code results in the following output:
{}
{Larry}
{Curly}
{Larry, Curly}
{Moe}
{Larry, Moe}
{Curly, Moe}
{Larry, Curly, Moe}
I asked a similar question a while back (2005) and received this excellent code from John Coleman:
Sub MAIN()
Dim i As Long, st As String
Dim a(1 To 12) As Integer
Dim ary
For i = 1 To 12
a(i) = i
Next i
st = ListSubsets(a)
ary = Split(st, vbCrLf)
For i = LBound(ary) To UBound(ary)
Cells(i + 1, 1) = ary(i)
Next i
End Sub
Function ListSubsets(Items As Variant) As String
Dim CodeVector() As Integer
Dim i As Integer
Dim lower As Integer, upper As Integer
Dim SubList As String
Dim NewSub As String
Dim done As Boolean
Dim OddStep As Boolean
OddStep = True
lower = LBound(Items)
upper = UBound(Items)
ReDim CodeVector(lower To upper) 'it starts all 0
Do Until done
'Add a new subset according to current contents
'of CodeVector
NewSub = ""
For i = lower To upper
If CodeVector(i) = 1 Then
If NewSub = "" Then
NewSub = Items(i)
Else
NewSub = NewSub & ", " & Items(i)
End If
End If
Next i
If NewSub = "" Then NewSub = "{}" 'empty set
SubList = SubList & vbCrLf & NewSub
'now update code vector
If OddStep Then
'just flip first bit
CodeVector(lower) = 1 - CodeVector(lower)
Else
'first locate first 1
i = lower
Do While CodeVector(i) <> 1
i = i + 1
Loop
'done if i = upper:
If i = upper Then
done = True
Else
'if not done then flip the *next* bit:
i = i + 1
CodeVector(i) = 1 - CodeVector(i)
End If
End If
OddStep = Not OddStep 'toggles between even and odd steps
Loop
ListSubsets = SubList
End Function
The original question and answer:
John Coleman
I need help with a macro that exports all combinations of a range in same row each one ( I mean horizontal exports).
Every combination I want to be in one cell each time.
I want to change any time the number of strings in the range and also the number of strings combinations (In the example below 4 strings in the range and 3 for combinations)
1. A B C D -------------ABC --ABD--ACD--BCD
2. E F G H--------------EFG---EFH--EGH--FGH
3. I G K L----------------IGK----IGL---IKL---GKL
Below its a module that I found in web that is very close to what I need.
I am very new to Vba macros and I cannot achieve what I am looking for with the below code
Private NextRow As Long
Sub Test()
Dim V() As Variant, SetSize As Integer, i As Integer
SetSize = Cells(2, Columns.count).End(xlToLeft).Column
ReDim V(1 To SetSize)
For i = 1 To SetSize
V(i) = Cells(2, i).Value
Next i
NextRow = 4
CreateCombinations V, 3, 3
End Sub
Sub CreateCombinations( _
OriginalSet() As Variant, _
MinSubset As Integer, MaxSubset As Integer)
Dim SubSet() As Variant, SubSetIndex As Long
Dim SubSetCount As Integer, Bit As Integer
Dim k As Integer, hBit As Integer
Dim MaxIndex As Long
hBit = UBound(OriginalSet) - 1
ReDim SubSet(1 To UBound(OriginalSet))
MaxIndex = 2 ^ UBound(OriginalSet) - 1
For SubSetIndex = 1 To MaxIndex
SubSetCount = BitCount(SubSetIndex)
If SubSetCount >= MinSubset And SubSetCount <= MaxSubset Then
k = 1
For Bit = 0 To hBit
If 2 ^ Bit And SubSetIndex Then
SubSet(k) = OriginalSet(Bit + 1)
k = k + 1
End If
Next Bit
DoSomethingWith SubSet, SubSetCount
End If
Next SubSetIndex
End Sub
Sub DoSomethingWith(SubSet() As Variant, ItemCount As Integer)
Dim i As Integer
For i = 1 To ItemCount
Cells(NextRow, i) = SubSet(i)
Next i
NextRow = NextRow + 1
End Sub
Function BitCount(ByVal Pattern As Long) As Integer
BitCount = 0
While Pattern
If Pattern And 1 Then BitCount = BitCount + 1
Pattern = Int(Pattern / 2)
Wend
End Function
Here is a way to do it:
In your excel sheet, add an array formula like this:
A B C D E
1
2 A B C D {=k_combinations(CONCATENATE(A2;B2;C2;D2);3)}
3 E F G H {=k_combinations(CONCATENATE(A3;B3;C3;D3);3)}
Note that you should extend the array formula to columns F, G, H and so on so that you get all results. (The { and } are not to be inserted manually, they are the mark of the array formula) :
Select cells E2, F2, G2, H2, and so on to Z2
Type the formula
To validate input, press Ctrl+Shift+Enter
Put the following code into a code module.
Public Function k_combinations(ByVal chLetters As String, ByVal k As Long) As Variant
Dim chCombinations() As String
Dim uCount As Long
Dim vReturn() As Variant
Dim i As Long
uCount = Get_k_combinations(chLetters, chCombinations, k)
ReDim vReturn(0 To uCount - 1) As Variant
For i = 0 To uCount - 1
vReturn(i) = chCombinations(i)
Next i
k_combinations = vReturn
End Function
Private Function Get_k_combinations(chLetters As String, chCombinations() As String, ByVal k As Long) As Long
Dim i As Long
Dim M As Long
M = Len(chLetters)
If k > 1 Then
Get_k_combinations = 0
For i = 1 To M - (k - 1)
Dim chLetter As String
Dim uNewCombinations As Long
Dim chSubCombinations() As String
Dim j As Long
chLetter = Mid$(chLetters, i, 1)
uNewCombinations = Get_k_combinations(Right$(chLetters, M - i), chSubCombinations, k - 1)
ReDim Preserve chCombinations(0 To Get_k_combinations + uNewCombinations) As String
For j = 0 To uNewCombinations - 1
chCombinations(Get_k_combinations + j) = chLetter & chSubCombinations(j)
Next j
Get_k_combinations = Get_k_combinations + uNewCombinations
Next i
Else
ReDim chCombinations(0 To M - 1) As String
For i = 1 To M
chCombinations(i - 1) = Mid$(chLetters, i, 1)
Next i
Get_k_combinations = M
End If
End Function
Get_k_combinations is called recursively. The performance of this method is quite poor (because it uses string arrays and makes a lot of reallocations). If you consider bigger data sets, you will have to optimize it.