Match The Nth Instance In Excel - excel

I am using the match function on spreadsheets and the spreadsheets have the same keywords but in different rows, I am attempting to get the row number and to do this I want to use the second instance of a keyword. How would this be done in VBA my current code is
Application.WorksheetFunction.Match("Hello", Range("A1:A100"), 0)
I was thinking about using the Index function, but I am not exactly sure how to use it.

Start the second match just below the first:
Sub dural()
Dim rw As Long
With Application.WorksheetFunction
rw = .Match("Hello", Range("A1:A1000"), 0)
rw = .Match("Hello", Range("A" & (rw + 1) & ":A1000"), 0) + rw
MsgBox rw
End With
End Sub
If you want the Nth match, I would use Find() and a FindNext() loop.
EDIT#1:
Another way to find the Nth instance is to Evaluate() the typical array formula within VBA. For N=3, in the worksheet, the array formula would be:
=SMALL(IF(A1:A1000="Hello",ROW(A1:A1000)),3)
So with VBA:
Sub dural()
Dim rw As Long, N As Long
N = 3
rw = Evaluate("SMALL(IF(A1:A1000=""Hello"",ROW(A1:A1000))," & N & ")")
MsgBox rw
End Sub

Here is a method using Range.Find.
Option Explicit
Sub FindSecond()
Dim rSearch As Range, C As Range
Const sSearchFor As String = "Hello"
Dim sFirstAddress As String
Set rSearch = Range("A1:A100")
With rSearch 'Note that search starts at the bottom
Set C = .Find(what:=sSearchFor, after:=rSearch(.Rows.Count, 1), _
LookIn:=xlValues, lookat:=xlWhole, searchorder:=xlByRows, _
searchdirection:=xlNext, MatchCase:=False)
If Not C Is Nothing Then
sFirstAddress = C.Address
Set C = .FindNext(C)
If C.Address <> sFirstAddress Then
MsgBox "2nd instance of " & sSearchFor & " on row " & C.Row
Else
MsgBox "Only one instance of " & sSearchFor & " and it is on row " & C.Row
End If
Else
MsgBox "No instance of " & sSearchFor
End If
End With
End Sub

There might be a better way, but this works:
=MATCH("Hello",INDIRECT("A"&(1+MATCH("Hello",A1:A100,0))&":A100"),0)
This would return the index of the second occurrence, by searching for the first occurrence and using that to define the range to search for the next one.

Related

Excel - how to select a range in VBA using an expression like VLOOPUP?

I need to create a macro button, which adds +1 value to a cell.
The problem is, the correct cell should be chosen from a table, using a VLOOKUP function, like this one:
VLOOKUP(A38;$A$50:$Q$59;6)
How can I program this in VBA?
Thanks alot!!
VLookup returns a value, to return a cell reference use Find. Assuming A38 and the lookup range $A$50:$Q$59 is on Sheet1 then try
Option Explicit
Sub AddOne()
Dim rngSearch As Range, rngFound As Range, v
v = Sheet1.Range("A38")
Set rngSearch = Sheet1.Range("A50:A59")
Set rngFound = rngSearch.Find(v, LookIn:=xlValues, lookat:=xlWhole)
If rngFound Is Nothing Then
MsgBox "'" & v & "' not found in range " & rngSearch.Address, vbExclamation
Else
MsgBox "'" & v & "' found at range " & rngFound.Address
'increment column F
rngFound.Offset(0, 5) = rngFound.Offset(0, 5) + 1
End If
End Sub

Why is Find object searching through the whole data instead of a specified Range?

I am new to Vba(and coding in general) and have written a very basic macro to find out "wood" inside a particular column from a larger set of data, however when I try to run it, it's still searching from the whole data set instead of the specified column.
I started with keeping the range of Cel to the whole data set, and then just narrowed it to the third column. However, when I remove the find element the immediate window shows me the address of cells in the third column, just as I want, but as soon as I use Find, it searches through the whole dataset.
I've tried defining propertied in Find object such as After and SearchOrder, but then it shows an error.
Dim emptyrow As Long
emptyrow = WorksheetFunction.CountA(Range("A:A")) + 1
Dim Cel As Range
Dim n As Integer
Set Cel = Range("C2:C54")
For n = 2 To emptyrow
Debug.Print (Cel.Cells(n,3).Find("wood","C2",,,xlByColumn).Address)
Next n
On using properties of Find, I get a type mismatch error.
It's not clear what exactly you want to do with the results, but here are a couple of possible outputs.
Sub xx()
Dim rFind As Range, s As String, v() As Variant, i As Long
With Range("C2:C54")
Set rFind = .Find(What:="Wood", After:=.Cells(.Cells.Count), _
Lookat:=xlWhole, MatchCase:=False, SearchFormat:=False)
If Not rFind Is Nothing Then
s = rFind.Address
Do
Debug.Print rFind.Offset(, -2).Value 'column A value in immediate window
i = i + 1
ReDim Preserve v(1 To i)
v(i) = rFind.Offset(, -2).Value 'or store values in an array
Set rFind = .FindNext(rFind)
Loop While rFind.Address <> s
End If
End With
MsgBox Join(v, ", ")
End Sub
Use this:
Sub fnd_all_wood()
Dim c As Range
Dim firstaddress As String
With Range("C2:C54")
Set c = .Find("wood", LookIn:=xlValues)
If Not c Is Nothing Then
firstaddress = c.Address
Do
Debug.Print c.Address
Set c = .FindNext(c)
If c Is Nothing Then
GoTo DoneFinding
End If
Loop While c.Address <> firstaddress
End If
DoneFinding:
MsgBox "Done All"
End With
End Sub
This will print all the Cell Address in Range C2:C54 where it finds Wood
More information about the Formula Range.FindNext in the Link.
Update:
You can change the line Debug.Print c.Address to any other code where you can get the row by c.row and use it to get the other values like cells(c.row,1) to get the value from Ist column.
Demo:

Excel VBA: Finding column number of Nth field name

I have a function where I specify the field I want and the header row number and it returns the column. E.g. =findField("Region",1) would return the column number containing the header "Region". This worked well until I encountered a report containing duplicate names in the header row. E.g. instead of 1st and last name it would have "Name" for both fields so I needed to specify the occurrence I wanted as in =findField("Name",1,2) for the 2nd occurrence. I came up with a solution but it has 2 issues. The first is that if the field is in the first column it won't work properly. E.g. if columns A and B have "Name" then =findField("Name",1,1) would return the second field instead of the first and =findField("Name",1,2) would wrap around and return the 1st which is not what I want. The second issue is that it wraps around which I would prefer it not to do at all. What I came up with is as follows:
Function findField2(fieldName As String, Optional rowStart As Long = 0, Optional occurrence As Long = 1)
Dim Found As Range, lastRow As Long, count As Integer, myCol As Long
If rowStart = 0 Then rowStart = getHeaderRow()
myCol = 1
For count = 1 To occurrence
Set Found = Rows(rowStart).Find(what:=fieldName, LookIn:=xlValues, lookat:=xlWhole, After:=Cells(rowStart, myCol))
If Found Is Nothing Then
MsgBox "Error: Can't find '" & fieldName & "' in row " & rowStart
Exit Function
Else
myCol = Found.Column
End If
Next count
lastRow = Cells(Rows.count, Found.Column).End(xlUp).Row
findField2 = Found.Column
What do I need to do to allow for the field being in column A? Putting in 0 for myCol doesn't work. The initial finding function was based off https://www.mrexcel.com/forum/excel-questions/629346-vba-finding-text-row-1-return-column.html and I was tweaking it to suit my needs.
Thanks,
Ben
Here's something not using Find() which should still meet your goals:
Function findField2(fieldName As String, Optional rowStart As Long = 0, _
Optional occurrence As Long = 1)
Dim a, rw As Range, m
If rowStart = 0 Then rowStart = getHeaderRow()
With ActiveSheet 'might be better to pass the sheet as a parameter
Set rw = Application.Intersect(.Rows(rowStart), .UsedRange)
a = .Evaluate("=IF(" & rw.Address & "=""" & fieldName & _
""",COLUMN(" & rw.Address & "),FALSE)")
End With
m = Application.Small(a, occurrence) 'find the n'th match (will return an error if none)
If IsError(m) Then MsgBox "No occurrence #" & occurrence & " of '" & _
fieldName & "' on row# " & rowStart, vbExclamation
findField2 = IIf(IsError(m), 0, m)
End Function
Sub Tester()
Debug.Print findField2("A", 5, 40)
End Sub
Found-in-row Column feat. the Wrap Around Issue
No object references here i.e. everything refers to the ActiveSheet (of the ActiveWorkbook).
Find (After)
By default the Find method starts the search from the next cell (6. SearchDirection xlNext or 1) of the supplied cell range parameter of the After argument (2. After) i.e. in case you use cell A1 by row (5. SearchOrder xlByRows or 1), the search will start from B1, continue until the last column, wrap around and continue with A1 last. Therefore the last cell of the row has to be used to start the search with the first cell A1.
Wrap Around
The Wrap Around issue is solved with an If statement only if the Occurrence Number is greater than 1. If no occurrence was found, 0 is returned.
The column number of the found cell (intCol) is passed to a variable (intWrap) and every next occurrence of the value, they are checked against each other. Now, if the variable is equal to the column number, the function returns -1, indicating that the value was found but the specified occurrence has not been found.
'*******************************************************************************
' Purpose: Finds the Nth occurrence of a value in cells of a row *
' to return the column number of the cell where it was found. *
'*******************************************************************************
' Inputs *
' FindValue: The value to search for. *
' FindRow: The row to search in. *
' OccurrenceNumber: The occurrence number of the value to search for. *
'*******************************************************************************
' Returns: The column number of the Nth occurrence of the value. *
' 0 if value was not found. *
' -1 if value was found, but not the specified occurrence of it. *
' -2 if worksheet has no values (-4163). *
' -3 if workbook is add-in (No ActiveSheet). *
'*******************************************************************************
Function FoundinrowColumn(FindValue As Variant, Optional FindRow As Long = 0, _
Optional OccurrenceNumber As Integer = 1) As Integer
Dim intCol As Integer ' Search Start Column Number
Dim intCount As Integer ' OccurrenceNumber Counter
Dim intWrap As Integer ' Wrap Around Stopper
' Check if ActiveSheet exists.
If ActiveSheet Is Nothing Then FoundinrowColumn = -3: Exit Function
' Check if sheet has no values.
If Cells.Find("*", Cells(Rows.count, Columns.count), -4163, 1, 1) _
Is Nothing Then FoundinrowColumn = -2: Exit Function
' Find first used row if no FindRow parameter.
If FindRow = 0 Then
FindRow = Cells.Find("*", Cells(Rows.count, Columns.count)).Row
End If
' Set initial Search Start Column Number.
intCol = Columns.count
' Try to find the Nth occurence of 'FindValue' in 'FindRow'.
For intCount = 1 To OccurrenceNumber
If Not Rows(FindRow).Find(FindValue, Cells(FindRow, intCol)) Is Nothing Then
intCol = Rows(FindRow).Find(FindValue, Cells(FindRow, intCol)).Column
If intCount > 1 Then
If intCol = intWrap Then FoundinrowColumn = -1: Exit Function
Else
intWrap = intCol
End If
Else
FoundinrowColumn = 0: Exit Function
End If
Next
FoundinrowColumn = intCol
End Function
'*******************************************************************************
This version uses FindNext to search for occurrences after the first.
It searches Sheet1 of the workbook that the code is in (ThisWorkbook):
Sub Test()
Dim MyCell As Range
'Second occurrence default row.
Set MyCell = FindField("Date", Occurrence:=3)
If Not MyCell Is Nothing Then
MsgBox "Found in cell " & MyCell.Address & "." & vbCr & _
"Row: " & MyCell.Row & vbCr & "Column: " & MyCell.Column & vbCr & _
"Sheet: '" & MyCell.Parent.Name & "'" & vbCr & _
"Workbook: '" & MyCell.Parent.Parent.Name & "'", vbOKOnly + vbInformation
Else
MsgBox "Value not found."
End If
End Sub
Public Function FindField(FieldName As String, Optional RowStart As Long = 0, _
Optional Occurrence As Long = 1) As Range
Dim rFound As Range
Dim x As Long
Dim sFirstAdd As String
If RowStart = 0 Then RowStart = 1
x = 1
With ThisWorkbook.Worksheets("Sheet1").Rows(RowStart)
Set rFound = .Find( _
What:=FieldName, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
After:=.Cells(RowStart, .Columns.Count))
If Not rFound Is Nothing Then
Set FindField = rFound
If Occurrence <> 1 Then
sFirstAdd = rFound.Address
Do
Set rFound = .FindNext(rFound)
x = x + 1
Loop While x <> Occurrence And rFound.Address <> sFirstAdd
If rFound.Address = sFirstAdd Then
Set FindField = Nothing
End If
End If
End If
End With
End Function
Thanks for your responses. I am picking up useful techniques which is of great help. I actually fixed the first issue based on #TimWilliams to set myCol to the last column so it starts the find at the first column and added a check for the wrap around per the below. I also changed the msgBox to return a value instead per #VBasic2008.
Function findField2(fieldName As String, Optional rowStart As Long = 0, Optional occurrence As Long = 1)
Dim Found As Range, lastRow As Long, count As Integer, myCol As Long
If rowStart = 0 Then rowStart = getHeaderRow()
myCol = 16384
For count = 1 To occurrence
Set Found = Rows(rowStart).Find(what:=fieldName, LookIn:=xlValues, lookat:=xlWhole, After:=Cells(rowStart, myCol))
' Check if nothing found or for wrap around and Nth occurrence not found
If Found Is Nothing Or count > 1 And Found.Column <= myCol Then
findField2 = 0
Exit Function
Else
myCol = Found.Column
End If
Next count
lastRow = Cells(Rows.count, Found.Column).End(xlUp).Row
findField2 = Found.Column
End Function
Here is the getHeaderRow function mentioned in the findField function above:
Function getHeaderRow() As Long
Dim i As Long, lastCol As Long, lastRow As Long
lastCol = Cells.Find("*", [a1], , , xlByColumns, xlPrevious).Column
lastRow = Cells.Find("*", [a1], , , xlByRows, xlPrevious).Row
i = 1
Do While Cells(i, lastCol).Value = ""
i = i + 1
If i > lastRow Then
i = 0
Exit Do
End If
Loop
getHeaderRow = i
End Function

Find a string in a column and return and return an array with row numbers

I want to search in a column and find a string. However, there are several cells with that string and I want to return an array that contains all row position.
Dim r As Range
Set r = Sheets("Sheet3").columns(3).Find(What:="TEST", LookAt:=xlWhole, MatchCase:=False, SearchFormat:=False)
this return only the first row, but not all.
How can I return all of rows that contain "TEST"?
Thanks.
This should do the trick...
It will loop through Column 3 of Sheet3 to produce an array containing the row numbers of every occurrence of TEXT. Based on the options used in your example, it is case sensitive and must occupy the whole cell.
Sub demo_FindIntoArray()
Const searchFor = "TEST" 'case sensitive whole-cell search term
Const wsName = "Sheet3" 'worksheet name to search
Const colNum = 3 'column# to search
Dim r As Range, firstAddress As String, strTxt As String, arrRows
With Sheets("Sheet3").Columns(3)
Set r = .Find(searchFor, LookAt:=xlWhole, MatchCase:=False, SearchFormat:=False)
If Not r Is Nothing Then
firstAddress = r.Address
Do
If strTxt <> "" Then strTxt = strTxt & ","
strTxt = strTxt & r.Row
Set r = .FindNext(r)
Loop While Not r Is Nothing And r.Address <> firstAddress
End If
End With
If strTxt <> "" Then
arrRows = Split(strTxt,",")
MsgBox "Found " & UBound(arrRows)+1 & " occurrences of '" & searchFor & "':" & vbLf & vbLf & strTxt
Else
MsgBox "'" & searchFor & "' was not found."
End If
'[arrRows] is now an array containing row numbers
End Sub
It works by first building a string with a comma separated list of values, and then using the Split function to split it into an array.
This will unionize the ranges that equal "Test",
Sub SelectA1()
Dim FrstRng As Range
Dim UnionRng As Range
Dim c As Range
Set FrstRng = Range("C:C").SpecialCells(xlCellTypeConstants, 23)
For Each c In FrstRng.Cells
If LCase(c) = "test" Then
If Not UnionRng Is Nothing Then
Set UnionRng = Union(UnionRng, c) 'adds to the range
Else
Set UnionRng = c
End If
End If
Next c
UnionRng.Select ' or whatever you want to do with it.
End Sub
You don't indicate what you want to do with the results. However, you can return an array of the row numbers containing the word "TEST" with a worksheet formula:
Case Insensitive:
=AGGREGATE(15,6,SEARCH("TEST",$C:$C)*ROW($C:$C),ROW(INDIRECT("1:" & COUNTIF($C:$C,"*TEST*"))))
Case Sensitive:
=AGGREGATE(15,6,FIND("TEST",$C:$C)*ROW($C:$C),ROW(INDIRECT("1:" & SUMPRODUCT(--ISNUMBER(FIND("TEST",$C:$C))))))
Case Insensitive; cell = test (eg entire cell contents):
=AGGREGATE(15,6,1/($C:$C="test")*ROW($C:$C),ROW(INDIRECT("1:"&COUNTIF($C:$C,"test"))))
If you want to use this array for something else, such as to return the contents of the rows where the third column = "test", you could also do this with a filter, either in VBA or on the worksheet.
There are many different appropriate solutions, depending on what you are going to do with the results of these row numbers.

Error capture while using .Find is not identifing error

When .Find does not find a result, I want an error msg. I have used the method that is almost universally recommended online, but it is not working. When a value is not found, nothing happens. There should be a msg box identified the error.
If Not rFoundCell Is Nothing Then
MsgBox "val: " & rValue.Value & " Matching Cell: " & rFoundCell.Address
Cells(Range(rFoundCell.Address).Row, Range(rFoundCell.Address).Column).Select
Else
MsgBox (rValue.Value & " not found.")
GoTo end_search
End If
I've tried the other way as well:
If rFoundCell Is Nothing Then
Display a msg "not found"
else
Keep going.
That didn't work either. What am i missing?
Full code follows:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim PostRng As Range
Dim PendRng As Range
Dim rValue As Range
Dim lLoop As Long
Dim rFoundCell As Range
Dim INTRng As Range
Set PostRng = Range("g:g")
Set PendRng = Range("k:k")
'"Intersect" will ensure your current cell lies on correct column.
Set INTRng = Intersect(Target, PostRng)
'IF conditions to trigger code.
'This IF confirms only one cell changed. -- I think
If Target.Columns.Count = 1 And Target.Rows.Count = 1 Then
If Not INTRng Is Nothing And LCase(Target.Text) = "y" Then
'This block will return the range & value on the row where "y" or "Y" are entered.
Set rValue = Target.Offset(0, -3) 'Returns value in Col D
If rValue = 0 Or rValue = "" Then Set rValue = Target.Offset(0, -2)
Debug.Print "Target "; Target
Debug.Print "rvalue.value "; rValue.Value
'This will loop through a different column, to find the value identified above, and return its cell address in the other column.
With PendRng
Set rFoundCell = .Cells(1, 1)
For lLoop = 1 To WorksheetFunction.CountIf(.Cells, rValue.Value)
Set rFoundCell = .Find(What:=rValue.Value, _
After:=rFoundCell, _
LookIn:=xlValues, _
LookAt:=xlPart, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=False)
Debug.Print "rfoundcell " & rFoundCell
If Not rFoundCell Is Nothing Then
MsgBox "val: " & rValue.Value & " Matching Cell: " & rFoundCell.Address
'This will use the cell address identified above to move the active cell to that address.
'Have to convert the address to row/column to use in Cell.Select.
Cells(Range(rFoundCell.Address).Row, Range(rFoundCell.Address).Column).Select
Else
MsgBox (rValue.Value & " not found.")
GoTo end_search
End If
Next lLoop
End With
End If
End If
end_search:
End Sub
Received help w/ this code here:
Execute a subroutine when a user enters a trigger into a cell
I believe that your code is skipping the If statement that generates the error box if there is not a match.
This is due to For lLoop = 1 To WorksheetFunction.CountIf(.Cells, rValue.Value) exiting when there is no matches because it equates to For lLoop = 1 To 0
I moved all of your error message code into an If statement above the lLoop as follows:
If WorksheetFunction.CountIf(.Cells, rValue.Value) = 0 Then
MsgBox (rValue.Value & " not found.")
GoTo end_search
End If

Resources