The small(er) things before we tackle the elephant in the room:
Naming
Your names aren't terrible, but could be a lot better. However, whatever you name things, you should comply with standard naming conventions. To wit:
Local Variables: Written in camelCase
.
Dim localVariable As String
includes method arguments.
Module / Global Variables: Written in PascalCase
.
Private ModuleVariable As String
Global PublicVariable As Long
Method Names: Verbs. Written in PascalCase
Private Function ReturnThisValue() As Long
Public Sub DoThisThing()
Constants: Written in SHOUTY_SNAKE_CASE
Public Const CONSTANT_VALUE As String = "This Value Never Changes"
Also, Block
is declared and set but never used. It should be removed.
Don't use :
Just don't. Keep your instructions on separate lines. They're too easy to miss and violate too many conventions.
Put things in Variables
Range("C" & i)
You see this? This is telling Excel to go and find that range. Every time you write it. What if you want to check a different column? Right now, you'll have to rewrite the declaration on 20 different lines.
Instead, put it in a variable then just reference the Variable. Now, if the variables need to change, you only have to change them in 1 place, and the rest takes care of itself.
Dim cCell As Range, dCell As Range, eCell As Range
Set cCell = Range("C" & i)
Set dCell = Range("D" & i)
Set eCell = Range("E" & i)
Dim countryName As String
countryName = "Canada"
If Instr(1, cCell, countryName) Or Instr(1, dCell, countryName) Or Instr(1, eCell, countryName) Then
...
...
Don't Repeat Yourself
Also known as DRY. Take your i = i + 1
statement. That is always going to happen. So why write it 20 times when you can just put it at the start or end of your loop?
While i <= lastRow
Code
Code
Code
...
i = i + 1
Wend
Boom. 12 lines of code gone
And now the big stuff:
Refactoring
Refactoring is the process of splitting one big thing into many little things.
Any time you find yourself copy-pasting code, you should be thinking "Hmm, this can probably be turned into a method of some kind".
1st Refactoring
This check:
If Instr(1, cCell, countryName) Or Instr(1, dCell, countryName) Or Instr(1, eCell, countryName) Then
Can be a Separate Method Like so:
Public Function NameIsInRange(ByVal searchName As String, ByRef range1 As Range, range2 As Range, range3 As Range) As Boolean
Dim result As Boolean
result = InStr(1, range1, searchName) Or InStr(1, range2, searchName) Or InStr(1, range3, searchName)
NameIsInRange = result
End Function
And now we're down to:
Sub PullCountries()
Dim i As Long
i = 1
Dim LastRow As Long
LastRow = Range("B1").End(xlDown).Row
While i <= LastRow
Dim resultRange As Range
Set resultRange = Range("A" & i)
Dim cCell As Range, dCell As Range, eCell As Range
Set cCell = Range("C" & i)
Set dCell = Range("D" & i)
Set eCell = Range("E" & i)
If NameIsInRange("Canada", cCell, dCell, eCell) Then
resultRange = "Canada"
ElseIf NameIsInRange("United States", cCell, dCell, eCell) Then
resultRange = "United States"
ElseIf NameIsInRange("Britian", cCell, dCell, eCell) Then
resultRange = "UK"
ElseIf NameIsInRange("UK", cCell, dCell, eCell) Then
resultRange = "UK"
ElseIf NameIsInRange("Spain", cCell, dCell, eCell) Then
resultRange = "Spain"
ElseIf NameIsInRange("Portugal", cCell, dCell, eCell) Then
resultRange = "Portugal"
ElseIf NameIsInRange("Ireland", cCell, dCell, eCell) Then
resultRange = "Ireland"
ElseIf NameIsInRange("Japan", cCell, dCell, eCell) Then
resultRange = "Japan"
ElseIf NameIsInRange("Greece", cCell, dCell, eCell) Then
resultRange = "Greece"
ElseIf NameIsInRange("Italy", cCell, dCell, eCell) Then
resultRange = "Italy"
End If
i = i + 1
Wend
End Sub
Public Function NameIsInRange(ByVal searchName As String, ByRef range1 As Range, range2 As Range, range3 As Range) As Boolean
Dim result As Boolean
result = InStr(1, range1, searchName) Or InStr(1, range2, searchName) Or InStr(1, range3, searchName)
NameIsInRange = result
End Function
2nd Refactoring
The only things that actually change in the loop are the name to check and the name to output. So, why don't we make those a list? For an iterable list with more than 1 element per line, I'd use an Array
.
Let's make a new sheet and give it the codename wsCountryNames
. Then a function to get the table and pass it to an Array:
Public Function GetCountryNamesTable() As Variant
With wsCountryNames
Dim topLeftCell As Range
Set topLeftCell = .Cells(1, 1) '/ "A1"
Dim finalRow As Long
finalRow = .Cells(.Rows.Count, topLeftCell.Column).End(xlUp).Row
Dim tableWidth As Long
tableWidth = 2
Dim tableRange As Range
Set tableRange = .Range(topLeftCell, Cells(finalRow, topLeftCell.Column + tableWidth - 1))
End With
GetCountryNamesTable = tableRange
End Function
The value in Cell A1
is now in GetCountryNamesTable(1, 1)
, A2, (2, 1)
, B1, (1, 2)
etc.
Now, we can just iterate through your list:
Sub PullCountries()
Dim i As Long, j As Long
i = 1
Dim LastRow As Long
LastRow = Range("B1").End(xlDown).Row
Dim namesList As Variant
namesList = GetCountryNamesTable
Dim searchName As String, displayName As String
While i <= LastRow
Dim resultRange As Range
Set resultRange = Range("A" & i)
Dim cCell As Range, dCell As Range, eCell As Range
Set cCell = Range("C" & i)
Set dCell = Range("D" & i)
Set eCell = Range("E" & i)
For j = LBound(namesList, 1) + 1 To UBound(namesList, 1) '/ +1 for header row
searchName = namesList(j, 1)
displayName = namesList(j, 2)
If NameIsInRange(searchName, cCell, dCell, eCell) Then
resultRange = displayName
Exit For '/ We found our result so we can terminate the loop early
End If
Next j
i = i + 1
Wend
End Sub
Public Function NameIsInRange(ByVal searchName As String, ByRef range1 As Range, range2 As Range, range3 As Range) As Boolean
Dim result As Boolean
result = InStr(1, range1, searchName) Or InStr(1, range2, searchName) Or InStr(1, range3, searchName)
NameIsInRange = result
End Function
Public Function GetCountryNamesTable() As Variant
With wsCountryNames
Dim topLeftCell As Range
Set topLeftCell = .Cells(1, 1) '/ "A1"
Dim finalRow As Long
finalRow = .Cells(.Rows.Count, topLeftCell.Column).End(xlUp).Row
Dim tableWidth As Long
tableWidth = 2
Dim tableRange As Range
Set tableRange = .Range(topLeftCell, Cells(finalRow, topLeftCell.Column + tableWidth - 1))
End With
GetCountryNamesTable = tableRange
End Function
3rd Refactoring
I'm going to put your search range in an Array:
Public Function GetSearchRange() As Variant
With (codename of your sheet here, call it wsSearchSheet for now)
Dim topLeftCell As Range
Set topLeftCell = .Cells(1, 1) '/ "A1"
Dim finalRow As Long
finalRow = .Cells(.Rows.Count, 2).End(xlUp).Row '/ "2" for "B" column
Dim finalCol As Long
finalCol = 5 '/ "E" column
Dim tableRange As Range
Set tableRange = .Range(topLeftCell, Cells(finalRow, finalCol))
End With
GetCountryNamesTable = tableRange
End Function
And Re-Jig the IsInRange
function to deal with an array value instead:
Public Function ValueContainsString(ByVal valueToSearch As Variant, ByVal searchString As String) As Boolean
ValueContainsString = InStr(1, CStr(valueToSearch), searchString)
End Function
And implement these changes to the main sub:
Option Explicit
Sub PullCountries()
Dim i As Long, j As Long, k As Long
Dim namesList As Variant
namesList = GetCountryNamesTable
Dim searchNameCol As Long, displayNameCol As Long
searchNameCol = 1
displayNameCol = 2
Dim searchArray As Variant
searchArray = GetSearchValues
Dim searchStartCol As Long, searchEndCol As Long
searchStartCol = 3
searchEndCol = 5
Dim outputCol As Long
outputCol = 1
Dim foundMatch As Boolean
Dim valueToSearch As Variant
Dim searchName As String, displayName As String
For i = LBound(searchArray, 1) To UBound(searchArray, 1)
foundMatch = False
For j = searchStartCol To searchEndCol
valueToSearch = searchArray(i, j)
For k = LBound(namesList, 1) + 1 To UBound(namesList, 1) '/ +1 for header row
searchName = namesList(k, searchNameCol)
If ValueContainsString(valueToSearch, searchName) Then
displayName = namesList(k, displayNameCol)
searchArray(i, outputCol) = displayName
foundMatch = True
Exit For '/ We found our result so we can terminate the loop early
End If
Next k
If foundMatch Then Exit For
Next j
Next i
'/ Read output back to sheet.
For i = LBound(searchArray, 1) To UBound(searchArray, 1)
Range("A" & i) = searchArray(i, outputCol)
Next i
End Sub
Public Function ValueContainsString(ByVal valueToSearch As Variant, ByVal searchString As String) As Boolean
ValueContainsString = InStr(1, CStr(valueToSearch), searchString)
End Function
Public Function GetCountryNamesTable() As Variant
With wsCountryNames
Dim topLeftCell As Range
Set topLeftCell = .Cells(1, 1) '/ "A1"
Dim finalRow As Long
finalRow = .Cells(.Rows.Count, topLeftCell.Column).End(xlUp).Row
Dim tableWidth As Long
tableWidth = 2
Dim tableRange As Range
Set tableRange = .Range(topLeftCell, Cells(finalRow, topLeftCell.Column + tableWidth - 1))
End With
GetCountryNamesTable = tableRange
End Function
Public Function GetSearchValues() As Variant
With (codename of your sheet here, call it wsSearchSheet for now)
Dim topLeftCell As Range
Set topLeftCell = .Cells(1, 1) '/ "A1"
Dim finalRow As Long
finalRow = .Cells(.Rows.Count, 2).End(xlUp).Row '/ "2" for "B" column
Dim finalCol As Long
finalCol = 5 '/ "E" column
Dim tableRange As Range
Set tableRange = .Range(topLeftCell, Cells(finalRow, finalCol))
End With
GetSearchValues = tableRange
End Function
Want to change your country names? Just change the values in your table. Your data moves about? Just change the GetSearchValues
targets. You can extend either as far as you like.
Total number of values you now have to change in your code: Maybe 10. And then only if your sheet data moves around positions.
Codenames Addendum
Codenames are big and clever. Every worksheet and workbook has a "name" that the user can see and change.
MyCurrentWB.Worksheets("Country Names")
is referencing a sheet name.
A Codename on the other hand is a secret name that can only be set/changed in the IDE.
the name in brackets is the "name". The name not in brackets is the "codename". It is set in the properties window.
If you give a sheet a codename then the user can change the name as much as they like, all you have to do is use
wsCountryNames.Cells()
in your code and it will keep running.