Option Explicit
Dim mlRankOffset() As Long
Dim msngRankPercentage() As Single
Dim mlBestMatchPtr As Long
Const msngSoundexMatchPercent As Single = 0.95
Const msngMetaphoneMatchPercent As Single = 0.95
Function FuzzyCount(ByVal LookupValue As String, _
ByVal TableArray As Range, _
Optional NFPercent As Single = 0.05, _
Optional Algorithm As Variant = 3) As Long
Dim lMatchCount As Long
Dim rCur As Range
Dim sString1 As String
Dim sString2 As String
sString1 = LCase$(Application.Trim(LookupValue))
For Each rCur In Intersect(TableArray.Resize(, 1), Sheets(TableArray.Parent.Name).UsedRange)
sString2 = LCase$(Application.Trim(CStr(rCur)))
If sString2 <> "" Then
If FuzzyPercent(String1:=sString1, _
String2:=sString2, _
Algorithm:=Algorithm, _
Normalised:=False) >= NFPercent Then
lMatchCount = lMatchCount + 1
End If
End If
Next rCur
FuzzyCount = lMatchCount
End Function
Function FuzzyPercent(ByVal String1 As String, _
ByVal String2 As String, _
Optional Algorithm As Variant = 3, _
Optional Normalised As Boolean = False) As Single
Dim bSoundex As Boolean
Dim bBasicMetaphone As Boolean
Dim intLen1 As Integer, intLen2 As Integer
Dim intCurLen As Integer
Dim intTo As Integer
Dim intPos As Integer
Dim intPtr As Integer
Dim intScore As Integer
Dim intTotScore As Integer
Dim intStartPos As Integer
Dim lngAlgorithm As Long
Dim sngScore As Single
Dim strWork As String
bSoundex = LCase$(CStr(Algorithm)) = "soundex"
bBasicMetaphone = LCase$(CStr(Algorithm)) = "metaphone"
If Normalised = False Then
If bSoundex Or bBasicMetaphone Then
String1 = NormaliseStringAtoZ(String1)
String2 = NormaliseStringAtoZ(String2)
Else
String1 = LCase$(Application.Trim(String1))
String2 = LCase$(Application.Trim(String2))
End If
End If
If String1 = String2 Then
FuzzyPercent = 1
Exit Function
End If
If bSoundex Then
String1 = Soundex(Replace(String1, " ", ""))
String2 = Soundex(Replace(String2, " ", ""))
If String1 = String2 Then
FuzzyPercent = msngSoundexMatchPercent
Else
FuzzyPercent = 0
End If
Exit Function
ElseIf bBasicMetaphone Then
String1 = Metaphone1(String1)
String2 = Metaphone1(String2)
If String1 = String2 Then
FuzzyPercent = msngMetaphoneMatchPercent
Else
FuzzyPercent = 0
End If
Exit Function
End If
intLen1 = Len(String1)
intLen2 = Len(String2)
If intLen1 = 0 Or intLen2 = 0 Then
FuzzyPercent = 0
Exit Function
End If
If intLen1 < 2 Then
FuzzyPercent = 0
Exit Function
End If
intTotScore = 0
intScore = 0
lngAlgorithm = Val(Algorithm)
If (lngAlgorithm And 1) <> 0 Then
If intLen1 < intLen2 Then
FuzzyAlg1 String1, String2, intScore, intTotScore
Else
FuzzyAlg1 String2, String1, intScore, intTotScore
End If
End If
If (lngAlgorithm And 2) <> 0 Then
If intLen1 < intLen2 Then
FuzzyAlg2 String1, String2, intScore, intTotScore
Else
FuzzyAlg2 String2, String1, intScore, intTotScore
End If
End If
If (lngAlgorithm And 4) <> 0 Then
If intLen1 < intLen2 Then
sngScore = GetLevenshteinPercentMatch(String1:=String1, _
String2:=String2, _
Normalised:=True)
Else
sngScore = GetLevenshteinPercentMatch(String1:=String2, _
String2:=String1, _
Normalised:=True)
End If
intScore = intScore + (sngScore * 100)
intTotScore = intTotScore + 100
End If
FuzzyPercent = intScore / intTotScore
End Function
Private Sub FuzzyAlg1(ByVal String1 As String, _
ByVal String2 As String, _
ByRef Score As Integer, _
ByRef TotScore As Integer)
Dim intLen1 As Integer, intPos As Integer, intPtr As Integer, intStartPos As Integer
intLen1 = Len(String1)
TotScore = TotScore + intLen1
intPos = 0
For intPtr = 1 To intLen1
intStartPos = intPos + 1
intPos = InStr(intStartPos, String2, Mid$(String1, intPtr, 1))
If intPos > 0 Then
If intPos > intStartPos + 3 Then
intPos = intStartPos
Else
Score = Score + 1
End If
Else
intPos = intStartPos
End If
Next intPtr
End Sub
Private Sub FuzzyAlg2(ByVal String1 As String, _
ByVal String2 As String, _
ByRef Score As Integer, _
ByRef TotScore As Integer)
Dim intCurLen As Integer, intLen1 As Integer, intTo As Integer, intPtr As Integer, intPos As Integer
Dim strWork As String
intLen1 = Len(String1)
For intCurLen = 1 To intLen1
strWork = String2
intTo = intLen1 - intCurLen + 1
TotScore = TotScore + Int(intLen1 / intCurLen)
For intPtr = 1 To intTo Step intCurLen
intPos = InStr(strWork, Mid$(String1, intPtr, intCurLen))
If intPos > 0 Then
Mid$(strWork, intPos, intCurLen) = String$(intCurLen, &H0)
Score = Score + 1
End If
Next intPtr
Next intCurLen
End Sub
Public Function GetLevenshteinPercentMatch(ByVal String1 As String, _
ByVal String2 As String, _
Optional Normalised As Boolean = False) As Single
Dim iLen As Integer
If Normalised = False Then
String1 = UCase$(WorksheetFunction.Trim(String1))
String2 = UCase$(WorksheetFunction.Trim(String2))
End If
iLen = WorksheetFunction.Max(Len(String1), Len(String2))
GetLevenshteinPercentMatch = (iLen - LevenshteinDistance(String1, String2)) / iLen
End Function
Public Function LevenshteinDistance(ByVal s As String, ByVal t As String) As Integer
Dim d() As Integer
Dim m As Integer
Dim N As Integer
Dim I As Integer
Dim j As Integer
Dim s_i As String
Dim t_j As String
Dim cost As Integer
N = Len(s)
m = Len(t)
If N = 0 Then
LevenshteinDistance = m
Exit Function
End If
If m = 0 Then
LevenshteinDistance = N
Exit Function
End If
ReDim d(0 To N, 0 To m) As Integer
For I = 0 To N
d(I, 0) = I
Next I
For j = 0 To m
d(0, j) = j
Next j
For I = 1 To N
s_i = Mid$(s, I, 1)
For j = 1 To m
t_j = Mid$(t, j, 1)
If s_i = t_j Then
cost = 0
Else
cost = 1
End If
d(I, j) = WorksheetFunction.Min(d(I - 1, j) + 1, d(I, j - 1) + 1, d(I - 1, j - 1) + cost)
Next j
Next I
LevenshteinDistance = d(N, m)
End Function
Function FuzzyVLookup(ByVal LookupValue As String, _
ByVal TableArray As Range, _
ByVal IndexNum As Integer, _
Optional NFPercent As String = "5%", _
Optional Rank As String = "1", _
Optional Algorithm As Variant = 3, _
Optional AdditionalCols As Integer = 0, _
Optional LookupColOffset As Integer = 0, _
Optional GroupColOffset As String = "0", _
Optional GroupValue As Variant = "") As Variant
Dim bWanted As Boolean
Dim bError As Boolean
Dim iPtr1 As Integer
Dim iPtr2 As Integer
Dim iaRank() As Integer
Dim iaRankLowHigh() As Integer
Dim iRankHigh As Integer
Dim iRankLow As Long
Dim iRankStep As Integer
Dim lEndRow As Long
Dim iaGroupColOffsets() As Integer
Dim iGroupHighestOffset As Integer
Dim rCur As Range
Dim rSearchRange As Range
Dim sngNFPercent As Single
Dim sngaNFPercent() As Single
Dim sChar As String
Dim sPrevChar As String
Dim sRankDelimiter As String
Dim sResult As String
Dim strNFPercent As String
Dim straNFPercent() As String
Dim straGroupColOffsets() As String
Dim vCurValue As Variant
Dim vCurValues As Variant
Dim wsTable As Worksheet
LookupValue = LCase$(Application.Trim(LookupValue))
If IsNumeric(Algorithm) = False Then
If LCase$(CStr(Algorithm)) = "soundex" Then
Algorithm = "soundex"
ElseIf LCase$(CStr(Algorithm)) = "metaphone" Then
Algorithm = "metaphone"
Else
FuzzyVLookup = "*** 'Algorithm' parameter invalid ***"
Exit Function
End If
End If
If IsMissing(NFPercent) Then
sngaNFPercent(0) = 0.05
sngaNFPercent(1) = 1
Else
straNFPercent = Split(NFPercent, "~")
ReDim sngaNFPercent(0 To 1)
If UBound(straNFPercent) = 0 Then
ReDim Preserve straNFPercent(0 To 1)
straNFPercent(1) = "1"
End If
For iPtr1 = 0 To 1
sngaNFPercent(iPtr1) = Val(straNFPercent(iPtr1))
If sngaNFPercent(iPtr1) > 1 Then sngaNFPercent(iPtr1) = sngaNFPercent(iPtr1) / 100
If (sngaNFPercent(iPtr1) <= 0) Or (sngaNFPercent(iPtr1) > 1) Then
FuzzyVLookup = "*** 'NFPercent' is invalid ***"
Exit Function
End If
Next iPtr1
sngNFPercent = sngaNFPercent(0)
If sngaNFPercent(0) > sngaNFPercent(1) Then
sngNFPercent = sngaNFPercent(1)
sngaNFPercent(1) = sngaNFPercent(0)
sngaNFPercent(0) = sngNFPercent
End If
End If
ReDim iaRank(1 To 2)
sRankDelimiter = ""
If IsNumeric(Rank) Then
iaRank(1) = Val(Rank)
iaRank(2) = iaRank(1)
iRankLow = iaRank(1)
iRankHigh = iaRank(1)
If iaRank(1) < 1 Then
FuzzyVLookup = "*** 'Rank' must be a positive integer or format integer-delimiter-integer ***"
Exit Function
End If
iRankStep = 1
Else
iPtr2 = 1
sPrevChar = "0"
For iPtr1 = 1 To Len(Rank)
sChar = Mid$(Rank, iPtr1, 1)
If IsNumeric(sChar) Then
iaRank(iPtr2) = (iaRank(iPtr2) * 10) + Val(sChar)
Else
If IsNumeric(sPrevChar) Then
If iPtr2 = 2 Then Exit For
iPtr2 = 2
End If
sRankDelimiter = sRankDelimiter & sChar
End If
sPrevChar = sChar
Next iPtr1
bError = False
If iaRank(1) < 1 Then bError = True
If iaRank(2) < 1 Then bError = True
If bError Then
FuzzyVLookup = "*** 'Rank' must be a positive integer or format integer-delimiter-integer ***"
Exit Function
End If
If iaRank(2) < iaRank(1) Then
iRankStep = -1
iRankLow = iaRank(2)
iRankHigh = iaRank(1)
Else
iRankStep = 1
iRankLow = iaRank(1)
iRankHigh = iaRank(2)
End If
End If
Set wsTable = Sheets(TableArray.Parent.Name)
ReDim mlRankOffset(1 To iRankHigh)
ReDim msngRankPercentage(1 To iRankHigh)
lEndRow = TableArray.Rows.Count
If VarType(TableArray.Cells(lEndRow, 1).Value) = vbEmpty Then
lEndRow = TableArray.Cells(lEndRow, 1).End(xlUp).Row
End If
Set rSearchRange = Intersect(TableArray.Resize(, 1), wsTable.UsedRange)
If rSearchRange Is Nothing Then
FuzzyVLookup = CVErr(xlErrNA)
Else
If Len(GroupValue) <> 0 Then
straGroupColOffsets = Split(GroupColOffset, ",")
ReDim iaGroupColOffsets(0 To UBound(straGroupColOffsets))
iGroupHighestOffset = 0
For iPtr1 = 0 To UBound(straGroupColOffsets)
iaGroupColOffsets(iPtr1) = Val(straGroupColOffsets(iPtr1))
If iaGroupColOffsets(iPtr1) > iGroupHighestOffset Then iGroupHighestOffset = iaGroupColOffsets(iPtr1)
Next iPtr1
For Each rCur In rSearchRange
If UBound(iaGroupColOffsets) = 0 Then
vCurValue = rCur.Offset(, iaGroupColOffsets(0)).Value
Else
vCurValues = rCur.Resize(, iGroupHighestOffset + 1).Value
vCurValue = ""
For iPtr1 = 0 To UBound(iaGroupColOffsets)
vCurValue = vCurValue & CStr(vCurValues(1, iaGroupColOffsets(iPtr1) + 1))
Next iPtr1
End If
If VarType(vCurValue) = vbString Or UBound(iaGroupColOffsets) > 0 Then
bWanted = LCase$(CStr(vCurValue)) = LCase$(CStr(GroupValue))
Else
bWanted = Val(vCurValue) = Val(GroupValue)
End If
If bWanted Then
FuzzyVlookupMain LookupValue:=LookupValue, _
TableArray:=rCur, _
IndexNum:=IndexNum, _
NFPercent:=sngNFPercent, _
Rank:=iRankHigh, _
Algorithm:=Algorithm, _
AdditionalCols:=AdditionalCols, _
LookupColOffset:=LookupColOffset
End If
Next rCur
Else
For Each rCur In rSearchRange
FuzzyVlookupMain LookupValue:=LookupValue, _
TableArray:=rCur, _
IndexNum:=IndexNum, _
NFPercent:=sngNFPercent, _
Rank:=iRankHigh, _
Algorithm:=Algorithm, _
AdditionalCols:=AdditionalCols, _
LookupColOffset:=LookupColOffset
Next rCur
End If
If (msngRankPercentage(iaRank(1)) < sngNFPercent) _
And (msngRankPercentage(iaRank(2)) < sngNFPercent) Then
FuzzyVLookup = CVErr(xlErrNA)
Else
sResult = ""
For iPtr2 = iaRank(1) To iaRank(2) Step iRankStep
If (msngRankPercentage(iPtr2) >= sngaNFPercent(0)) _
And (msngRankPercentage(iPtr2) <= sngaNFPercent(1)) Then
mlBestMatchPtr = mlRankOffset(iPtr2) - TableArray.Cells(1, 1).Row + 1
If sResult <> "" Then sResult = sResult & sRankDelimiter
If IndexNum > 0 Then
sResult = sResult & CStr(TableArray.Cells(mlBestMatchPtr, IndexNum))
Else
sResult = sResult & CStr(mlBestMatchPtr)
End If
End If
Next iPtr2
FuzzyVLookup = sResult
End If
End If
End Function
Private Sub FuzzyVlookupMain(ByVal LookupValue As String, _
ByVal TableArray As Range, _
ByVal IndexNum As Integer, _
Optional NFPercent As Single = 0.05, _
Optional Rank As Integer = 1, _
Optional Algorithm As Variant = 3, _
Optional AdditionalCols As Integer = 0, _
Optional LookupColOffset As Integer = 0)
Dim I As Integer
Dim intRankPtr As Integer
Dim intRankPtr1 As Integer
Dim strListString As String
Dim sngCurPercent As Single
Dim sngMinPercent As Single
Dim vCurValue As Variant
vCurValue = ""
For I = 0 To AdditionalCols
vCurValue = vCurValue & CStr(TableArray.Offset(0, I + LookupColOffset).Value)
Next I
If VarType(vCurValue) = vbString Then
strListString = LCase$(Application.Trim(vCurValue))
sngCurPercent = FuzzyPercent(String1:=LookupValue, _
String2:=strListString, _
Algorithm:=Algorithm, _
Normalised:=True)
If sngCurPercent >= NFPercent Then
For intRankPtr = 1 To Rank
If sngCurPercent > msngRankPercentage(intRankPtr) Then
mlRankOffset
msngRankPercentage
For intRankPtr1 = Rank To intRankPtr + 1 Step -1
mlRankOffset(intRankPtr1) = mlRankOffset(intRankPtr1 - 1)
msngRankPercentage(intRankPtr1) = msngRankPercentage(intRankPtr1 - 1)
Next intRankPtr1
mlRankOffset(intRankPtr) = TableArray.Row
msngRankPercentage(intRankPtr1) = sngCurPercent
Exit Sub
End If
Next intRankPtr
End If
End If
End Sub
Function Soundex(Surname As String) As String
Dim Result As String, c As String * 1
Dim Location As Integer
Surname = UCase(Surname)
If Surname = "" Then
Soundex = ""
Exit Function
End If
If Asc(Left(Surname, 1)) < 65 Or Asc(Left(Surname, 1)) > 90 Then
Soundex = ""
Exit Function
Else
If Left(Surname, 3) = "ST." Then
Surname = "SAINT" & Mid(Surname, 4)
End If
Result = Left(Surname, 1)
For Location = 2 To Len(Surname)
Result = Result & SoundexCategory(Mid(Surname, Location, 1))
Next Location
Location = 2
Do While Location < Len(Result)
If Mid(Result, Location, 1) = Mid(Result, Location + 1, 1) Then
Result = Left(Result, Location) & Mid(Result, Location + 2)
Else
Location = Location + 1
End If
Loop
If SoundexCategory(Left(Result, 1)) = Mid(Result, 2, 1) Then
Result = Left(Result, 1) & Mid(Result, 3)
End If
For Location = 2 To Len(Result)
If Mid(Result, Location, 1) = "/" Then
Result = Left(Result, Location - 1) & Mid(Result, Location + 1)
End If
Next
Select Case Len(Result)
Case 4
Soundex = Result
Case Is < 4
Soundex = Result & String(4 - Len(Result), "0")
Case Is > 4
Soundex = Left(Result, 4)
End Select
End If
End Function
Private Function SoundexCategory(c) As String
Select Case True
Case c Like "[AEIOUY]"
SoundexCategory = "/"
Case c Like "[BPFV]"
SoundexCategory = "1"
Case c Like "[CSKGJQXZ]"
SoundexCategory = "2"
Case c Like "[DT]"
SoundexCategory = "3"
Case c = "L"
SoundexCategory = "4"
Case c Like "[MN]"
SoundexCategory = "5"
Case c = "R"
SoundexCategory = "6"
Case Else
SoundexCategory = ""
End Select
End Function
Private Function NormaliseStringAtoZ(ByVal String1 As String) As String
Dim iPtr As Integer
Dim sChar As String
Dim sResult As String
sResult = ""
For iPtr = 1 To Len(String1)
sChar = LCase$(Mid$(String1, iPtr, 1))
If sChar <> UCase$(sChar) Then sResult = sResult & sChar
Next iPtr
NormaliseStringAtoZ = sResult
End Function
Function Metaphone1(ByVal String1 As String) As String
Dim iPtr As Integer
Dim iLen As Integer
Dim sString As String
Dim sResult As String
Dim sChar As String
Dim sLeft As String
Dim sRight As String
Dim sAtoZ As String
Dim vArray As Variant
Dim vCur As Variant
sString = NormaliseStringAtoZ(String1)
sString = Replace(sString, "dge", "j")
sString = Replace(sString, "dgy", "j")
sString = Replace(sString, "dgi", "j")
sString = Replace(sString, "d", "t")
vArray = Array("kn", "gn", "pn", "ae", "wr")
For Each vCur In vArray
If Left$(sString, 2) = CStr(vCur) Then
sString = Right$(sString, Len(sString) - 1)
Exit For
End If
Next vCur
iPtr = InStr(sString, "g")
iLen = Len(sString)
Do
If iPtr < 1 Or iPtr >= Len(sString) Then Exit Do
If Mid$(sString & " ", iPtr, 2) <> "gg" Then
If InStr("iey", Mid$(sString & " ", iPtr + 1, 1)) > 0 Then
Mid$(sString, iPtr, 1) = "j"
Else
Mid$(sString, iPtr, 1) = "k"
End If
End If
iPtr = InStr(iPtr + 1, sString, "g")
Loop
sAtoZ = "abdefghijklmnopqrstuvwxyz"
For iPtr = 1 To Len(sAtoZ)
sChar = Mid$(sAtoZ, iPtr, 1)
sString = Replace(sString, sChar & sChar, sChar)
Next iPtr
If Right$(sString, 2) = "mb" Then sString = Left$(sString, Len(sString) - 1)
sString = Replace(sString, "ck", "k")
sString = Replace(sString, "sch", "k")
sString = Replace(sString, "cia", "x")
sString = Replace(sString, "ch", "x")
sString = Replace(sString, "ci", "s")
sString = Replace(sString, "ce", "s")
sString = Replace(sString, "cy", "s")
sString = Replace(sString, "c", "k")
If Right$(sString, 4) = "gned" Then sString = Left$(sString, Len(sString) - 4) & "ned"
If Right$(sString, 2) = "gn" Then sString = Left$(sString, Len(sString) - 2) & "n"
iPtr = InStr(sString, "g")
Do
iLen = Len(sString)
If iPtr >= iLen Or iPtr < 1 Then Exit Do
If Mid$(sString, iPtr, 2) = "gh" Then
If InStr("aeiou", Mid$(sString, iPtr + 2)) < 1 Then
sLeft = Left$(sString, iPtr - 1)
sRight = Mid$(sString, iPtr + 1)
sString = sLeft & sRight
End If
End If
iPtr = InStr(iPtr + 1, sString, "g")
Loop
sString = Replace(sString, "tio", "xio")
sString = Replace(sString, "tia", "xia")
sString = Replace(sString, "tch", "ch")
sString = Replace(sString, "th", "0")
iPtr = InStr(sString, "h")
Do
iLen = Len(sString)
If iPtr < 1 Then Exit Do
If InStr("aeiou", Mid$(sString & " ", iPtr + 1, 1)) < 1 Then
If InStr("aeiou", Mid$(" " & sString, iPtr, 1)) > 0 Then
sLeft = Left$(sString, iPtr - 1)
sRight = Mid$(sString, iPtr + 1)
sString = sLeft & sRight
iPtr = iPtr - 1
End If
End If
iPtr = InStr(iPtr + 1, sString, "h")
Loop
sString = Replace(sString, "ph", "f")
sString = Replace(sString, "q", "k")
sString = Replace(sString, "sio", "xio")
sString = Replace(sString, "sia", "xia")
sString = Replace(sString, "sh", "xh")
sString = Replace(sString, "v", "f")
If Left$(sString, 2) = "wh" Then sString = "w" & Mid$(sString, 3)
If Left$(sString, 1) = "x" Then sString = "s" & Mid$(sString, 2)
sString = Replace(sString, "x", "ks")
iPtr = InStr(sString, "y")
Do
If iPtr < 1 Then Exit Do
If InStr("aeiou", Mid$(sString & " ", iPtr + 1, 1)) < 1 Then
sLeft = Left$(sString, iPtr - 1)
sRight = Mid$(sString, iPtr + 1)
sString = sLeft & sRight
iPtr = iPtr - 1
End If
iPtr = InStr(iPtr + 1, sString, "y")
Loop
sString = Replace(sString, "z", "s")
sResult = Left$(sString, 1)
For iPtr = 2 To Len(sString)
sChar = Mid$(sString, iPtr, 1)
If InStr("aeiou", sChar) < 1 Then sResult = sResult & sChar
Next iPtr
Metaphone1 = sResult
End Function