VBA Finding and reading multiple ranges - vba

I'm trying to automate a file we use regularly at work. It's more or less a quote with numbers and descriptions of tasks, organized into different groups.
Usually we enter the data manually, then we have to create a Powerpoint presentation and copy-paste a lot of infos. I'm trying to automate the process so we can click on a button -> creates our Powerpoint with all the data in it.
I'm having trouble on one part : My macro currently retrieves the group names and creates slides for each group and puts the title in it. I'd like to retrieve the values of the cells of each group to paste them into the slide also. But I can't find a solution to do it... doesn't seem simple.
Range().Value can't read more than one cell. I tried setting ranges variables and retrieving the cells but no luck so far.
A lot is going on but I'll try to be as clear as possible. I'm a beginner in VBA and used a function I found online to search for our group names. I'll try to describe the code as best as I can, not everything will be relevant to the question but I guess context will help.
First the sub to find the group names that all start with the keyword "Lot" :
Public FoundCells As Range
Public FoundCell As Range
Public NomsLots As String
Sub FindLots()
Dim SearchRange As Range
Dim FindWhat As Variant
NomsLots = ""
Set SearchRange = Range("C1:C500") 'Where to search
FindWhat = "Lot" 'Value to look for
Set FoundCells = FindAll(SearchRange:=SearchRange, _
FindWhat:=FindWhat, _
LookIn:=xlValues, _
LookAt:=xlPart, _
SearchOrder:=xlByColumns, _
MatchCase:=True, _
BeginsWith:=vbNullString, _
EndsWith:=vbNullString, _
BeginEndCompare:=vbTextCompare) 'LookIn indicates whether to look in cell values, cell formulas, or cell comments.
'The default is cell values. LookAt indicates whether to look at the entire cell (a match occurs only if the entire content of the cell matches FindWhat). The default is match entire cell.
'SearchOrder indicates whether the search should proceed row-by-row or column-by-column. The default is row-by-row.
'MatchCase indicates whether the text match is case sensitive (MatchCase = True or case insensitive (MatchCase = False). The default if False.
'BeginsWith is a string that indicates that a cell will match only if it begins with the string specified in BeginsWith.
'EndsWith is a string that indicates that a cell will match only if it ends with the string in EndsWith. The comparisons carried out against BeginsWith and EndsWith are case sensitive if BeginEndCompare is vbBinaryCompare. If BeginEndCompare is vbTextCompare, the comparison is case-insensitive. The default is vbTextCompare. If both BeginsWith and EndsWith are empty string, no tests of the cell content are performed. If either or both BeginsWith or EndsWith are not empty strings, the LookAt parameter is automatically changed to xlPart.
If FoundCells Is Nothing Then
Debug.Print "Value Not Found"
Found = False
Else
For Each FoundCell In FoundCells
NomsLots = NomsLots & FoundCell.Value & Chr(10)
Debug.Print "Value Found In Cell: " & FoundCell.Value & " in : " & FoundCell.Address
Next FoundCell
End If
NomsLots = Left(NomsLots, Len(NomsLots) - 1)
End Sub
I use it to retrieve the FoundCell.Value to get the group names contained in C1:C500. For example, "Group 1" is in C6, "Group 2" is in C13, etc. I take the values, but I can also retrieve the adresses with FoundCell.address.
I tried to retrieve FoundCell.Address and put them in a range variable but it throws an error, the formatting must be incorrect. What I wanted to do was get the different adresses, and extrapolate.
Exemple : if we have "Group 1" in C6 and "Group 2" in C13, the content of Group 1 I'm looking for is contained in cells C7 through C12. I tried offsetting the first FoundCell.Address and the next, but I couldn't make it work.
Here is the function called, btw :
Function FindAll(SearchRange As Range, _
FindWhat As Variant, _
Optional LookIn As XlFindLookIn = xlValues, _
Optional LookAt As XlLookAt = xlWhole, _
Optional SearchOrder As XlSearchOrder = xlByRows, _
Optional MatchCase As Boolean = False, _
Optional BeginsWith As String = vbNullString, _
Optional EndsWith As String = vbNullString, _
Optional BeginEndCompare As VbCompareMethod = vbTextCompare) As Range
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' FindAll
' This searches the range specified by SearchRange and returns a Range object
' that contains all the cells in which FindWhat was found. The search parameters to
' this function have the same meaning and effect as they do with the
' Range.Find method. If the value was not found, the function return Nothing. If
' BeginsWith is not an empty string, only those cells that begin with BeginWith
' are included in the result. If EndsWith is not an empty string, only those cells
' that end with EndsWith are included in the result. Note that if a cell contains
' a single word that matches either BeginsWith or EndsWith, it is included in the
' result. If BeginsWith or EndsWith is not an empty string, the LookAt parameter
' is automatically changed to xlPart. The tests for BeginsWith and EndsWith may be
' case-sensitive by setting BeginEndCompare to vbBinaryCompare. For case-insensitive
' comparisons, set BeginEndCompare to vbTextCompare. If this parameter is omitted,
' it defaults to vbTextCompare. The comparisons for BeginsWith and EndsWith are
' in an OR relationship. That is, if both BeginsWith and EndsWith are provided,
' a match if found if the text begins with BeginsWith OR the text ends with EndsWith.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim FoundCell As Range
Dim FirstFound As Range
Dim LastCell As Range
Dim ResultRange As Range
Dim XLookAt As XlLookAt
Dim Include As Boolean
Dim CompMode As VbCompareMethod
Dim Area As Range
Dim MaxRow As Long
Dim MaxCol As Long
Dim BeginB As Boolean
Dim EndB As Boolean
CompMode = BeginEndCompare
If BeginsWith <> vbNullString Or EndsWith <> vbNullString Then
XLookAt = xlPart
Else
XLookAt = LookAt
End If
' this loop in Areas is to find the last cell
' of all the areas. That is, the cell whose row
' and column are greater than or equal to any cell
' in any Area.
For Each Area In SearchRange.Areas
With Area
If .Cells(.Cells.Count).Row > MaxRow Then
MaxRow = .Cells(.Cells.Count).Row
End If
If .Cells(.Cells.Count).Column > MaxCol Then
MaxCol = .Cells(.Cells.Count).Column
End If
End With
Next Area
Set LastCell = SearchRange.Worksheet.Cells(MaxRow, MaxCol)
On Error GoTo 0
Set FoundCell = SearchRange.Find(what:=FindWhat, _
after:=LastCell, _
LookIn:=LookIn, _
LookAt:=XLookAt, _
SearchOrder:=SearchOrder, _
MatchCase:=MatchCase)
If Not FoundCell Is Nothing Then
Set FirstFound = FoundCell
Do Until False ' Loop forever. We'll "Exit Do" when necessary.
Include = False
If BeginsWith = vbNullString And EndsWith = vbNullString Then
Include = True
Else
If BeginsWith <> vbNullString Then
If StrComp(Left(FoundCell.Text, Len(BeginsWith)), BeginsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
If EndsWith <> vbNullString Then
If StrComp(Right(FoundCell.Text, Len(EndsWith)), EndsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
End If
If Include = True Then
If ResultRange Is Nothing Then
Set ResultRange = FoundCell
Else
Set ResultRange = Application.Union(ResultRange, FoundCell)
End If
End If
Set FoundCell = SearchRange.FindNext(after:=FoundCell)
If (FoundCell Is Nothing) Then
Exit Do
End If
If (FoundCell.Address = FirstFound.Address) Then
Exit Do
End If
Loop
End If
Set FindAll = ResultRange
End Function
And my code to create the powerpoint and insert slides, etc. (edited to keep the relevant only, hope I didn't break/forget anything :)) The variable Contentofthegroup doesn't exist, it's just a placeholder to understand where I want to put the data (content of the group) in the slide.
Private Sub CommandButton1_Click()
Dim PptApp As PowerPoint.Application
Dim PptDoc As PowerPoint.Presentation
Dim Diapo As PowerPoint.Slide
Dim PPTtable As PowerPoint.Table
Dim Sh As PowerPoint.Shape
Dim Cs1 As ColorScheme
Dim NbShpe As Integer
Set PptApp = CreateObject("Powerpoint.Application")
Set PptDoc = PptApp.Presentations.Open(ThisWorkbook.Path & "\" & "Powerpointpresentation.pptx")
With PptDoc
'Slide 4
'Insert Titles on a summary page
Set Sh = .Slides(4).Shapes("ShapenameTitle")
FindLots
Sh.TextFrame.TextRange.Text = "Quote for the following actions :" & Chr(13) & NomsLots
Sh.TextFrame.TextRange.Paragraphs(2).IndentLevel = 2
'Creation Slides for each group
Dim MyAr
Dim i As Long 'index of groups
Dim j As Long
Dim pptLayout As CustomLayout
j = 7
MyAr = Split(NomsLots, Chr(10))
For i = LBound(MyAr) To UBound(MyAr)
.Slides.Add Index:=j, Layout:=ppLayoutText
Set Sh = .Slides(j).Shapes("ContentShape")
Sh.TextFrame.TextRange.Text = MyAr(i) & vbCrLf & Contentofthegroup
Sh.TextFrame.TextRange.Paragraphs(2).IndentLevel = 2
j = j + 1
Next
End With
MsgBox "Done"
End Sub
So does anybody have an idea how I should proceed to achieve the desired result ? I'm not sure it is clear enough but I tried to be as thorough as possible.
Thanks.

If I understand what you're trying to accomplish, then what you need to do is to form a Range that defines the group data. In order to capture this, you need to compare the first "found" cell to the next "found" cell. The trick comes when you form the last data group.
Building off your code, I came up with this example to illustrate:
Sub FindLots()
Dim SearchRange As Range
Dim FindWhat As Variant
NomsLots = ""
Set SearchRange = Range("C1:C500") 'Where to search
FindWhat = "Lot" 'Value to look for
Set FoundCells = FindAll(SearchRange:=SearchRange, _
FindWhat:=FindWhat, _
LookIn:=xlValues, _
LookAt:=xlPart, _
SearchOrder:=xlByColumns, _
MatchCase:=True, _
BeginsWith:=vbNullString, _
EndsWith:=vbNullString, _
BeginEndCompare:=vbTextCompare)
If FoundCells Is Nothing Then
Debug.Print "Value Not Found"
'Found = False
Else
Dim group As Range
For Each FoundCell In FoundCells
NomsLots = NomsLots & FoundCell.Value & Chr(10)
Debug.Print "Value Found In Cell: " & FoundCell.Value & " in : " & FoundCell.Address
If group Is Nothing Then
'--- first time through the loop, so capture the
' start of the group
Set group = FoundCell
Else
'--- all other times through the loop (except the last)
' we find the beginning of the next group and, logically,
' the end of the previous group
Set group = group.Offset(1, 0).Resize(FoundCell.Row - group.Row - 1, 1)
DoSomethingWithThisGroupData group
'--- we're done with the data, so set the start of the next group
Set group = FoundCell
End If
Next FoundCell
'--- now process the last group, so we have to determine where the
' end of the group data is
Dim lastRow As Long
lastRow = Cells(Cells.Rows.Count, "C").End(xlUp).Row
Set group = group.Offset(1, 0).Resize(lastRow - group.Row, 1)
DoSomethingWithThisGroupData group
End If
NomsLots = Left(NomsLots, Len(NomsLots) - 1)
End Sub
Sub DoSomethingWithThisGroupData(ByRef dataGroup As Range)
'--- something cool happens here
Debug.Print "Range of group data = " & dataGroup.Address
End Sub

Related

Search Column Header Label Values

Is it possible to search row 1 (headers) for a value defined by a table from another sheet? I need "FName" to be a column or range of values as opposed to a single cell.
Here is a sample of what I was able to get working so far:
FName = Workbooks("IntChk.xlsm").Worksheets("Data").Range("B3")
Set rngFound = Worksheets("File").Rows(1).Find(What:=FName, LookIn:=xlValues, LookAt:=xlWhole, _
SearchOrder:=xlByColumns, SearchDirection:=xlNext, MatchCase:=False)
After identifying the search term from another workbook, you want to locate one or more occurrences in row 1 of this workbook (...?) and record the columns that correspond to the match(es).
Option Explicit
Sub get_em_all()
Dim fName As String, addr As String
Dim rng As Range, fnd As Range
'get search criteria
fName = Workbooks("IntChk.xlsm").Worksheets("Data").Range("B3")
With ThisWorkbook '<~~ different from IntChk.xlsm...?
With .Worksheets("File").Rows(1)
'perform first search
Set fnd = .Rows(1).Find(What:=fName, MatchCase:=False, _
LookIn:=xlValues, LookAt:=xlWhole)
'was anything found
If Not fnd Is Nothing Then
'record the first find
Set rng = fnd
addr = rng.Address
'loop and collect results until we arrive at the first find
Do
Set rng = Union(rng, fnd)
Set fnd = .FindNext(after:=fnd)
Loop Until addr = fnd.Address
'expand the found cells from the first row to the columns within the current region
With .Parent.Cells(1, 1).CurrentRegion
Set rng = Intersect(rng.EntireColumn, .Cells)
End With
'report the address(es) of the cell(s) found
Debug.Print rng.Address(0, 0)
Else
Debug.Print 'nothing found"
End If
End With
End With
End Sub
edited to correct some "optimization" typos
I think you want to select from a "headers" row all the cells whose value is on another range
If that's your goal you could try the following
Option Explicit
Function GetRange(fnameRng As Range, dataRng As Range) As Range
Dim fName As String
'get search criteria
fName = GetJoinFromRange(fnameRng)
With dataRng
.Rows(1).Insert
With .Offset(-1).Resize(1)
.FormulaR1C1 = "=if(isnumber(search(""-"" & R2C & ""-"" ,""" & fName & """)),1,"""")"
.Value = .Value
Set GetRange = .SpecialCells(xlCellTypeConstants)).Offset(1)
End With
.Rows(1).Offset(-1).EntireRow.Delete
End With
End Function
Function GetJoinFromRange(rng As Range) As String
If rng.Rows.Count > 1 Then
GetJoinFromRange = "-" & Join(Application.Transpose(rng), "-") & "-"
Else
GetJoinFromRange = "-" & Join(rng, "-") & "-"
End If
End Function
that can be called by a "main" sub like follows
Option Explicit
Sub main()
Dim fnameRng As Range, dataRng As Range, rngFound As Range
Set fnameRng = Workbooks("IntChk.xlsm").Worksheets("Data").Range("B3:B6") '<== adapt it to your needs
Set dataRng = ThisWorkbook.Worksheets("File").Range("B1:I1000") '<== adapt it to your needs
Set rngFound = GetRange(fnameRng, dataRng)
End Sub
after a week of trial and error, I was able to create this code. it works well and its light.
sub IntChk
Dim i As Integer
Lastcol = 5
For i = 1 To 1
For j = 1 To Lastcol
MsgBox "Cell Value = " & Cells(j) & vbNewLine & "Column Number = " & j
For Each c In Workbooks("IntChk.xlsm").Worksheets("Data").Range("A1:A50")
If c.Value = Cells(j) Then
MsgBox "Match"
Match = "True"
End If
Next c
Next j
If Match = "True" Then
MsgBox "Yes, True!"
Else:
MsgBox "not true ;("
End If
Next I
end sub

Excel Macro, Find All Duplicates in Column and see coorisponding value

Excel Macro that will do the following:
To Find All Duplicates in (ColumnA) and to see if (ColumnB) contains a certain value and run a code against that result.
How i would write the code if i could:
If (ColumnB) .value in that (group of duplicates_found) in any row is "R-".value then
Keep the row with "R-".value and delete the rest. Else if "R-".value not exist and "M-".value Exist, delete all duplicates except first "R-".value found.
Else
If duplicate group contains "R-".value more than once, keep first "R-".value row found and delete the rest
Endif
Continue to loop until all duplicates found and run through above code.
^^sorry if not making sense up there:
I guess we can select first group of duplicates and run check on it like described below.^^
in this group all would be deleted, except one row.
(in this group we could specify to keep first "R-".value found and delete rest)
(this group has a "R-".value so the "M-".value gets deleted.)
(this group has a "R-".value so the "M-".value gets deleted.)
Code I used once to delete all "M-".value(s), hoping to reverse to do above as described per a first group found and to continue:
Sub DeleteRowWithContents()
Dim rFnd As Range, dRng As Range, rFst As String, myList, ArrCnt As Long
myList = Array("M-")
For ArrCnt = LBound(myList) To UBound(myList)
With Range("B1:B" & Range("B" & Rows.Count).End(xlUp).Row)
Set rFnd = .Find(What:=myList(ArrCnt), _
LookIn:=xlValues, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=True)
If Not rFnd Is Nothing Then
rFst = rFnd.Address
Do
If dRng Is Nothing Then
Set dRng = Range("A" & rFnd.Row)
Else
Set dRng = Union(dRng, Range("A" & rFnd.Row))
End If
Set rFnd = .FindNext(After:=rFnd)
Loop Until rFnd.Address = rFst
End If
Set rFnd = Nothing
End With
Next ArrCnt
If Not dRng Is Nothing Then dRng.EntireRow.Delete
End Sub
this code goes through column and finds duplicates and highlights them. Maybe this could be rewritten to highlight each duplicate a separate color?
Sub MarkDuplicates()
Dim iWarnColor As Integer
Dim rng As Range
Dim rngCell As Variant
Range(Range("A2"), Range("A2").End(xlDown)).Select ' area to check '
Set rng = Selection
iWarnColor = xlThemeColorAccent2
For Each rngCell In rng.Cells
vVal = rngCell.Text
If (WorksheetFunction.CountIf(rng, vVal) = 1) Then
rngCell.Interior.Pattern = xlNone
Else
rngCell.Interior.ColorIndex = iWarnColor
End If
Next rngCell
End Sub
this code Looks for colored cells a specific RGB color and selects them, maybe for each group that is colored differently select that color and do a function on it?
Sub SelectColoredCells()
Dim rCell As Range
Dim lColor As Long
Dim rColored As Range
'Select the color by name (8 possible)
'vbBlack, vbBlue, vbGreen, vbCyan,
'vbRed, vbMagenta, vbYellow, vbWhite
lColor = RGB(156, 0, 6)
'If you prefer, you can use the RGB function
'to specify a color
'Default was lColor = vbBlue
'lColor = RGB(0, 0, 255)
Set rColored = Nothing
For Each rCell In Selection
If rCell.Interior.Color = lColor Then
If rColored Is Nothing Then
Set rColored = rCell
Else
Set rColored = Union(rColored, rCell)
End If
End If
Next
If rColored Is Nothing Then
MsgBox "No cells match the color"
Else
rColored.Select
MsgBox "Selected cells match the color:" & _
vbCrLf & rColored.Address
End If
Set rCell = Nothing
Set rColored = Nothing
End Sub
This has had me tied down to the computer for a week now and i cant seem to resolve it.
Here's an answer, it's a complicated one, but I took the question as a challenge to improve my use of particular methods in VBA.
This goes through your cells and creates an array of the results as you like.
I was using numbers in my testing, so every time you see str(Key) you might just need to remove the str() function.
This results in printing the array to columns D:E rather than removing rows from your list. You could just clear columns A:B and then print to "A1:B" & dict.Count - that would have the same effect, essentially.
Sub test()
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim lastrow As Integer
lastrow = Cells(Rows.Count, "A").End(xlUp).Row
Dim strA As String
For i = 1 To lastrow
strA = Cells(i, 1)
dict(strA) = 1
Next
Dim vResult() As Variant
ReDim vResult(dict.Count - 1, 1)
Dim x As Integer
x = 0
Dim strB As String
Dim strKey As String
For Each Key In dict.keys
vResult(x, 0) = Key
x = x + 1
For Each c In Range("A1:A" & lastrow)
strA = Str(c)
strB = c.Offset(0, 1).Value
If strA = Str(Key) Then
If Left(strB, 1) = "r" Then
vResult(x - 1, 1) = c.Offset(, 1)
GoTo label
End If
End If
Next
If vResult(x - 1, 1) = Empty Then
For Each c In Range("A1:A" & lastrow)
strA = Str(c)
If strA = Str(Key) Then
vResult(x - 1, 1) = c.Offset(, 1)
GoTo label
End If
Next
End If
label:
Next
Range("D1:E" & dict.Count).Value = vResult()
End Sub

Finding repetitions of values and changing the value of some other cell based on the result of search in VBA excel

I have an Excel file which I want to write a VBA code for. I want to check the values in a specific column and if some value has more than one occurrences, the value of all related rows in some other column will be summed up and set for themselves.
Let me bring you an example. I have a worksheet like this:
I check column "C". There are 3 occurrences of 0 in rows 1, 4 and 6. I sum up the value of "B1", "B4" and "B6", which will be 444 + 43434 + 43434 = 87312 and set this summation for the same columns, i.e. all "B1", "B4" and "B6" cells will have the value 87312.
I have found a code for finding all occurrences of some value and with some change it fits my problem; but I can't find related cells on the other column. This is the code I use:
Sub FindRepetitions()
Dim ws As Worksheet
Dim rng As Range
Dim lastRow As Long
Dim SearchRange As Range
Dim FindWhat As Variant
Dim FoundCells As Range
Dim FoundCell As Range
Dim Summation As Integer
Dim ColNumber As Integer
Dim RelatedCells As Range
Set ws = ActiveWorkbook.Sheets("Sheet1")
lastRow = ws.Range("C" & ws.Rows.Count).End(xlUp).Row
Set SearchRange = ws.Range("C1:C" & lastRow)
For Each NewCell In SearchRange
FindWhat = NewCell.Value
Set FoundCells = FindAll(SearchRange:=SearchRange, _
FindWhat:=FindWhat, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByColumns, _
MatchCase:=False, _
BeginsWith:=vbNullString, _
EndsWith:=vbNullString, _
BeginEndCompare:=vbTextCompare)
If FoundCells.Count > 1 Then
' 2 is the Number of letter B in alphabet '
ColNumber = 2
For i = 1 To FoundCells.Count
Set RelatedCells(i) = ws.Cells(FoundCells(i).Row, ColNumber)
Next
Set Summation = Application.WorksheetFunction.Sum(RelatedCells)
For Each RelatedCell In RelatedCells
Set Cells(RelatedCell.Row, RelatedCell.Column).Value = Summation
Next RelatedCell
End If
Next
End Sub
Function FindAll(SearchRange As Range, _
FindWhat As Variant, _
Optional LookIn As XlFindLookIn = xlValues, _
Optional LookAt As XlLookAt = xlWhole, _
Optional SearchOrder As XlSearchOrder = xlByRows, _
Optional MatchCase As Boolean = False, _
Optional BeginsWith As String = vbNullString, _
Optional EndsWith As String = vbNullString, _
Optional BeginEndCompare As VbCompareMethod = vbTextCompare) As Range
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' FindAll
' This searches the range specified by SearchRange and returns a Range object
' that contains all the cells in which FindWhat was found. The search parameters to
' this function have the same meaning and effect as they do with the
' Range.Find method. If the value was not found, the function return Nothing. If
' BeginsWith is not an empty string, only those cells that begin with BeginWith
' are included in the result. If EndsWith is not an empty string, only those cells
' that end with EndsWith are included in the result. Note that if a cell contains
' a single word that matches either BeginsWith or EndsWith, it is included in the
' result. If BeginsWith or EndsWith is not an empty string, the LookAt parameter
' is automatically changed to xlPart. The tests for BeginsWith and EndsWith may be
' case-sensitive by setting BeginEndCompare to vbBinaryCompare. For case-insensitive
' comparisons, set BeginEndCompare to vbTextCompare. If this parameter is omitted,
' it defaults to vbTextCompare. The comparisons for BeginsWith and EndsWith are
' in an OR relationship. That is, if both BeginsWith and EndsWith are provided,
' a match if found if the text begins with BeginsWith OR the text ends with EndsWith.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim FoundCell As Range
Dim FirstFound As Range
Dim LastCell As Range
Dim ResultRange As Range
Dim XLookAt As XlLookAt
Dim Include As Boolean
Dim CompMode As VbCompareMethod
Dim Area As Range
Dim MaxRow As Long
Dim MaxCol As Long
Dim BeginB As Boolean
Dim EndB As Boolean
CompMode = BeginEndCompare
If BeginsWith <> vbNullString Or EndsWith <> vbNullString Then
XLookAt = xlPart
Else
XLookAt = LookAt
End If
' this loop in Areas is to find the last cell
' of all the areas. That is, the cell whose row
' and column are greater than or equal to any cell
' in any Area.
For Each Area In SearchRange.Areas
With Area
If .Cells(.Cells.Count).Row > MaxRow Then
MaxRow = .Cells(.Cells.Count).Row
End If
If .Cells(.Cells.Count).Column > MaxCol Then
MaxCol = .Cells(.Cells.Count).Column
End If
End With
Next Area
Set LastCell = SearchRange.Worksheet.Cells(MaxRow, MaxCol)
On Error GoTo 0
Set FoundCell = SearchRange.Find(what:=FindWhat, _
after:=LastCell, _
LookIn:=LookIn, _
LookAt:=XLookAt, _
SearchOrder:=SearchOrder, _
MatchCase:=MatchCase)
If Not FoundCell Is Nothing Then
Set FirstFound = FoundCell
Do Until False ' Loop forever. We'll "Exit Do" when necessary.
Include = False
If BeginsWith = vbNullString And EndsWith = vbNullString Then
Include = True
Else
If BeginsWith <> vbNullString Then
If StrComp(Left(FoundCell.Text, Len(BeginsWith)), BeginsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
If EndsWith <> vbNullString Then
If StrComp(Right(FoundCell.Text, Len(EndsWith)), EndsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
End If
If Include = True Then
If ResultRange Is Nothing Then
Set ResultRange = FoundCell
Else
Set ResultRange = Application.Union(ResultRange, FoundCell)
End If
End If
Set FoundCell = SearchRange.FindNext(after:=FoundCell)
If (FoundCell Is Nothing) Then
Exit Do
End If
If (FoundCell.Address = FirstFound.Address) Then
Exit Do
End If
Loop
End If
Set FindAll = ResultRange
End Function
I get Runtime Error '91': Object variable or With block variable not set for this line:
Set RelatedCells(i) = ws.Cells(FoundCells(i).Row, ColNumber)
I removed the Set and got the same error. What is wrong?
Based on your comment this should work:
Sub FindRepetitions()
Dim ws As Worksheet, lastRow As Long, SearchRange As Range
Set ws = ActiveWorkbook.Sheets("Sheet1")
lastRow = ws.Range("C" & ws.Rows.Count).End(xlUp).Row
Set SearchRange = ws.Range("C1:C" & lastRow)
'~~> First determine the values that are repeated
Dim repeated As Variant, r As Range
For Each r In SearchRange
If WorksheetFunction.CountIf(SearchRange, r.Value) > 1 Then
If IsEmpty(repeated) Then
repeated = Array(r.Value)
Else
If IsError(Application.Match(r.Value,repeated,0)) Then
ReDim Preserve repeated(Ubound(repeated) + 1)
repeated(Ubound(repeated)) = r.Value
End If
End If
End If
Next
'~~> Now use your FindAll function finding the ranges of repeated items
Dim rep As Variant, FindWhat As Variant, FoundCells As Range
Dim Summation As Long
For Each rep In repeated
FindWhat = rep
Set FoundCells = FindAll(SearchRange:=SearchRange, _
FindWhat:=FindWhat, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByColumns, _
MatchCase:=False, _
BeginsWith:=vbNullString, _
EndsWith:=vbNullString, _
BeginEndCompare:=vbTextCompare).Offset(0, -1)
'~~> Take note that we use Offset to return Cells in B instead of C
'~~> Sum FoundCells
Summation = WorksheetFunction.Sum(FoundCells)
'~~> Output in those ranges
For Each r In FoundCells
r = Summation
Next
Next
End Sub
Not tested. Also this assumes that FindAll function works perfectly.
Also I am not explicit on using WorksheetFunction but it should work as well. HTH
Could you just use a sumif function.
The following code inserts a column (to prevent overwriting) uses a sumif function to calculate the value you want and then copies the values back into column B and erases the temporary column.
Sub temp()
Dim ws As Worksheet
Dim lastrow As Long
Set ws = ActiveWorkbook.Sheets("Sheet1")
lastrow = ws.Range("C" & ws.Rows.Count).End(xlUp).Row
'Insert a column so nothing is overwritten
Range("E1").EntireColumn.Insert
'Assign formula
Range("E1").Formula = "=sumif(C:C,C1,B:B)"
Range("E1:E" & lastrow).FillDown
'copy value back into column B
Range("B1:B" & lastrow).Value = Range("E1:E" & lastrow).Value
'delete column
Range("E1").EntireColumn.Delete
End Sub
Just as an aside to reiterate and try and explain better what I point out in my comment re accessing a range by RelatedCells(i) where RelatedCells is a range object - this comes down to calling the Item method on the RelatedCells range object, so unless the RelatedCells object actually exists when doing so VBA will throw the type of error you are seeing, since you can't call a method on an object that doesn't exist
Another more litteral and maybe easier way to look at it is that by RelatedCells(i), you are trying to refer to the cell at the ith position:
relative to a certain cell of reference
offset from this cell of reference by a certain number of rows and columns
So you need to have some kind of reference that is set in the first place - which are all provided by the RelatedCells object:
the first cell of this range will act as the cell of reference
its shape - number of rows and columns - will determine the offset pattern
Hope that helps clarify a little

Find all matches in workbook using Excel VBA

I am trying to write a VBA routine that will take a string, search a given Excel workbook, and return to me all possible matches.
I currently have an implementation that works, but it is extremely slow as it is a double for loop. Of course the built in Excel Find function is "optimized" to find a single match, but I would like it to return an array of initial matches that I can then apply further methods to.
I will post some pseudocode of what I have already
For all sheets in workbook
For all used rows in worksheet
If cell matches search string
do some stuff
end
end
end
As previously stated, this double for loop makes things run very slowly, so I am looking to get rid of this if possible. Any suggestions?
UPDATE
While the below answers would have improved my method, I ended up going with something slightly different as I needed to do multiple queries over and over.
I instead decided to loop through all rows in my document and create a dictionary containing a key for each unique row. The value this points to will then be a list of possible matches, so that when I query later, I can simply just check if it exists, and if so, just get a quick list of matches.
Basically just doing one initial sweep to store everything in a manageable structure, and then query that structure which can be done in O(1) time
Using the Range.Find method, as pointed out above, along with a loop for each worksheet in the workbook, is the fastest way to do this. The following, for example, locates the string "Question?" in each worksheet and replaces it with the string "Answered!".
Sub FindAndExecute()
Dim Sh As Worksheet
Dim Loc As Range
For Each Sh In ThisWorkbook.Worksheets
With Sh.UsedRange
Set Loc = .Cells.Find(What:="Question?")
If Not Loc Is Nothing Then
Do Until Loc Is Nothing
Loc.Value = "Answered!"
Set Loc = .FindNext(Loc)
Loop
End If
End With
Set Loc = Nothing
Next
End Sub
Based on Ahmed's answer, after some cleaning up and generalization, including the other "Find" parameters, so we can use this function in any situation:
'Uses Range.Find to get a range of all find results within a worksheet
' Same as Find All from search dialog box
'
Function FindAll(rng As Range, What As Variant, Optional LookIn As XlFindLookIn = xlValues, Optional LookAt As XlLookAt = xlWhole, Optional SearchOrder As XlSearchOrder = xlByColumns, Optional SearchDirection As XlSearchDirection = xlNext, Optional MatchCase As Boolean = False, Optional MatchByte As Boolean = False, Optional SearchFormat As Boolean = False) As Range
Dim SearchResult As Range
Dim firstMatch As String
With rng
Set SearchResult = .Find(What, , LookIn, LookAt, SearchOrder, SearchDirection, MatchCase, MatchByte, SearchFormat)
If Not SearchResult Is Nothing Then
firstMatch = SearchResult.Address
Do
If FindAll Is Nothing Then
Set FindAll = SearchResult
Else
Set FindAll = Union(FindAll, SearchResult)
End If
Set SearchResult = .FindNext(SearchResult)
Loop While Not SearchResult Is Nothing And SearchResult.Address <> firstMatch
End If
End With
End Function
Usage is the same as native .Find, but here is a usage example as requested:
Sub test()
Dim SearchRange As Range, SearchResults As Range, rng As Range
Set SearchRange = MyWorksheet.UsedRange
Set SearchResults = FindAll(SearchRange, "Search this")
If SearchResults Is Nothing Then
'No match found
Else
For Each rng In SearchResults
'Loop for each match
Next
End If
End Sub
Function GetSearchArray(strSearch)
Dim strResults As String
Dim SHT As Worksheet
Dim rFND As Range
Dim sFirstAddress
For Each SHT In ThisWorkbook.Worksheets
Set rFND = Nothing
With SHT.UsedRange
Set rFND = .Cells.Find(What:=strSearch, LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlRows, SearchDirection:=xlNext, MatchCase:=False)
If Not rFND Is Nothing Then
sFirstAddress = rFND.Address
Do
If strResults = vbNullString Then
strResults = "Worksheet(" & SHT.Index & ").Range(" & Chr(34) & rFND.Address & Chr(34) & ")"
Else
strResults = strResults & "|" & "Worksheet(" & SHT.Index & ").Range(" & Chr(34) & rFND.Address & Chr(34) & ")"
End If
Set rFND = .FindNext(rFND)
Loop While Not rFND Is Nothing And rFND.Address <> sFirstAddress
End If
End With
Next
If strResults = vbNullString Then
GetSearchArray = Null
ElseIf InStr(1, strResults, "|", 1) = 0 Then
GetSearchArray = Array(strResults)
Else
GetSearchArray = Split(strResults, "|")
End If
End Function
Sub test2()
For Each X In GetSearchArray("1")
Debug.Print X
Next
End Sub
Careful when doing a Find Loop that you don't get yourself into an infinite loop... Reference the first found cell address and compare after each "FindNext" statement to make sure it hasn't returned back to the first initially found cell.
You may use the Range.Find method:
http://msdn.microsoft.com/en-us/library/office/ff839746.aspx
This will get you the first cell which contains the search string. By repeating this with setting the "After" argument to the next cell you will get all other occurrences until you are back at the first occurrence.
This will likely be much faster.
Based on the idea of B Hart's answer, here's my version of a function that searches for a value in a range, and returns all found ranges (cells):
Function FindAll(ByVal rng As Range, ByVal searchTxt As String) As Range
Dim foundCell As Range
Dim firstAddress
Dim rResult As Range
With rng
Set foundCell = .Find(What:=searchTxt, _
After:=.Cells(.Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
If Not foundCell Is Nothing Then
firstAddress = foundCell.Address
Do
If rResult Is Nothing Then
Set rResult = foundCell
Else
Set rResult = Union(rResult, foundCell)
End If
Set foundCell = .FindNext(foundCell)
Loop While Not foundCell Is Nothing And foundCell.Address <> firstAddress
End If
End With
Set FindAll = rResult
End Function
To search for a value in the whole workbook:
Dim wSh As Worksheet
Dim foundCells As Range
For Each wSh In ThisWorkbook.Worksheets
Set foundCells = FindAll(wSh.UsedRange, "YourSearchString")
If Not foundCells Is Nothing Then
Debug.Print ("Results in sheet '" & wSh.Name & "':")
Dim cell As Range
For Each cell In foundCells
Debug.Print ("The value has been found in cell: " & cell.Address)
Next
End If
Next
You can read the data into an array. From there you can do the match in memory, instead of reading one cell at a time.
Pass cell contents into VBA Array
Below code avoids creating infinite loop. Assume XYZ is the string which we are looking for in the workbook.
Private Sub CommandButton1_Click()
Dim Sh As Worksheet, myCounter
Dim Loc As Range
For Each Sh In ThisWorkbook.Worksheets
With Sh.UsedRange
Set Loc = .Cells.Find(What:="XYZ")
If Not Loc Is Nothing Then
MsgBox ("Value is found in " & Sh.Name)
myCounter = 1
Set Loc = .FindNext(Loc)
End If
End With
Next
If myCounter = 0 Then
MsgBox ("Value not present in this worrkbook")
End If
End Sub

Find everything that are NOT

One of our customers sent me this huge excel file and one of my tasks is to build a macro that will clean up data in few sheets. Now, I found this excellent example where one user is suggesting to use the Excel Find Method to speed up the process...which BTW is working perfectly.
BUT, in my case I want to keep the matching rows and delete rest of them. How can I do that in VBA? For example, where they are saying...
Set rFound = .Columns(1).Find(What:="Cat", After:=.Cells(1, 1), LookIn:=xlValues, LookAt:=...)
How can I set the value for WHAT to 'NOT'? I tried few different things like:
Dim cat As String || cat = "Cat" || Set notCat <> cat ..... What:=notCat
DIM notCat <> "Cat" ..... What:=notCat
What:<>"Cat"
What:="<>" & cat...{I'm not sure why they suggested this way, it just changes the string from 'Cat' to '<>Cat'...which i think is wrong}
This might be a very silly question...but I just can't find the correct answer anywhere and feeling very frustrated :( Any helps will be greatly appreciated!!
With ActiveSheet.Range(Range("A1"), Cells(Rows.Count, 1).End(xlUp))
.AutoFilter Field:=1, Criteria1:="<>cat"
.Offset(1, 0).SpecialCells(xlCellTypeVisible).EntireRow.Delete
.AutoFilter
End With
Per my comment above, and also #TimWilliams' answer, I think AutoFilter is probably the way to go with this.
I keep forgetting that Excel does not have a range Difference method (the opposite of Intersect, but that doesn't mean you can't use a similar logic with some UDF creativity.
Have a look at Chip Pearson's excellent FindAll method. Normally, Excel's .Find method only returns a single cell range (or Nothing). This is not particularly useful for your problem, since it requires you to do a clunky iteration and FindNext until you've exhausted the range.
Using the FindAll method, returns a range of all matching cells. We can then iterate over the cells in your column, and test whether they intersect the range returned from the FindAll method. If they do not intersect, then there is no match, so we can delete the row.
Sub TestFindAll()
Dim ws As Worksheet
Dim col As Range
Dim allFound As Range
Dim c As Long
Dim cl As Range
Set ws = ActiveSheet
Set col = Intersect(ws.Columns(1), ws.UsedRange)
Set allFound = FindAll(col, "Cat", xlValues, xlPart)
For c = col.Cells.Count To 1 Step -1
Set cl = col.Cells(c)
If Intersect(allFound, cl) Is Nothing Then
'cl.Interior.ColorIndex = 39 '## I use this line for debugging
cl.EntireRow.Delete
End If
Next
End Sub
'http://www.cpearson.com/excel/findall.aspx
Function FindAll(SearchRange As Range, _
FindWhat As Variant, _
Optional LookIn As XlFindLookIn = xlValues, _
Optional LookAt As XlLookAt = xlWhole, _
Optional SearchOrder As XlSearchOrder = xlByRows, _
Optional MatchCase As Boolean = False, _
Optional BeginsWith As String = vbNullString, _
Optional EndsWith As String = vbNullString, _
Optional BeginEndCompare As VbCompareMethod = vbTextCompare) As Range
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' FindAll
' This searches the range specified by SearchRange and returns a Range object
' that contains all the cells in which FindWhat was found. The search parameters to
' this function have the same meaning and effect as they do with the
' Range.Find method. If the value was not found, the function return Nothing. If
' BeginsWith is not an empty string, only those cells that begin with BeginWith
' are included in the result. If EndsWith is not an empty string, only those cells
' that end with EndsWith are included in the result. Note that if a cell contains
' a single word that matches either BeginsWith or EndsWith, it is included in the
' result. If BeginsWith or EndsWith is not an empty string, the LookAt parameter
' is automatically changed to xlPart. The tests for BeginsWith and EndsWith may be
' case-sensitive by setting BeginEndCompare to vbBinaryCompare. For case-insensitive
' comparisons, set BeginEndCompare to vbTextCompare. If this parameter is omitted,
' it defaults to vbTextCompare. The comparisons for BeginsWith and EndsWith are
' in an OR relationship. That is, if both BeginsWith and EndsWith are provided,
' a match if found if the text begins with BeginsWith OR the text ends with EndsWith.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim FoundCell As Range
Dim FirstFound As Range
Dim LastCell As Range
Dim ResultRange As Range
Dim XLookAt As XlLookAt
Dim Include As Boolean
Dim CompMode As VbCompareMethod
Dim Area As Range
Dim MaxRow As Long
Dim MaxCol As Long
Dim BeginB As Boolean
Dim EndB As Boolean
CompMode = BeginEndCompare
If BeginsWith <> vbNullString Or EndsWith <> vbNullString Then
XLookAt = xlPart
Else
XLookAt = LookAt
End If
' this loop in Areas is to find the last cell
' of all the areas. That is, the cell whose row
' and column are greater than or equal to any cell
' in any Area.
For Each Area In SearchRange.Areas
With Area
If .Cells(.Cells.Count).Row > MaxRow Then
MaxRow = .Cells(.Cells.Count).Row
End If
If .Cells(.Cells.Count).Column > MaxCol Then
MaxCol = .Cells(.Cells.Count).Column
End If
End With
Next Area
Set LastCell = SearchRange.Worksheet.Cells(MaxRow, MaxCol)
On Error GoTo 0
Set FoundCell = SearchRange.Find(what:=FindWhat, _
after:=LastCell, _
LookIn:=LookIn, _
LookAt:=XLookAt, _
SearchOrder:=SearchOrder, _
MatchCase:=MatchCase)
If Not FoundCell Is Nothing Then
Set FirstFound = FoundCell
Do Until False ' Loop forever. We'll "Exit Do" when necessary.
Include = False
If BeginsWith = vbNullString And EndsWith = vbNullString Then
Include = True
Else
If BeginsWith <> vbNullString Then
If StrComp(Left(FoundCell.Text, Len(BeginsWith)), BeginsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
If EndsWith <> vbNullString Then
If StrComp(Right(FoundCell.Text, Len(EndsWith)), EndsWith, BeginEndCompare) = 0 Then
Include = True
End If
End If
End If
If Include = True Then
If ResultRange Is Nothing Then
Set ResultRange = FoundCell
Else
Set ResultRange = Application.Union(ResultRange, FoundCell)
End If
End If
Set FoundCell = SearchRange.FindNext(after:=FoundCell)
If (FoundCell Is Nothing) Then
Exit Do
End If
If (FoundCell.Address = FirstFound.Address) Then
Exit Do
End If
Loop
End If
Set FindAll = ResultRange
End Function