How to extract values of multiple listboxes on Excel sheet? - excel

I have a userform with multiple checkboxes and listboxes, where each checkbox controls the values of one listbox each.
After clicking on 'Next' the userform inputs the selected values of each listbox on the Excel sheet. I am able to achieve this only for one pair of checkbox and listbox at a time. But I want the results of each shortlisted items one after the other.
Private Sub cmdFDB_Next_Click()
Dim ColCount As Integer, lastrow As Integer
Dim lastrow1 As Integer
Dim Data As Integer
Dim i As Integer
lastrow = Worksheets("Model Portfolio").Cells(Rows.Count, 2).End(xlUp).Row
With Worksheets("Model Portfolio").Cells(lastrow, 2)
.Offset(2, 0).Value = "Fixed Deposits and Bonds"
.Offset(2, 0).Font.Bold = True
.Offset(2, 0).Font.Size = 12
For i = 2 To lastrow
If Me.chkGB.Value = True Then
.Offset(3, 0).Value = "Government Bonds"
.Offset(3, 0).Font.Bold = True
.Offset(3, 2).Value = Format(Me.txtGB.Value, "Currency")
lastrow1 = lastrow + 4
ColCount = 2
With Me.lbxGB
'loop through each listbox item to see if they are selected
For Data = 0 To .ListCount - 1
If .Selected(Data) = True Then
Cells(lastrow1, ColCount).Value = .List(Data)
lastrow1 = lastrow1 + 1
End If
Next Data
End With
End If
If Me.chkCFD.Value = True Then
.Offset(3, 0).Value = "Corporate Fixed Deposits"
.Offset(3, 0).Font.Bold = True
.Offset(3, 2).Value = Format(Me.txtCFD.Value, "Currency")
lastrow1 = lastrow + 4
ColCount = 2
With Me.lbxCFD
'loop through each listbox item to see if they are selected
For Data = 0 To .ListCount - 1
If .Selected(Data) = True Then
Cells(lastrow1, ColCount).Value = .List(Data)
lastrow1 = lastrow1 + 1
End If
Next Data
End With
End If
If Me.chkTSB.Value = True Then
.Offset(3, 0).Value = "Tax Saving Bonds"
.Offset(3, 0).Font.Bold = True
.Offset(3, 2).Value = Format(Me.txtTSB.Value, "Currency")
lastrow1 = lastrow + 4
ColCount = 2
With Me.lbxTSB
'loop through each listbox item to see if they are selected
For Data = 0 To .ListCount - 1
If .Selected(Data) = True Then
Cells(lastrow1, ColCount).Value = .List(Data)
lastrow1 = lastrow1 + 1
End If
Next Data
End With
End If
Next i
End With
With MultiPage1
.Value = (.Value + 1) Mod (.Pages.Count)
End With
End Sub

Extract selected listbox items to sheet
As you aren't consequent in your row numbering (never changing lastrow mixed with additional offsets and increments), you are loosing track of the actual row numbers.
It's also better practice to use a Sub procedure (here: WriteItems) for repetitive calls and to redefine your lastrow (here: start row) each time. Furthermore I demonstrate how to extract a whole listbox "row" using the Application.Index() function.
Further hint: Instead of direct formatting, consider to use conditional formatting (CF) as you needn't clear old formats in deleted cells (sure you find a lot of examples at SO :-)
BTW I'd prefer to avoid control names containing an underscore "_" as this has some relevance in class implementations.
Main event
Private Sub cmdFDB_Next_Click()
'[0] Define data sheet
Const SHEETNAME As String = "Model Portfolio"
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets(SHEETNAME)
'[1] Define abbreviations to identify securities controls
Dim Abbreviations, abbr
Abbreviations = Array("", "GB", "CFD", "TSB") ' first item is EMPTY!
'[2] write data for each security type
Dim OKAY As Boolean
For Each abbr In Abbreviations
'[2a] check
If abbr = vbNullString Then ' Main Title
OKAY = True
ElseIf Me.Controls("chk" & abbr) Then ' individual security checked
OKAY = True
Else
OKAY = False
End If
'==================================
'[2b] write selected data in blocks
'----------------------------------
If OKAY Then WriteItems abbr, ws ' call sub procedure
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Next
End Sub
Sub procedure WriteItems
Private Sub WriteItems(ByVal abbrev As String, ws As Worksheet)
'Purpose: write caption and selected listbox items to sheet
'Note: called by cmdFDB_Next_Click()
Const EMPTYROWS As Long = 1 ' << change to needed space
Const LBXPREFIX As String = "lbx" ' << change to individual checkbox prefix
Const TITLE As String = "Fixed Deposits and Bonds"
With ws
'[0] Define new startrow
Dim StartRow As Long
StartRow = .Cells(Rows.Count, 2).End(xlUp).Row + EMPTYROWS + 1
'[1] Write caption
ws.Cells(StartRow, 2) = getTitle(abbrev) ' function call, see below
If abbrev = vbNullString Then Exit Sub ' 1st array term writes main caption only
'other stuff (e.g. formatting of title above)
'...
'[2] Write data to worksheet
With Me.Controls(LBXPREFIX & abbrev)
Dim i As Long, ii As Long, temp As Variant
For i = 1 To .ListCount
If .Selected(i - 1) = True Then
ii = ii + 1
ws.Cells(StartRow + ii, .ColumnCount).Resize(1, 2).Value = Application.Index(.List, i, 0)
End If
Next i
End With
End With
End Sub
Further note: The Application.Index function allows to get a whole listbox "row" by passing zero (..,0) as second function argument.
Helper function GetTitle()
Function getTitle(ByVal abbrev As String) As String
'Purpose: return full name/caption of security abbreviation
Select Case UCase(abbrev)
Case vbNullString
getTitle = "Fixed Deposits and Bonds"
Case "GB": getTitle = "Government Bonds"
Case "CFD": getTitle = "Corporate Fixed Deposits"
Case "TSB": getTitle = "Tax Saving Bonds"
Case Else: getTitle = "All Other"
End Select
End Function

Related

Previous and Next Button function to VBA Data Entry form is not working

Previous Record and Next Record sub routine is not working. I marked with 1 and 2. These two navigation bars (1&2) works on the what is entered on WaypointId.
Say for example, if I say waypoint id=1235, then next record should appear in a data entry form. My vba code is first search the row number of waypoint id in observation sheet and then I decrease the row number by 1 for displaying previous record and increase the row number by 1 for next record. Depends on the functionality it shows data in the Data Entry Form.
My VBA code is not working for those two things. Attach workbook with name Problem-1.xlsm See Navigation Control Module.
Sub FindRecord(WyPt)
Dim Value As String
WyPtRow = 0
ReadRow = 2
Value = Cells(ReadRow, 2)
While Value <> ""
If WyPt = Value Then
WyPtRow = ReadRow
Exit Sub
End If
ReadRow = ReadRow + 1
Value = Cells(ReadRow, 2)
Wend
End Sub
Sub ViewPreviousRecord()
Set DEFrm = Sheets("DataEntryForm")
Set ObsData = Sheets("Observations")
Dim WyPt As String
WyPt = Trim(DEFrm.Cells(6, 2))
Call FindRecord(WyPt)
LastRow = WyPtRow - 1
With DEFrm
.Cells(6, 2).Value = ObsData.Cells(LastRow, 2).Value 'WaypointID
.Cells(6, 4).Value = ObsData.Cells(LastRow, 3).Value 'ObsType
.Cells(8, 2).Value = ObsData.Cells(LastRow, 4).Value 'Date
.Cells(8, 4).Value = ObsData.Cells(LastRow, 5).Value 'LoggedBy
End With
End Sub
Sub ViewNextRecord()
Set DEFrm = Sheets("DataEntryForm")
Set ObsData = Sheets("Observations")
Dim WyPt As String
WyPt = Trim(DEFrm.Cells(6, 2))
Call FindRecord(WyPt)
LastRow = WyPtRow + 1
With DEFrm
.Cells(6, 2).Value = ObsData.Cells(LastRow, 2).Value 'WaypointID
.Cells(6, 4).Value = ObsData.Cells(LastRow, 3).Value 'ObsType
.Cells(35, 10).Value = ObsData.Cells(LastRow, 115) 'Photo4Desc
End With
End Sub
This is the most important procedure in your project.
Sub DisplayRecord(ByVal Rs As Long)
' 235
Dim Arr As Variant ' Data from row Rs in database
Dim Target() As String ' Dashboard addresses matching Arr
Dim i As Long ' loop counter: Arr(Index)
' cell addresses are aligned with column numbers in database (-2)
Arr = "B6,D6,B8,D8,G6,H6,G7,H7,G8,H8,B11,C11,D11,E11,F11,G11,H11,I11"
Arr = Arr & ",B14,C14,D14,E14,F14,G14,B17,C17,D17,E17,F17,G17"
Arr = Arr & ",I14,J14,I15,J15,I16,J16,I17,J17,B20,C20,D20,E20,F20,G20"
Arr = Arr & ",B23,C23,D23,E23,F23"
Arr = Arr & ",I20,J20,K20,I21,J21,K21,I2,J22,K22,I23,J23,K23"
Arr = Arr & ",B26,C26,D26,E26,F26,G26,H26,I26,J26,K26"
Arr = Arr & ",B27,C27,D27,E27,F27,G27,H27,I27,J27,K27"
Arr = Arr & ",B28,C28,D28,E28,F28,G28,H28,I28,J28,K28"
Arr = Arr & ",B29,C29,D29,E29,F29,G29,H29,I29,J29,K29"
Arr = Arr & ",B32,H32,I32,J32,H33,I33,J33,H34,I34,J34,H35,I35,J35"
Target = Split(Arr, ",")
With Sheets("Observations")
Arr = .Range(.Cells(Rs, 1), .Cells(Rs, 115)).Value
End With
Application.ScreenUpdating = False ' speed up execution
For i = 2 To UBound(Arr, 2) ' skip first database column
Sheets("DataEntryForm").Range(Target(i - 2)).Value = Arr(1, i)
Next i
Application.ScreenUpdating = True
End Sub
It displays the data of the row Rs given to it as an argument. You already have a function that finds the row number needed by the above procedure. Below please find an improvement.
Function RecordRow(ByVal WyPt As String) As Long
' 235
' return the row number where WyP was found or 0
Dim Fnd As Range
With Worksheets("Observations")
Set Fnd = .Range(.Cells(2, "B"), .Cells(.Rows.Count, "B").End(xlUp))
Set Fnd = Fnd.Find(WyPt, , LookIn:=xlValues, lookat:=xlWhole)
If Not Fnd Is Nothing Then
RecordRow = Fnd.Row
End If
End With
End Function
The deal is simple: you give the Waypoint ID and receive the row number where it was found. If it isn't found the function returns 0, and that is how you avoid crashes.
With these two procedures in place you can easily call up the first and the last records.
Sub ViewFirstRecord()
' 235
DisplayRecord 2
End Sub
Sub ViewLastRecord()
' 235
With Worksheets("Observations")
DisplayRecord .Cells(.Rows.Count, "A").End(xlUp).Row
End With
End Sub
The next and previous records are just a matter of finding the row number and displaying its data.
Sub ViewNextRecord()
' 235
Dim Rs As Long ' data source row
Rs = RecordRow(Trim(Cells(6, 2).Value)) + 1
If Rs > 1 Then
With Worksheets("Observations")
If Rs <= .Cells(.Rows.Count, "A").End(xlUp).Row Then
DisplayRecord Rs
Else
MsgBox "No more records to show.", vbInformation, "Last record"
End If
End With
End If
End Sub
Sub ViewPreviousRecord()
' 235
Dim Rs As Long ' data source row
Rs = RecordRow(Trim(Cells(6, 2).Value)) - 1
If Rs > 1 Then
DisplayRecord Rs
Else
MsgBox "No more records to show.", vbInformation, "First record"
End If
End Sub
If that's the whole code, you may be finding a problem with scope. It seems ViewPreviousRecord() is not able to see WyPtRow.
You can try adding
dim WyPtRow
Before the Sub FindRecord(WyPt) definition.
Another implementation would be changing the Sub for a function, and returning the WyPtRow value.

How to find duplicates values in a row and then find largest value in another column based on the duplicates?

I have three columns - ID, Stage, and Revenue.
The ID string can be duplicated two or more times, so the number of duplicate ids is unknown.
There are two criteria, one is solved.
If Stage = 5 then Accept is to be written in the last column.
If not 5 then Reject is to be written.
Find each duplicate ID and find the highest revenue value for that duplicate ID.
If it is the highest then mark Accept.
The lower values will already have reject beside them due to the way I set up the first criteria.
Sub FindandAssignValue()
'This will check to see if Stage is a 5 if yes it will Accept if not it
'will say Remove - this works properly
For currentRow = 2 To LastRow
'Will tell me the current value in the leadstage column
currentValue = Range("I" & currentRow).Value
If currentValue = "5" Then
Range("N" & currentRow).Value = "Accept"
Else
Range("N" & currentRow).Value = "Remove"
End If
Next currentRow
currentValue = Range("A" & currentRow).Value
Dim MyArray(1 To lr, 1 To lc) As Variant
'fill up the rows
For r = 1 To lr
For c = 1 To lc 'fill the columns up
MyArray(r, c) = Cells(r + 1, c).Value
Next c
Next r
End Sub
If you add a necessary reference (Open VB Editor > Tools > References > Scroll down until you find "Microsoft Scripting Runtime" > Tick it > Click OK), I think this code should work.
You will likely need to change the name of someSheet to whatever your worksheet is called.
I've assumed your source data (including headers) begins in cell A1 and ends at some row for column C. You can change this as needed.
Results will be written to sheet starting from cell H1. You can change this as needed.
Private Sub AcceptOrRejectSomeValues()
Dim someSheet As Worksheet
Set someSheet = ThisWorkbook.Worksheets("Sheet10") ' Change to whatever yours is called.
Dim lastRow As Long
lastRow = someSheet.Cells(someSheet.Rows.Count, "A").End(xlUp).Row
Debug.Assert lastRow > 1
Dim dataIncludingHeaders As Range
Set dataIncludingHeaders = someSheet.Range("A1", "C" & lastRow)
Dim inputArray() As Variant
inputArray = dataIncludingHeaders.Resize(, dataIncludingHeaders.Columns.Count + 1).Value
Const ID_COLUMN_INDEX As Long = 1
Const STAGE_COLUMN_INDEX As Long = 2
Const REVENUE_COLUMN_INDEX As Long = 3
Const RESULT_COLUMN_INDEX As Long = 4
Dim booleanArray() As Boolean
ReDim booleanArray(1 To UBound(inputArray))
Dim idsAndRowIndexes As Scripting.Dictionary
Set idsAndRowIndexes = New Scripting.Dictionary
Dim rowIndex As Long
For rowIndex = (LBound(inputArray, 1) + 1) To UBound(inputArray, 1)
booleanArray(rowIndex) = (inputArray(rowIndex, STAGE_COLUMN_INDEX) = "5")
Dim currentKey As String
currentKey = CStr(inputArray(rowIndex, ID_COLUMN_INDEX))
If idsAndRowIndexes.Exists(currentKey) Then
If inputArray(rowIndex, REVENUE_COLUMN_INDEX) > inputArray(idsAndRowIndexes(currentKey), REVENUE_COLUMN_INDEX) Then
idsAndRowIndexes(currentKey) = rowIndex
End If
Else
idsAndRowIndexes(currentKey) = rowIndex
End If
Next rowIndex
Dim id As Variant
For Each id In idsAndRowIndexes.Keys
booleanArray(idsAndRowIndexes(id)) = True
Next id
For rowIndex = LBound(inputArray, 1) To UBound(inputArray, 1)
If booleanArray(rowIndex) Then
inputArray(rowIndex, RESULT_COLUMN_INDEX) = "Accept"
Else
inputArray(rowIndex, RESULT_COLUMN_INDEX) = "Reject"
End If
Next rowIndex
someSheet.Range("H1").Resize(UBound(inputArray, 1), UBound(inputArray, 2)).Value = inputArray
End Sub

How to create a nested loop to check if a value exists in a second list

I am trying to compare values in two lists. I want my code to compare a value in the first list and check all the entries in the second list. If there is a match then the code will print true next to the value in the first list and if not it will print false.
The problem I am having is that my code only compares values that are in the same row.
The code runs and I have tried it on a two smaller lists to make sure the data types are to same and there aren't any extra spaces or commas in the lists that would lead to a "False" output. I have also tried changing the order of the for and if statements but this doesn't work either.
Sub findvalues()
For i = 2 To 16
For j = 2 To 16
If Cells(i, 3).Value = Cells(i, 1).Value Then
Cells(i, 4).Value = "TRUE"
ElseIf Cells(i, 3).Value = Cells(j + 1, 1).Value Then
Cells(i, 4).Value = "TRUE"
Else
Cells(i, 4).Value = "FALSE"
End If
Next j
Next i
End Sub
Here are the two lists I am testing the code on
Slight mods to your code based on the data you provided in columns 1 & 3. As always, things could be improved but this should get you going ...
Sub findvalues()
Dim i As Long, j As Long, bResult As Boolean
For i = 2 To 16
strValueToLookFor = Cells(i, 1)
For j = 2 To 16
bResult = False
If strValueToLookFor = Cells(j, 3).Value Then
bResult = True
Exit For
End If
Next j
Cells(i, 6).Value = bResult
Next i
End Sub
... you may just need to flick the columns over so the first list searches on the second list or vice versa.
I don't see any need for VBA - formulas are the way to go - but to avoid two loops one could do this:
Sub findvalues()
Dim i As Long
For i = 2 To 130
Cells(i, 4).Value = IsNumeric(Application.Match(Cells(i, 1).Value, Range("C2:C130"), 0))
Next i
End Sub
Update: this does not cater for multiple matches.
There are many was to achieve that. one of them is by using IF & COUNTIF
Formula
=IF(COUNTIF($E$2:$E$6,A2)>0,"TRUE","FALSE")
Results:
VBA CODE
Option Explicit
Sub findvalues()
Dim i As Long
Dim rng As Range
With ThisWorkbook.Worksheets("Sheet1") 'Change if needed
Set rng = .Range("A2:A130") 'set rng to includes values from column A, rows 2:130
For i = 2 To 130 'Loop from row 2 to 130
'Check if the values in column C includes in the rng
If Application.WorksheetFunction.CountIf(rng, .Range("C" & i).Value) > 0 Then
.Range("D" & i).Value = "TRUE"
Else
.Range("D" & i).Value = "FALSE"
End If
Next i
End With
End Sub
VBA code to reconcile two lists.
Sub Reconciliation()
Dim endRow As Long
Dim ICount As Long
Dim Match1() As Variant
Dim Match2() As Variant
Dim ws As Worksheet
Set ws = Worksheets("Recon")
ICount = 0
endRow = ws.Cells(ws.Rows.Count, 2).End(xlUp).Row
endRow1 = ws.Cells(ws.Rows.Count, 11).End(xlUp).Row
Match1 = Sheet1.Range("b2:b" & endRow)
Match2 = Sheet1.Range("K2:K" & endRow1)
For i = LBound(Match1) To UBound(Match1)
For j = LBound(Match2) To UBound(Match2)
If Match1(i, 1) = Match2(j, 1) Then
ICount = ICount + 1
Sheet1.Range("C" & i + 1).Value = ICount
Sheet1.Range("L" & j + 1).Value = ICount
Else
End If
Next j
Next i
End Sub

Searching multiple tables on the same sheet with the column in varying locations and copying them to a different sheet

Hopefully the title is clear. I am trying to search through multiple tables on a single sheet. The information I am looking for is the same for all of the tables, just that the corresponding column is located in different spots (e.g. in one table the column I want to search is in I, while for another table it could be in O.) which makes it a bit more challenging for me.
I want to search through each column that has the same title (Load Number) and depending on its value, copy that entire row over to a sheet that corresponds with that value.
Below is what I have so far in VBA as well as a picture to hopefully clarify my issue.
Any help/advice is appreciated!
http://imgur.com/a/e9DyH
Sub Load_Number_Finder()
Dim ws As Worksheet
Dim i As Integer
Dim j As Integer
j = 1
Set ws = Sheets.Add(After:=Sheets("Master"))
ws.Name = ("Test Load " & j)
i = 1
Sheets("Master").Select
For Each cell In Sheets("Master").Range("M:M")
If cell.Value = "1" Then
j = 1
'Set WS = Sheets.Add(After:=Sheets("Master"))
'WS.Name = ("Test Load " & j)
matchRow = cell.Row
Rows(matchRow & ":" & matchRow).Select
Selection.Copy
Sheets("Test Load " & j).Select
ActiveSheet.Rows(i).Select
ActiveSheet.Paste
Sheets("Master").Select
i = i + 1
ElseIf cell.Value = "" Then
' 2, 3, 4, 5, cases
Else
' Something needs to go here to catch when it doesnt have a load number on it yet
End If
' Err_Execute:
' MsgBox "An error occurred."
Next
End Sub
Try this function. This should work for you. Let me know what the results are with your sheet. I made a mock up sheet and tested it, it worked. I can make changes if this is not exactly what you are looking for.
Option Explicit
Sub copyPaste()
Dim rowCount, row_ix, temp, i As Integer
Dim TD_COL_IX As Integer
Dim td_value As String
Dim td_values() As String
rowCount = Worksheets("Master").Cells(Rows.Count, "A").End(xlUp).Row
For row_ix = 1 To rowCount
temp = isNewTable(CInt(row_ix))
If temp > 0 Then
TD_COL_IX = temp
ElseIf TD_COL_IX > 0 Then
td_value = Worksheets("Master").Cells(row_ix, TD_COL_IX)
If Not td_value = "" Then
td_values = Split(td_value, " ")
For i = 0 To UBound(td_values)
If Not sheetExists("Test Load " & td_values(i)) Then
Sheets.Add.Name = "Test Load " & td_values(i)
End If
If Worksheets("Test Load " & td_values(i)).Cells(1, 1).Value = "" Then
Worksheets("Master").Range(Worksheets("Master").Cells(row_ix, 1), Worksheets("Master").Cells(row_ix, TD_COL_IX - 1)).Copy _
Destination:=Worksheets("Test Load " & td_values(i)).Cells(1, 1)
Else
Dim rowCount_pasteSheet As Integer
rowCount_pasteSheet = Worksheets("Test Load " & td_values(i)).Cells(Rows.Count, "A").End(xlUp).Row
Worksheets("Master").Range(Worksheets("Master").Cells(row_ix, 1), Worksheets("Master").Cells(row_ix, TD_COL_IX - 1)).Copy _
Destination:=Worksheets("Test Load " & td_values(i)).Cells(rowCount_pasteSheet + 1, 1)
End If
Next i
End If
End If
Next row_ix
End Sub
Function isNewTable(row_ix As Integer) As Integer
Dim colCount, col_ix As Integer
colCount = Worksheets("Master").Cells(row_ix, Columns.Count).End(xlToLeft).Column
For col_ix = 1 To colCount
If Not IsError(Worksheets("Master").Cells(row_ix, col_ix).Value) Then
If Worksheets("Master").Cells(row_ix, col_ix).Value = "LD #" Then
isNewTable = col_ix
Exit Function
End If
End If
Next col_ix
isNewTable = 0
End Function
' ####################################################
' sheetExists(sheetToFind As String) As Boolean
'
' Returns true if the sheet exists, False otherwise
' ####################################################
Public Function sheetExists(sheetToFind As String) As Boolean
Dim sheet As Worksheet
sheetExists = False
For Each sheet In Worksheets
If sheetToFind = sheet.Name Then
sheetExists = True
Exit Function
End If
Next sheet
End Function

Removing duplicates based on their occurrence

I would like to check a certain column (W) for duplicates (number of occurrences is stored in another column (AZ)) and than delete all row this way:
Value is found two times in the column - delete only one row containing the value.
Value is found more times in the column - delete all the rows with the values.
My code works quite well but sometimes it doesn't delete all the duplicates as it should do. Any idea for improvement?
EDIT: The updated code works really good except that it always misses one duplicate and leaves it not deleted.
fin = ws.UsedRange.Rows.count
For i = 2 To fin
ws.Range("AZ" & i).value = Application.WorksheetFunction.CountIf(ws.Range("W2:W" & fin), ws.Range("W" & i))
Next i
For j = fin To 2 Step -1
If ws.Range("AZ" & j).value > 2 Then
ws.Range("AZ" & j).EntireRow.Delete
fin = ws.UsedRange.Rows.count
ElseIf ws.Range("AZ" & j).value = 2 Then
Set rng = Range("W:W").Find(Range("W" & j).value, , xlValues, xlWhole, , xlNext)
rngRow = rng.Row
If rngRow <> j Then
ws.Range("AZ" & rngRow) = "1"
ws.Range("AZ" & j).EntireRow.Delete
fin = ws.UsedRange.Rows.count
Else
MsgBox "Error at row " & rngRow
End If
End If
Next j
If speed is an issue, here is a method that should be faster, as it creates a collection of rows to be deleted, then deletes them. Since everything, except for the actual row deletion, is done in VBA, there are far fewer calls back and forth to the worksheet.
The routine could be sped up as noted in the inline comments.
If it is still too slow, depending on the size of the worksheet, it might be feasible to read the entire worksheet into a VBA Array; test for duplicates; write back the results to a new array and write that out to the worksheet. (If your worksheet is too large, this method might run out of memory, though).
In any event, we need both a Class Module which YOU must rename cPhrases, as well as a Regular Module
Class Module
Option Explicit
Private pPhrase As String
Private pCount As Long
Private pRowNums As Collection
Public Property Get Phrase() As String
Phrase = pPhrase
End Property
Public Property Let Phrase(Value As String)
pPhrase = Value
End Property
Public Property Get Count() As Long
Count = pCount
End Property
Public Property Let Count(Value As Long)
pCount = Value
End Property
Public Property Get RowNums() As Collection
Set RowNums = pRowNums
End Property
Public Function ADDRowNum(Value As Long)
pRowNums.Add Value
End Function
Private Sub Class_Initialize()
Set pRowNums = New Collection
End Sub
Regular Module
Option Explicit
Sub RemoveDuplicateRows()
Dim wsSrc As Worksheet
Dim vSrc As Variant
Dim CP As cPhrases, colP As Collection, colRowNums As Collection
Dim I As Long, K As Long
Dim R As Range
'Data worksheet
Set wsSrc = Worksheets("sheet1")
'Read original data into VBA array
With wsSrc
vSrc = .Range(.Cells(1, "W"), .Cells(.Rows.Count, "W").End(xlUp))
End With
'Collect list of items, counts and row numbers to delete
'Collection object will --> error when trying to add
' duplicate key. Use that error to increment the count
Set colP = New Collection
On Error Resume Next
For I = 2 To UBound(vSrc, 1)
Set CP = New cPhrases
With CP
.Phrase = vSrc(I, 1)
.Count = 1
.ADDRowNum I
colP.Add CP, CStr(.Phrase)
Select Case Err.Number
Case 457 'duplicate
With colP(CStr(.Phrase))
.Count = .Count + 1
.ADDRowNum I
End With
Err.Clear
Case Is <> 0 'some other error. Stop to debug
Debug.Print "Error: " & Err.Number, Err.Description
Stop
End Select
End With
Next I
On Error GoTo 0
'Rows to be deleted
Set colRowNums = New Collection
For I = 1 To colP.Count
With colP(I)
Select Case .Count
Case 2
colRowNums.Add .RowNums(2)
Case Is > 2
For K = 1 To .RowNums.Count
colRowNums.Add .RowNums(K)
Next K
End Select
End With
Next I
'Revers Sort the collection of Row Numbers
'For speed, if necessary, could use
' faster sort routine
RevCollBubbleSort colRowNums
'Delete Rows
'For speed, could create Unions of up to 30 rows at a time
Application.ScreenUpdating = False
With wsSrc
For I = 1 To colRowNums.Count
.Rows(colRowNums(I)).Delete
Next I
End With
Application.ScreenUpdating = True
End Sub
'Could use faster sort routine if necessary
Sub RevCollBubbleSort(TempCol As Collection)
Dim I As Long
Dim NoExchanges As Boolean
' Loop until no more "exchanges" are made.
Do
NoExchanges = True
' Loop through each element in the array.
For I = 1 To TempCol.Count - 1
' If the element is less than the element
' following it, exchange the two elements.
If TempCol(I) < TempCol(I + 1) Then
NoExchanges = False
TempCol.Add TempCol(I), after:=I + 1
TempCol.Remove I
End If
Next I
Loop While Not (NoExchanges)
End Sub
no need to use that inefficient second loop in the second section, just use a live count like so
fin = ws.UsedRange.Rows.count
For i = 2 To fin
ws.Range("AZ" & i).value = Application.WorksheetFunction.CountIf(ws.Range("W2:W" & fin), ws.Range("W" & i))
Next i
For j = fin To 2 Step -1
If ws.Range("AZ" & j).value > 2 OR Application.WorksheetFunction.CountIf(ws.Range("W2:W" & fin), ws.Range("W" & j)) = 2 Then
ws.Range("AZ" & j).EntireRow.Delete
End If
Next j
While your logic is basically sound, the method is not the most efficient. The AutoFilter Method can quickly remove all counts greater than 2 and the Range.RemoveDuplicates¹ method cansubsequently make quick work of removing one of the rows that still contain duplicate values in column W.
Dim r As Long, c As Long
With ws
If .AutoFilterMode Then .AutoFilterMode = False
r = .Cells.SpecialCells(xlLastCell).Row
c = Application.Max(52, .Cells.SpecialCells(xlLastCell).Column)
With .Range("A1", .Cells(r, c)) '.UsedRange
With .Columns(52)
If IsEmpty(.Cells(1, 1)) Then .Cells(1, 1) = "count"
With .Resize(.Rows.Count - 1, 1).Offset(1, 0)
.Cells.FormulaR1C1 = "=COUNTIF(C[-29], RC[-29])"
.Cells = .Cells.Value
End With
.AutoFilter field:=1, Criteria1:=">2"
With .Resize(.Rows.Count - 1, 1).Offset(1, 0)
If CBool(Application.Subtotal(103, .Cells)) Then
.SpecialCells(xlCellTypeVisible).EntireRow.Delete
End If
End With
.AutoFilter
End With
.RemoveDuplicates Columns:=23, Header:=xlYes
End With
End With
When you rewrite the count values in column AZ, you are likely going to rewrite 3 counts to 2, etc.
¹ The Range.RemoveDuplicates method removes duplicate rows from the bottom up.

Resources