Faster matching code - excel

I have a code that takes a list of Airline flightlegs and matches them up to give me full lines of flight. The code works but..... it takes a very long time (45-60 min for just 35,000 rows) due to the amount of data it has to go through. This is compounded and the overall code takes about 2 hours to run. Is there a faster method to get the same results?
Here is my current code that really bogs down the entire process:
Sub BuildingLines()
'strings together segments into trip
Dim i As Long
Dim z As Long
Dim T As Long
Dim c As Long
Dim a As Long
Dim f As Long
Dim l As Long
Dim g As Long
Dim y As String
Dim b As String
Set ref = Sheets("Ref")
With Sheets("MoveData")
z = .Cells(1, 1).End(xlDown).Row
x = .Cells(2, 1).End(xlToRight).Column
Range(.Cells(1, 3), (.Cells(z, x + 3))).Clear
z = .Cells(1, 1).End(xlDown).Row
g = ref.Cells(24, 1).End(xlDown).Row
For a = 24 To g
If ref.Cells(a, 2) = "" Then GoTo nexta
f = ref.Cells(a, 2)
c = ref.Cells(a, 3)
l = ref.Cells(a, 4)
Set LegTable = Range(.Cells(f, 1), .Cells(l, 1))
For i = f To l
Application.StatusBar = "Progress: Step 5 of 20 - Building lines for " & ref.Cells(a, 1) & " (" & (a - 23) & " A/C types of " & (g - 23) & ") : " & (i - f) + 1 & " Legs of " & c & " analyzed. (" & Format(((i - f) + 1) / c, "Percent") & ")"
DoEvents
'On Error GoTo NextI
If IsError(Application.Match(.Cells(i, 2), LegTable, 0)) Then
GoTo nexti
Else
y = Application.Match(.Cells(i, 2), LegTable, 0) + f - 1
.Cells(i, 1).End(xlToRight).Offset(0, 1).Value2 = .Cells(y, 2)
Do
'On Error GoTo NextI
If IsError(Application.Match(.Cells(y, 2), LegTable, 0)) Then
GoTo nexti
Else
b = Application.Match(.Cells(y, 2), LegTable, 0) + f - 1
h = .Cells(b, 2)
.Cells(i, 1).End(xlToRight).Offset(0, 1) = h
y = b
End If
Loop
nexti:
End If
b = ""
y = ""
Next i
nexta:
Next a
End With
End Sub
The data is all string data of about 50+ chars.
Thank you for any recomendations.

Thank you very much A.S.H. with your help, I not only learned alot about using arrays, but also ended up cutting my runtime from about 90 minutes to just over 3 minutes. This is my final working code that used a combination of your suggestions.
Sub BuildingLines()
'strings together segments into trip
Dim i As Long
Dim z As Long
Dim c As Long
Dim f As Long
Dim l As Long
Dim LegTable As Range
Dim TurnTable As Range
Dim FirstTurn() As Variant
Dim NextTurn() As Variant
Dim y As String
Dim b As String
Dim FTtext As String
Dim wb As Workbook
Dim ref As Worksheet
Set wb = ThisWorkbook
Set ref = wb.Sheets("Ref")
With Sheets("MoveData")
z = .Cells(1, 1).End(xlDown).Row
x = .Cells(2, 1).End(xlToRight).Column
Range(.Cells(1, 3), (.Cells(z, x + 3))).Clear
z = .Cells(1, 1).End(xlDown).Row
g = ref.Cells(24, 1).End(xlDown).Row
For a = 24 To g
If ref.Cells(a, 2) = "" Then GoTo NextA
f = ref.Cells(a, 2)
c = ref.Cells(a, 3)
l = ref.Cells(a, 4)
Set LegTable = Range(.Cells(f, 1), .Cells(l, 1))
Set TurnTable = Range(.Cells(f, 1), .Cells(l, 2))
FirstTurn = TurnTable
.Sort.SortFields.Clear
.Sort.SortFields.Add Key:=LegTable, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With .Sort
.SetRange TurnTable
.Header = xlNo
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
For i = f To l
Application.StatusBar = "Progress: Step 5 of 20 - Building lines for " & ref.Cells(a, 1) & " (" & (a - 23) & " A/C types of " & (g - 23) & ") : " & (i - f) + 1 & " Legs of " & c & " analyzed. (" & Format(((i - f) + 1) / c, "Percent") & ")"
DoEvents
y = 0
b = 0
y = Application.Match(.Cells(i, 2), LegTable, 1)
If .Cells(i, 2) <> FirstTurn(y, 1) Then GoTo NextI
NextLeg = NextLeg + 1
ReDim Preserve NextTurn(0, 1 To NextLeg)
NextTurn(0, NextLeg) = FirstTurn(y, 2)
Do
FTtext = FirstTurn(y, 2)
On Error GoTo errhdlr
b = Application.WorksheetFunction.Match(FTtext, LegTable, 1)
If FTtext <> FirstTurn(b, 1) Then GoTo NextI
NextLeg = NextLeg + 1
ReDim Preserve NextTurn(0, 1 To NextLeg)
NextTurn(0, NextLeg) = FirstTurn(b, 2)
y = b
Loop
errhdlr:
Resume NextI
NextI:
If NextLeg > 0 Then Range(.Cells(i, 3), .Cells(i, NextLeg + 2)).Value = NextTurn
Erase NextTurn
NextLeg = 0
Next i
Set LegTable = Nothing
Set TurnTable = Nothing
Erase NextTurn
Erase FirstTurn
NextA:
Next a
End With
End Sub
I first tried using just the arrays, but the Match function was WAY SLOWER in the arrays. So I ended up using the Match to find the index and then grabbed the data from the array to build my second array which then became my output. I can't wait to adapt my new found knowledge with the rest of this project and cut my runtime from 2+ hours to just minutes!! Thanks, again!!!

Related

Multiple Criteria Evaluate Match Function Prohibitively Slow?

The following code successfully executes for small data sets:
Option Explicit
Option Base 1
Sub Left()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim ws1, _
ws2 As Worksheet, _
wb As Workbook
Set wb = ThisWorkbook
Set ws1 = wb.Worksheets("Adj")
Set ws2 = wb.Worksheets("Deleted")
Dim a, _
b, _
i, _
j, _
k As Long
a = 957
b = 290150
Dim Item1, _
Item2, _
Arr() As Variant
With ws2
For i = 2 To a
.Cells(i, 6) = Left(.Cells(i, 1), 11)
.Cells(i, 7) = Right(.Cells(i, 1), 4)
Next i
End With
With ws1
For j = 2 To b
ReDim Preserve Arr(j - 1)
Item1 = Chr(34) & .Cells(j, 7) & Chr(34)
Item2 = Chr(34) & .Cells(j, 9) & Chr(34)
On Error Resume Next
k = Evaluate("=MATCH(1,('Deleted'!F:F = " & Item1 & ")*('Deleted'!G:G = " & Item2 & "),0)")
If Err.Number = 13 Then
Arr(j - 1) = ""
Else: Arr(j - 1) = k
End If
On Error GoTo 0
Next j
.Range(.Cells(2, 15), .Cells(b, 15)) = WorksheetFunction.Transpose(Arr())
End With
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
End Sub
However, for large data sets - such as 290,150 rows - the macro spins its wheels. It's known that Evaluate is expensive to run and I have tried running for sample sizes of 30 (success) and 1,000 (unsuccessful) and debugged carefully. Obviously in-cell array formulation drag-and-drop is not a practical alternative. So, the problem reduces to resolving endless spinning for the given multiple criteria match function required.
How do I bypass this constraint?
Try this approach using a dictionary as a lookup:
Sub Left()
Dim wsAdj As Worksheet, wsDel As Worksheet, wb As Workbook
Dim lrDel As Long, lrAdj As Long, r As Long
Dim dict, t, arr, arrG, arrI, arrRes, k
Set wb = ThisWorkbook
Set wsAdj = wb.Worksheets("Adj")
Set wsDel = wb.Worksheets("Deleted")
lrAdj = 290150
lrDel = 957
t = Timer
'load a dictionary with lookup values constructed from wsDel ColA
Set dict = CreateObject("scripting.dictionary")
arr = wsDel.Range("A2:A" & lrDel).Value
For r = 1 To UBound(arr, 1)
k = Left(arr(r, 1), 11) & Chr(0) & Right(arr(r, 1), 4)
dict(k) = r + 1 '+1 to adjust for starting at row 2
Next r
arrG = wsAdj.Range("G2:G" & lrAdj).Value 'get the match columns as arrays
arrI = wsAdj.Range("I2:I" & lrAdj).Value
ReDim arrRes(1 To UBound(arrG, 1), 1 To 1) 'resize the "result" array
'loop the values from wsAdj
For r = 1 To UBound(arrG, 1)
k = arrG(r, 1) & Chr(0) & arrI(r, 1) 'build the "key"
If dict.exists(k) Then
arrRes(r, 1) = dict(k) 'get the matched row
End If
Next r
wsAdj.Cells(2, 15).Resize(UBound(arrRes, 1), 1).Value = arrRes 'put the array on the sheet
Debug.Print "done", Timer - t ' <1 sec
End Sub
Stating Ranges instead of Columns and removing ReDim on loop helped.

Any way I can speed up this sub procedure?

I figure there must be a way to drastically shorten this sub procedure. The reason why I even want to is because it's freezing up the application on the elseif userform1.optTerm line because that worksheet it pulls the data from is 6x longer in rows than the optInSeat list.
Sub LoadEmployee_Cmb_HC()
Dim isWS As Worksheet: Set isWS = ThisWorkbook.Worksheets("In Seat")
Dim tWs As Worksheet: Set tWs = ThisWorkbook.Worksheets("Terms")
Dim a, b As Long, c As Variant, i As Long
If UserForm1.optInSeat = True Then
If UserForm1.optEmployeeName = True Then
For i = 2 To isWS.Cells(Rows.Count, 4).End(xlUp).row
x = Application.CountIf(isWS.Range("D" & 4, "D" & i), _
isWS.Cells(i, 4).Value)
If x = 1 Then
UserForm1.cmbEmployee.AddItem isWS.Cells(i, 4) & " - " & isWS.Cells(i, 1)
End If
Next i
For a = 0 To UserForm1.cmbEmployee.ListCount - 1
For b = 0 To UserForm1.cmbEmployee.ListCount - 1
If UserForm1.cmbEmployee.List(b) > UserForm1.cmbEmployee.List(a) Then
c = UserForm1.cmbEmployee.List(a)
UserForm1.cmbEmployee.List(a) = UserForm1.cmbEmployee.List(b)
UserForm1.cmbEmployee.List(b) = c
End If
Next
Next
ElseIf UserForm1.optEmployeeID = True Then
For i = 2 To isWS.Cells(Rows.Count, 1).End(xlUp).row
x = Application.CountIf(isWS.Range("A" & 1, "A" & i), _
isWS.Cells(i, 1).Value)
If x = 1 Then
UserForm1.cmbEmployee.AddItem isWS.Cells(i, 1) & " - " & isWS.Cells(i, 4)
End If
Next i
For a = 0 To UserForm1.cmbEmployee.ListCount - 1
For b = 0 To UserForm1.cmbEmployee.ListCount - 1
If UserForm1.cmbEmployee.List(b) > UserForm1.cmbEmployee.List(a) Then
c = UserForm1.cmbEmployee.List(a)
UserForm1.cmbEmployee.List(a) = UserForm1.cmbEmployee.List(b)
UserForm1.cmbEmployee.List(b) = c
End If
Next
Next
End If
ElseIf UserForm1.optTerm = True Then
If UserForm1.optEmployeeName = True Then
For i = 2 To tWs.Cells(Rows.Count, 4).End(xlUp).row
x = Application.CountIf(tWs.Range("D" & 4, "D" & i), _
tWs.Cells(i, 4).Value)
If x = 1 Then
UserForm1.cmbEmployee.AddItem tWs.Cells(i, 4) & " - " & tWs.Cells(i, 1)
End If
Next i
For a = 0 To UserForm1.cmbEmployee.ListCount - 1
For b = 0 To UserForm1.cmbEmployee.ListCount - 1
If UserForm1.cmbEmployee.List(b) > UserForm1.cmbEmployee.List(a) Then
c = UserForm1.cmbEmployee.List(a)
UserForm1.cmbEmployee.List(a) = UserForm1.cmbEmployee.List(b)
UserForm1.cmbEmployee.List(b) = c
End If
Next
Next
ElseIf UserForm1.optEmployeeID = True Then
For i = 2 To tWs.Cells(Rows.Count, 1).End(xlUp).row
x = Application.CountIf(tWs.Range("A" & 1, "A" & i), _
tWs.Cells(i, 1).Value)
If x = 1 Then
UserForm1.cmbEmployee.AddItem tWs.Cells(i, 1) & " - " & tWs.Cells(i, 4)
End If
Next i
For a = 0 To UserForm1.cmbEmployee.ListCount - 1
For b = 0 To UserForm1.cmbEmployee.ListCount - 1
If UserForm1.cmbEmployee.List(b) > UserForm1.cmbEmployee.List(a) Then
c = UserForm1.cmbEmployee.List(a)
UserForm1.cmbEmployee.List(a) = UserForm1.cmbEmployee.List(b)
UserForm1.cmbEmployee.List(b) = c
End If
Next
Next
End If
End If
End Sub
Instead of trying to shape the data using code, I would suggest creating an SQL statement based on runtime logic, opening a recordset with that data, and pushing the result back into the combobox.
Add a reference (Tools -> References...) to Microsoft ActiveX Data Objects; the latest version, usually 6.1.
(Credit goes to CDP1802's answer, which is the basis for much of the logic here.)
Dim source As String
If optInSeat = True Then
source = "'In Seat$'"
ElseIf optTerm = True Then
source = "Terms$"
End If
If Len(source) = 0 Then Exit Sub ' Do nothing
' sort by columns
Dim orderBy As String, expr As String
If optEmployeeName Then
expr = "Trim(F1) & ' - ' & Trim(F4)"
orderBy = "F1, F4"
ElseIf optEmployeeID Then
expr = "Trim(F4) & ' - ' & Trim(F1)"
orderBy = "F4, F1"
Else
expr = "Trim(F1) & ' - ' & Trim(F4)"
End If
Dim connectionString As String
connectionString = _
"Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=""" & ThisWorkbook.FullName & """;" & _
"Extended Properties=""Excel 12.0;HDR=No"""
Dim sql As String
sql = _
"SELECT " & expr & " " & _
"FROM [" & source & "]"
If Len(orderBy) > 0 Then sql = sql & " ORDER BY " & orderBy
Dim rs As New ADODB.Recordset
rs.Open sql, connectionString
' The 2D array comes back in the wrong direction to be set directly.
' We use WorksheetFunctions.Transpose to switch the direction.
cmbEmployees.List = WorksheetFunction.Transpose(rs.GetRows)
Select unique items using a Dictionary Object and sort them in an array. This sorts in ascending order.
Sub LoadEmployee_Cmb_HC()
Dim wb As Workbook, ws As Worksheet
Dim dict, k As String, i As Long
Dim order(2) As Integer
Set wb = ThisWorkbook
Set dict = CreateObject("Scripting.Dictionary")
' data source
If UserForm1.optInSeat = True Then
Set ws = wb.Sheets("In Seat")
ElseIf UserForm1.optTerm = True Then
Set ws = wb.Sheets("Terms")
End If
' sort by columns
If UserForm1.optEmployeeName = True Then
order(1) = 4: order(2) = 1
ElseIf UserForm1.optEmployeeID = True Then
order(1) = 1: order(2) = 4
End If
If order(1) = 0 Or ws Is Nothing Then
' do nothing
Else
' get unique values start in row 4
For i = 4 To ws.Cells(Rows.Count, order(1)).End(xlUp).Row
k = Trim(ws.Cells(i, order(1)).Value)
If Len(k) > 0 And Not dict.exists(k) Then
dict.Add k, k & " - " & Trim(ws.Cells(i, order(2)))
End If
Next
' sort and populate combo
Call SortCombo(dict, UserForm1.ComboBox1)
End If
End Sub
Sub SortCombo(ByRef dict, cmb As ComboBox)
Dim ar, a As Long, b As Long, i As Long, tmp As String
ar = dict.keys
i = UBound(ar)
For a = 0 To i
For b = a To i
If ar(b) < ar(a) Then
tmp = ar(a)
ar(a) = ar(b)
ar(b) = tmp
End If
Next
ar(a) = dict.Item(ar(a)) ' replace with value after it sort
Next
cmb.List = ar
End Sub
Alternative sort using temporary sheet
Sub SortCombo2(ByRef dict, cmb As ComboBox)
Dim wsTmp As Worksheet, rng As Range, k, ar() As String, i As Long
Set wsTmp = ThisWorkbook.Sheets(3)
wsTmp.Cells.Clear
ReDim ar(dict.Count - 1, 0)
i = 0
For Each k In dict.keys
ar(i, 0) = dict(k)
i = i + 1
Next
Set rng = wsTmp.Range("A1:A" & dict.Count)
rng = ar
With wsTmp.Sort
.SetRange rng
.Header = xlGuess
.MatchCase = False
.Orientation = xlTopToBottom
.Apply
End With
cmb.List = rng.Value2
wsTmp.Cells.Clear
End Sub
Test data generator
Sub data()
Dim ws As Worksheet, i, s, n
Set ws = Sheets("Terms")
ws.Cells.Clear
For i = 4 To 35000
s = ""
For n = 1 To 25
s = s & Chr(65 + Int(Rnd() * 26))
Next
ws.Cells(i, 1) = s
ws.Cells(i, 4) = "D" & i
Next
MsgBox "done " & i - 1
End Sub

Rolling Countif

I have a very large Dataset ~ 100000 rows with 2 columns, I want to calculate a rolling count based on 2 criterias, basically how many times value in col 1 wrt col 2.
Dataset looks like this
I have written the following code
This is partial dataset, actual has 100000 rows, I want the answer in col c
Sub test()
Application.EnableEvents = False
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim id, data_week, ans, a As Variant
Dim p As Double
a = 100000
Debug.Print Now()
id = Sheet1.Range("A2:A" & a).Value
data_week = Sheet1.Range("B2:B" & a).Value
ans = Sheet1.Range("c2:c" & a).Value
For p = 1 To a
ans(p, 1) = Application.WorksheetFunction.CountIfs(Sheet1.Range("A2:A" & p + 1), id(p, 1),
Sheet1.Range("b2:b" & p + 1), data_week(p, 1))
Next p
Sheet1.Range("c2:c" & a).Value = ans
Debug.Print Now()
Application.Calculation = xlCalculationAutomatic
End Sub
This is taking awfully long in VBA, wondering if there's a faster way to do it interms optimising the code, appreciate your help.
Try. This takes 3 seconds to run.
Sub test3()
Dim vDB, ans()
Dim Ws As Worksheet
Dim a As Long
Dim i As Long, id, myDay
Dim n As Integer
Set Ws = Sheets(1)
a = 100000
Debug.Print Now()
With Ws
vDB = .Range("a2", .Range("b" & a))
ReDim ans(1 To UBound(vDB, 1), 1 To 1)
id = vDB(1, 1)
myDay = vDB(1, 2)
For i = 1 To UBound(vDB, 1)
If vDB(i, 1) <> "" Then
If id = vDB(i, 1) And myDay = vDB(i, 2) Then
n = n + 1
ans(i, 1) = n
Else
id = vDB(i, 1)
myDay = vDB(i, 2)
n = 1
ans(i, 1) = n
End If
End If
DoEvents
Next
.Range("c2").Resize(UBound(ans)) = ans
End With
Debug.Print Now()
End Sub

Extract Unique Values From Multiple Columns With Macro

I have a list of codes in A and the image links in B and C.
What i want to do is remove the duplicates and arrange the unique links in a single column and give them a series name with incrementing no code_1 before image link 1 and code_2 before link 2 as shown in the picture.
I am trying this code to delete the duplicates but clueless about how to put the name before the link.
Sub tgr()
Dim wb As Workbook
Dim wsDest As Worksheet
Dim rData As Range
Dim rArea As Range
Dim aData As Variant
Dim i As Long, j As Long
Dim hUnq As Object
'Prompt to select range. Uniques will be extracted from the range selected.
'Can select a non-contiguous range by holding CTRL
On Error Resume Next
Set rData = Application.InputBox("Select range of names where unique names will be extracted:", "Data Selection", Selection.Address, Type:=8)
On Error GoTo 0
If rData Is Nothing Then Exit Sub 'Pressed cancel
Set hUnq = CreateObject("Scripting.Dictionary")
For Each rArea In rData.Areas
If rArea.Cells.Count = 1 Then
ReDim aData(1 To 1, 1 To 1)
aData(1, 1) = rArea.Value
Else
aData = rArea.Value
End If
For i = 1 To UBound(aData, 1)
For j = 1 To UBound(aData, 2)
If Not hUnq.Exists(aData(i, j)) And Len(Trim(aData(i, j))) > 0 Then hUnq(Trim(aData(i, j))) = Trim(aData(i, j))
Next j
Next i
Next rArea
Set wb = rData.Parent.Parent 'First parent is the range's worksheet, second parent is the worksheet's workbook
Set wsDest = wb.Sheets.Add(After:=wb.Sheets(wb.Sheets.Count))
wsDest.Range("A1").Resize(hUnq.Count).Value = Application.Transpose(hUnq.Items)
End Sub
This custom VBA function would create the desired result of getting the SKU code. I broke it up to show how to get each position.
Function Drop_Bucks(inputText As String) As String
Dim beginSpot As Long, endSpot As Long
'Finds last /
beginSpot = InStrRev(inputText, "/", -1, vbTextCompare) + 1
'Finds jpg
endSpot = InStrRev(inputText, ".jpg", -1, vbTextCompare)
Drop_Bucks = Replace(Mid(inputText, beginSpot, endSpot - beginSpot), "-", "_")
End Function
As a followup, you could also create the sku without VBA. If you put this formula in cell c4 with a sku in d4. It should do without macro.
=SUBSTITUTE(SUBSTITUTE(LEFT(SUBSTITUTE(SUBSTITUTE(RIGHT(SUBSTITUTE(d4, "/",REPT("?", 999)), 999),"?",""), ".jpg",REPT("?", 999)), 999),"?",""),"-","_")
This may helps you:
Option Explicit
Sub TEST()
Dim LastRow As Long, i As Long, LastRow2 As Long
Dim arr As Variant
With ThisWorkbook.Worksheets("Sheet1")
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
.Range("$A$2:$C$" & LastRow).RemoveDuplicates Columns:=Array(1, 2, 3), Header:=xlNo
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
arr = .Range("A2:C" & LastRow)
For i = LBound(arr) To UBound(arr)
LastRow2 = .Cells(.Rows.Count, "E").End(xlUp).Row
.Range("E" & LastRow2 + 1).Value = arr(i, 1) & "_1"
.Range("F" & LastRow2 + 1).Value = arr(i, 2)
Next i
For i = LBound(arr) To UBound(arr)
LastRow2 = .Cells(.Rows.Count, "E").End(xlUp).Row
.Range("E" & LastRow2 + 1).Value = arr(i, 1) & "_2"
.Range("F" & LastRow2 + 1).Value = arr(i, 3)
Next i
End With
End Sub
This builds a list of all the item duplicates and all. It will then uses the function Range.RemoveDuplicates to remove duplicates of the SKU Code in combination with the URL within the range.
Option Explicit
Sub Test()
Dim oCurSourceSheet As Worksheet
Set oCurSourceSheet = Sheet1 ' What sheet is your Source Data on?
Dim oSourceRow As Long ' Which Row/Column does your data start on?
oSourceRow = 2 ' First Row of First "Link"
Dim oSourceCol As Long
oSourceCol = 2 ' First Column of First "Link"
Dim oOutputRange As Range
Set oOutputRange = Sheet1.Range("A10") ' What Sheet/Cell do you want the output to start on/in?
Dim oCurRow As Long ' Row counter for Output
oCurRow = 1
Dim oCurSourceRow As Long
Dim oCurSourceCol As Long
For oCurSourceRow = oSourceRow To oCurSourceSheet.UsedRange.Rows.Count
For oCurSourceCol = oSourceCol To oCurSourceSheet.UsedRange.Columns.Count
oOutputRange.Cells(oCurRow, 1) = oCurSourceSheet.Cells(oCurSourceRow, 1) & "_" & oCurSourceCol - 1
oOutputRange.Cells(oCurRow, 2) = oCurSourceSheet.Cells(oCurSourceRow, oCurSourceCol)
oCurRow = oCurRow + 1
Next
Next
'Reize range from output's starting cell & remove duplicates
Set oOutputRange = oOutputRange.Resize(oCurRow - 1, 2)
oOutputRange.RemoveDuplicates Columns:=Array(1, 2)
End Sub
Try this, please: I adapted your code. The Dictionary is used just like a tool for avoiding duplicate values (due to the fact it exists...). Everything works in memory and should be very fast:
Option Base 1
Sub tgr_bis()
Dim wb As Workbook, rData As Range, wsDest As Worksheet, rArea As Range
Dim aData As Variant, aDataSorted() As String
Dim i As Long, hUnq As Scripting.Dictionary, nrColumns As Long
On Error Resume Next
Set rData = Application.InputBox("Select range of names where unique names will be extracted:", "Data Selection", Selection.Address, Type:=8)
On Error GoTo 0
If rData Is Nothing Then Exit Sub 'Pressed cancel
'Debug.Print rData.Columns.Count: Stop
If rData.Columns.Count > 6 Then MsgBox "More then 6 columns..." & vbCrLf & _
"Please select only six columns and run the procedure again", vbInformation, _
"Too many columns": Exit Sub
nrColumns = rData.Columns.Count
Set hUnq = CreateObject("Scripting.Dictionary")
For Each rArea In rData.Areas
If rArea.Cells.Count = 1 Then
ReDim aData(1 To 1, 1 To 1)
aData(1, 1) = rArea.value
Else
aData = rArea.value
End If
ReDim aDataSorted(nrColumns, 1)
Dim k As Long
k = 1
For i = 1 To UBound(aData, 1)
If Not hUnq.Exists(aData(i, 1)) And Len(Trim(aData(i, 1))) > 0 Then
aDataSorted(1, k) = aData(i, 1): aDataSorted(2, k) = aData(i, 2): aDataSorted(3, k) = aData(i, 3)
Select Case nrColumns
Case 4
If aData(i, 4) <> "" Then aDataSorted(4, k) = aData(i, 4)
Case 5
If aData(i, 4) <> "" Then aDataSorted(4, k) = aData(i, 4)
If aData(i, 5) <> "" Then aDataSorted(5, k) = aData(i, 5)
Case 6
If aData(i, 4) <> "" Then aDataSorted(4, k) = aData(i, 4)
If aData(i, 5) <> "" Then aDataSorted(5, k) = aData(i, 5)
If aData(i, 6) <> "" Then aDataSorted(6, k) = aData(i, 6)
Case > 6
MsgBox "Too many selected columns!": Exit Sub
End Select
k = k + 1
ReDim Preserve aDataSorted(nrColumns, k)
hUnq(Trim(aData(i, 1))) = Trim(aData(i, 1))
End If
Next i
Next rArea
'Process the new array in order to be tansformed in what is needed:
Dim finalCol() As String
k = k - 1: Z = 1
ReDim finalCol(2, Z)
Dim lngIndex As Long
Dim totalRows As Long
For i = 1 To k
lngIndex = 1
finalCol(1, Z) = aDataSorted(1, i) & "_" & lngIndex: lngIndex = lngIndex + 1: _
finalCol(2, Z) = aDataSorted(2, i): totalRows = totalRows + 1
Z = Z + 1: ReDim Preserve finalCol(2, Z)
finalCol(1, Z) = aDataSorted(1, i) & "_" & lngIndex: lngIndex = lngIndex + 1: _
finalCol(2, Z) = aDataSorted(3, i): totalRows = totalRows + 1
Z = Z + 1: ReDim Preserve finalCol(2, Z)
If nrColumns < 4 Then GoTo EndLoop
If aDataSorted(4, i) <> "" Then finalCol(1, Z) = aDataSorted(1, i) & "_" & lngIndex: _
lngIndex = lngIndex + 1: finalCol(2, Z) = aDataSorted(4, i): totalRows = totalRows + 1: _
Z = Z + 1: ReDim Preserve finalCol(2, Z)
If nrColumns < 5 Then GoTo EndLoop
If aDataSorted(5, i) <> "" Then finalCol(1, Z) = aDataSorted(1, i) & "_" & lngIndex: _
lngIndex = lngIndex + 1: finalCol(2, Z) = aDataSorted(5, i): totalRows = totalRows + 1: _
Z = Z + 1: ReDim Preserve finalCol(2, Z)
If nrColumns < 6 Then GoTo EndLoop
If aDataSorted(6, i) <> "" Then finalCol(1, Z) = aDataSorted(1, i) & "_" & lngIndex: _
lngIndex = lngIndex + 1: finalCol(2, Z) = aDataSorted(6, i): totalRows = totalRows + 1: _
Z = Z + 1: ReDim Preserve finalCol(2, Z)
EndLoop:
Next i
Set wb = rData.Parent.Parent
Set wsDest = wb.Sheets.Add(After:=wb.Sheets(wb.Sheets.Count))
wsDest.Range("A1:B" & totalRows) = Application.Transpose(finalCol)
End Sub
'A reference to "Microsoft Scripting Runtime" must be added. Otherwise, you can declare hUnq As Object...
And do not forget to have Option Base on tot of the module where this code exists. It is necessary to work with the way you built your initial code.
Edited: I adapted the code to accept up to six columns as you suggested. Please give it a try.
But it only check the unique SKU Code and select the first occurrence. If the other occurrences appear, the will not be considered even if they have different strings on its row. The code can be adapted to work also from this point of view, but now I think is your turn to make some tests...

Shuffling a 2D array

I have the follow script to put a list of people with there know skills in an array and then match the first match with a customer with the same skill. Every time it runs the results are the same. I would like to have it be a random order of the array, but keeping the two columns in the array together. How can I shuffle(rearrange) the array that keeps the rows in the array the same? Or would it be better to erase the array, randomly sort the columns and set the array back up?
Sub Assign()
Dim arOne()
ReDim arOne(1000, 15)
Dim o As Integer
Dim p As Integer
Dim StartTime As Double
Dim MinutesElapsed As String
p = 0
o = 0
For i = 2 To 920
If Cells(i, 12).Value <> Cells(i - 1, 12) Then
p = p + 1
arOne(p, 0) = Cells(i, 12).Value
arOne(p, 1) = Cells(i, 13).Value
o = 2
Else
arOne(p, o) = Cells(i, 13).Value
o = o + 1
End If
Next
For i = 2 To 612
For o = LBound(arOne, 1) + 1 To UBound(arOne, 1)
If arOne(o, 0) <> "" Then
iUsed = Application.WorksheetFunction.CountIf(Range("C2:C" & i), "=" & arOne(o, 0))
If iUsed < Application.WorksheetFunction.VLookup(arOne(o, 0), Range("Q2:R62"), 2, False) Then
For j = LBound(arOne, 2) + 1 To UBound(arOne, 2)
If arOne(o, j) = Cells(i, 2).Value Then
Cells(i, 3).Value = arOne(o, 0)
ActiveSheet.Calculate
GoTo NextIR
End If
Next j
End If
End If
Next o
NextIR:
Next i
End Sub
Multiple loops and multiple access to range objects makes your code very, very slow (I don't know if performance is important).
I would read all necessary data to arrays and use filter and rnd to get a random person with the relevant skill:
Option Explicit
Sub PeopleBusiness()
Dim People, Customers, FilterArray
Dim I As Long, Idx As Long
People = Application.Transpose([L2:L920 & "|" & M2:M8])
Customers = Range("A2:C612").Value2
For I = 1 To UBound(Customers, 1)
FilterArray = Filter(People, Customers(I, 2))
If UBound(FilterArray) > -1 Then
Idx = Round(Rnd() * UBound(FilterArray), 0)
Customers(I, 3) = Left(FilterArray(Idx), InStr(1, FilterArray(Idx), "|") - 1)
End If
Next I
Range("A2:C612").Value = Customers
End Sub
I was able to get done what I needed by erasing the array and redimming it after sorting the data based on a rand() number in the table. It takes about 15 minutes to run 7000 assignment but it is a lot better than 7+ hours it takes to do manually.
Sub Assign()
Dim arOne()
ReDim arOne(1000, 15)
Dim o As Integer
Dim p As Integer
Dim StartTime As Double
Dim MinutesElapsed As String
Application.Calculation = xlAutomatic
StartTime = Timer
NextIR:
ReDim arOne(1000, 15)
p = 0
o = 0
QAlr = Sheets("Sheet1").Range("L" & Rows.Count).End(xlUp).Row
For I = 2 To QAlr
If Cells(I, 12).Value <> Cells(I - 1, 12) Then
p = p + 1
arOne(p, 0) = Cells(I, 12).Value
arOne(p, 1) = Cells(I, 13).Value
o = 2
Else
arOne(p, o) = Cells(I, 13).Value
o = o + 1
End If
Next
AQAlr = Sheets("Sheet1").Range("C" & Rows.Count).End(xlUp).Row
AgtLr = Sheets("Sheet1").Range("A" & Rows.Count).End(xlUp).Row
For I = AQAlr + 1 To AgtLr
For o = LBound(arOne, 1) + 1 To UBound(arOne, 1)
If arOne(o, 0) <> "" Then
iUsed = Application.WorksheetFunction.CountIf(Range("C2:C" & I), "=" & arOne(o, 0))
If iUsed < Application.WorksheetFunction.VLookup(arOne(o, 0), Range("Q2:R62"), 2, False) Then
For j = LBound(arOne, 2) + 1 To UBound(arOne, 2)
If arOne(o, j) = Cells(I, 2).Value Then
Cells(I, 3).Value = arOne(o, 0)
ActiveSheet.Calculate
Erase arOne()
ActiveWorkbook.Worksheets("Sheet1").ListObjects("Table1").Sort.SortFields.Clear
ActiveWorkbook.Worksheets("Sheet1").ListObjects("Table1").Sort.SortFields.Add _
Key:=Range("Table1[[#All],[Random '#]]"), SortOn:=xlSortOnValues, Order:= _
xlDescending, DataOption:=xlSortTextAsNumbers
With ActiveWorkbook.Worksheets("Sheet1").ListObjects("Table1").Sort
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
GoTo NextIR
End If
Next j
End If
End If
Next o
Next I
MinutesElapsed = Format((Timer - StartTime) / 86400, "hh:mm:ss")
MsgBox "Assignments completed in " & MinutesElapsed & " minutes", vbInformation
End Sub
Not entirely sure I got your set-up right but you can try this:
Option Explicit
Sub Assign()
Randomize
Range("C2", Range("C" & Rows.Count).End(xlUp)).ClearContents
Dim R1 As Range: Set R1 = Range("L2:M920") 'People skill table
Dim R2 As Range: Set R2 = Range("A2:B612") 'Cusotmers skill talbe
Dim D0 As Object: Set D0 = CreateObject("scripting.dictionary")
Dim i As Integer, j As Integer, Rand as Integer
For i = 1 To R2.Rows.Count
Rand = Int(R1.Rows.Count * Rnd + 1)
For j = 1 To R1.Rows.Count
If R1.Cells(Rand, 2) = R2(i, 2) And Not D0.exists(Rand) Then
R2.Cells(i, 2).Offset(0, 1) = R1(Rand, 1)
D0.Add Rand, Rand
Exit For
End If
Rand = (Rand Mod R1.Rows.Count) + 1
Next j
Next i
End Sub
The idea is to check the people skill list starting from a random point and making sure a key is not used twice.
EDIT:
According to your comment I assume a "people / skill" can then be assigned more than once as there are 7000+ customers ?
Code below randomly assign with a fairly good distribution 1500 peoples to 7000 customers in +/- 1 second.
Have a try and see if you can adapt it to your project.
Option Explicit
Sub Assign()
Application.ScreenUpdating = False
Dim Start: Start = Timer
Randomize
Range("C2:C99999").ClearContents
Dim D1 As Object
Dim R1 As Range: Set R1 = Range("L2", Range("M" & Rows.Count).End(xlUp))
Dim R2 As Range: Set R2 = Range("A2", Range("B" & Rows.Count).End(xlUp))
Dim T1: T1 = R1
Dim T2: T2 = R2
Dim T3()
Dim a As Integer: a = 1
Dim i As Integer, j As Integer, k As Integer, Rnd_Val As Integer, j_loop As Integer
For i = 1 To (Int(R2.Rows.Count / R1.Rows.Count) + 1)
Set D1 = CreateObject("scripting.dictionary")
For j = (R1.Rows.Count * i - R1.Rows.Count + 1) To R1.Rows.Count * i
ReDim Preserve T3(1 To j)
Rnd_Val = Int(Rnd * R1.Rows.Count + 1)
For k = 1 To R1.Rows.Count
If T1(Rnd_Val, 2) = T2(j, 2) And Not D1.exists(Rnd_Val) And T3(j) = "" Then
T3(j) = T1(Rnd_Val, 1)
D1.Add Rnd_Val, Rnd_Val
Exit For
End If
Rnd_Val = (Rnd_Val Mod R1.Rows.Count) + 1
Next k
If T3(j) = "" Then
For k = 1 To R1.Rows.Count
If T1(Rnd_Val, 2) = T2(j, 2) Then
T3(j) = T1(Rnd_Val, 1)
Exit For
End If
Rnd_Val = (Rnd_Val Mod R1.Rows.Count) + 1
Next k
End If
a = a + 1
If a > R2.Rows.Count Then GoTo EndLoop
Next j
Set D1 = Nothing
Next i
EndLoop:
Range("C2").Resize(UBound(T3), 1) = Application.Transpose(T3)
Debug.Print Timer - Start
Application.ScreenUpdating = True
End Sub

Resources