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
Related
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.
For the following image:
I have the list of unique players in our quiz nights. I need a way to select and show the best player across all games we play.
Each game is on its own separate table. We have only had 2 quiz nights so far but would like to do more so it needs to be dynamic.
I need a function to select the best player on any of the teams (they can play on different teams each game) who plays in the highest-scoring team.
So a function that selects all the headers, and compares against the unique list of players, and then finds the player who has played on both/all the highest scoring/winning teams for all games that we have played, and will play.
And needs to be able to add a new table to index each time a new game is played.
Also, each time we play, there can be more or less teams playing.
Edit: Thanks #CDP1802 for the excellent answer. It worked perfectly.
I added some nice formatting and colouring, and etc to the final table. For anyone trying to achieve a similar result, here is the final code:
Private Sub Worksheet_Activate()
Call FindHighestPlayer
End Sub
Function FindHighestPlayer()
Dim wb As Workbook, ws As Worksheet, tbl As ListObject
Dim r As Long, c As Long, data As Range
Dim team As String, score As Single, qcount As Long
Set wb = ThisWorkbook
Set ws = wb.Sheets("Sheet1") ' score sheet
Dim dict As Object, key, ar
Set dict = CreateObject("Scripting.Dictionary")
' scan each table
For Each tbl In ws.ListObjects
Set data = tbl.DataBodyRange
For c = 1 To tbl.HeaderRowRange.Columns.Count
' don't count answer and question
If InStr(1, LCase(tbl.HeaderRowRange.Cells(1, c)), "question") = 0 And InStr(1, LCase(tbl.HeaderRowRange.Cells(1, c)), "answer") = 0 Then
' team from header row
team = tbl.HeaderRowRange.Cells(1, c)
qcount = tbl.DataBodyRange.Rows.Count
score = WorksheetFunction.Sum(data.Cells(1, c).Resize(qcount))
' update team members performance
For Each key In Split(team, ", ")
key = Trim(key) ' team members name
If dict.exists(key) Then
ar = dict(key)
ar(0) = ar(0) + score
ar(1) = ar(1) + qcount
ar(2) = ar(2) + 1 ' number of quizes
dict(key) = ar
Else
dict.Add key, Array(score, qcount, 1)
End If
Next
End If
Next
Next
' dump results to another sheet
Set ws = Sheet2 ' wb.sheets("Player Scores")
With ws
.Cells.Clear
.Range("A1:D1") = Array("Player", "Score", "Avg %", "Number of Quiz's Played")
.Range("C:C").NumberFormat = "0%"
r = 1
For Each key In dict
r = r + 1
ar = dict(key)
.Cells(r, 1) = key
.Cells(r, 2) = ar(0) & " out of " & ar(1)
.Cells(r, 3).FormulaR1C1 = "=" & ar(0) & "/" & ar(1)
.Cells(r, 4) = ar(2)
Next
End With
' Sort table
With ws.Sort
.SortFields.Clear
.SortFields.Add ws.Range("C1"), SortOn:=xlSortOnValues, _
Order:=xlDescending, DataOption:=xlSortNormal
.SetRange ws.Range("A1:D" & r)
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
' Format headers
With ws
.Range("A1:D1").HorizontalAlignment = xlCenter
.Range("A1:D1").VerticalAlignment = xlBottom
.Range("A1:D1").Font.FontStyle = "Bold"
.Range("A1:D1").Font.Size = 15
.Range("A1:D1").Font.Color = RGB(68, 84, 106)
.Range("A1:D1").Borders(xlEdgeBottom).LineStyle = xlContinuous
.Range("A1:D1").Borders(xlEdgeBottom).Weight = xlThick
.Range("A1:D1").Borders(xlEdgeBottom).Color = RGB(68, 114, 196)
End With
' Delete existing format conditions
ws.Range("A1:D" & r).FormatConditions.Delete
' Format data
With ws
.Range("A1:D" & r).Locked = True
.Range("B2:B" & r).NumberFormat = "General"
.Range("B2:B" & r).HorizontalAlignment = xlRight
.Range("D2:D" & r).HorizontalAlignment = xlCenter
End With
' Format Avg
Dim cs As ColorScale
Set cs = Range("C2:C" & r).FormatConditions.AddColorScale(ColorScaleType:=3)
With cs
' the first color is light red
With .ColorScaleCriteria(1)
.FormatColor.Color = RGB(248, 105, 107)
.Type = xlConditionValueNumber
.Value = 0
End With
' the second color is light yellow
With .ColorScaleCriteria(2)
.FormatColor.Color = RGB(255, 235, 132)
.Type = xlConditionValueNumber
.Value = 0.5
End With
' the third color is light green
With .ColorScaleCriteria(3)
.FormatColor.Color = RGB(99, 190, 123)
.Type = xlConditionValueNumber
.Value = 1
End With
End With
End Function
Use another sheet to collate the results and sort to find the best.
Option Explicit
Sub LeagueTable()
Dim wb As Workbook, ws As Worksheet, tbl As ListObject
Dim r As Long, c As Long, data As Range
Dim team As String, score As Single, qcount As Long
Set wb = ThisWorkbook
Set ws = wb.Sheets("Sheet1") ' score sheet
Dim dict As Object, key, ar
Set dict = CreateObject("Scripting.Dictionary")
' scan each table
For Each tbl In ws.ListObjects
Set data = tbl.DataBodyRange
For c = 1 To tbl.HeaderRowRange.Columns.Count
' team from header row
team = tbl.HeaderRowRange.Cells(1, c)
qcount = tbl.DataBodyRange.Rows.Count
score = WorksheetFunction.Sum(data.Cells(1, c).Resize(qcount))
' update team members performance
For Each key In Split(team, ",")
key = Trim(key) ' team members name
If dict.exists(key) Then
ar = dict(key)
ar(0) = ar(0) + score
ar(1) = ar(1) + qcount
ar(2) = ar(2) + 1 ' number of quizes
dict(key) = ar
Else
dict.Add key, Array(score, qcount, 1)
End If
Next
Next
Next
' dump results to another sheet
Set ws = Sheet2 'wb.sheets("Player Scores")
With ws
.Cells.Clear
.Range("A1:E1") = Array("Player", "Score", "Count", "Avg %", "Quiz Count")
.Range("D:D").NumberFormat = "0%"
r = 1
For Each key In dict
r = r + 1
ar = dict(key)
.Cells(r, 1) = key
.Cells(r, 2) = ar(0)
.Cells(r, 3) = ar(1)
.Cells(r, 4).FormulaR1C1 = "=RC[-2]/RC[-1]"
.Cells(r, 5) = ar(2)
Next
End With
' sort table
With ws.Sort
.SortFields.Clear
.SortFields.Add ws.Range("D1"), SortOn:=xlSortOnValues, _
Order:=xlDescending, DataOption:=xlSortNormal
.SetRange ws.Range("A1:E" & r)
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
ws.Activate
ws.Range("A1").Select
MsgBox "Done"
End Sub
if I understand your question you need to detect which column name has the largest value in the last row
so first you need a new row you may hide contain the sum of ones above without "/16"
and in the cell use (index & match & max)
=INDEX("range of players"; MATCH( MAX("range of scores"); "range of scores" ; 0))
you could use "," instead of ";" according to your office numbers and date settings
What I want the Macro to accomplish:
I want the user to be able to fill in data from E2 to E9 on the spreadsheet. When the user presses the "Add Car" button the macro is supposed to be executed. The makro then should take the handwritten data, copy everything from E2:E9 and put it into a table that starts at with C13 and spans over 7 columns, always putting the new set of data in the next free row. It is also supposed to check for duplicates and give an alert while not overwriting the original set of data
So my problem is, that I want the Macro I'm writing to take the information put into certain cells and then copy them into a table underneath.
I'm starting the Macro like this
Sub addData()
Dim lastrow As Long, nextBlankRow As Long
lastrow = Cells.Find(What:="*", _
After:=Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlPrevious, _
MatchCase:=False).Row
nextBlankRow = lastrow + 1
Here I try to define how the Macro is supposed to find the last empty cell and also define lastrow and nextBlankRow.
After that I'm starting with a simple If statement to see if the person has at least something in E2 on the same sheet.
If Range("E2") = "" Then
MsgBox "Wählen Sie ein KFZ aus!"
Range("E2").Select
Exit Sub
End If
This works. When I'm not putting something into E2 I get the textbox with the alert.
Anyway if the IF-Statement is not triggered to exit the sub the Macro is given the instructions to get the information and put it in the table below
Cells(nextBlankRow, 3) = Range("E2")
Cells(nextBlankRow, 4) = Range("E3")
Cells(nextBlankRow, 5) = Range("E4")
Cells(nextBlankRow, 6) = Range("E5")
Cells(nextBlankRow, 7) = Range("E6")
Cells(nextBlankRow, 8) = Range("E7")
Cells(nextBlankRow, 9) = Range("E8")
Here seems to be a problem that probably relates to me failing to define variables correctly?
Because the Macro finds the right row but only overwrites into that row. So it ignores the fact that it "should" skip to the nextBlankrow which I defined earlier as
nextBlankRow = lastrow + 1
In addition to that I also have a line of code inplace which is supposed to check for duplicates
Dim p As Long, q As Long
p = 13
q = p + 1
Do While Cells(p, 3) <> ""
Do While Cells(q, 3) <> ""
If Cells(p, 3) = Cells(q, 3) And Cells(p, 4) = Cells(q, 4) Then
MsgBox "Datensatz schon vorhanden!"
Range(Cells(q, 3), Cells(q, 9)).ClearContents
Else
q = q + 1
End If
Loop
p = p + 1
q = p + 1
Loop
End Sub
Which always gives a false return. So even if the same set of Data is copied twice into the same row (as it does) it only "refreshes" the data and doesn't say "you're not allowed to do that".
I'm at a loss here.
Here's the full code for ease of use
Sub addData()
Dim lastrow As Long, nextBlankRow As Long
lastrow = Cells.Find(What:="*", _
After:=Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlPrevious, _
MatchCase:=False).Row
nextBlankRow = lastrow + 1
If Range("E2") = "" Then
MsgBox "Wählen Sie ein KFZ aus!"
Range("E2").Select
Exit Sub
End If
Cells(nextBlankRow, 3) = Range("E2")
Cells(nextBlankRow, 4) = Range("E3")
Cells(nextBlankRow, 5) = Range("E4")
Cells(nextBlankRow, 6) = Range("E5")
Cells(nextBlankRow, 7) = Range("E6")
Cells(nextBlankRow, 8) = Range("E7")
Cells(nextBlankRow, 9) = Range("E8")
Dim p As Long, q As Long
p = 13
q = p + 1
Do While Cells(p, 3) <> ""
Do While Cells(q, 3) <> ""
If Cells(p, 3) = Cells(q, 3) And Cells(p, 4) = Cells(q, 4) Then
MsgBox "Datensatz schon vorhanden!"
Range(Cells(q, 3), Cells(q, 9)).ClearContents
Else
q = q + 1
End If
Loop
p = p + 1
q = p + 1
Loop
End Sub
```![enter image description here](https://i.stack.imgur.com/dJozM.jpg)![enter image description here](https://i.stack.imgur.com/Q90Ah.jpg)
Please, test the next code:
Sub copyRangeOnLastEmptyRow()
Dim sh As Worksheet, arr, lastERow As Long, matchCel As Range
Set sh = ActiveSheet
arr = sh.Range("E2:E9").value
lastERow = sh.Range("C" & sh.rows.Count).End(xlUp).row + 1
If lastERow < 13 Then lastERow = 13
'check if the range has not been alredy copied:
Set matchCel = sh.Range("C13:C" & lastERow - 1).Find(WHAT:=sh.Range("E2").value, LookIn:=xlValues, Lookat:=xlWhole, MatchCase:=False)
If Not matchCel Is Nothing Then
MsgBox sh.Range("E2").value & " has been found in cell " & matchCel.Address & "."
'bring up the data of the existing row:
sh.Range("E3:E9").value = Application.Transpose(sh.Range(matchCel.Offset(0, 1), matchCel.Offset(0, 7)).value)
Exit Sub
End If
sh.Range("C" & lastERow).Resize(1, UBound(arr)).value = Application.Transpose(arr)
sh.Range("E2:E9").ClearContents
End Sub
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
I have a For loop nested inside another For loop which iterates over every row in a spreadsheet.
The nested for loop below checks the current row and then loops over every row in the spreadsheet to see if it matches the If statement criteria. If so, it changes a bool to True and exits the nested loop.
This method takes far too long. The spreadsheet is 1000 rows x 27 columns and will take forever to run through on the small PC I'm using.
The Code:
Sub Check_Errors()
Dim x As Integer
Dim lastRow As Long
Dim duplicateData As Boolean
Set Data = ThisWorkbook.Sheets("Data") 'Worksheet with Raw data
Set Errors = ThisWorkbook.Sheets("Errors") 'Where any flagged rows are copied to.
x = 2
lastRow = Data.Cells(Data.Rows.Count, "A").End(xlUp).Row
duplicateData = False
'Copies the headings from Data worksheet to Error worksheet
For j = 1 To 26
Errors.Cells(1, j).Value = Data.Cells(1, j).Value
Next j
Errors.Cells(1, 27).Value = "Error Type"
For i = 2 To lastRow
wrongSpeciality = False
For j = 2 To 300
If ((Data.Cells(i, 19) < Data.Cells(j, 19) + (Data.Cells(j, 20) / 1440) + (Data.Cells(j, 21) / 1440) _
And Data.Cells(i, 19) >= Data.Cells(j, 19)) _
Or _
(Data.Cells(i, 19) + (Data.Cells(i, 20) / 1440) + (Data.Cells(i, 21) / 1440) <= Data.Cells(j, 19) + (Data.Cells(j, 20) / 1440) + (Data.Cells(j, 21) / 1440) _
And Data.Cells(i, 19) + (Data.Cells(i, 20) / 1440) + (Data.Cells(i, 21) / 1440) > Data.Cells(j, 19))) _
_
And Data.Cells(i, 18) = Data.Cells(j, 18) _
And Data.Cells(i, 22) = Data.Cells(j, 22) _
And Not i = j Then
duplicateData = True
Exit For
End If
Next j
'If true then copy flagged row to Error worksheet and add additional column with reason row was flagged.
If duplicateData Then
For j = 1 To 26
Errors.Cells(x, j).Value = Data.Cells(i, j).Value
Next j
Errors.Cells(x, 27).Value = "Time overlapping"
x = x + 1
End If
Next i
The Data
ClinicalTime and AdminTime are in minutes and need to be divided by 1440 before being added to Time to get the correct finish time.
James sees someone at 13:00 on the 12th Jan and finishes at 13:30. But it also shows he saw someone at 13:25 which isn't possible as he was with someone during that time.
The above code will change duplicateData to True for both these rows, but will take a very long time to do so over thousands of these instances.
Columns 18 19 20 21 22
Date Time ClinicalTime AdminTime Clinician
12/01/18 13:00 20 10 James
12/01/18 13:25 10 20 James
12/01/18 14:30 40 0 James
14/01/18 10:00 20 20 Samantha
Worth noting is a finish time can be the same as a start time, so James could see a patient at 11:00, finish at 11:30, and have start time for the next patient at 11:30 and there would be no need to flag these two.
Imagine the following data:
Sort it by BOTH:
Column V (Clinician) A … Z
AND Column W (START) low … high
I added 3 columns W, X and Y (you can use others). Formulas are:
Column W: =R:R+S:S (adds together start date and time)
Column X: =R:R+S:S+T:T/1440+U:U/1440 (calculates end date/time)
Y2 (and copy down): =IF(AND(V2=V1,W2<X1),"Overlapping","-")
Then filter by Column Y (Overlapping) and you have your data.
How does the formula work?
The formula checks for each row if the Clinician is the same as in the row before (otherwise it is the first row of that Clinician which can never be a overlap). Then it checks if the START is before the END of the row before. If so it is overlapping, otherwise not.
Note that this method works only on properly sorted data.
If using formulas does not meet your needs then the idea of this method could also be used in VBA. This should be much faster because only minimal amount of data would be needed to test and you need to loop through all rows only once.
Option Explicit
Public Sub CheckForOverlappings()
Dim wsData As Worksheet
Set wsData = ThisWorkbook.Worksheets("Data")
Dim wsErrors As Worksheet
Set wsErrors = ThisWorkbook.Worksheets("Errors")
Dim LastDataRow As Long
LastDataRow = wsData.Cells(wsData.Rows.Count, "A").End(xlUp).Row
Dim LastErrorRow As Long
LastErrorRow = 2
'sort data by …
With wsData.Sort
.SortFields.Clear
'… field Clinician
.SortFields.Add2 Key:=Range("V2"), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
'… field Date
.SortFields.Add2 Key:=Range("R2"), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
'… field Time
.SortFields.Add2 Key:=Range("S2"), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
.SetRange Range("1:" & LastDataRow)
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
'write headers for error sheet
wsErrors.Cells(1, 1).Resize(ColumnSize:=26).Value = wsData.Cells(1, 1).Resize(ColumnSize:=26).Value
wsErrors.Cells(1, 27).Value = "Error Type"
'read relevant data into array
Dim arrData As Variant
arrData = wsData.Range(wsData.Cells(1, 18), wsData.Cells(LastDataRow, 22))
'initialize start/enddate with first data row (= row 2)
Dim StartDate As Date
StartDate = arrData(2, 1) + arrData(2, 2)
Dim EndDate As Date
EndDate = StartDate + arrData(2, 3) / 1440 + arrData(2, 4) / 1440
Dim iRow As Long
For iRow = 3 To UBound(arrData, 1) 'loop from data row 2 (= row 3) we used data row 1 in initialization already
'determine start date of current row
StartDate = arrData(iRow, 1) + arrData(iRow, 2)
If arrData(iRow, 1) = arrData(iRow - 1, 1) And StartDate < EndDate Then 'check same cinician and overlapping
'copy column 1 … 26 to error sheet
wsErrors.Cells(LastErrorRow, 1).Resize(ColumnSize:=26).Value = wsData.Cells(iRow, 1).Resize(ColumnSize:=26).Value
LastErrorRow = LastErrorRow + 1
End If
'remember end date of current row (for comparison with next row)
EndDate = StartDate + arrData(iRow, 3) / 1440 + arrData(iRow, 4) / 1440
Next iRow
End Sub