I have a loop that goes through two-dimensional range
set rng = Range("A1:C3")
For each cell in rng
Debug.Print cell.value
next cell
It prints like:
A1
B1
C1
A2
B2...
I would prefer:
A1
A2
A3
B1
B2...
I know there is a way to do that looping with indexes but for each is much more elegant and on the top of that my range can be non-contiguous like:
Range("A1:C3,G9:H16,B14:D18")
Is there a way how to change default (horizontal) excel behavior?
Loop Through the Cells of a Non-Contiguous Range (Multi-Range)
By Areas
The numbers in the yellow cells in the image are illustrating the looping order by areas and by rows. The results of the procedures in the J and K columns are copied from the Immediate window and are showing both of the looping orders.
Option Explicit
Sub WriteCellsByAreaRows()
Const sAddress As String = "A1:C3,G9:H16,B14:D18"
Dim ws As Worksheet: Set ws = ActiveSheet
Dim mrg As Range: Set mrg = ws.Range(sAddress)
Dim arg As Range
Dim aCell As Range
For Each arg In mrg.Areas
For Each aCell In arg.Cells
Debug.Print aCell.Address(0, 0) & " = " & aCell.Value
Next aCell
Next arg
End Sub
Sub WriteCellsByAreaColumns()
Const sAddress As String = "A1:C3,G9:H16,B14:D18"
Dim ws As Worksheet: Set ws = ActiveSheet
Dim mrg As Range: Set mrg = ws.Range(sAddress)
Dim arg As Range
Dim acrg As Range
Dim aCell As Range
For Each arg In mrg.Areas
For Each acrg In arg.Columns
For Each aCell In acrg.Cells
Debug.Print aCell.Address(0, 0) & " = " & aCell.Value
Next aCell
Next acrg
Next arg
End Sub
EDIT
Area Independent (The Challenge)
The numbers in the yellow cells in the image are illustrating the looping order by rows. The results of the procedures in the J and K columns are copied from the Immediate window and are showing both of the looping orders.
Short Function Description (By Columns)
It will loop through the columns of each area and write the column number to an Array List which will finally be sorted. When looping through the elements of the Array List (each column number), a reference to the intersection of the worksheet column and the initial multi-range will be created and each single range reference will be written to an array. The array will be (bubble) sorted by the row numbers of every single column range and then will be added as an element of the resulting array of arrays.
Sub WriteCellsByColumn()
Const sAddress As String = "A1:C3,G9:H16,B14:D18"
Dim ws As Worksheet: Set ws = ActiveSheet
On Error Resume Next
Dim mrg As Range: Set mrg = ws.Range(sAddress)
On Error GoTo 0
If mrg Is Nothing Then Exit Sub
Dim ColumnRanges As Variant: ColumnRanges = GetSortedColumnRanges(mrg)
If IsEmpty(ColumnRanges) Then Exit Sub ' e.g. 'ArrayList' is not working
Dim cCell As Range
Dim a As Long
Dim b As Long
For a = 0 To UBound(ColumnRanges)
For b = 0 To UBound(ColumnRanges(a))
For Each cCell In ColumnRanges(a)(b).Cells
Debug.Print cCell.Address(0, 0) & " = " & cCell.Value
Next cCell
Next b
Next a
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the sorted column range references of a range ('mrg')
' in an array of arrays.
' Remarks: Supports non-contiguous ranges (multi-ranges).
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetSortedColumnRanges( _
ByVal mrg As Range) _
As Variant
If mrg Is Nothing Then Exit Function
On Error Resume Next ' The ArrayList needs 'Microsoft .NET Framework 3.5'.
Dim arl As Object: Set arl = CreateObject("System.Collections.ArrayList")
On Error GoTo 0
If arl Is Nothing Then Exit Function
Dim arg As Range ' Area (Single) Range
Dim crg As Range ' Column Range (in Area Range)
Dim cColumn As Long ' Column Number (of Column Range)
For Each arg In mrg.Areas
For Each crg In arg.Columns
cColumn = crg.Column
If Not arl.Contains(cColumn) Then
arl.Add cColumn
End If
Next crg
Next arg
arl.Sort
' Create a reference to the worksheet ('ws').
Dim ws As Worksheet: Set ws = mrg.Worksheet
' Define the Multi Range Array ('mrgArr').
Dim mrgArr As Variant: ReDim mrgArr(0 To arl.Count - 1)
Dim icrg As Range ' Intersect Column Range (in Worksheet Column)
Dim trg As Range ' Temp Range
Dim trgArr() As Range ' Temp Range Array
Dim Key As Variant '
Dim aUpper As Long
Dim a As Long
Dim b As Long
Dim n As Long
For Each Key In arl
Set icrg = Intersect(mrg, ws.Columns(Key))
aUpper = icrg.Areas.Count - 1
If aUpper = 0 Then
' Write current Intersect Row Range reference to Temp Range Array.
ReDim trgArr(0 To 0): Set trgArr(0) = icrg
Else
' Write current Intersect Row Range references to Temp Range Array.
ReDim trgArr(0 To aUpper)
For a = 0 To aUpper
Set trgArr(a) = icrg.Areas(a + 1)
Next a
' (Bubble) Sort the references by rows.
For a = 0 To aUpper - 1
For b = a To aUpper
If trgArr(a).Row > trgArr(b).Row Then
Set trg = trgArr(a)
Set trgArr(a) = trgArr(b)
Set trgArr(b) = trg
End If
Next b
Next a
End If
' Write the current Temp Range Array to the current element
' of the Multi Range Array.
mrgArr(n) = trgArr
n = n + 1
Next Key
GetSortedColumnRanges = mrgArr
End Function
Sub WriteCellsByRow()
Const sAddress As String = "A1:C3,G9:H16,B14:D18"
Dim ws As Worksheet: Set ws = ActiveSheet
On Error Resume Next
Dim mrg As Range: Set mrg = ws.Range(sAddress)
On Error GoTo 0
If mrg Is Nothing Then Exit Sub
Dim RowRanges As Variant: RowRanges = GetSortedRowRanges(mrg)
If IsEmpty(RowRanges) Then Exit Sub ' e.g. 'ArrayList' is not working
Dim rCell As Range
Dim a As Long
Dim b As Long
For a = 0 To UBound(RowRanges)
For b = 0 To UBound(RowRanges(a))
For Each rCell In RowRanges(a)(b).Cells
Debug.Print rCell.Address(0, 0) & " = " & rCell.Value
Next rCell
Next b
Next a
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the sorted row range references of a range ('mrg')
' in an array of arrays.
' Remarks: Supports non-contiguous ranges (multi-ranges).
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetSortedRowRanges( _
ByVal mrg As Range) _
As Variant
If mrg Is Nothing Then Exit Function
On Error Resume Next ' The ArrayList needs 'Microsoft .NET Framework 3.5'.
Dim arl As Object: Set arl = CreateObject("System.Collections.ArrayList")
On Error GoTo 0
If arl Is Nothing Then Exit Function
Dim arg As Range ' Area (Single) Range
Dim rrg As Range ' Row Range (in Area Range)
Dim cRow As Long ' Row Number (of Row Range)
For Each arg In mrg.Areas
For Each rrg In arg.Rows
cRow = rrg.Row
If Not arl.Contains(cRow) Then
arl.Add cRow
End If
Next rrg
Next arg
arl.Sort
' Create a reference to the worksheet ('ws').
Dim ws As Worksheet: Set ws = mrg.Worksheet
' Define the Multi Range Array ('mrgArr').
Dim mrgArr As Variant: ReDim mrgArr(0 To arl.Count - 1)
Dim irrg As Range ' Intersect Row Range (in Worksheet Row)
Dim trg As Range ' Temp Range
Dim trgArr() As Range ' Temp Range Array
Dim Key As Variant '
Dim aUpper As Long
Dim a As Long
Dim b As Long
Dim n As Long
For Each Key In arl
Set irrg = Intersect(mrg, ws.Rows(Key))
aUpper = irrg.Areas.Count - 1
If aUpper = 0 Then
' Write current Intersect Row Range reference to Temp Range Array.
ReDim trgArr(0 To 0): Set trgArr(0) = irrg
Else
' Write current Intersect Row Range references to Temp Range Array.
ReDim trgArr(0 To aUpper)
For a = 0 To aUpper
Set trgArr(a) = irrg.Areas(a + 1)
Next a
' (Bubble) Sort the references by rows.
For a = 0 To aUpper - 1
For b = a To aUpper
If trgArr(a).Column > trgArr(b).Column Then
Set trg = trgArr(a)
Set trgArr(a) = trgArr(b)
Set trgArr(b) = trg
End If
Next b
Next a
End If
' Write the current Temp Range Array to the current element
' of the Multi Range Array.
mrgArr(n) = trgArr
n = n + 1
Next Key
GetSortedRowRanges = mrgArr
End Function
Maybe
Sub Test()
Dim rng As Range, rRow As Range, rCol As Range
Set rng = Range("A1:C3")
For Each rCol In rng.Columns
For Each rRow In rCol.Cells
Debug.Print rRow.Value
Next rRow
Next rCol
End Sub
As a proof-of-concept with non-contiguous ranges that may overlap. Determine the lowest row and right most column of the areas, create a array that encloses all those areas, and fill it with the cell addresses of the cells in each area. Then traverse the array horizontally or vertically skipping over the blank entries.
Option Explicit
Sub demo_by_column()
Dim ar, a, cell As Range, rng As Range, i As Long
Dim rmax As Long, cmax As Long, rmin As Long, cmin As Long
Dim r As Long, c As Long
rmin = Rows.Count
cmin = Columns.Count
' non-contiguous range
Set rng = Sheet1.Range("A1:C3,D9:H16,B14:D18,A21:J21")
' dimension an array that encloses all the areas
For Each a In rng.Areas
r = a.Row + a.Rows.Count - 1
If r > rmax Then rmax = r
If a.Row < rmin Then rmin = a.Row
c = a.Column + a.Columns.Count - 1
If c > cmax Then cmax = c
If a.Column < cmin Then cmin = a.Column
Next
ReDim ar(1 To rmax, 1 To cmax)
' fill array with cell addresses
For Each a In rng.Areas
For Each cell In a
ar(cell.Row, cell.Column) = cell.Address
Next
Next
' loop the array by column, result to sheet2
i = 1
For c = cmin To cmax
For r = rmin To rmax
If Len(ar(r, c)) > 0 Then
Sheet2.Cells(i, 1) = Sheet1.Range(ar(r, c))
i = i + 1
End If
Next
Next
' show array on sheet3
Sheet3.Range("A1").Resize(rmax, cmax) = ar
End Sub
Related
I am struggling to find the row number from a list of values that matches a set of values of a range from different worksheets.
Sub findmyrow()
Dim myrange As Range
Dim myrow As Long
Dim mysheet As Worksheet
Set myrange = ThisWorkbook.Sheets("Sheet2").Range("A1:Z1")
myrow = ThisWorkbook.Sheets("Sheet1").Range("A1:Z1000").Find(What:=myrange, LookIn:=xlValues).ROW
Debug.Print myrow
End Sub
The code only matches the value of the first column of 'Sheet2' = A and not 'A B' against the result found in the list of 'Sheet1'.
I don't think that find can do that job as it only takes one value to find.
Maybe one of the formula-masters here on Stackoverflow have a better solution.
With VBA you can do it this way:
Option Explicit
Public Sub test_getRow()
Dim rgFind As Range, rgLookup As Range
With ThisWorkbook
Set rgFind = .Worksheets("Sheet2").Range("A1:Z1")
Set rgLookup = .Worksheets("Sheet1").Range("A1:Z1000")
End With
Debug.Print getRow(rgFind, rgLookup)
End Sub
'This is the generic routine to find the row
Public Function getRow(rgFind As Range, rgLookup As Range) As Long
'First check the input ranges
If rgFind.Columns.Count > rgLookup.Columns.Count Then
MsgBox "Number of columns of rgFind have to be less or equal to the number of columns of rgLookup.", vbExclamation
Exit Function
ElseIf rgFind.Rows.Count > 1 Then
MsgBox "rgFind has to be a single row.", vbExclamation
Exit Function
End If
'use arrays of values to increase performance
Dim arrFind As Variant: arrFind = rgFind.Value
Dim arrLookup As Variant: arrLookup = rgLookup.Value
Dim rowL As Long
Dim colF As Long
Dim rowMatch As Long
For rowL = 1 To UBound(arrLookup, 1)
'checking each row of the lookup range
For colF = 1 To UBound(arrFind, 2)
'now compare each value of the lookup row to each value of the find row
If arrFind(1, colF) = arrLookup(rowL, colF) Then
'if equal store current row
rowMatch = rowL
Else
'if not reset rowMatch and exit for-loop as this row can't match anylonger
rowMatch = 0
Exit For
End If
Next
If rowMatch > 0 Then Exit For 'we found the row
Next
getRow = rowMatch
End Function
Match a Row
Sub MatchRow()
Dim lrg As Range: Set lrg = ThisWorkbook.Worksheets("Sheet2").Range("A1:Z1")
Dim lData As Variant: lData = lrg.Value
Dim srg As Range
Set srg = ThisWorkbook.Worksheets("Sheet1").Range("A1:Z1000")
Dim rCount As Long: rCount = srg.Rows.Count
Dim cCount As Long: cCount = srg.Columns.Count
Dim sData As Variant: sData = srg.Value
Dim MyRow As Long, r As Long, c As Long
For r = 1 To rCount
If sData(r, 1) = lData(1, 1) Then
For c = 2 To cCount
If sData(r, c) <> lData(1, c) Then Exit For
Next c
If c > cCount Then MyRow = r: Exit For
End If
Next r
Debug.Print MyRow
End Sub
I'm trying to return the partial match string on a column right beside the column with the text I'm trying to search within. This is the code I tried to write. What would be a better way to do this?
Essentially I have a column with:
Column 1
aaaaa1111
...
zzzzz9999
Column 2
aaa
bbb
..
zzz
I want to return column 2 values to the column adjacent to column 1 where the column 2's string can be found within column 1.
Sub match()
Dim ws As Worksheet
Dim vendors As Range
Dim description As Range
Dim match As Range
Dim cell As Range
Dim j As Integer
Dim i As Integer
Set vendors = ws.Range("ae2:ae1007").Text
Set description = ws.Range("o2:o32609")
Set match = ws.Range("p2:p32609")
For i = 2 To 32609
For j = 2 To 1007
If InStr(description.Cells(i, "O"), vendors.Range(j, "AE")) > 0 Then
match.Cells(i, "P") = vendors.Range(j, "AE").Text
Else: match.Cells(i, "P") = "#N/A"
End If
Next j
Next i
End Sub
Update: (I get run-time error '91' on line 9)
Sub match()
Dim ws As Worksheet
Dim cell As Range
Dim j As Integer
Dim i As Integer
For i = 2 To 32609
For j = 2 To 1007
If InStr(ws.Cells(i, "O"), ws.Cells(j, "AE")) > 0 Then
ws.Cells(i, "P") = ws.Cells(j, "AE").Text
Else: ws.Cells(i, "P") = "#N/A"
End If
Next j
Next i
End Sub
You are getting error 91 because you declared ws but did not set ws to any worksheet.
The code below should run pretty fast since it process the data in an array (read/write from cells is a very slow process).
Option Explicit
Sub FindMatch()
Const vendorCol As String = "AE"
Const descCol As String = "O"
Const matchCol As String = "P"
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1") 'Change name accordingly
'==== Get a list of unique vendor names
Dim vendorDict As Object
Set vendorDict = CreateObject("Scripting.Dictionary")
vendorDict.CompareMode = vbTextCompare
Dim vendorLastRow As Long
Dim vendorInput As Variant
'Assign the values of the vendor names to array
vendorLastRow = ws.Cells(ws.Rows.Count, vendorCol).End(xlUp).Row
vendorInput = ws.Range(ws.Cells(2, vendorCol), ws.Cells(vendorLastRow, vendorCol)).Value
'Loop through the array and add to dictionary if it's not already in it
Dim n As Long
For n = 1 To UBound(vendorInput, 1)
If Not vendorDict.Exists(vendorInput(n, 1)) Then vendorDict.Add vendorInput(n, 1), 1
Next n
Dim vendorArr As Variant
vendorArr = vendorDict.keys
Set vendorDict = Nothing
Erase vendorInput
'====
'Assign the values of description to array
Dim descLastRow As Long
Dim descArr As Variant
descLastRow = ws.Cells(ws.Rows.Count, descCol).End(xlUp).Row
descArr = ws.Range(ws.Cells(2, descCol), ws.Cells(descLastRow, descCol)).Value
'Create an array of the same size as the description for match result, will be used to write in to the worksheet once at the end
Dim matchArr() As Variant
ReDim matchArr(1 To UBound(descArr, 1), 1 To 1) As Variant
'Loop through the description array and within the loop, check if there's a match in the vendor array
Dim i As Long
For i = 1 To UBound(descArr, 1)
For n = 0 To UBound(vendorArr)
If InStr(1, descArr(i, 1), vendorArr(n), vbTextCompare) <> 0 Then
'If match found, assign the vendor name to the match array
matchArr(i, 1) = vendorArr(n)
Exit For
End If
Next n
'If no match, return NA error
If matchArr(i, 1) = vbNullString Then matchArr(i, 1) = CVErr(xlErrNA)
Next i
ws.Cells(2, matchCol).Resize(UBound(matchArr, 1)).Value = matchArr
Erase descArr
Erase matchArr
End Sub
Compare Two Columns
This is a basic example that loops through column O and compares each value against each value in column AE. Match is no good because the values in AE need to be contained in O. You can always improve efficiency by using arrays as illustrated in Raymond Wu's answer.
On the other hand, you could loop through column AE and use the Find and FindNext methods to find all matches in column O which might also be more efficient.
Option Explicit
Sub MatchVendors()
' s - Source (read from ('vendors'))
' d - Destination (read from ('description') and written to ('match'))
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim sws As Worksheet: Set sws = wb.Worksheets("Sheet1") ' adjust, often...
Dim dws As Worksheet: Set dws = wb.Worksheets("Sheet1") ' ... different
Dim slRow As Long: slRow = sws.Range("AE" & sws.Rows.Count).End(xlUp).Row
If slRow < 2 Then Exit Sub ' no data in source
Dim srg As Range: Set srg = sws.Range("AE2:AE" & slRow)
Dim dlRow As Long: dlRow = dws.Range("O" & dws.Rows.Count).End(xlUp).Row
If dlRow < 2 Then Exit Sub ' no data in destination
Dim drg As Range: Set drg = dws.Range("O2:O" & dlRow)
Application.ScreenUpdating = False
Dim sCell As Range
Dim dCell As Range
Dim IsMatch As Boolean
For Each dCell In drg.Cells
' Read (Search)
For Each sCell In srg.Cells
' Either 'contains'...
If InStr(1, dCell.Value, sCell.Value, vbTextCompare) > 0 Then
' ... or 'begins with':
'If InStr(1, dCell.Value, sCell.Value, vbTextCompare) = 1 Then
IsMatch = True
Exit For
End If
Next sCell
' Write
If IsMatch Then
dCell.EntireRow.Columns("P").Value = sCell.Value
IsMatch = False
Else
dCell.EntireRow.Columns("P").Value = "#N/A"
End If
Next dCell
Application.ScreenUpdating = True
MsgBox "Vendors matched to Descriptions.", vbInformation
End Sub
I'm seeking guidance on how to create a macro that can delete duplicate and original values if the duplicates are located in another worksheet. Here is an example of the workbook:
Sheet1
Class People
cs101 12
cs102 13
cs102 13
Sheet2
People Class
12 cs101
15 cs105
Sheet3
Room People Class
key1 12 cs101
key2 16 cs106
In this dataset, I want to remove rows that have "cs101" in all 3 worksheets, but I don't want to remove "cs102" since the duplicate is listed on the same worksheet.
The code below will help me on deleting duplicates and original values across multiple worksheets, but it will remove "cs102" row as well. Any guidance is appreciated, thank you.
Option Explicit
Sub RemoveDupes()
Const wsNamesList As String = "Sheet1,Sheet2,Sheet3"
Const HeaderTitle As String = "Class"
Const HeaderRow As Long = 1
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim wsNames() As String: wsNames = Split(wsNamesList, ",")
Dim nUpper As Long: nUpper = UBound(wsNames)
Dim cRanges() As Range: ReDim cRanges(0 To nUpper)
Dim cData() As Variant: ReDim cData(0 To nUpper)
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare ' A = a
Dim ws As Worksheet
Dim crg As Range
Dim hCell As Range
Dim n As Long
For n = 0 To nUpper
' Attempt to create a reference to the worksheet ('ws').
Set ws = RefWorksheet(wb, wsNames(n))
If Not ws Is Nothing Then
' Attempt to create a reference to the header cell ('hCell').
Set hCell = RefHeader(ws, HeaderTitle, HeaderRow)
If Not hCell Is Nothing Then
' Attempt to create a reference to the range ('crg').
Set crg = RefColumnRange(hCell.Offset(1))
If Not crg Is Nothing Then
' Store the range in an array ('cRanges').
Set cRanges(n) = crg
' Write the values from the ranges to an array ('cData').
cData(n) = GetColumnRange(crg)
' Write and count the unqiue values from the array
' to a dictionary ('dict').
FirstColumnToDictionaryWithCount dict, cData(n)
End If
End If
End If
Next n
Dim drg() As Range: ReDim drg(0 To nUpper)
Dim r As Long
' Combine all cells containing duplicates (and the originals)
' into one range ('drg()') per worksheet.
For n = 0 To nUpper
If Not cRanges(n) Is Nothing Then
For r = 1 To UBound(cData(n), 1)
If dict(cData(n)(r, 1)) > 1 Then
Set drg(n) = GetCombinedRange(drg(n), cRanges(n).Cells(r))
End If
Next r
End If
Next n
Application.ScreenUpdating = True
' Delete the entire rows of the ranges in one go per worksheet.
For n = 0 To nUpper
If Not drg(n) Is Nothing Then
drg(n).EntireRow.Delete
End If
Next n
Application.ScreenUpdating = False
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: In a workbook ('wb'), creates a reference to the worksheet
' named after a value ('WorksheetName').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefWorksheet( _
ByVal wb As Workbook, _
ByVal WorksheetName As String) _
As Worksheet
If wb Is Nothing Then Exit Function
On Error Resume Next
Set RefWorksheet = wb.Worksheets(WorksheetName)
On Error GoTo 0
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: In a worksheet's ('ws') row ('HeaderRow'), creates a reference
' to the cell containing a value ('Title').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefHeader( _
ByVal ws As Worksheet, _
ByVal Title As String, _
Optional ByVal HeaderRow As Long = 1) _
As Range
If ws Is Nothing Then Exit Function
If HeaderRow < 1 Then Exit Function
If HeaderRow > ws.Rows.Count Then Exit Function
Dim hCell As Range
With ws.Rows(HeaderRow)
Set hCell = .Find(Title, .Cells(.Cells.Count), xlFormulas, xlWhole)
End With
If hCell Is Nothing Then Exit Function
Set RefHeader = hCell
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Creates a reference to a one-column range from a cell
' ('FirstCellRange') to the bottom-most non-empty cell.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefColumnRange( _
ByVal FirstCellRange As Range) _
As Range
If FirstCellRange Is Nothing Then Exit Function
Dim lCell As Range
With FirstCellRange.Cells(1)
Set lCell = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If lCell Is Nothing Then Exit Function
Set RefColumnRange = .Resize(lCell.Row - .Row + 1)
End With
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a one-column range ('ColumnRange')
' in a 2D one-based one-column array.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetColumnRange( _
ByVal ColumnRange As Range) _
As Variant
If ColumnRange Is Nothing Then Exit Function
Dim rCount As Long: rCount = ColumnRange.Rows.Count
Dim cData As Variant
With ColumnRange.Columns(1)
If rCount = 1 Then
ReDim cData(1 To 1, 1 To 1): cData(1, 1) = .Value
Else
cData = .Value
End If
End With
GetColumnRange = cData
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Appends the unique values and their count of the first column
' of a 2D one-based array ('cData') to a dictionary ('dict').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub FirstColumnToDictionaryWithCount( _
ByRef dict As Object, _
ByVal cData As Variant)
If dict Is Nothing Then Exit Sub
If IsEmpty(cData) Then Exit Sub
Dim cValue As Variant
Dim r As Long
For r = 1 To UBound(cData, 1)
cValue = cData(r, 1)
If Not IsError(cValue) Then
If Len(cValue) > 0 Then
dict(cValue) = dict(cValue) + 1
End If
End If
Next r
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Combines two ranges into one range.
' Note that the ranges have to be located in the same worksheet.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetCombinedRange( _
ByVal BuiltRange As Range, _
ByVal AddRange As Range) _
As Range
If BuiltRange Is Nothing Then
Set GetCombinedRange = AddRange
Else
Set GetCombinedRange = Union(BuiltRange, AddRange)
End If
End Function
With all due respect to those who would employ dictionaries and classes, I think this can be resolved with more pedestrian methods. The code below applies the logic that if a "Class ID" from Sheet1 is found in either Sheet2 or Sheet3 (or both) it should be deleted there and in Sheet1.
Option Explicit
Sub RemoveDuplicates()
' 291
' change the tab names to match actual names
' use Chr(124) vertical bar as separator
Const TabNames As String = "Sheet1|Sheet2|Sheet3"
' "Class" is in column A on Sheet1, column B on Sheet2 and column C on Sheet3
Const ClassClm As String = "A,B,C"
Const FirstRow As Long = 2 ' same on all sheets
Dim Ws() As Worksheet
Dim Clm() As Long ' columns in which "Class" is found
Dim Rng() As Range ' used ranges in Ws()
Dim Idx As Integer ' index of Ws(), Rng() and Clm()
Dim Fnd As Range ' found duplicate's cell
Dim Del As Boolean ' deletopn tok place
Dim Tmp As Variant ' helper
Dim R As Long ' loop counter: rows of Ws(1)
Tmp = Split(TabNames, "|")
ReDim Ws(1 To UBound(Tmp) + 1)
ReDim Clm(1 To UBound(Tmp) + 1)
ReDim Rng(1 To UBound(Tmp) + 1)
For Idx = 1 To UBound(Ws)
Set Ws(Idx) = ThisWorkbook.Worksheets(Tmp(Idx - 1))
Clm(Idx) = Columns(Split(ClassClm, ",")(Idx - 1)).Column
Next Idx
With Application
.ScreenUpdating = False
.EnableEvents = False
End With
SetSheetRanges Rng, Ws, Clm
For R = Rng(1).Rows.Count To FirstRow Step -1
For Idx = 2 To UBound(Ws)
Set Fnd = Rng(Idx).Find(Rng(1).Cells(R).Value, _
LookIn:=xlValues, LookAt:=xlWhole)
If Not Fnd Is Nothing Then
With Fnd
.Worksheet.Rows(.Row).Delete
End With
SetSheetRanges Rng, Ws, Clm, Idx
Del = True
End If
Next Idx
If Del Then
Ws(1).Rows(R).Delete
SetSheetRanges Rng, Ws, Clm, 1
Del = False
End If
Next R
With Application
.ScreenUpdating = True
.EnableEvents = True
End With
End Sub
Private Sub SetSheetRanges(Fun() As Range, _
Ws() As Worksheet, _
Clm() As Long, _
Optional ByVal Idx As Integer)
' 291
Dim i As Integer ' index of Ws()
Dim Rl As Long
For i = LBound(Ws) To UBound(Ws)
If (i = Idx) Or (Idx = 0) Then
With Ws(i)
Rl = .Cells(.Rows.Count, Clm(i)).End(xlUp).Row
Set Fun(i) = .Range(.Cells(1, Clm(i)), .Cells(Rl, Clm(i)))
End With
End If
Next i
End Sub
How about a dictionary of class ID's whose values are the number of sheets on which the class exists, and another dictionary that tests if the class has already been counted for any worksheet? Something like:
Dim oWS As Worksheet
Dim oClassRange As Range
Dim oClass As Range
Dim oClasses As Object: Set oClasses = CreateObject("Scripting.Dictionary")
Dim oWSClass As Object
For Each oWS in ThisWorkbook
'start with a blank dictionary for each worksheet
Set oWSClass = CreateObject("Scripting.Dictionary")
Set oClassRange = {define the range with class ID's}
For Each oClass In oClassRange
sID = oClass.Value2
If Not oWSClass.Exists(sID) Then 'only add or increment if it's the first time class appears on this worksheet
oWSClass.Add sID, 1
If oClasses.Exists(sID) Then oClasses(sID) = oClasses(sID) + 1 _
Else oClasses.Add sID, 1
End If
Next oClassRange
Next oWS
Then iterate the oClasses dictionary entries, and any value of 3 triggers deleting the rows from each worksheet.
I'm a beginner on VBA and I'm seeking advice on how to create a macro that can remove duplicate values (by deleting rows) including the original values itself on multiple sheets. In addition, the macro can search for the specific value even though the specific value is listed on different columns. Here is an example of the sheets:
Sheet1
Class People
cs101 12
cs102 13
Sheet2
People Class
12 cs101
15 cs105
Sheet3
Room People Class
key1 12 cs101
key2 16 cs106
In this dataset, I want to remove rows that have identical class codes (cs101) in multiple sheets.
Here is the macro that I have created:
Sub Remove_Duplicates()
Dim Rng As Range
Set Rng = Range("A1:A1048576")
Rng.RemoveDuplicates Columns:=1, Header:=xlYes
End Sub
The issues in this macro are:
This macro only works on 1 sheet
This macro removes the duplicates, but not the original value
This macro can only be used on values that are listed in column A
Thank you for your time!
Remove Duplicates and Originals in Multiple Worksheets
Option Explicit
Sub RemoveDupes()
Const wsNamesList As String = "Sheet1,Sheet2,Sheet3"
Const HeaderTitle As String = "Class"
Const HeaderRow As Long = 1
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim wsNames() As String: wsNames = Split(wsNamesList, ",")
Dim nUpper As Long: nUpper = UBound(wsNames)
Dim cRanges() As Range: ReDim cRanges(0 To nUpper)
Dim cData() As Variant: ReDim cData(0 To nUpper)
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare ' A = a
Dim ws As Worksheet
Dim crg As Range
Dim hCell As Range
Dim n As Long
For n = 0 To nUpper
' Attempt to create a reference to the worksheet ('ws').
Set ws = RefWorksheet(wb, wsNames(n))
If Not ws Is Nothing Then
' Attempt to create a reference to the header cell ('hCell').
Set hCell = RefHeader(ws, HeaderTitle, HeaderRow)
If Not hCell Is Nothing Then
' Attempt to create a reference to the range ('crg').
Set crg = RefColumnRange(hCell.Offset(1))
If Not crg Is Nothing Then
' Store the range in an array ('cRanges').
Set cRanges(n) = crg
' Write the values from the ranges to an array ('cData').
cData(n) = GetColumnRange(crg)
' Write and count the unqiue values from the array
' to a dictionary ('dict').
FirstColumnToDictionaryWithCount dict, cData(n)
End If
End If
End If
Next n
Dim drg() As Range: ReDim drg(0 To nUpper)
Dim r As Long
' Combine all cells containing duplicates (and the originals)
' into one range ('drg()') per worksheet.
For n = 0 To nUpper
If Not cRanges(n) Is Nothing Then
For r = 1 To UBound(cData(n), 1)
If dict(cData(n)(r, 1)) > 1 Then
Set drg(n) = GetCombinedRange(drg(n), cRanges(n).Cells(r))
End If
Next r
End If
Next n
Application.ScreenUpdating = True
' Delete the entire rows of the ranges in one go per worksheet.
For n = 0 To nUpper
If Not drg(n) Is Nothing Then
drg(n).EntireRow.Delete
End If
Next n
Application.ScreenUpdating = False
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: In a workbook ('wb'), creates a reference to the worksheet
' named after a value ('WorksheetName').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefWorksheet( _
ByVal wb As Workbook, _
ByVal WorksheetName As String) _
As Worksheet
If wb Is Nothing Then Exit Function
On Error Resume Next
Set RefWorksheet = wb.Worksheets(WorksheetName)
On Error GoTo 0
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: In a worksheet's ('ws') row ('HeaderRow'), creates a reference
' to the cell containing a value ('Title').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefHeader( _
ByVal ws As Worksheet, _
ByVal Title As String, _
Optional ByVal HeaderRow As Long = 1) _
As Range
If ws Is Nothing Then Exit Function
If HeaderRow < 1 Then Exit Function
If HeaderRow > ws.Rows.Count Then Exit Function
Dim hCell As Range
With ws.Rows(HeaderRow)
Set hCell = .Find(Title, .Cells(.Cells.Count), xlFormulas, xlWhole)
End With
If hCell Is Nothing Then Exit Function
Set RefHeader = hCell
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Creates a reference to a one-column range from a cell
' ('FirstCellRange') to the bottom-most non-empty cell.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefColumnRange( _
ByVal FirstCellRange As Range) _
As Range
If FirstCellRange Is Nothing Then Exit Function
Dim lCell As Range
With FirstCellRange.Cells(1)
Set lCell = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If lCell Is Nothing Then Exit Function
Set RefColumnRange = .Resize(lCell.Row - .Row + 1)
End With
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a one-column range ('ColumnRange')
' in a 2D one-based one-column array.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetColumnRange( _
ByVal ColumnRange As Range) _
As Variant
If ColumnRange Is Nothing Then Exit Function
Dim rCount As Long: rCount = ColumnRange.Rows.Count
Dim cData As Variant
With ColumnRange.Columns(1)
If rCount = 1 Then
ReDim cData(1 To 1, 1 To 1): cData(1, 1) = .Value
Else
cData = .Value
End If
End With
GetColumnRange = cData
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Appends the unique values and their count of the first column
' of a 2D one-based array ('cData') to a dictionary ('dict').
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub FirstColumnToDictionaryWithCount( _
ByRef dict As Object, _
ByVal cData As Variant)
If dict Is Nothing Then Exit Sub
If IsEmpty(cData) Then Exit Sub
Dim cValue As Variant
Dim r As Long
For r = 1 To UBound(cData, 1)
cValue = cData(r, 1)
If Not IsError(cValue) Then
If Len(cValue) > 0 Then
dict(cValue) = dict(cValue) + 1
End If
End If
Next r
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Combines two ranges into one range.
' Note that the ranges have to be located in the same worksheet.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetCombinedRange( _
ByVal BuiltRange As Range, _
ByVal AddRange As Range) _
As Range
If BuiltRange Is Nothing Then
Set GetCombinedRange = AddRange
Else
Set GetCombinedRange = Union(BuiltRange, AddRange)
End If
End Function
Option Explicit
Sub DeleteDuplRows()
'https://stackoverflow.com/questions/68342367/ _
how-to-delete-duplicate-values-and-original-values-across-multiple-sheets-in-exc
Dim wb As Workbook, wShs As Object
Dim i As Long, Col As Range, ColRange() As Range, ColRangeStr As String
Dim ColRangeStrArr() As String, str As Variant, Cl As Range, ShtDelRange As Range
Dim ClassNames As Object
Set ClassNames = CreateObject("Scripting.Dictionary")
Dim DuplClassNames As Object
Set DuplClassNames = CreateObject("Scripting.Dictionary")
Set wb = ThisWorkbook
Set wShs = wb.Worksheets
ReDim ColRange(1 To wShs.Count)
'Make ColRange array of ranges for "Class" column from each sheet
For i = 1 To wShs.Count
Set Col = wShs(i).Rows(1).Find("Class")
If Not Col Is Nothing Then
Set ColRange(i) = wShs(i).Range(wShs(i).Cells(2, Col.Column), _
wShs(i).Cells(wShs(i).UsedRange.Rows.Count, Col.Column))
End If
Next i
'Make ColRangeStr string of values of cells from each range element of the above ColRange array,
ColRangeStr = ""
For i = LBound(ColRange) To UBound(ColRange)
ColRangeStr = ColRangeStr & "," & Join(Application.Transpose(Application.Index( _
ColRange(i), 0, 1)), ",")
Next i
ColRangeStr = Right(ColRangeStr, Len(ColRangeStr) - 1)
'Split the ColRangeStr string into ColRangeStrArr array
ColRangeStrArr = Split(ColRangeStr, ",")
'Make DuplClassNames dictionary of duplicate values from the above ColRangeStrArr array
With ClassNames
.CompareMode = TextCompare
For Each str In ColRangeStrArr
If Not Len(str) = 0 Then
If Not .Exists(str) Then
.Add str, Nothing
Else
If Not DuplClassNames.Exists(str) Then DuplClassNames.Add str, Nothing
End If
End If
Next str
End With
'Make ShtDelRange union of DUPLICATE cells from "class" column of each sheet if the cell value _
exists in DuplClassNames dictionary.
For i = LBound(ColRange) To UBound(ColRange)
Set ShtDelRange = Nothing
For Each Cl In ColRange(i)
If DuplClassNames.Exists(Cl.Value) Then
If ShtDelRange Is Nothing Then
Set ShtDelRange = Cl
Else
Set ShtDelRange = Union(ShtDelRange, Cl)
End If
End If
Next Cl
'Delete entire rows of the Sheets(i) if the cells are in ShtDelRange
If Not ShtDelRange Is Nothing Then ShtDelRange.EntireRow.Delete
Next i
End Sub
I have master Data in Sheet 2 (Column B) and search criteria in Sheet 1 (Column A), i want VBA to find all the data from Sheet 1 (Column A) in Sheet 2 (Column B) if found cut the entire row and past it into Sheet 3 next available row.
Sub remDup()
Dim LR As Long, LRSheet2 As Long, i As Long, a As Long
Dim vAllSheet2Values() As Variant
LRSheet2 = Worksheets("Sheet2").Cells(Rows.Count, 1).End(xlUp).Row
LR = Worksheets("Sheet1").Cells(Rows.Count, 1).End(xlUp).Row
a = 1
For i = 1 To LRSheet2 'Load all values in ColumnA of Sheet2 into an array
ReDim Preserve vAllSheet2Values(i)
vAllSheet2Values(i) = Worksheets("Sheet2").Cells(i, 2).Value
Next i
For i = LR To 1 Step -1
If IsInArray(Worksheets("Sheet1").Cells(i, 1).Value, vAllSheet2Values) Then
Worksheets("Sheet1").Rows(i).Copy Worksheets("Sheet3").Rows(a)
Worksheets("Sheet1").Rows(i).Delete
a = a + 1
End If
Next i
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
in above code data is getting deleted from sheet 1 :( and not sheet 2
Backup Matching Rows
In the current setup, the code will search for all values in column A of Sheet1 in column B of Sheet2. The cells of each found value will be combined into a Total Range whose entire rows will be copied to Sheet3 (in one go) and then removed (deleted) from Sheet1 (in another go).
The Code
Option Explicit
Sub remDup()
' Constants
Const sName As String = "Sheet1"
Const sFirst As String = "A1"
Const lName As String = "Sheet2"
Const lFirst As String = "B1"
Const dName As String = "Sheet3"
Const dFirst As String = "A1"
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Source
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim srg As Range: Set srg = refColumn(sws.Range(sFirst))
If srg Is Nothing Then Exit Sub
Dim sData As Variant: sData = getColumn(srg)
' Lookup
Dim lws As Worksheet: Set lws = wb.Worksheets(lName)
Dim lrg As Range: Set lrg = refColumn(lws.Range(lFirst))
If lrg Is Nothing Then Exit Sub
Dim lData As Variant: lData = getColumn(lrg)
' Match
Dim trg As Range
Dim i As Long
For i = 1 To UBound(sData)
If foundMatchInVector(sData(i, 1), lData) Then
Set trg = getCombinedRange(trg, srg.Cells(i))
End If
Next i
' Destination
If Not trg Is Nothing Then
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
' This is a kind of a ridiculous use of "refColumn".
Dim drg As Range: Set drg = refColumn(dws.Range(dFirst))
If drg Is Nothing Then
Set drg = dws.Range(dFirst).EntireRow
Else
Set drg = drg.Cells(drg.Cells.Count).Offset(1).EntireRow
End If
trg.EntireRow.Copy drg
trg.EntireRow.Delete
End If
End Sub
' Assumptions: 'FirstCellRange' is a one-cell range e.g. 'Range("A1")'.
' Returns: Either the range from 'FirstCellRange' to the bottom-most
' non-empty cell in the column, or 'Nothing' if all cells
' below 'FirstCellRange' (incl.) are empty.
Function refColumn( _
ByVal FirstCellRange As Range) _
As Range
With FirstCellRange
Dim cel As Range
Set cel = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If Not cel Is Nothing Then
Set refColumn = .Resize(cel.Row - .Row + 1)
End If
End With
End Function
' Assumptions: 'rg' is a one-column range e.g. 'Range("A1")', 'Range("A1:A2")'.
' Returns: A 2D one-based one-column array.
Function getColumn( _
rg As Range) _
As Variant
If rg.Rows.Count > 1 Then
getColumn = rg.Value
Else
Dim OneElement As Variant: ReDim OneElement(1 To 1, 1 To 1)
OneElement(1, 1) = rg.Value
getColumn = OneElement
End If
End Function
' Assumptions: 'MatchValue' is a simple data type (not an object or an array).
' 'Vector' is a structure that 'Application.Match' can handle,
' e.g. a 1D array, a one-column or one-row range or 2D array.
' Returns: 'True' or 'False' (boolean).
' Remarks: Error values and blanks are ignored ('False').
Function foundMatchInVector( _
ByVal MatchValue As Variant, _
ByVal Vector As Variant) _
As Boolean
If Not IsError(MatchValue) Then
If Len(MatchValue) > 0 Then
foundMatchInVector _
= IsNumeric(Application.Match(MatchValue, Vector, 0))
End If
End If
End Function
' Assumptions: 'AddRange' is not 'Nothing' and it is in the same worksheet
' as 'BuiltRange'.
' Returns: A range (object).
Function getCombinedRange( _
ByVal BuiltRange As Range, _
ByVal AddRange As Range)
If BuiltRange Is Nothing Then
Set getCombinedRange = AddRange
Else
Set getCombinedRange = Union(BuiltRange, AddRange)
End If
End Function
If I'm understanding correctly this should do it. I put comments on the changed lines
Sub remDup()
Dim LR As Long, LRSheet2 As Long, i As Long, a As Long
Dim vAllSheet1Values() As Variant 'This should be referencing sheet 1 not 2
LRSheet2 = Worksheets("Sheet2").Cells(Rows.Count, 1).End(xlUp).Row
LR = Worksheets("Sheet1").Cells(Rows.Count, 1).End(xlUp).Row
a = 1
ReDim Preserve vAllSheet1Values(LR) 'No need for this to be in a loop
For i = 1 To LR 'Load all values in ColumnA of Sheet1 into an array
vAllSheet1Values(i) = Worksheets("Sheet1").Cells(i, 1).Value 'This should be sheet1
Next i
For i = LRSheet2 To 1 Step -1 'This and all sheet1 references after should be sheet 2
If IsInArray(Worksheets("Sheet2").Cells(i, 1).Value, vAllSheet1Values) Then
Worksheets("Sheet2").Rows(i).Copy Worksheets("Sheet3").Rows(a)
Worksheets("Sheet2").Rows(i).Delete
a = a + 1
End If
Next i
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
Please, try the next code:
Sub remDup()
Dim LR As Long, LRSheet2 As Long, arr, i As Long, rngCopy As Range, rngDel As Range
Dim sh1 As Worksheet, sh2 As Worksheet, sh3 As Worksheet, a As Long
Set sh1 = Worksheets("Sheet1")
Set sh2 = Worksheets("Sheet2")
Set sh3 = Worksheets("Sheet3")
LRSheet2 = sh2.cells(Rows.count, 2).End(xlUp).row
LR = sh1.cells(Rows.count, 1).End(xlUp).row
a = 1 'The Sheet3 row where the rows to be copied
arr = sh2.Range("B1:B" & LRSheet2).Value 'put the range in a 2D array
arr = Application.Transpose(Application.Index(arr, 0, 1)) 'obtain 1D array
For i = 1 To LR
If IsInArray(sh1.cells(i, 1).Value, arr) Then
If rngCopy Is Nothing Then
Set rngCopy = sh1.cells(i, 1) 'create a range to be copied/deleted
Else
Set rngCopy = Union(rngCopy, sh1.cells(i, 1))
End If
End If
Next i
rngCopy.EntireRow.Copy sh3.Range("A" & a) 'copy the range entirerow at once
rngCopy.EntireRow.Delete 'delete the range entirerow
End Sub
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = UBound(Filter(arr, stringToBeFound)) > -1
End Function