Exclude counter number in selecting random names in Excel VBA - excel

I have working code I got on the internet where a name is randomly picked from Column A with a default counter of "0" (Column B). I added a modification where if the name has been picked, the value of "0" becomes "1". However I am confused as to where I can add the logic where if the value in Column B is already 1, it will not be included in the next random pick since technically, the person with the value of 1 in the counter has already won.
Sample data:
Names | Counter
Newt | 0
Thomas | 0
Teresa | 1
Katniss | 0
Peeta | 0
Gale | 0
Haymitch | 0
Hazel Grace | 0
Augustus | 0
Code when "Draw Winner" is clicked:
Sub draw_winners()
draw
End Sub
Function draw()
Dim x As Integer
Dim delay_ms As Integer
Dim prize_y As Integer
Dim name_matched As Boolean
Dim randm As Integer
x = get_max
'CELL (ROW, COLUMN)
delay_ms = 20 'how many draws before final
draw_winner:
randm = rand_num(x)
Cells(1, 3).Value = Cells(randm, 1).Value
'winner_window.winner_name.Caption = Cells(1, 3).Value
name_matched = check_names(Cells(1, 3).Value, 1)
If delay_ms > 0 Then
WaitFor (0.1)
delay_ms = delay_ms - 1
GoTo draw_winner
End If
If name_matched = True Then
GoTo draw_winner
End If
Cells(randm, 2).Value = 1
End Function
Function check_names(name As String, rndm As Integer) As Boolean
Dim i As Integer
Dim winner As String
check_names = False
i = 2
check_name:
winner = Cells(i, 4).Value
If winner <> "" Then
If winner = name And i <> rndm Then
check_names = True
End If
End If
i = i + 1
If i < 1000 Then
GoTo check_name
End If
End Function
Function get_max() As Integer
Dim i As Integer
i = 2
check_blank_cell:
If Cells(i, 1).Value <> "" Then 'starts at the second row
i = i + 1
If i > 10000 Then
MsgBox "Max Limit Reached!"
Else
GoTo check_blank_cell
End If
End If
get_max = i - 1
End Function
Function rand_num(max As Integer) As Integer
Dim Low As Double
Dim High As Double
Low = 2 '<<< CHANGE AS DESIRED
High = max '20 '<<< CHANGE AS DESIRED
r = Int((High - Low + 1) * Rnd() + Low)
rand_num = r
End Function
Sub WaitFor(NumOfSeconds As Single)
Dim SngSec As Single
SngSec = Timer + NumOfSeconds
Do While Timer < SngSec
DoEvents
Loop
End Sub
Apologies if this has been asked. Your help will be deeply appreciated.

An easy (and fast) way would be to sort data by counter as a first step (so all 0 counters come first) before drawing a new name.
With ActiveSheet.Sort
.SortFields.Clear
.SortFields.Add2 Key:=Range("B:B"), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
.SetRange Range("A:B")
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
and use the amount of zeros x = Application.WorksheetFunction.CountIf(Range("B:B"), 0) as maximum for your random number generator rand_num(x). This way only names with 0 are drawn.
Image 1: Only the selected rows are used to draw a name.
Also see How to Sort Data in Excel using VBA (A Step-by-Step Guide).

The below will return an array of names that have not yet won. A random name gets picked and column B gets adjusted accordingly. Maybe it comes in handy:
Sub Test()
Dim lr As Long
Dim arr As Variant
Dim nom As String
Dim rng As Range
With Sheet1 'Change accordingly
'Get last used row
lr = .Cells(.Rows.Count, 1).End(xlUp).Row
'Get range into memory (array)
arr = Filter(.Evaluate("TRANSPOSE(If(B2:B" & lr & "=0,A2:A" & lr & ",""|""))"), "|", False)
If UBound(arr) = -1 Then Exit Sub
'Get a random name from array
nom = arr(Int(Rnd() * (UBound(arr) + 1)))
'Get the range where name resides
Set rng = .Range("A2:A" & lr).Find(nom, LookIn:=xlValues, lookat:=xlWhole)
'Change value in B column
rng.Offset(, 1).Value = 1
'Do something with randomly picked name
Debug.Print nom
End With
End Sub

Related

Prevent a macro from freezing / crashing / turning white the Excel window?

At work I am working on a macro/UserForm in Excel for someone. It's worked great on spreadsheets with 15k rows, but takes about 10 minutes to run, while Excel appears to be frozen (not responding). We've tried to run it on a 250k row sheet for about 8 hours and it never completed.
Is there a way to make it more effecient, or at least allow the user to see view its progress without Excel being locked up?
About the Macro
Users are asigned tasks, and arn't supposed to be assigned the same one within 365 days. There are 47 Columens and 250k Rows of users. Rows are sorted by username, create date, task. The macro goes row by row to first make sure its the same user, and then find instances of a task being asigned within the 365 day window, and flagging the row red. Then it checks the next row against the initial to make sure its also not within 365 days.
After reading a few dozens other posts, I'm not sure if this is the most effecient way of doing it. Iif you see anyway to make my code more efficient that would be greatly appreciated!
Sub highlight_newer_dates_v2()
Dim i As Long, j As Long
Dim lastRow As Long
Dim AccountNo As String, SpecialtyTo As String, CreateDate1 As Date, CreateDate2 As Date
Dim lastNonRedRow As Long
lastRow = Cells(Rows.Count, "I").End(xlUp).Row
lastNonRedRow = 0
For i = 2 To lastRow
AccountNo = Cells(i, 9).Value
SpecialtyTo = Cells(i, 13).Value
CreateDate1 = Cells(i, 5).Value
If Cells(i, 9).Interior.Color = RGB(255, 0, 0) Then
If lastNonRedRow = 0 Then
For j = i - 1 To 2 Step -1
If Cells(j, 9).Interior.Color <> RGB(255, 0, 0) Then
lastNonRedRow = j
Exit For
End If
Next j
End If
If lastNonRedRow <> 0 Then
CreateDate1 = Cells(lastNonRedRow, 5).Value
End If
Else
lastNonRedRow = i
End If
For j = i + 1 To lastRow
If Cells(j, 9).Value = AccountNo And Cells(j, 13).Value = SpecialtyTo Then
CreateDate2 = Cells(j, 5).Value
If Abs(CreateDate2 - CreateDate1) <= 365 Then
If CreateDate2 > CreateDate1 Then
Rows(j).Interior.Color = RGB(255, 0, 0)
Else
Rows(i).Interior.Color = RGB(255, 0, 0)
End If
End If
End If
Next j
Next i
End Sub
I've tried doing a loop to make it more effecient but couldn't get it to work properly.
Looping through row by row in vba is going to be inefficient. If I am understanding your problem correctly, you should 1. sort by date 2. sort by task 3 sort by user 4. use the formulas below. You would have to add even more columns if you might have more than 3 sequences, but its a pattern. You could also have vba sort by these values, put in the formulas, past the any true column as values and then sort back to your original order.
SEQUNCE
=COUNTIFS($A$5:A5,A5,$B$5:B5,B5)
2nd too close to 1st
=IF(AND(D6=2,(C6-C5)<365),1,0)
3rd too close to 2nd
=IF(AND(D7=3,(C7-C6)<365,E6=0),1,0)
ANY TRUE
=MAX(E5:F5)
Process 15,000 rows in a few seconds, 250,000 in < 1 minute.
Option Explicit
Sub highlight_newer_dates_v4()
Const COLS = 47
Dim ws As Worksheet
Dim dictTask, k, colDates As Collection
Dim bRed() As Boolean
Dim lastrow As Long, i As Long, j As Long
Dim n As Long, r As Long, r1 As Long, r2 As Long
Dim dt As Date, dt1 As Date, dt2 As Date
Dim task As String, accno As String
Dim t0 As Single
' store task and dates for one account
Set dictTask = CreateObject("Scripting.Dictionary")
' specify your sheet here
Set ws = ThisWorkbook.Sheets("Testdata")
'Call testData(ws, 250000)
' process data sheet
t0 = Timer
Application.ScreenUpdating = False
With ws
lastrow = .Cells(.Rows.Count, "I").End(xlUp).Row
ReDim bRed(1 To lastrow)
For r = 2 To lastrow
accno = Trim(.Cells(r, 9)) 'I
task = Trim(.Cells(r, 13)) 'M
dt = CDate(.Cells(r, 5)) 'E
' store tasks in dictionary
If Not dictTask.exists(task) Then
dictTask.Add task, New Collection
n = n + 1
End If
dictTask(task).Add Array(r, dt)
' is ths last task for person then check dates
If (.Cells(r + 1, 9)) <> accno Then
For Each k In dictTask
task = CStr(k)
Set colDates = dictTask(k)
' check interval for all permutations
' is > 365 days
For i = 1 To colDates.Count - 1
r1 = colDates(i)(0)
dt1 = colDates(i)(1)
dt = DateAdd("d", 365, dt1) '365 days later
If bRed(r1) = False Then
For j = i + 1 To colDates.Count
' color rows red if <= 365
r2 = colDates(j)(0)
dt2 = colDates(j)(1)
If bRed(r2) = False And dt2 <= dt Then
bRed(r2) = True
'.Cells(r2, 1).Resize(, COLS).Interior.Color = vbRed
.Cells(r2, 1).Interior.Color = vbYellow
End If
Next
End If
Next
Next
' clear data for next account
dictTask.RemoveAll
n = 0
End If
Next
End With
Application.ScreenUpdating = True
' result
MsgBox Format(lastrow, "0,000") & " rows scanned in " _
& Format(Timer - t0, "0.0 secs")
End Sub
Sub testData(maxrow As Long, ws As Worksheet)
Const USERS = 99
Dim TaskCount As Long
TaskCount = Int(maxrow / USERS)
Dim n As Long, t0 As Single: t0 = Timer
Application.ScreenUpdating = False
With ws
.Cells.Clear
.Columns("E:E").NumberFormat = "dd-mmm-yyyy"
.Range("E1") = "Date"
.Range("I1") = "AccNo"
.Range("M1") = "Task"
For n = 2 To maxrow
.Cells(n, 5) = DateAdd("d", 1000 * Rnd(), DateSerial(2020, 1, 1))
.Cells(n, 9) = "ID_" & Format(Rnd() * USERS + 1, "000")
.Cells(n, 13) = "Task_" & Format(Rnd() * TaskCount + 1, "0000")
Next
.Columns.AutoFit
' sort username, create date, task
With .Sort
With .SortFields
.Clear
.Add2 Key:=Range("I1:I" & maxrow), SortOn:=xlSortOnValues, _
Order:=xlAscending, DataOption:=xlSortNormal
.Add2 Key:=Range("E1:E" & maxrow), SortOn:=xlSortOnValues, _
Order:=xlAscending, DataOption:=xlSortNormal
.Add2 Key:=Range("M1:M" & maxrow), SortOn:=xlSortOnValues, _
Order:=xlAscending, DataOption:=xlSortNormal
End With
.SetRange Range("E1:M" & maxrow)
.Header = xlGuess
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End With
Application.ScreenUpdating = True
MsgBox Format(maxrow, "0,000") & " rows created in " & Format(Timer - t0, "0.0 secs")
End Sub

speed up Excel vba program

Kindly see below code where it takes too much time run for more than 30rows in a range. (its similar to knapsack algorithm requirements)
let me try to explain below in detail,
Input Base sheet: Column A having values (For ex: 1555),
Column B having its Assignment value (A1),
Column C & D its filter value which will perform against input data sheet file.
Program working concept:
it takes first row(2) data from base sheet and apply filter (C2 & D2 value) in input data sheet (Column A & B respectively) then it checks value in column C and it find best sum to match the value (1555) or nearest to it and after it assigns value (which is A1) against those rows and repeats the same for next rows.
I have posted image below.
Kindly refer for Input Base sheet and Input Data sheet and
copy the codes in another workbook.
Run the macro, Choose Base sheet and the Data sheet. Program would run and assigns in Input data sheet. It runs super fast in lesser rows when I have more rows it gets hang/takes too hours to run.
Help me to where it can be speed up.
Appreciate your supports.
Thanks
input base sheet
input data sheet:
Sub sample1()
Dim lrow As Integer
Dim frow As Integer
Dim row As Integer
Dim ar As Variant
Dim aar As Variant
Dim Sol(), csol()
Dim arr As Variant
Dim pos As Integer
Dim arow() As Integer
Dim rng As Range
Dim rn As Range
Dim r As Range
Dim k As Integer
Dim itr As Integer
Dim path As String
Dim tm_base As Workbook
Dim tm_data As Workbook
Dim sh_base As Worksheet
Dim sh_data As Worksheet
Dim sh As Worksheet
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Set sh = ActiveSheet
ReDim arr(0)
arr(0) = ""
path = FileSelection("Input Base")
If path = "" Then Exit Sub
Set tm_base = Workbooks.Open(path)
path = FileSelection("Input Data")
If path = "" Then Exit Sub
Set tm_data = Workbooks.Open(path)
Set sh_base = tm_base.ActiveSheet
Set sh_data = tm_data.ActiveSheet
lrow = sh_data.Cells(Rows.Count, "A").End(xlUp).row
frow = sh_base.Cells(Rows.Count, "A").End(xlUp).row
SortMacro ActiveSheet, sh_base.Range("A2:A" & frow), sh_base.Range("A1:G" & frow), 2
SortMacro ActiveSheet, sh_data.Range("C2:C" & lrow), sh_data.Range("A1:G" & lrow), 2
For row = 2 To frow
If sh_base.Cells(row, "H") <> "Done" Then
itr = 1
sh_data.Range("A1:G" & lrow).AutoFilter Field:=4, Criteria1:="="
op2:
sh_data.Range("A1:G" & lrow).AutoFilter Field:=1, Criteria1:=sh_base.Cells(row, "C").Value
sh_data.Range("A1:G" & lrow).AutoFilter Field:=2, Criteria1:=sh_base.Cells(row, "D").Value
Set rn = Nothing
On Error Resume Next
Set rn = sh_data.Range("C2:C" & lrow).SpecialCells(xlCellTypeVisible)
On Error GoTo 0
If Not rn Is Nothing Then
ReDim ar(0)
ReDim arow(0)
k = 1
For Each r In rn
ReDim Preserve arow(k)
ReDim Preserve ar(k)
ar(k) = r.Value
arow(k) = r.row
k = k + 1
Next
ReDim Sol(LBound(ar) To UBound(ar))
ReDim csol(LBound(ar) To UBound(ar))
limsum = sh_base.Cells(row, "A").Value
For i = LBound(ar) To UBound(ar)
If ar(i) > limsum Then
ar(i) = -1
End If
Next
maxsum = 0
findsum ar, Sol, csol, maxsum, limsum, UBound(ar), UBound(ar)
ss = ""
For i = 1 To Sol(0)
'ss = ss & sep & ar(sol(i))
'sep = ","
If Not arr(UBound(arr)) = "" Then
ReDim Preserve arr(UBound(arr) + 1)
End If
arr(UBound(arr)) = ar(Sol(i))
Next i
'MsgBox ss & " sum =" & maxsum
For j = LBound(arr) To UBound(arr)
pos = Application.Match(arr(j), ar, False)
If ar(pos - 1) > 0 Then
ar(pos - 1) = -1
End If
pos = arow(pos - 1)
If sh.Range("B1") = "Option 01" Then
sh_data.Cells(pos, "D") = sh_base.Cells(row, "B").Value
Else
sh_data.Cells(pos, "D") = sh_base.Cells(row, "B").Value & " " & Format(itr, "00")
End If
Next
ReDim arr(0)
arr(0) = ""
sh_base.Cells(row, "H") = "Done"
If sh.Range("B1") = "Option 02" Then
sh_data.Range("A1:G" & lrow).AutoFilter Field:=4, Criteria1:="="
Set rng = sh_data.Range("A1:A" & lrow).SpecialCells(xlCellTypeVisible)
If rng.Cells.Count > 1 Then
itr = itr + 1
GoTo op2
End If
End If
End If
sh_data.Range("A1:G" & frow).AutoFilter Field:=1
sh_data.Range("A1:G" & frow).AutoFilter Field:=2
sh_data.Range("A1:G" & lrow).AutoFilter Field:=4
End If
Next
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
Sub findsum(ByVal a, ByRef Sol, ByRef csol, ByRef maxsum, ByRef limsum, si, maxcount, Optional s = 0, Optional lvl = 1, Optional dif = 100000, Optional minuscount = 0, Optional tsol, Optional j = 0)
' recursive sub
For i = LBound(a) To si
If a(i) > 0 Then
If s + a(i) > limsum Then findsum a, Sol, csol, maxsum, limsum, i - 1, maxcount, s, lvl + 1, dif, minuscount, tsol
s = s + a(i)
csol(lvl) = i
If s <= limsum Then
If s > maxsum Then ' we found a sum greater than current max we save it
maxsum = s
Sol(0) = lvl
For j = 1 To lvl
Sol(j) = csol(j)
Next j
End If
If i > LBound(a) Then ' pick another number
findsum a, Sol, csol, maxsum, limsum, i - 1, maxcount, s, lvl + 1, dif, minuscount, tsol
End If
End If
s = s - a(i)
If maxsum = limsum Then Exit For 'exit if exact match
End If
Next i
End Sub
Sub SortMacro(ws As Worksheet, rn As Range, rng As Range, ord As Integer)
ws.Sort.SortFields.Clear
ws.Sort.SortFields.Add Key:=rn, _
SortOn:=xlSortOnValues, Order:=ord, DataOption:=xlSortNormal
With ws.Sort
.SetRange rng
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End Sub
Function FileSelection(file As String)
Dim path As String
Dim st As String
Dim i As Integer
Dim j As Integer
FileSelection = ""
With Application.FileDialog(3)
.title = "Select the " & file & " file"
.AllowMultiSelect = False
.InitialFileName = st
.Show
If .SelectedItems.Count = 0 Then
MsgBox "You didn't select the file!", vbExclamation, "Canceled"
Exit Function
Else
FileSelection = .SelectedItems(1)
End If
End With
End Function
You can't. I ran it. With 20 base data points and 100 data points you already have sub findsum called 79 million times. It's a combinatorial explosion and no amount of code tweaking will fix that. You'll have to find a better algorithm.

VBA - Picking up a value from list without repeats

I'm writing a code which chooses "x" client ids ​​then creates a loop that randomly chooses "x" ids ​​from a list, after this the code do a xlookup to get revenue from this clients, and finally sum all the chosen clients revenues, so far so good, but I don´t know how to choose randomly without repeating the previous id of the loop. I don't know if I was clear enough. Follow code below
Private Sub pick()
resultado = 0
For X = 1 To Range("G7").Value
valor_procurado = WorksheetFunction.Choose(WorksheetFunction.RandBetween(1, 10), _
Sheets("lista").Range("a1"), _
Sheets("lista").Range("a2"), _
Sheets("lista").Range("a3"), _
Sheets("lista").Range("a4"), _
Sheets("lista").Range("a5"), _
Sheets("lista").Range("a6"), _
Sheets("lista").Range("a7"), _
Sheets("lista").Range("a8"), _
Sheets("lista").Range("a9"), _
Sheets("lista").Range("a10"))
matriz_procurada = Sheets("lista").Range("a1:a10")
matriz_retorno = Sheets("lista").Range("b1:b10")
valores = WorksheetFunction.XLookup(valor_procurado, matriz_procurada, matriz_retorno, "-", 0)
resultado = resultado + valores
Next
Range("G8") = resultado
End Sub
This should do what you are looking for if I understand correctly.
This creates a dictionary with the key being a simple index and the item the values in column B. It takes a random number between 1 and the upper limit, originally set to the last row. It then reindexes the dictionary removing the value that was selected and lowering the upper limit by 1.
I added in lastrow but you can hardcode that if you'd like.
tested with 10,000 rows, should be fast.
Dim dict As Object
Dim lr As Long
Dim upper As Long
Dim i As Long
Dim j As Long
Dim randomizer As Long
Dim total As Long
Set dict = CreateObject("Scripting.Dictionary")
total = 0
With ActiveSheet
lr = .Cells(.Rows.Count, 1).End(xlUp).Row 'assign a value to lr if you don't want the last row
upper = lr
For i = 1 To lr
dict.Add i, .Cells(i, 2).Value
Next i
For i = 1 To .Cells(7, 7).Value
randomizer = Int(upper * Rnd + 1)
total = total + dict(randomizer)
Debug.Print randomizer, total
dict.Remove randomizer
For j = (randomizer + 1) To upper
dict.Key(j) = j - 1
Next j
upper = upper - 1
Next i
.Cells(7, 8).Value = total
End With

Distribute randomly to a matrix

I have a matrix, let's say 5 columns and 10 rows.
Then I have 30 stars. I want to put them into the matrix so that the number of stars on each row is the same and the number of stars on each column is the same (3 stars each row and 6 stars each column).
If I have 40 stars, there should be 4 stars each row and 8 stars each column.
I can do the matrix by hand and I really did both cases. But the bigger the matrix is, the harder I fill the stars.
I suppose there should be a principle behind it but still haven't figure it out.
I am using VBA in Excel to generate the 5x10 matrix with 30 stars, but it takes some minutes to try all possibilities with a loop.
Sub test()
Dim xRange As Range
Set xRange = Selection
GoTo FillX
GoTo CheckRows
FillX:
xRange.Clear
xRange.HorizontalAlignment = xlCenter
For i1 = 1 To 5
For i2 = 1 To 6
v = Int(Rnd() * 10 + 1)
While xRange.Cells(v, i1).Value = "x"
v = Int(Rnd() * 10 + 1)
Wend
xRange.Cells(v, i1).Value = "x"
Next
Next
CheckRows:
x = 0
For Each Row In xRange.Rows
If WorksheetFunction.CountA(Row) <> 3 Then
x = x + 1
End If
Next
While x <> 0
GoTo FillX
GoTo CheckRows
Wend
End Sub
Is there a solution which can randomly distribute stars to a range of any size?
Goto should be avoided. Use loops instead.
This code first calculates the number of stars for each row, then uses nested loops to enter them, so there are never more stars in a row. By stepping to the next row, but retaining the column position, and then starting in the same row in the first column, you can ensure that there are no more than the defined number of stars in a column, either. The blank cells will travel diagonally from top left to right. You can see this when you apply conditional formatting.
With this pattern, you don't need to use trial and error and the code runs super fast.
Sub test()
Dim gridRows As Long, gridColumns As Long, Stars As Long
Dim myRow As Long, myColumn As Long
Dim i As Long, j As Long
Dim rowCounter As Long, columnCounter As Long, rowOffset As Long
Dim ws As Worksheet
Set ws = Me
'You can get the grid rows and columns and the number of stars from
' user input or from worksheet cells in you want. Just make sure
' they end up in the variables below.
gridRows = 20
gridColumns = 10
Stars = 60
mycolumns = Stars / gridRows
myRows = Stars / gridColumns
j = 1
rowCounter = 1
columnCounter = 1
ws.Range("A1:zz9990").ClearContents
For j = 1 To gridRows
rowOffset = 0
For i = 1 To gridColumns
ws.Cells(j, i) = 1
columnCounter = columnCounter + 1
If columnCounter > mycolumns Then
j = j + 1
columnCounter = 1
rowOffset = 1
End If
Next i
j = j - rowOffset
Next j
' randomize the results
Dim SortRange As Range
' randomize the columns
Set SortRange = ws.Range(ws.Cells(gridRows + 1, 1), ws.Cells(gridRows + 1, gridColumns))
' enter random numbers
For Each cel In SortRange
Debug.Print cel.Address
cel.Value = Rnd
Next cel
' sort left to right
ws.Sort.SortFields.Clear
ws.Sort.SortFields.Add2 Key:=SortRange _
, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With ws.Sort
.SetRange Range("A1", Cells(gridRows + 1, gridColumns))
.Header = xlGuess
.Orientation = xlLeftToRight
.SortMethod = xlPinYin
.Apply
End With
' clear the random numbers
SortRange.ClearContents
Set SortRange = ws.Range(ws.Cells(1, gridColumns + 1), ws.Cells(gridRows, gridColumns + 1))
' randomize the rows
' enter random numbers
For Each cel In SortRange
Debug.Print cel.Address
cel.Value = Rnd
Next cel
'Sort Rows
ws.Sort.SortFields.Clear
ws.Sort.SortFields.Add2 Key:=SortRange _
, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With ws.Sort
.SetRange Range("A1", Cells(gridRows, gridColumns + 1))
.Header = xlGuess
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
' clear the random numbers
SortRange.ClearContents
End Sub
Edit after comment: Added code to randomize the results. Add a row with random numbers to the end of the table, sort by those random numbers left to right, and remove them again. Do the same with a column of random numbers and sort top to bottom, then remove the helper numbers.
The screenshot shows a grid with 20 rows, 10 columns and 60 stars using conditional formatting to show the distribution better.
A grid with 60 rows, 30 columns and 1200 stars takes less than a second to build (without using conditional formatting).
This code works a lot quicker (under 1 second mostly).
Sub Button1_Click()
Set xRange = [a1:e10]
x = 0
While x < 6
xRange.Clear
xRange.HorizontalAlignment = xlCenter
For i1 = 1 To 4
For i2 = 1 To 6
Do
g3 = False
Do
v = Int(Rnd * 10 + 1)
Loop While xRange.Cells(v, i1).Value = "x"
xRange.Cells(v, i1).Value = "x"
If WorksheetFunction.CountA(xRange.Rows(v)) > 3 Then
xRange.Cells(v, i1).Value = " "
g3 = True
End If
Loop While g3 = True
xRange.Cells(v, i1).Value = "x"
Next i2
Next i1
x = 0
For Each Row In xRange.Rows
If WorksheetFunction.CountA(Row) = 2 Then x = x + 1
Next Row
Wend
For i2 = 1 To 6
Do
g3 = False
Do
v = Int(Rnd * 10 + 1)
Loop While xRange.Cells(v, 5).Value = "x"
xRange.Cells(v, 5).Value = "x"
If WorksheetFunction.CountA(xRange.Rows(v)) > 3 Then
xRange.Cells(v, i1).Value = " "
g3 = True
End If
Loop While g3 = True
xRange.Cells(v, 5).Value = "x"
Next i2
End Sub
It performs a condition on the rows as well, checking that they have no more than 3 stars in them.
This is done for the first four rows, and then it checks to see if there are at least 6 rows in the last column that can take another star (i.e. have exactly 2 stars already).

Highest possible sum

I have a list of items in column A and each of this items has 10 different values in subsequent columns. I need to create a formula (or most probably more than one formula) that would return the highest possible sum of 10 values (one from each column) with a restriction that each item can be used one time at most. I would also need an order in which those items were used. I was trying to do it in a few steps:
Step 1:
Check the highest value in column B.
Step 2:
Check the highest value in column C.
Step 3:
If this is the same item then find the second highest value for columns B and C and check which sum is higher (1st of B and second of C or other way around).
This algorithm however in rare cases gives incorrect output and the formula grows exponentially as I need to add comparison for 10 different values for each column. It would be quite bothersome if I tried to expand the number of values someday. If you see a better solution please let me know. I wouldn't mind if that would need VBA.
If you need to take a look at all combinations and come up with the best solution, then this looks like a version of the Knapsack problem or another NP-complete problem:
Image: https://xkcd.com/287/
If someone is interested in the solution of the joke above, it can be achieved with 6 nested loops, if we consider that the solution consists of maximal 6×6 elements (e.g., if there was a dessert for 1 cent, then the obvious solution for 1505 x 1 cent will not be reached:
Option Explicit
Sub TestMe()
Dim myArr As Variant
Dim myLoop As Variant
Dim targetValue As Long
Dim currentSum As Long
myArr = Array(215, 275, 335, 355, 420, 580)
targetValue = 1505
Dim cnt0&, cnt1&, cnt2&, cnt3&, cnt4&, cnt5&, cnt6&
Dim cnt As Long
For cnt0 = 0 To 5
For cnt1 = 0 To 5
For cnt2 = 0 To 5
For cnt3 = 0 To 5
For cnt4 = 0 To 5
For cnt5 = 0 To 5
currentSum = 0
Dim printableArray As Variant
printableArray = Array(cnt0, cnt1, cnt2, cnt3, cnt4, cnt5)
For cnt = LBound(myArr) To UBound(myArr)
IncrementSum printableArray(cnt), myArr(cnt), currentSum
Next cnt
If currentSum = targetValue Then
printValuesOfArray printableArray, myArr
End If
Next: Next: Next: Next: Next: Next
End Sub
Public Sub printValuesOfArray(myArr As Variant, initialArr As Variant)
Dim cnt As Long
Dim printVal As String
For cnt = LBound(myArr) To UBound(myArr)
If myArr(cnt) Then
printVal = printVal & myArr(cnt) & " * " & initialArr(cnt) & vbCrLf
End If
Next cnt
Debug.Print printVal
End Sub
Public Sub IncrementSum(ByVal multiplicator As Long, _
ByVal arrVal As Long, ByRef currentSum As Long)
currentSum = currentSum + arrVal * multiplicator
End Sub
Thus the only solution is:
1 * 215
2 * 355
1 * 580
And if you have studied more than one semester of Algorithms and somehow you hate nested loops, then the above code can be written with recursion:
Option Explicit
Sub Main()
Dim posArr As Variant
Dim iniArr As Variant
Dim tryArr As Variant
Dim cnt As Long
Dim targetVal As Long: targetVal = 1505
iniArr = Array(215, 275, 335, 355, 420, 580)
ReDim posArr(UBound(iniArr))
ReDim tryArr(UBound(iniArr))
For cnt = LBound(posArr) To UBound(posArr)
posArr(cnt) = cnt
Next cnt
EmbeddedLoops 0, posArr, tryArr, iniArr, targetVal
End Sub
Function EmbeddedLoops(index As Long, posArr As Variant, tryArr As Variant, _
iniArr As Variant, targetVal As Long)
Dim myUnit As Variant
Dim cnt As Long
If index >= UBound(posArr) + 1 Then
If CheckSum(tryArr, iniArr, targetVal) Then
For cnt = LBound(tryArr) To UBound(tryArr)
If tryArr(cnt) Then Debug.Print tryArr(cnt) & " x " & iniArr(cnt)
Next cnt
End If
Else
For Each myUnit In posArr
tryArr(index) = myUnit
EmbeddedLoops index + 1, posArr, tryArr, iniArr, targetVal
Next myUnit
End If
End Function
Public Function CheckSum(posArr, iniArr, targetVal) As Boolean
Dim cnt As Long
Dim compareVal As Long
For cnt = LBound(posArr) To UBound(posArr)
compareVal = posArr(cnt) * iniArr(cnt) + compareVal
Next cnt
CheckSum = CBool(compareVal = targetVal)
End Function
The following VBA macro assumes that the Item Name is in Column A, the Values are in Columns B to K, that Row 1 is a header, and that the Values are Long (i.e. no Decimal points)
This is an inefficient brute-force method. For 10 items, it takes about 2 minutes to calculate. For 11 items, it takes about 7.5 minutes, etc - since growth will be exponential, you will want to pare down the possible answers before you run it. (e.g. the Item for each column will be taken from the top 10 Values for that column - so, you can delete any item that doesn't appear in the top 10 for any column)
Option Explicit
Sub VeryLongBruteForceMethod()
Dim Screen As Boolean, Calc As XlCalculation, Mouse As XlMousePointer
Mouse = Application.Cursor
Application.Cursor = xlDefault
Screen = Application.ScreenUpdating
Calc = Application.Calculation
Application.Cursor = xlWait
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
'Row / Value for each column
Dim MaxItems(0 To 9, 0 To 1) As Long, lMaxVal As Long
Dim TestItems(0 To 9, 0 To 1) As Long, lTestVal As Long
Dim lMaxRow As Long, lTestRow As Long, bTest As Boolean
Dim lCol0 As Long, lCol1 As Long, lCol2 As Long, lCol3 As Long, lCol4 As Long
Dim lCol5 As Long, lCol6 As Long, lCol7 As Long, lCol8 As Long, lCol9 As Long
Dim wsTarget As Worksheet
Set wsTarget = ThisWorkbook.Worksheets(1) 'First sheet in Workbook
lMaxRow = wsTarget.Cells(wsTarget.Rows.Count, 1).End(xlUp).Row 'Get Row for last item
lMaxVal = 0
For lCol0 = 2 To lMaxRow 'Assumes Row1 is a header
TestItems(0, 0) = lCol0 'Store row
TestItems(0, 1) = wsTarget.Cells(lCol0, 2).Value 'Store value
For lCol1 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
If lCol1 = lCol0 Then bTest = False 'Row already used in this permutation
If bTest Then
TestItems(1, 0) = lCol1 'Store row
TestItems(1, 1) = wsTarget.Cells(lCol1, 3).Value 'Store value
For lCol2 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 1
If TestItems(lTestRow, 0) = lCol2 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(2, 0) = lCol2 'Store row
TestItems(2, 1) = wsTarget.Cells(lCol2, 4).Value 'Store value
For lCol3 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 2
If TestItems(lTestRow, 0) = lCol3 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(3, 0) = lCol3 'Store row
TestItems(3, 1) = wsTarget.Cells(lCol3, 5).Value 'Store value
For lCol4 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 3
If TestItems(lTestRow, 0) = lCol4 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(4, 0) = lCol4 'Store row
TestItems(4, 1) = wsTarget.Cells(lCol4, 6).Value 'Store value
For lCol5 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 4
If TestItems(lTestRow, 0) = lCol5 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(5, 0) = lCol5 'Store row
TestItems(5, 1) = wsTarget.Cells(lCol5, 7).Value 'Store value
For lCol6 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 5
If TestItems(lTestRow, 0) = lCol6 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(6, 0) = lCol6 'Store row
TestItems(6, 1) = wsTarget.Cells(lCol6, 8).Value 'Store value
For lCol7 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 6
If TestItems(lTestRow, 0) = lCol7 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(7, 0) = lCol7 'Store row
TestItems(7, 1) = wsTarget.Cells(lCol7, 9).Value 'Store value
For lCol8 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 7
If TestItems(lTestRow, 0) = lCol8 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(8, 0) = lCol8 'Store row
TestItems(8, 1) = wsTarget.Cells(lCol8, 10).Value 'Store value
For lCol9 = 2 To lMaxRow 'Assumes Row1 is a header
bTest = True
For lTestRow = 0 To 8
If TestItems(lTestRow, 0) = lCol9 Then
bTest = False 'Row already used in this permutation
Exit For '1 failure is enough
End If
Next lTestRow
If bTest Then
TestItems(9, 0) = lCol9 'Store row
TestItems(9, 1) = wsTarget.Cells(lCol9, 11).Value 'Store value
lTestVal = 0
'Application.StatusBar = lCol0 & "|" & lCol1 & "|" & lCol2 & "|" & lCol3 & "|" & lCol4 & "|" & lCol5 & "|" & lCol6 & "|" & lCol7 & "|" & lCol8 & "|" & lCol9
For lTestRow = 0 To 9 'Total up our Value
lTestVal = lTestVal + TestItems(lTestRow, 1)
Next lTestRow
If lTestVal > lMaxVal Then 'Compare to current Max
For lTestRow = 0 To 9 'If more, replace with new Max
MaxItems(lTestRow, 0) = TestItems(lTestRow, 0)
MaxItems(lTestRow, 1) = TestItems(lTestRow, 1)
Next lTestRow
lMaxVal = lTestVal
End If
End If
Next lCol9
End If
Next lCol8
End If
Next lCol7
End If
DoEvents ' Try not to let Excel crash on us!
Next lCol6
End If
Next lCol5
End If
Next lCol4
End If
Next lCol3
End If
Next lCol2
End If
Next lCol1
Next lCol0
'Output to a message box:
'Column 1: ItemName01 | Value01
' ...
'Column 10: ItemName10 | Value10
'Total Value | TotalValue
Dim sOutput As String
sOutput = ""
For lTestRow = 0 To 9
sOutput = sOutput & "Column " & (lTestRow + 1) & ": " & wsTarget.Cells(MaxItems(lTestRow, 0), 1).Value & " | " & MaxItems(lTestRow, 1) & vbCrLf
Next lTestRow
sOutput = sOutput & "Total Value | " & lMaxVal
MsgBox sOutput
Erase TestItems
Erase MaxItems
Application.StatusBar = False
Application.Cursor = Mouse
Application.Calculation = Calc
Application.ScreenUpdating = Screen
End Sub

Resources