VBA Create array with userform items - arrays

I have a a list of checkbox in userform. I would like to create a code that is more efficient that the following one :
If checkbox1.value = true
Then Range("A1").value = 100
End if
If checkbox2.value = true
Then Range("A2").value = 200
End if
If checkbox3.value = true
Then Range("A3").value = 300
End if
The problem is that I have 40 checkbox and I would like to create something that says: in my list of checkbox, if it is true then add in my range A1. I'm not sure how to proceed but I tried this:
Dim Element as variant
For each element in MyList
If element.value = true Then
For i = 1 to NumberOfTrueElement
Range("A" & i + 1).value = Mylist(i)
Next i
End if
Next
Mylist is the frame of all my checkboxes.
Please help me if you have a hint.

It's not really clear where the array part comes into thing but if we assume a couple of things,
You have an array named Mylist with 40 items.
The checkboxes are named consistently e.g. checkbox1, checkbox2 etc
then you could use something like this.
For I = 1 To 40
If Me.Controls("checkbox" & I).Value
Range("A" & I).Value = Mylist(I)
End If
Next I

Please, try this way:
Dim i As Long, k As Long
For k = 0 To Me.Controls.count - 1
If TypeName(Me.Controls(k)) = "CheckBox" Then
i = i + 1
If Me.Controls("checkbox" & i).value Then
Range("A" & i).value = i * 100
End If
End If
Next k
If you need a faster way, please test the next code:
Dim i As Long, k As Long, arr
ReDim arr(Me.Controls.count)
For k = 0 To Me.Controls.count - 1
If TypeName(Me.Controls(k)) = "CheckBox" Then
i = i + 1
If Me.Controls("checkbox" & i).value Then
arr(i - 1) = i * 100
Else
arr(i - 1) = "False"
End If
End If
Next k
ReDim Preserve arr(i - 1)
Range("A1").Resize(UBound(arr) + 1, 1) = Application.Transpose(arr)
It loads the necessary values in an array, using "False" when the check box is not checked and drops the array values at once at the end.
Of course, the check boxes name should be consistently built. Starting from "checkbox1" to "checkboxn". If the name consistence exists, the code works for as many check boxes exist on the form...

Write Value If Checkbox Checked
A kind of an answer would be in the CommandButton_Click procedure.
This example is easily setup by anyone: open a new workbook, add a user form and add a command button on it. Double-click the command button and copy the following code to the just-opened window (user form code). Run the first procedure. Tick a few checkboxes and press the command button. See the copied values in the first column of the worksheet.
The Code
Option Explicit
Sub doShow()
UserForm1.Show
End Sub
Private Sub CommandButton1_Click()
Dim arr() As Variant: ReDim arr(1 To Me.Controls.Count)
Dim chb As MSForms.Control
Dim n As Long
For Each chb In Me.Controls
If TypeName(chb) = "CheckBox" Then
n = n + 1
If chb.Value = True Then
arr(n) = n * 100
End If
End If
Next chb
ReDim Preserve arr(1 To n)
With ThisWorkbook.Worksheets("Sheet1")
.Range("A1").Resize(n).Value = Application.Transpose(arr)
End With
End Sub
Private Sub UserForm_Initialize()
Const CheckBoxesCount As Long = 10
Dim n As Long
For n = 1 To CheckBoxesCount
With Me.Controls.Add("Forms.CheckBox.1", "CheckBox_" & n, True)
.Caption = "CheckBox" & n
.Left = 5
.Top = 5 + ((n - 1) * 20)
End With
Next n
End Sub

Related

Run-time error '381': Could not set the List property. Invalid property array index

As a single-column listbox, this works and brings back all the data I need, however, upon making this a multi-column listbox, it throws me the error "Run-time error '381': Could not set the List property. Invalid property array index" - see code line ListBox1.List(1, 0) = MyArray(i, 1)
Columns are correctly shown when I add the incorrect List Property and I've tried adding the List Property where I think it should be, but it shows the whole list again and replaces the 2nd row with one of the correct search results, the 1st row never changes.
Main code is from How to filter listbox values based on a Textbox value and I've used the comment by #xris23 to amend into the multi-column listbox thus far.
Please help!
Sub CommandButton1_Click()
Dim i As Long, j As Long, rw As Long
Dim xlApp As Excel.Application
Dim xlBook As Excel.Workbook
Dim ws As Excel.Worksheet
Dim rng As Range
Dim MyArray As Variant
Dim sPath As String
sPath = "U:\GroupEmailDataCut.xlsx"
On Error Resume Next
Set xlApp = GetObject(, "Excel.Application")
On Error GoTo 0
If xlApp Is Nothing Then
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = True
End If
Set xlBook = xlApp.Workbooks.Open(sPath)
Set ws = xlBook.Sheets("GroupEmailDataCut")
Set rng = ws.Range("A2:D" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
MyArray = rng
With UserForm1.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
If ws.Range("A" & ws.Rows.Count).End(xlUp).Row > 1 And Trim(UserForm1.TextBox1.Value) <> vbNullString Then
MyArray = ws.Range("A2:D" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row).Value2
For i = LBound(MyArray) To UBound(MyArray)
For j = LBound(MyArray, 1) To UBound(MyArray, 1)
If InStr(1, MyArray(i, 1), Trim(UserForm1.TextBox1.Value), vbTextCompare) Then
UserForm1.ListBox1.AddItem
ListBox1.List(1, 0) = MyArray(i, 1) '<----Error is across ListBox1.List (MyArray is working as intended)
ListBox1.List(1, 1) = MyArray(i, 2)
ListBox1.List(1, 2) = MyArray(i, 3)
ListBox1.List(1, 3) = MyArray(i, 4)
End If
Next
Next
End If
.List = MyArray
If UserForm1.ListBox1.ListCount = 1 Then UserForm1.ListBox1.Selected(0) = True
End With
End Sub
There are several points to observe to make the command button event readable or even runnable:
Don't address controls by referencing the Userform's default instance (returning the public members of that object),
reference instead the current instance starting either with the Me. qualifier or simply by naming the userform control alone. Understand the Userform (class) code behind as sort of pattern which is blue-printed to the current instance(s).
You are using .AddItem to fill a listbox with items (which is okay for e.g. 4 columns); consider, however that this method automatically builds 10 list columns, but not less or more; this can be avoided .. a) by assigning an entire 2-dim array to the .List (or .Column) property or .. b) by a workaround assigning an empty array plus immediate clearing which allows .AddItem also for greater column numbers (integrated in section 2a)
.List indices are zero-based, i.e. they start with 0 whereas the datafield array with its row and column indices is one-based: .List(cnt, c - 1) = data(r, c) - See section 2b)
Furthermore I separated the check for findings into a help function IsFound(), integrated list assignments into loops and tried to make code more readable by some other intermediate variable declarations.
Private Sub CommandButton1_Click()
'~~~~~~~~~~~~~~
'1) Define data
'~~~~~~~~~~~~~~
'1a)Define search string
Dim srch As String: srch = Trim(Me.TextBox1.Value)
'1b)Define data range
Dim ws As Worksheet: Set ws = Tabelle1
Dim lastRow As Long: lastRow = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
Dim rng As Range: Set rng = ws.Range("A2:D" & lastRow)
'1c)Provide for possible exit
If rng.Row < 1 Or srch = vbNullString Then Exit Sub
'1d)Get 1-based 2-dim datafield array
Dim data() As Variant: data = rng.Value
'~~~~~~~~~~~~~~
'2) Fill ListBox
'~~~~~~~~~~~~~~
With Me.ListBox1
'2a)Set listbox properties & clear list
.ColumnHeads = False
.MultiSelect = fmMultiSelectMulti
'Provide for ColumnCount > 10
Dim ColCnt As Long: ColCnt = rng.Columns.Count
.ColumnCount = ColCnt
If ColCnt > 10 Then
Dim tmp(): ReDim tmp(1 To 1, 1 To ColCnt)
.List = tmp
End If
.Clear ' clear the ListBox
'2b)Loop through rows & add findings to list
Dim r As Long, c As Long, cnt As Long
For r = LBound(data) To UBound(data) ' 1 ..
If IsFound(srch, data, r) Then ' << help function IsFound()
.AddItem ' add item
For c = LBound(data, 2) To UBound(data, 2)
.List(cnt, c - 1) = data(r, c) '<< .List is zero-based!
Next
cnt = cnt + 1 ' increment counter
End If
Next
If .ListCount = 1 Then .Selected(0) = True
End With
End Sub
Help function IsFound()
Function IsFound(srch As String, arr, ByVal r As Long) As Boolean
'Purp.: check if search string is found in given 2-dim array row
Dim c As Long ' column index
For c = LBound(arr, 2) To UBound(arr, 2)
If InStr(1, arr(r, c), srch, vbTextCompare) Then
IsFound = True ' positive function result
Exit For ' one finding suffices
End If
Next
End Function
Further links
You'll find an outstanding article giving you some insight at UserForm1.Show?
Treating the different show modes you might be interested also in Destroy a modeless Userform instance properly

Faster method to delete a range of rows other that using union

I am using the below code to:
Delete the similar rows, keeping only one and combine cells values in the range "N", separated by vbLf
it works ,but with big range (e.g. 30 thousands rows) the macro takes a very long time to finish.
After debugging the code, I find out that using union causes macro to takes a very long time to finish.
Set rngDel = Union(rngDel, ws.Range("A" & i + m))
So with the below code , How to adapt a faster method to delete that range of rows other that using union?
In advance, grateful for any helpful comments and answers.
Sub DeleteSimilarRows_combine_Last_Column_N()
Dim LastRow As Long, ws As Worksheet, arrWork, rngDel As Range, i As Long, j As Long, k As Long
Dim strVal As String, m As Long
Set ws = ActiveSheet: LastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
arrWork = ws.Range("A1:A" & LastRow).Value2 'Place the range in an array to make iteration faster
Application.DisplayAlerts = False: Application.ScreenUpdating = False
For i = 2 To UBound(arrWork) - 1 'Iterate between the array elements:
If arrWork(i, 1) = arrWork(i + 1, 1) Then
'Determine how many consecutive similar rows exist:______
For k = 1 To LastRow
If i + k + 1 >= UBound(arrWork) Then Exit For
If arrWork(i, 1) <> arrWork(i + k + 1, 1) Then Exit For
Next k '__
For j = 14 To 14 'Build the concatenated string of cells in range "N":
strVal = ws.Cells(i, j).Value
For m = 1 To k
strVal = strVal & vbLf & ws.Cells(i + m, j).Value
Next m
ws.Cells(i, j).Value = strVal: strVal = ""
Next j
For m = 1 To k 'Place the cells for rows to be deleted in a Union range, to delete at the end, at once
If rngDel Is Nothing Then
Set rngDel = ws.Range("A" & i + m)
Else
Set rngDel = Union(rngDel, ws.Range("A" & i + m)) 'This line causes macro takes very long time to finish.
End If
Next m
i = i + k: If i >= UBound(arrWork) - 1 Then Exit For 'Increment the i variable and exiting if the resulted value exits the array size
End If
Next i
If Not rngDel Is Nothing Then rngDel.EntireRow.Delete 'Delete the not necessary rows
Application.DisplayAlerts = True: Application.ScreenUpdating = True
End Sub
Union gets progressively slower as you add more cells/areas to the range (see numbers here: https://stackoverflow.com/a/56573408/478884). If you were working "bottom up" you could delete rngDel every (eg) 500 rows, but you can't take that approach since you're working top-down.
Here's a different approach - adding cells to a Collection and then processing the collection "bottom-up" at the end, using a batch-delete process.
Sub TestRowDeletion()
Dim rngRows As Range, data, rngDel As Range, i As Long
Dim t, nRows As Long, colCells As New Collection
Set rngRows = Range("A1:A10000") '10k rows for testing
'Approach #1 - your existing method
DummyData rngRows 'populate some dummy data
data = rngRows.Value
t = Timer
For i = 1 To UBound(data, 1)
'removing ~25% of cells...
If data(i, 1) > 0.75 Then BuildRange rngDel, rngRows.Cells(i)
Next i
If Not rngDel Is Nothing Then rngDel.EntireRow.Delete
Debug.Print "Regular single delete", Timer - t
'Approach #2 - batch-deleting rows
DummyData rngRows 'reset data
data = rngRows.Value
t = Timer
For i = 1 To UBound(data, 1)
If data(i, 1) > 0.75 Then colCells.Add rngRows.Cells(i)
Next i
RemoveRows colCells
Debug.Print "Batch-deleted", Timer - t
'Approach #3 - array of "delete" flags plus SpecialCells()
DummyData rngRows 'reset data
data = rngRows.Value
t = Timer
ReDim flags(1 To UBound(data, 1), 1 To UBound(data, 2))
For i = 1 To UBound(data, 1)
If data(i, 1) > 0.75 Then
flags(i, 1) = "x"
bDelete = True 'flag we have rows to delete
End If
Next i
If bDelete Then
With rngRows.Offset(0, 10) 'use an empty column....
.Value = flags 'populate with flags for deletion
.SpecialCells(xlCellTypeConstants).EntireRow.Delete
End With
End If
Debug.Print "Specialcells", Timer - t
End Sub
'Delete the row for any cell in `col`
' cells were added to `col` in a "top down" order
Sub RemoveRows(col As Collection)
Dim rngDel As Range, n As Long
For n = col.Count To 1 Step -1 'working from the bottom up...
BuildRange rngDel, col(n)
If n Mod 250 = 0 Then
rngDel.EntireRow.Delete
Set rngDel = Nothing
End If
Next n
If Not rngDel Is Nothing Then rngDel.EntireRow.Delete
End Sub
Sub DummyData(rng As Range)
With rng
.Formula = "=RAND()"
.Value = .Value
End With
End Sub
Sub BuildRange(ByRef rngTot As Range, rngAdd As Range)
If rngTot Is Nothing Then
Set rngTot = rngAdd
Else
Set rngTot = Application.Union(rngTot, rngAdd)
End If
End Sub
Times (sec) - note how differently the single-delete and batch-delete approaches scale as more rows are added.
# of rows deleted ~2.5k/10k ~5k/20k ~7.5k/30k
------------------------------------------------------------
1. Regular single delete 10.01 65.9 226
2. Batch-deleted 2.2 4.7 7.8
3. SpecialCells 1.6 3.1 4.7
You could also consider populating a "delete" flag in your dataset, then using the autofilter/delete visible rows approach (EDIT: added as method #3)
Posting this as a working (but faster) version of your actual use case, since my other answer is really just about timing the different approaches.
Sub DeleteSimilarRowsCombineColumnN()
Const SEP As String = ","
Dim arrKeys, arrVals, arrFlags, rngRows As Range, rngVals As Range, i As Long, key, currKey, s As String
Dim ws As Worksheet, ub As Long, t, n As Long
t = Timer
Set ws = ActiveSheet
Set ws = ActiveSheet
Set rngRows = ws.Range("A1:A" & ws.Cells(Rows.Count, 1).End(xlUp).Row)
Set rngVals = rngRows.EntireRow.Columns("N")
arrKeys = rngRows.Value
ub = UBound(arrKeys, 1)
arrVals = rngVals.Value
ReDim arrFlags(1 To UBound(arrKeys, 1), 1 To 1)
currKey = Chr(0) 'non-existing key...
For i = ub To 1 Step -1 'looping from bottom up
key = arrKeys(i, 1) 'this row's key
If key <> currKey Then 'different key from row below?
If i < ub Then arrVals(i + 1, 1) = s 'populate the collected info for any previous key
s = arrVals(i, 1) 'collect this row's "N" value
currKey = key 'set as current key
Else
If i < ub Then
arrFlags(i + 1, 1) = "x" 'flag for deletion
n = n + 1
End If
s = arrVals(i, 1) & SEP & s 'concatenate the "N" value
End If
Next i
arrVals(1, 1) = s 'populate the last (first) row...
rngVals.Value = arrVals 'drop the concatenated values
If n > 0 Then 'any rows to delete?
Debug.Print "About to delete " & n & " of " & ub & " rows", Timer - t
With rngRows.Offset(0, 100) 'use any empty column
.Value = arrFlags
.SpecialCells(xlCellTypeConstants).EntireRow.Delete
End With
Debug.Print "Done deleting in " & Round(Timer - t, 2) & " sec"
End If
End Sub

Looping through Unique Data Set to Return Matching Value on a Different Sheet

I have searched for something similar to what I am asking, and unfortunately there is nothing close to what I am looking for.
I have a unique data set here on Sheet(2): The goal is to return the values in the highlighted blue columns if it matches the same "Item#" for the box selected in a dropdown list of the box names on Sheet(1). Please see Sheet(1) here: Sheet(1) Set-Up.
The Item#'s on Sheet(1) are located in B3:B12 on Sheet(1). - I've added also another list where I would like my code to run In the column next to this is a blank where the matching items in blue would post.
I am trying to use For Loops to accomplish this. I understand that the data set is weird, but I want to keep it like that for the mere challenge of it (and also because I have a larger data set similar and am just using this as a test run)... My code so far is as follows:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
' In order to run code on sheet without a button or enabling in a module
Set KeyCells = Range("A1")
If Not Application.Intersect(KeyCells, Range(Target.Address)) Is Nothing Then
Dim i, j As Long
Dim n As Long
Dim box As String
Set sh2 = ThisWorkbook.Sheets(2)
Set rn2 = sh2.UsedRange
box = Sheets(1).Cells.Range("A1")
Dim k1 As Long
k1 = rn2.Rows.Count + rn2.Row - 1
n = 0
For i = 1 To k1
If Sheets(2).Cells(1, i) = box Then
If n = 0 Then
Sheets(1).Cells(3, 3).Value = Sheets(2).Cells(i, 2)
n = n + 1
End If
ElseIf n > 0 Then
For j = 3 To n + 2
If Sheets(2).Cells(2, i).Value = Sheets(1).Cells(j, 2).Value Then
If Sheets(2).Cells(2, i).Value <> Sheets(1).Cells(j, 2).Value Then
x = x
Else
x = x + 1
End If
End If
Next
If x = 0 Then
Sheets(1).Cells(3 + n, 3).Value = Sheets(2).Cells(2, i).Value
n = n + 1
End If
End If
x = 0
Next
End If
End Sub
Please let me know what you experts think!
Edit 2; the macro finds Sheet1.Range("A1").Value in Sheet2 row 1. It then loops through each cell below the found value in Sheet2. It then finds each cells value in Sheet1. It will then copy the cells value in Sheet2 from the next cell to the right, and place the value in the cell in Sheet1 to the next cell to the right. It then loops down to the next cell in sheet2, and performs the same task, etc.
Private Sub Worksheet_Change(ByVal target As Range) 'Works
Dim fndTrgt As Range, fndCel As Range
If target.Address = "$A$1" Then
Set fndTrgt = Sheets("Sheet2").Rows(1).Find(target.Value)
If Not fndTrgt Is Nothing Then
For i = 1 To 5
Set fndCel = Sheets("Sheet1").Range("A2:D12").Find(fndTrgt.Offset(i).Value)
If Not fndCel Is Nothing Then
fndCel.Offset(, 1).Value = fndTrgt.Offset(i, 1).Value
End If
Next i
End If
End If
End Sub

Looping through an array while grabbing certain elements

I have a giant dataset that looks like this
I am trying to go down the list of different companies and grab 3 per company and combine them. Based on the photo above, I would have 2 different lists with 3 companies each (except TH Repair which will have 2 in the final list).
My real dataset contains hundreds of different companies, each with dozens/hundreds of entries so I would finish with dozens of lists (each potentially hundreds long).
I tried to record a macro and ended up with this code
Sub Loop1()
'
' Loop1 Macro
'
'
Range("A4:E6").Select
Selection.Copy
Sheets("Sheet3").Select
Range("A18").Select
ActiveSheet.Paste
Sheets("Sheet2").Select
Range("A11:E13").Select
Application.CutCopyMode = False
Selection.Copy
Sheets("Sheet3").Select
Range("A21").Select
ActiveSheet.Paste
Sheets("Sheet2").Select
Range("A17:E19").Select
Application.CutCopyMode = False
Selection.Copy
Sheets("Sheet3").Select
Range("A24").Select
ActiveSheet.Paste
End Sub
However, this turned out to be WAY more complicated then I expected.
I am looking for the end result to look like this
See if something like this works for you. I only ran one scenario through it so you will want to test it more.
This makes the assumption that the data is sorted by column B on the original sheet
This procedure makes an assumption that there is either headers or no data on row 1.
You will need to change the "Sheet1" in this line Set ws1 = ActiveWorkbook.Worksheets("Sheet1") to the name of the sheet you are starting with.
Option Explicit
Public Sub MoveData()
Dim ws1 As Worksheet
Set ws1 = ActiveWorkbook.Worksheets("Sheet1")
Dim ws2 As Worksheet
Set ws2 = ActiveWorkbook.Worksheets.Add()
Dim rw As Long
Dim match_count As Integer
Dim list_multiplier As Integer
list_multiplier = 7
Dim list_row() As Long
ReDim list_row(0)
list_row(0) = 2
For rw = 2 To ws1.Range("A" & ws1.Rows.Count).End(xlUp).Row
If ws1.Range("B" & rw).Value <> ws1.Range("B" & rw).Offset(-1, 0).Value Then
match_count = 0
Else
match_count = match_count + 1
End If
Dim list_num As Integer
list_num = match_count \ 3
If list_num > UBound(list_row, 1) Then
ReDim Preserve list_row(list_num)
list_row(list_num) = 2
End If
ws2.Cells(list_row(list_num), 1 + list_multiplier * list_num).Value = ws1.Range("A" & rw).Value
ws2.Cells(list_row(list_num), 2 + list_multiplier * list_num).Value = ws1.Range("B" & rw).Value
ws2.Cells(list_row(list_num), 3 + list_multiplier * list_num).Value = ws1.Range("C" & rw).Value
ws2.Cells(list_row(list_num), 4 + list_multiplier * list_num).Value = ws1.Range("D" & rw).Value
ws2.Cells(list_row(list_num), 5 + list_multiplier * list_num).Value = ws1.Range("E" & rw).Value
list_row(list_num) = list_row(list_num) + 1
Next rw
End Sub
When you record your macro, ensure that "Use Relative References" on the Developer Ribbon tab is enabled, :)
assuming row 3 has your data headers, you could try this:
Option Explicit
Sub main()
Dim nLists As Long, iList As Long
Dim data As Variant
Dim dataToDelete As Range
With Range("F3", Cells(Rows.Count, 1).End(xlUp))
data = .Value
nLists = WorksheetFunction.Max(.Resize(,1))
nLists = nLists \ 3 + IIf(nLists - 3 * (nLists \ 3) = 0, -1, 0)
End With
With Range("A3").Resize(, 6)
For iList = 0 To nLists
Set dataToDelete = Nothing
With .Offset(, iList * 6).Resize(UBound(data))
.Value = data
.AutoFilter Field:=1, Criteria1:="<=" & iList * 3, Criteria2:=">" & (iList + 1) * 3, Operator:=xlOr
If Application.WorksheetFunction.Subtotal(103, .Cells) > 1 Then Set dataToDelete = .Offset(1).Resize(.Rows.Count - 1).SpecialCells(xlCellTypeVisible)
.Parent.AutoFilterMode = False
If Not dataToDelete Is Nothing Then dataToDelete.Delete xlShiftUp
End With
Next
End With
End Sub
Your task is actually slightly trickier than your online advice suggests. Basically, you have to do the following:
Find out how many unique 'keys' (ie unique items in column B) you have. This will tell you the total number of rows you need (ie number of unique keys * 3)
Count the number of items for each 'key'. This will tell you how many columns you need (ie max item count / 3 * number of columns in array [A:E = 5])
Loop through each line of data and it put on appropriate row for that 'key'. Once three has been reached, jump the column for that key 6 columns to the right, and continue.
If you were to use a Class object and Collection type of object, this could be really quite concise code, but judging by your post you are at the beginning of your programming journey in VBA. Therefore, I've broken down each task into separate chunks of code so you will hopefully see how arrays can work for you. Once you practise with arrays a little, perhaps you could have a go at making this code more efficient by combining some of the loops:
Public Sub RunMe()
Dim data As Variant
Dim r As Long, c As Long, i As Long, dataRows As Long, dataCols As Long, keyLen As Long, maxCount As Long
Dim keys As String
Dim k As Variant
Dim keyArray() As String
Dim keyCount() As Long, threeCount() As Long, rowNum() As Long, colNum() As Long
Dim output() As Variant
'Read the data - change "Sheet1" to your sheet name.
'Shows how to write range values into a variant to
'create an array of variants.
data = ThisWorkbook.Worksheets("Sheet1").UsedRange.Value2
dataRows = UBound(data, 1)
dataCols = UBound(data, 2)
'Create a list of unique keys.
'Note: not the most efficient way, but shows how to
'create an array from a value-separated string.
For r = 1 To dataRows
If InStr(keys, CStr(data(r, 2))) = 0 Then
If Len(keys) > 0 Then keys = keys & "|"
keys = keys & CStr(data(r, 2))
End If
Next
keyArray = Split(keys, "|")
keyLen = UBound(keyArray)
'Initialise the row and column numbers for each key.
'Shows how to iterate an array using For Each loop.
ReDim rowNum(keyLen)
ReDim colNum(keyLen)
r = 1
i = 0
For Each k In keyArray
rowNum(i) = r
colNum(i) = 1
r = r + 3
i = i + 1
Next
'Count the number of items for each key.
'Shows how to iterate an array using For [index] loop.
ReDim keyCount(keyLen)
For r = 1 To dataRows
i = IndexOfKey(keyArray, CStr(data(r, 2)))
keyCount(i) = keyCount(i) + 1
If keyCount(i) > maxCount Then maxCount = keyCount(i)
Next
'Size the output array.
c = WorksheetFunction.Ceiling(maxCount / 3, 1)
ReDim output(1 To (keyLen + 1) * 3, 1 To c * dataCols + c - 1)
'Populate the output array.
ReDim threeCount(keyLen)
For r = 1 To dataRows
i = IndexOfKey(keyArray, CStr(data(r, 2)))
'Copy the columns for this row.
For c = 1 To dataCols
output(rowNum(i), colNum(i) + c - 1) = data(r, c)
Next
'Increment the count and if it's equals 3 then
'reset the row num and increase the column number.
threeCount(i) = threeCount(i) + 1
rowNum(i) = rowNum(i) + 1
If threeCount(i) = 3 Then
rowNum(i) = rowNum(i) - 3
colNum(i) = colNum(i) + dataCols + 1
threeCount(i) = 0
End If
Next
'Write the data - change "Sheet2" to your sheet name.
'Shows how to write an array to a Range.
ThisWorkbook.Worksheets("Sheet2").Range("A3") _
.Resize(UBound(output, 1), UBound(output, 2)).Value = output
End Sub
Private Function IndexOfKey(list() As String, key As String) As Long
Dim i As Long
Dim k As Variant
'Helper function to find index position of key in array.
For Each k In list
If key = k Then
IndexOfKey = i
Exit Function
End If
i = i + 1
Next
IndexOfKey = -1
End Function

Why is this locking up? Loop through all rows, perform function on duplicate, delete duplicate row

The code works when I bite off a couple hundred rows at a time, but always hangs somewhere in the middle when I try to run it on 10,000.
What the code does: Looks for duplicate entries in column A, adds the values in columns c, d and e between the two rows, then deletes the original row.
Can anybody think of a more stable way to do this, or point me towards why it might be locking up?
Sub combineDelete ()
Const TEST_COLUMN As String = "A"
Dim i As Long
Dim iLastRow As Long
With ActiveSheet
iLastRow = .Cells(.Rows.Count, TEST_COLUMN).End(xlUp).Row
For i = iLastRow To 2 Step -1
If Cells(i, 1) = Cells(i - 1, 1) Then
s = Cells(i, 3).Value
t = Cells(i - 1, 3).Value
Cells(i - 1, 3) = s + t
u = Cells(i, 4).Value
v = Cells(i - 1, 4).Value
Cells(i - 1, 4) = u + v
w = Cells(i, 5).Value
y = Cells(i - 1, 5).Value
Cells(i - 1, 5) = w + y
Cells(i, 1).EntireRow.Delete
End If
Next i
End With
End Sub
Edit: Here's a link to a sample subset of the data.
Post-edit: Every one of these ideas is effective. Ron Rosenberg's solution below manages to handle it orders of magnitude faster than any solution I tinkered with. Thanks!
Start with this and let us know how things are going afterwards:
Option Explicit
Sub combineDelete()
Const TEST_COLUMN As String = "A"
Dim i As Long
Dim iLastRow As Long
Dim s As Double, t As Double, u As Double
Dim v As Double, w As Double, y As Double
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With
With ActiveSheet
iLastRow = .Cells(.Rows.Count, TEST_COLUMN).End(xlUp).Row
For i = iLastRow To 2 Step -1
If .Cells(i, 1).Value2 = .Cells(i - 1, 1).Value2 Then
s = .Cells(i, 3).Value2
t = .Cells(i - 1, 3).Value2
.Cells(i - 1, 3).Value2 = s + t
u = .Cells(i, 4).Value2
v = .Cells(i - 1, 4).Value2
.Cells(i - 1, 4).Value2 = u + v
w = .Cells(i, 5).Value2
y = .Cells(i - 1, 5).Value2
.Cells(i - 1, 5).Value2 = w + y
.Rows(i).EntireRow.Delete
End If
Next i
End With
With Application
.EnableEvents = True
.Calculation = xlCalculationAutomatic
.ScreenUpdating = True
End With
End Sub
Notes:
Disable screenupdating, calculations and events
Use .Value2 instead of .Value
Explicit coding
Missing references to ActiveSheet added by adding dots .
Dim all variables to avoid variants
Here is a routine that should run quite rapidly. You will note near the top of the code where to change the source and results worksheets if you want.
The work is done within VBA arrays, which will be much faster than working on the worksheet.
I create a User defined object whose properties are the contents of the TestColumn; the Maximum amount in Column B; and an array of the Sum of Columns C, D and E.
These are placed into a Collection object with the Key being the TestColumn. If there is a duplicate, the Collection object will return a 457 error, which we test for and use to combine the rows.
Finally, we write the collection object back to an array, and write that array to the worksheet.
You will use both a Class Module and a Regular Module
The original data does not need to be sorted, but you can if you want, either before or after running this macro.
Enjoy.
Class Module
Be sure to rename this module cCombo after inserting it
Rename this module **cCombo**
Option Explicit
Private pTestColumn As String
Private pMaxColumn As Double
Private pSumColumns(3 To 5) As Variant
Public Property Get TestColumn() As String
TestColumn = pTestColumn
End Property
Public Property Let TestColumn(Value As String)
pTestColumn = Value
End Property
Public Property Get MaxColumn() As Double
MaxColumn = pMaxColumn
End Property
Public Property Let MaxColumn(Value As Double)
pMaxColumn = IIf(pMaxColumn > Value, pMaxColumn, Value)
End Property
Public Property Get SumColumns() As Variant
SumColumns = pSumColumns
End Property
Public Property Let SumColumns(Value As Variant)
Dim I As Long
For I = LBound(Value) To UBound(Value)
pSumColumns(I) = pSumColumns(I) + Value(I)
Next I
End Property
Regular Module
Option Explicit
Sub combineDelete()
Const TEST_COLUMN As String = "A"
Dim vSrc As Variant, vRes As Variant, rRes As Range
Dim wsSrc As Worksheet, wsRes As Worksheet
Dim cC As cCombo, colC As Collection
Dim I As Long, J As Long, V As Variant, S As String
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2") 'could be same sheet if you want to overwrite
Set rRes = wsRes.Cells(2, 1)
'Get original data
With wsSrc
vSrc = Range(.Cells(2, TEST_COLUMN), .Cells(.Rows.Count, TEST_COLUMN).End(xlUp)).Resize(columnsize:=5)
End With
ReDim V(3 To UBound(vSrc, 2)) 'for storing rows
'Collect the data, eliminating duplicates
Set colC = New Collection
On Error Resume Next
For I = 1 To UBound(vSrc, 1)
Set cC = New cCombo
With cC
.TestColumn = vSrc(I, 1)
.MaxColumn = vSrc(I, 2)
For J = 3 To UBound(vSrc, 2)
V(J) = vSrc(I, J)
Next J
.SumColumns = V
colC.Add Item:=cC, Key:=.TestColumn
Select Case Err.Number
Case 457
Err.Clear
colC(.TestColumn).MaxColumn = .MaxColumn
colC(.TestColumn).SumColumns = .SumColumns
Case Is <> 0
Debug.Print Err.Number, Err.Description
Stop
End Select
End With
Next I
On Error GoTo 0
'Create results array
ReDim vRes(1 To colC.Count, 1 To 5)
For I = 1 To colC.Count
With colC(I)
vRes(I, 1) = .TestColumn
vRes(I, 2) = .MaxColumn
V = .SumColumns
For J = LBound(V) To UBound(V)
vRes(I, J) = V(J)
Next J
End With
Next I
'Write the results
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.EntireColumn.Clear
.Value = vRes
.EntireColumn.ColumnWidth = 5
End With
End Sub
Working with ~10K rows would benefit immensely from a variant array but you can also make significant improvements by deleting all of the rows at once. While you could gather a Union of the rows to delete, a Range.RemoveDuplicates method is also appropriate in this case.
It is unclear on whether your data is sorted on a primary key of column A. Your current code depends upon this but I've changed the criteria check to the Excel Application object's MATCH function to accommodate unsorted data.
Your code appears to avoid text column header labels in row 1. I've used the Range.CurrentRegion property to localize the cells to be processed.
Sub combineDelete()
Const TEST_COLUMN As String = "A"
Dim i As Long, mtch As Long
'appTGGL bTGGL:=False 'uncomment this line once you have completed debugging
With ActiveSheet
With .Cells(1, 1).CurrentRegion
For i = .Rows.Count To 2 Step -1
mtch = Application.Match(.Cells(i, 1).Value, .Columns(1), 0)
If mtch < i Then
.Cells(mtch, 3) = Application.Sum(.Cells(mtch, 3), .Cells(i, 3))
.Cells(mtch, 4) = Application.Sum(.Cells(mtch, 4), .Cells(i, 4))
.Cells(mtch, 5) = Application.Sum(.Cells(mtch, 5), .Cells(i, 5))
End If
Next i
.RemoveDuplicates Columns:=1, Header:=xlYes
End With
End With
appTGGL
End Sub
Public Sub appTGGL(Optional bTGGL As Boolean = True)
With Application
.ScreenUpdating = bTGGL
.EnableEvents = bTGGL
.DisplayAlerts = bTGGL
.Calculation = IIf(bTGGL, xlCalculationAutomatic, xlCalculationManual)
.StatusBar = vbNullString
End With
Debug.Print Timer
End Sub
The use of Application.Sum(..., ...) is a trifle slower than straight addition but it has the benefit of providing error control over text values. This may or may not be a desired behavior; i.e. you might want to know when you are trying to add text to a number instead of skipping over it.
There were many places inside your With ... End With statement where you used Cells(i, 3) and not .Cells(i, 3) (note the prefix . ). If you are going to take the time to reference the Range.Parent property (and you should always do so!) then it seems a shame not to use it.
I've included a reusable 'helper' sub that 'turns off' many application environment states but left it commented. Uncomment it once you havew completed debugging for additional speed and stability.
Addendum for lookup strings with length > 255
Sub combineDelete()
Dim i As Long, mtch As Long
Dim vCOLAs As Variant, dCOLAs As Object
'appTGGL bTGGL:=False 'uncomment this line once you have completed debugging
Set dCOLAs = CreateObject("Scripting.Dictionary")
dCOLAs.CompareMode = vbTextCompare
With ActiveSheet
With .Cells(1, 1).CurrentRegion
'strings in column A may exceed 255 chars; build array and and a dictionary from array
vCOLAs = .Resize(.Rows.Count, 1).Value2
For i = UBound(vCOLAs, 1) To LBound(vCOLAs, 1) Step -1
'fast overwrite method
dCOLAs.Item(vCOLAs(i, 1)) = i
Next i
For i = .Rows.Count To 2 Step -1
mtch = dCOLAs.Item(vCOLAs(i, 1))
If mtch < i Then
.Cells(mtch, 3) = Application.Sum(.Cells(mtch, 3), .Cells(i, 3))
.Cells(mtch, 4) = Application.Sum(.Cells(mtch, 4), .Cells(i, 4))
.Cells(mtch, 5) = Application.Sum(.Cells(mtch, 5), .Cells(i, 5))
End If
Next i
.RemoveDuplicates Columns:=1, Header:=xlYes
End With
End With
Erase vCOLAs
dCOLAs.RemoveAll: Set dCOLAs = Nothing
appTGGL
End Sub
A dictionary object provides lightning fast lookups due to its unique keys. Since these are a variant type, there is no 255 character limit.

Resources