Using VBA Match and Index function - vba

Trying to use match & Index with specified ranges. Does not recognise RefreshDrNumbers in the code.
I am using the Case Function to specify ranges.
Can't seem to make the Case, Match & Index function connect or talk to each other?
The other Forum I've asked is
https://www.mrexcel.com/board/threads/add-ranges-to-match-and-index-functions.1162701/
Private Sub Jobcard_Demands_Click()
If Jobcard_Demands = ("Drawing No`s Update") Then
Dim matchRange As Range
Dim ODict As Object
Dim PartsListLastRow As Long, DestLastRow As Long
Dim LookupRange As Range
Dim i As Integer
Dim wsSource As Worksheet, wsDest As Worksheet
Set wsSource = ThisWorkbook.Worksheets("Parts List")
Set wsDest = ThisWorkbook.Worksheets("Job Card Master")
PartsListLastRow = wsSource.Range("A" & Rows.Count).End(xlUp).Row
DestLastRow = wsDest.Range("A" & Rows.Count).End(xlUp).Row
'This holds the lookup range (including both the lookup key
'column and the value column)
Set matchRange = wsSource.Range("E1:F" & PartsListLastRow)
'Get a dictionary of all the lookup values. The function, as
'defined below, takes the range as well as the relative column
'of the keys and values. In our case, the first column of our
'range has the keys, and the second has the values
Set ODict = GetDictionary(matchRange, 5, 6)
'Below, define the lookup range. In your specific code, this
'varies based on the combobox value, but I think you'll be able
'to figure out how to define it (I'm just hardcoding mine
Set LookupRange = wsDest.Range("A1:A" & DestLastRow)
'Loop over the lookup range
For i = 1 To DestLastRow
'Since the GetPartInfo function handles cases where there isn't a match
' (it returns a blank string), you don't have to use an if/else statement
wsDest.Range("B" & i).Value = GetPartInfo(ODict, wsDest.Range("E" & i).Value)
Next i
End If
End Sub
Private Function GetDictionary(rng As Range, keyCol As Long, valCol As Long) As Object
Dim sht As Worksheet
Dim rCell As Range
Dim ODict As Object
Set sht = rng.Parent
Set ODict = CreateObject("Scripting.Dictionary")
For Each rCell In rng.Columns(keyCol).Cells
If Not ODict.Exists(rCell.Offset(, keyCol - 1).Value) Then
ODict.Add rCell.Offset(, keyCol - 1).Value, rCell.Offset(, valCol - 1).Value
End If
Next rCell
Set GetDictionary = ODict
End Function
'This is just a helper function to de-clutter the main subroutine. Returns an
' empty string in cases where the part doesn't exist in the dictionary
Private Function GetPartInfo(ByRef ODict As Object, sKey As String)
Dim Output As String
Output = ""
If ODict.Exists(sKey) Then
Output = ODict(sKey)
End If
GetPartInfo = Output
End Function

Whenever I'm working with code that performs many lookups over the same range, I tend to package that lookup range into a dictionary. Lookups in a dictionary are highly efficient, so you don't have to worry about the "cost" of the lookup. There is an overhead to populate the dictionary, but this is often recovered as the number of lookups grows.
I took that approach in the below solution. I use helper functions to create the dictionary and to lookup dictionary values. This helps to declutter the main routine. See if you can work with the code below, and adapt it to your solution. I commented it where I felt it would add value, and I think you should be able to adapt to your needs. Write back with any issues.
Sub RefreshStuff()
Dim matchRange As Range
Dim oDict As Object
Dim lastRow As Long
Dim lookupRange As Range
Dim wsDest As Worksheet
'This holds the lookup range (including both the lookup key
'column and the value column)
Set matchRange = Sheets("Parts List").Range("E1:F6")
'Get a dictionary of all the lookup values. The function, as
'defined below, takes the range as well as the relative column
'of the keys and values. In our case, the first column of our
'range has the keys, and the second has the values
Set oDict = GetDictionary(matchRange, 1, 2)
'Below, define the lookup range. In your specific code, this
'varies based on the combobox value, but I think you'll be able
'to figure out how to define it (I'm just hardcoding mine
lastRow = 10
Set wsDest = Sheets("Job Card Master")
Set lookupRange = wsDest.Range("A1:A" & lastRow)
'Loop over the lookup range
For i = 1 To lastRow
'Since the GetPartInfo function handles cases where there isn't a match
' (it returns a blank string), you don't have to use an if/else statement
wsDest.Range("B" & i).Value = GetPartInfo(oDict, wsDest.Range("A" & i).Value)
Next i
End Sub
Private Function GetDictionary(rng As Range, keyCol As Long, valCol As Long) As Object
Dim sht As Worksheet
Dim rCell As Range
Dim oDict As Object
Set sht = rng.Parent
Set oDict = CreateObject("Scripting.Dictionary")
For Each rCell In rng.Columns(keyCol).Cells
If Not oDict.exists(rCell.Offset(, keyCol - 1).Value) Then
oDict.Add rCell.Offset(, keyCol - 1).Value, rCell.Offset(, valCol - 1).Value
End If
Next rCell
Set GetDictionary = oDict
End Function
'This is just a helper function to de-clutter the main subroutine. Returns an
' empty string in cases where the part doesn't exist in the dictionary
Private Function GetPartInfo(ByRef oDict As Object, sKey As String)
Dim output As String
output = ""
If oDict.exists(sKey) Then
output = oDict(sKey)
End If
GetPartInfo = output
End Function

Related

Copy unknown range from specific worksheet

I am trying to do a copy in VBA, as part of a bigger macro so it needs to be in VBA, of an unknown range in a specific worksheet.
I have this code that work if I am in that worksheet:
Sub Copy()
Range("O2", Range("O" & Cells(Rows.Count, "A").End(xlUp).Row)).copy
End Sub()
And I have below that works for a specific range:
Sub Test()
Worksheets("Data").Range("O2:O10").Copy
End Sub()
How can I make the second code work as unspecific.
Thanks,
You should practice to always fully qualify all your Sheet and Range objects.
The code below is a little long, but it's good practice to define and set all your objects and variables.
Code
Option Explicit
Sub Test()
Dim Sht As Worksheet
Dim LastRow As Long
' set your worksheet object
Set Sht = ThisWorkbook.Worksheets("Data")
With Sht
' get last row in column A
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
' copy dynamic range in column O
.Range("O2:O" & LastRow).Copy
End With
End Sub
The simplest & dirtiest solution is this one:
Range("O2:O" & Cells(Rows.Count, "A").End(xlUp).Row).Copy
or you can isolate the last row as a separate variable:
lastRow = Cells(Rows.Count, "A").End(xlUp).Row
Range("O2:O" & lastRow).Copy
at the end, one may decide to declare the range to be copied as a separate variable and to work with it, declaring the parent worksheet as well:
Public Sub TestMe()
Dim lastRow As Long
Dim ws As Worksheet
Dim rangeToCopy As Range
Set ws = workshetes("Sheet1")
With ws
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
Set rangeToCopy = .Range("O2:O" & lastRow)
rangeToCopy.Copy
End With
End Sub
And going really one step further is using a dedicated function for finding the last row per worksheet (GitHub repo here):
Function lastRow(wsName As String, Optional columnToCheck As Long = 1) As Long
Dim ws As Worksheet
Set ws = Worksheets(wsName)
lastRow = ws.Cells(ws.Rows.Count, columnToCheck).End(xlUp).Row
End Function
At some point, your code will have to know the range that's going to be copied, right? You assign that to a variable and you use it.
Option Explicit
Sub Test()
Dim startRow as Long
startRow = 'your method of determining the starting row
Dim startCol as Long
startCol = 'your method of determining the starting column
Dim endRow as Long
endRow = 'your method of determining the ending row (Last used row would work just fine)
Dim endCol as Long
endCol = 'your method of determining the ending column
With Worksheets("Data")
.Range(.Cells(startRow, startCol), .Cells(endRow, endCol)).Copy
End with
End Sub
you could use a Function you pass the "seed" range to and returning a range from passed one to the last not empty cell in the same column, as follows (explanations in comments)
Function GetRange(rng As Range) As Range
With rng.Parent ' reference passed range parent worksheet
Set GetRange = .Range(rng, .Cells(.Rows.Count, rng.Column).End(xlUp)) ' return referenced sheet range from passed range to passed range column last not empty cell
End With
End Function
to be used as follows:
Sub Test()
GetRange(Worksheets("Data").Range("O2")).Copy
End Sub
you could enhance the function and have it handle a given "final" row
Function GetRange(rng As Range, Optional finalRow As Variant) As Range
With rng.Parent ' reference passed range parent worksheet
If IsMissing(finalRow) Then ' if no "final" row passed
Set GetRange = .Range(rng, .Cells(.Rows.Count, rng.Column).End(xlUp)) ' return referenced sheet range from passed range to passed range column last not empty cell
Else 'else
Set GetRange = .Range(rng, .Cells(finalRow, rng.Column)) ' return referenced sheet range from passed range to passed range column cell in give "final" row
End If
End With
to be used as follows:
Sub Test()
GetRange(Worksheets("Data").Range("O2"), 2).Copy
End Sub
having kept "final" row as optional, the function can be used with or without passing it:
Sub Test()
GetRange(Worksheets("Data").Range("O2")).Copy ' this will copy worksheet "Data" range from row 2 down to its column "O" last not empty row
GetRange(Worksheets("Data").Range("O2"), 3).Copy ' this will copy worksheet "Data" range from row 2 down to row 3
End Sub
You clearly don't enjoy using variables, so:
Worksheets("Data").Range("O2", Worksheets("Data").Range("O" & Cells(Rows.Count, "A").End(xlUp).Row)).copy
would suffice.
Generally, a more common solution would be to use intersect and CurrentRegion:
Application.intersect(Worksheets("Data").Range("O2").CurrentRegion,Worksheets("Data").Range("O2:O999999")).copy

VBA conditional formatting of rows based on cell text value

The order of my data from my query comes out as desired - Column A asc, Column B asc.
Code Completion Date Receipt
P81800A1 09/03/2018 167,000.00
P81800A1 14/03/2018 178,000.00
P82080A 12/03/2018 352,500.00
P83103C1 02/03/2018 570,000.00
P83103C1 02/03/2018 358,000.00
P83103C1 02/03/2018 357,500.00
P83103C1 12/03/2018 340,000.00
P83103C1 12/03/2018 457,000.00
P83103C1 13/03/2018 415,000.00
P83180C1 06/03/2018 645,000.00
P83180C1 06/03/2018 520,000.00
This means if I get a completion for P81800A1 on 15/03/18 when I refresh the data, it will go in between lines 2 and 3 of above.
I have tried to summarise my goal in the attached image.
I want to VBA Conditional format each row based on the cell value of A in that row. Ie P81800A1 rows have one colour. All distinct codes have the same colour. The actual colour does not matter.
I want to do it in VBA so it is robust. I do not want to be creating any additional columns and basing it on formula in standard conditional formatting.
So this is not perfect but a reasonable start. It uses a dictionary to collect the unique codes and randbetween with dictionary item count to generate an associated colour. Conditional formatting rules are applied using the distinct codes.
Notes:
You might want to improve the random colour generation part (at present range is limited and you may occasionally get very dark formats - though you could run the macro again)
Make range selection more robust as start position is hard coded at present and later parts of code use this start position as well
Required, for early binding, reference to Microsoft scripting runtime to be added via VBE>Tools>References. I have included one example of how to use late binding (commented out). If using late binding you would need to specify Object instead of Dictionary for parameters and function return types (where dictionary returned).
Assumes data starts in A2 at present (sheet 9)
Option Explicit
Public Sub FormatMatchingCodes()
Dim wb As Workbook
Dim wsTarget As Worksheet
Set wb = ThisWorkbook
Set wsTarget = wb.Worksheets("Sheet9") 'change as appropriate
Dim lastRow As Long
Application.ScreenUpdating = False
lastRow = GetLastRow(wsTarget)
Dim formatRange As Range
If Not lastRow <= 2 Then
Set formatRange = wsTarget.Range("A2:C" & lastRow) 'Excludes header row
Else
MsgBox "End row is before start row"
Exit Sub
End If
Dim codeColoursDictionary As Dictionary
Set codeColoursDictionary = GetDistinctCodeCount(formatRange.Value2)
wsTarget.Cells.FormatConditions.Delete
AddFormatting formatRange, codeColoursDictionary
Application.ScreenUpdating = True
End Sub
Public Function GetDistinctCodeCount(ByVal sourceData As Variant) As Dictionary 'as object if latebound
''LATE binding
' Dim distinctDict As Object
' Set distinctDict = CreateObject("Scripting.Dictionary")
''Early binding add reference to VBE > tools > references > Microsoft scripting runtime
Dim distinctDict As Scripting.Dictionary
Set distinctDict = New Scripting.Dictionary
Dim currentCode As Long
For currentCode = LBound(sourceData, 1) To UBound(sourceData, 1)
If Not distinctDict.exists(sourceData(currentCode, 1)) Then
distinctDict.Add sourceData(currentCode, 1), Application.WorksheetFunction.RandBetween(13434828, 17777777) + distinctDict.Count
End If
Next currentCode
Set GetDistinctCodeCount = distinctDict
End Function
Public Function GetLastRow(ByVal wsTarget As Worksheet) As Long
With wsTarget
GetLastRow = .Cells(.Rows.Count, "A").End(xlUp).row 'change to column containing last row up to which you want to format
End With
End Function
Public Sub AddFormatting(ByVal formatRange As Range, ByVal codeColoursDictionary As Dictionary) 'note pass as object if late binding
Dim key As Variant
Dim counter As Long
For Each key In codeColoursDictionary.Keys
counter = counter + 1
With formatRange
.FormatConditions.Add Type:=xlExpression, Formula1:="=$A2=""" & key & """"
.FormatConditions(counter).StopIfTrue = False
With .FormatConditions(counter).Interior
.PatternColorIndex = xlAutomatic
.Color = codeColoursDictionary(key)
' .TintAndShade = 0
End With
End With
Next key
End Sub
Data in sheet after run:
Version 2 for OP
Option Explicit
Public Sub FormatMatchingCodes2()
Dim wb As Workbook
Dim wsTarget As Worksheet
Set wb = ThisWorkbook
Set wsTarget = wb.Worksheets("Sheet9") 'change as appropriate
Dim lastRow As Long
Application.ScreenUpdating = False
lastRow = GetLastRow(wsTarget)
Dim formatRange As Range
If Not lastRow <= 2 Then
Set formatRange = wsTarget.Range("A2:G" & lastRow) 'Excludes header row
Else
MsgBox "End row is before start row"
Exit Sub
End If
Dim codeColoursDictionary As Dictionary
Set codeColoursDictionary = GetDistinctCodeCount(formatRange.Value2)
wsTarget.Cells.FormatConditions.Delete
AddFormatting formatRange, codeColoursDictionary
Application.ScreenUpdating = True
End Sub
Public Function GetDistinctCodeCount(ByVal sourceData As Variant) As Dictionary 'as object if latebound
''LATE binding
' Dim distinctDict As Object
' Set distinctDict = CreateObject("Scripting.Dictionary")
''Early binding add reference to VBE > tools > references > Microsoft scripting runtime
Dim distinctDict As Scripting.Dictionary
Set distinctDict = New Scripting.Dictionary
Dim currentCode As Long
For currentCode = LBound(sourceData, 1) To UBound(sourceData, 1)
If Not distinctDict.exists(sourceData(currentCode, 5)) Then
distinctDict.Add sourceData(currentCode, 5), Application.WorksheetFunction.RandBetween(13434828, 17777777) + distinctDict.Count
End If
Next currentCode
Set GetDistinctCodeCount = distinctDict
End Function
Public Function GetLastRow(ByVal wsTarget As Worksheet) As Long
With wsTarget
GetLastRow = .Cells(.Rows.Count, "E").End(xlUp).row 'change to column containing last row up to which you want to format
End With
End Function
Public Sub AddFormatting(ByVal formatRange As Range, ByVal codeColoursDictionary As Dictionary) 'note pass as object if late binding
Dim key As Variant
Dim counter As Long
For Each key In codeColoursDictionary.Keys
counter = counter + 1
With formatRange
.FormatConditions.Add Type:=xlExpression, Formula1:="=$E2=""" & key & """"
.FormatConditions(counter).StopIfTrue = False
With .FormatConditions(counter).Interior
.PatternColorIndex = xlAutomatic
.Color = codeColoursDictionary(key)
' .TintAndShade = 0
End With
End With
Next key
End Sub

VBA 'Vlookup' function operating on dynamic number of rows

I am not sure how to combine a Function with a Sub. Most likely, the Sub I have below needs corrections.
I have two tables in two separate sheets: Sheet1 and Sheet2.
Both tables have dynamic number of rows but the first rows always start in the same place and the number of columns in both tables is constant, too. Sheet1 data starts in A2 and ends in R2:R and Sheet2 data starts in A3 and ends in H3:H.
I am trying to implement VLOOkUP in column O of Sheet1, that would populate each cell in column O of Sheet1 with relevant values of column D in Sheet2. So far I managed to come up with code as below.
Public Function fsVlookup(ByVal pSearch As Range, ByVal pMatrix As Range, ByVal pMatColNum As Integer) As String
Dim s As String
On Error Resume Next
s = Application.WorksheetFunction.VLookup(pSearch, pMatrix, pMatColNum, False)
If IsError(s) Then
fsVlookup = ""
Else
fsVlookup = s
End If
End Function
Public Sub Delinquency2()
Dim ws1 As Worksheet, ws2 As Worksheet
Dim rng As Range
Dim rCell As Range
Set ws1 = Worksheets("Sheet1")
Set ws2 = Worksheets("Sheet2")
pSearch = ws1.Range("D2:D" & Cells(Rows.Count, "A").End(xlDown).Row)
pMatrix = ws2.Range("$A3:$H" & Cells(Rows.Count, "C").End(xlDown).Row)
pMatColNum = 4
Set rng = ws1.Range("O2:O" & Cells(Rows.Count, "A").End(xlDown).Row)
For Each rCell In rng.Cells
With rCell
rCell.FormulaR1C1 = s
End With
Next rCell
End Sub
You will need to call the function in your sub using a similar line to below. It then takes your values from your sub and inputs them into the function and returns the value.
You need to dim the ranges in order for them to be recognized correctly in your function. I have updated your code to make it work and you can fiddle around with it to make it work the way you want it to. I also updated a few other spots to figure out the correct ranges, you don't want to use xlDown where you were using it, causes an enormous loop covering cells you don't want it to.
Public Function fsVlookup(ByVal pSearch As Range, ByVal pMatrix As Range, ByVal pMatColNum As Integer) As String
Dim s As String
On Error Resume Next
s = Application.WorksheetFunction.VLookup(pSearch, pMatrix, pMatColNum, False)
If IsError(s) Then
fsVlookup = ""
Else
fsVlookup = s
End If
End Function.
Public Sub Delinquency2()
Dim ws1 As Worksheet, ws2 As Worksheet
Dim rng As Range
Dim rCell As Range, pMatrix As Range
Set ws1 = Worksheets("Sheet1")
Set ws2 = Worksheets("Sheet2")
pSearchCol = ws1.Range("D2:D2").Column
Set pMatrix = ws2.Range("$A3:$H" & ws2.Cells(Rows.Count, "C").End(xlUp).Row)
pMatColNum = 4
Set rng = ws1.Range("O2:O" & ws1.Cells(Rows.Count, "A").End(xlUp).Row)
For Each rCell In rng.Cells
With rCell
rCell.Value = fsVlookup(ws1.Cells(rCell.Row, pSearchCol), pMatrix, pMatColNum)
End With
Next rCell
End Sub

Find row with multiple search criteria on substrings in VBA

I need to find the row where a cell in column B contains two substrings.
For example these Strings in B1:B3
A string with Cows
Cows and stuff
A string with Chickens
I need to find the row B2where the 2 substrings Cows and shit are present.
What i tried so far:
Find formula that doesent do multiple search criteria. :(
=MATCH(1;INDEX((B:B="Cows")*(B:B="shit"););) that doesent do substrings
A lot other stuff i forgot,
If it is possible i would like a pure VBA solution.
Any ideas on this one?
http://www.mrexcel.com/forum/excel-questions/74933-matching-multiple-criteria-visual-basic-applications.html This is what you are looking for with your MATCH function.
The below will assign all values within column B to an array, and then assess each element of the array to see if it contains the strings "Cows" and "excrement".
To assess the string within the element, we use the InStr() Function.
Sub findStrings()
Dim wb As Workbook, ws As Worksheet
Dim arrValues() As Variant
Dim lrow As Long, i As Long
Set wb = ThisWorkbook
Set ws = wb.Sheets(1)
lrow = ws.Cells(Rows.Count, 2).End(xlUp).Row
arrValues = Range(Cells(1, 2), Cells(lrow, 2))
For i = 1 To UBound(arrValues, 1)
If InStr(1, arrValues(i, 1), "Cows") Then
If InStr(1, arrValues(i, 1), "excrement") Then
MsgBox ("Cell " & Cells(i, 2).Address & " contains both strings.")
Exit Sub
End If
End If
Next i
End Sub
This will only find 1 match containing the strings you specify, if you require further matches then you will need a different solution.
This function returns range that contains all the cells having in its content both words given as parameters:
Public Function findCellsWithWords(firstWord As String, secondWord As String) As Excel.Range
Dim wks As Excel.Worksheet
Dim rng As Excel.Range
Dim rngFirst As Excel.Range
Dim rngSecond As Excel.Range
'--------------------------------------------------
Set wks = Excel.ActiveSheet
Set rng = wks.Columns(2).EntireColumn
Set rngFirst = findAll(rng, firstWord)
Set rngSecond = findAll(rng, secondWord)
Set findCellsWithWords = Excel.Intersect(rngFirst, rngSecond)
End Function
Public Function findAll(rng As Excel.Range, what As Variant) As Excel.Range
Dim rngResult As Excel.Range
Dim found As Excel.Range
Dim firstFound As String
'----------------------------------------------------------------------------
With rng
Set found = rng.Find(what)
Do Until found Is Nothing
If rngResult Is Nothing Then
firstFound = found.Address
Set rngResult = found
Else
Set rngResult = Excel.Union(rngResult, found)
End If
'Find next occurrence.
Set found = .FindNext(found)
If found.Address = firstFound Then Exit Do
Loop
End With
Set findAll = rngResult
End Function

Speed up loop in excel

I had some great help to get this search tool working in excel but I was wondering if there is room for speed improvement. I did some research and with what little I understand about VB for i = LBOUND(array) To UBOUND(array) seems most optimal. Would 'For Each' be faster? I am wondering if there is a way to isolate the records currently in the worksheet, or if it is already doing this with L/UBOUND? If it is, is there a way to do 'ignore special characters' similar to SQL? After adding screenupdating and calculation, I was able to shave about 10 seconds off of the total run time. And further I was using FormulaR1C1 for my search before this new loop and it would limit the amount of columns to search while being super fast.
Range("W2:W" & LastRow).FormulaR1C1 = _
"=IF(ISERR(SEARCH(R1C23,RC[-22]&RC[-21]&RC[-20]&RC[-19]&RC[-18]&RC[-17]&RC[-16]&RC[-15]&RC[-15]&RC[-14]&RC[-13]&RC[-12]&RC[-11]&RC[-10]&RC[-9]&RC[-8]&RC[-7]&RC[-6]&RC[-5]&RC[-4]&RC[-3]&RC[-2]&RC[-1])),0,1)"
If WorksheetFunction.CountIf(Columns(23), 1) = 0 Then
Columns(23).Delete
Any help or recommendations are greatly appreciated.
Sub FindFeature()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim shResults As Worksheet
Dim vaData As Variant
Dim i As Long, j As Long
Dim sSearchTerm As String
Dim sData As String
Dim rNext As Range
Dim v As Variant
Dim vaDataCopy As Variant
Dim uRange As Range
Dim findRange As Range
Dim nxtRange As Range
Dim ws As Range
'Put all the data into an array
vaData = ActiveSheet.UsedRange.Value
'Get the search term
sSearchTerm = Application.InputBox("What are you looking for?")
'Define and clear the results sheet
Set shResults = ActiveWorkbook.Worksheets("Results")
shResults.Range("A3").Resize(shResults.UsedRange.Rows.Count, 1).EntireRow.Delete
Set uRange = ActiveSheet.UsedRange
vaData = uRange.Value
vaDataCopy = vaData
For Each v In vaDataCopy
v = Anglicize(v)
Next
Application.WorksheetFunction.Transpose (vaDataCopy)
ActiveSheet.UsedRange.Value = vaDataCopy
'Loop through the data
Set ws = Cells.Find(What:=uRange, After:="ActiveCell", LookIn:=xlValues, LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
If Not ws Is Nothing Then
Set findRange = ws
Do
Set nxtRange = Cells.FindNext(After:=ws)
Set findRange = nxtRange
Loop Until ws.Address = findRange.Address
ActiveSheet.UsedRange.Value = vaData
'Write the row to the next available row on Results
Set rNext = shResults.Cells(shResults.Rows.Count, 1).End(xlUp).Offset(1, 0)
rNext.Resize(1, uRange(vaData, 2)).Value = Application.Index(vaData, i, 0)
'Stop looking in that row after one match
End If
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
Ultimately, the execution speed here is severely hampered by the apparent requirement to operate on every cell in the range, and because you're asking about performance, I suspect this range may contain many thousands of cells. There are two things I can think of:
1. Save your results in an array and write to the Results worksheet in one statement
Try replacing this:
'Write the row to the next available row on Results
Set rNext = shResults.Cells(shResults.Rows.Count, 1).End(xlUp).Offset(1, 0)
rNext.Resize(1, UBound(vaData, 2)).Value = Application.Index(vaData, i, 0)
'Stop looking in that row after one match
Exit For
With a statement that assigns the value Application.Index(vaData, i, 0) to an array variable, and then when you're completed the For i loop, you can write the results in one pass to the results worksheet.
NOTE This may be noticeably faster if and only if there are many thousands of results. If there are only a few results expected, then exeuction speed is primarily affected by the need to iterate over every cell, not the operation of writing the results to another sheet.
2. Use another method than cell iteration
If you can implement this method, I would use it in conjunction with the above.
Ordinarily I would recommend using the .Find and .FindNext methods as considerably more efficient than using the i,j iteration. But since you need to use the Anglicize UDF on every cell in the range, you would need to make some restructure your code to accommodate. Might require multiple loops, for example, first Anglicize the vaData and preserve a copy of the non-Anglicized data, like:
Dim r as Long, c as Long
Dim vaDataCopy as Variant
Dim uRange as Range
Set uRange = ActiveSheet.UsedRange
vaData = uRange.Value
vaDataCopy = vaData
For r = 1 to Ubound(varDataCopy,1)
For c = 1 to Ubound(varDataCopy,2)
varDataCopy(r,c) = Anglicize(varDataCopy(r,c))
Next
Next
Then, put the Anglicize version on to the worksheet.
ActiveSheet.UsedRange.Value = vaDataCopy
Then, instead of the For i =... For j =... loop, use the .Find and .FindNext method on the uRange object.
Here is an example of how I implement Find/FindNext.
Finally, put the non-Anglicized version back on the worksheet, again with the caveat that it might require use of Transpose function:
ActiveSheet.UsedRange.Value = vaData
Whil this still iterates over every value to perform the Anglicize function, it does not operate on every value a second time (Instr function). So, you're essentially operating on the values only once, rather than twice. I suspect this should be much faster, especially if you combine it with the #1 above.
UPDATE BASED ON OP REVISION EFFORTS
After some comment discussion & emails back and forth, we arrive at this solution:
Option Explicit
Sub FindFeature()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim shSearch As Worksheet:
Dim shResults As Worksheet
Dim vaData As Variant
Dim i As Long, j As Long, r As Long, c As Long
Dim sSearchTerm As String
Dim sData As String
Dim rNext As Range
Dim v As Variant
Dim vaDataCopy As Variant
Dim uRange As Range
Dim findRange As Range
Dim nxtRange As Range
Dim rng As Range
Dim foundRows As Object
Dim k As Variant
Set shSearch = Sheets("City")
shSearch.Activate
'Define and clear the results sheet
Set shResults = ActiveWorkbook.Worksheets("Results")
shResults.Range("A3").Resize(shResults.UsedRange.Rows.Count, 1).EntireRow.Delete
'# Create a dictionary to store our result rows
Set foundRows = CreateObject("Scripting.Dictionary")
'Get the search term
sSearchTerm = Application.InputBox("What are you looking for?")
'# set and fill our range/array variables
Set uRange = shSearch.UsedRange
vaData = uRange.Value
vaDataCopy = Application.Transpose(vaData)
For r = 1 To UBound(vaDataCopy, 1)
For c = 1 To UBound(vaDataCopy, 2)
'MsgBox uRange.Address
vaDataCopy(r, c) = Anglicize(vaDataCopy(r, c))
Next
Next
'# Temporarily put the anglicized text on the worksheet
uRange.Value = Application.Transpose(vaDataCopy)
'# Loop through the data, finding instances of the sSearchTerm
With uRange
.Cells(1, 1).Activate
Set rng = .Cells.Find(What:=sSearchTerm, After:=ActiveCell, _
LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
If Not rng Is Nothing Then
Set findRange = rng
Do
Set nxtRange = .Cells.FindNext(After:=findRange)
Debug.Print sSearchTerm & " found at " & nxtRange.Address
If Not foundRows.Exists(nxtRange.Row) Then
'# Make sure we're not storing the same row# multiple times.
'# store the row# in a Dictionary
foundRows.Add nxtRange.Row, nxtRange.Column
End If
Set findRange = nxtRange
'# iterate over all matches, but stop when the FindNext brings us back to the first match
Loop Until findRange.Address = rng.Address
'# Iterate over the keys in the Dictionary. This contains the ROW# where a match was found
For Each k In foundRows.Keys
'# Find the next empty row on results page:
With shResults
Set rNext = .Cells(.Rows.Count, 1).End(xlUp).Offset(1, 0). _
Resize(1, UBound(Application.Transpose(vaData), 1))
End With
'# Write the row to the next available row on Results
rNext.Value = Application.Index(vaData, k, 0)
Next
Else:
MsgBox sSearchTerm & " was not found"
End If
End With
'# Put the non-Anglicized values back on the sheet
uRange.Value = vaData
'# Restore application properties
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
'# Display the results
shResults.Activate
End Sub
Public Function Anglicize(ByVal sInput As String) As String
Dim vaGood As Variant
Dim vaBad As Variant
Dim i As Long
Dim sReturn As String
Dim c As Range
'Replace any 'bad' characters with 'good' characters
vaGood = Split("S,Z,s,z,Y,A,A,A,A,A,A,C,E,E,E,E,I,I,I,I,D,N,O,O,O,O,O,U,U,U,U,Y,a,a,a,a,a,a,c,e,e,e,e,i,i,i,i,d,n,o,o,o,o,o,u,u,u,u,y,y", ",")
vaBad = Split("Š,Ž,š,ž,Ÿ,À,Á,Â,Ã,Ä,Å,Ç,È,É,Ê,Ë,Ì,Í,Î,Ï,Ð,Ñ,Ò,Ó,Ô,Õ,Ö,Ù,Ú,Û,Ü,Ý,à,á,â,ã,ä,å,ç,è,é,ê,ë,ì,í,î,ï,ð,ñ,ò,ó,ô,õ,ö,ù,ú,û,ü,ý,ÿ", ",")
sReturn = sInput
Set c = Range("D1:G1")
For i = LBound(vaBad) To UBound(vaBad)
sReturn = Replace$(sReturn, vaBad(i), vaGood(i))
Next i
Anglicize = sReturn
'Sheets("Results").Activate
End Function