Fuzzy Matching - new version plus explanation

al_b_cnu

Well-known Member
Joined
Jul 18, 2003
Messages
4,546
It has been a while since I originally posted my Fuzzy matching UDF’s on the board, and several variants have appeared subsequently.

I thought it time to ‘put the record straight’ & post a definitive version which contains slightly more efficient code, and better matching algorithms, so here it is.

Firstly, I must state that the Fuzzy matching algorithms are very CPU hungry, and should be used sparingly. If for instance you require to lookup a match for a string which starts with, contains or ends with a specified value, this can be performed far more efficiently using the MATCH function:
Fuzzy Examples.xls
ABCDE
1Starts WithEndsContains
2BilljelenBill
3Mr Bill Jelen433
4Bill Jelen
5Joe Bloggs
6Fred Smith
MATCH Example


... Continued ...
 
Here's what I've been thinking of:basically using something like a Soundex function (like the one here) as a template to create a coding method for the Metaphone algorithm, which is more detailed, then comparing the results for my two columns. The limitations I've found with the Soundex function linked above is that it only returns a string of four characters, regardless of how many distinct phonetic units there are in a term, and by retaining the first letter is not as flexible as Metaphone. Changing to Metaphone seems ideal, but what does the expert think? Sound feasible?
 
Upvote 0

Excel Facts

Show numbers in thousands?
Use a custom number format of #,##0,K. Each comma after the final 0 will divide the displayed number by another thousand
Hi NPC,
Have coded the Soundex algorithm (parameter Algorithm = 'Soundex'), need to look into Metaphone algorithm coding, watch this space!)
 
Upvote 0
ok, propose to code Metaphone function using the following "Basic" rules. I believe that there is also "Double Metaphone" and "Metaphone 3", but haven't found the rules for these.


Excel 2003
C
11Metaphone Rules
121. Drop duplicate adjacent letters, except for C.
132. If the word begins with 'KN', 'GN', 'PN', 'AE', 'WR', drop the first letter.
143. Drop 'B' if after 'M' at the end of the word.
154. 'C' transforms to 'X' if followed by 'IA' or 'H' (unless in latter case, it is part of '-SCH-', in which case it transforms to 'K'). 'C' transforms to 'S' if followed by 'I', 'E', or 'Y'. Otherwise, 'C' transforms to 'K'.
165. 'D' transforms to 'J' if followed by 'GE', 'GY', or 'GI'. Otherwise, 'D' transforms to 'T'.
176. Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel. Drop 'G' if followed by 'N' or 'NED' and is at the end.
187. 'G' transforms to 'J' if before 'I', 'E', or 'Y', and it is not in 'GG'. Otherwise, 'G' transforms to 'K'.
198. Drop 'H' if after vowel and not before a vowel.
209. 'CK' transforms to 'K'.
2110. 'PH' transforms to 'F'.
2211. 'Q' transforms to 'K'.
2312. 'S' transforms to 'X' if followed by 'H', 'IO', or 'IA'.
2413. 'T' transforms to 'X' if followed by 'IA' or 'IO'. 'TH' transforms to '0'. Drop 'T' if followed by 'CH'.
2514. 'V' transforms to 'F'.
2615. 'WH' transforms to 'W' if at the beginning. Drop 'W' if not followed by a vowel.
2716. 'X' transforms to 'S' if at the beginning. Otherwise, 'X' transforms to 'KS'.
2817. Drop 'Y' if not followed by a vowel.
2918. 'Z' transforms to 'S'.
3019. Drop all vowels unless it is the beginning.
Sheet1
 
Upvote 0
Hi Alan,

Great work. What am I saying... Excellent work. I am sure you have helped a million people with your code. I have tested your most recent version and been tweaking it to meet my needs. Also if you don't mind, I will contribute something. This is a method using Jaccard similarity, which I have found works very quickly to compare the similarity between paragraphs, sentences, etc.

Code:
Option Explicit
Option Base 1
' New Scripting Dictionary (Early Binding - Microsoft Scripting Runtime required)
  Dim Dict As New Dictionary
' Declare variables
  Dim sMerged
  Dim Arr()
Sub Jaccard_similarity()
Dim x%, y%, Found%, r As Long, c%, z As Long, LRComp As Long, LRresults As Long, Pct As Double, Primary As Boolean
Dim sComp_1, sComp_2, sCommonComp, arrRngComp, arrRngResults
Dim CheckString1$, CheckString2$
  ' Turn off some Excel functions that are not needed
    With Application
        .ScreenUpdating = False
        .EnableEvents = False
        .DisplayStatusBar = False
        .Calculation = xlCalculationManual
    End With
  ' Create arrays
    LRComp = Range("A" & Rows.Count).End(xlUp).Row
    arrRngComp = Range("D2:D" & LRComp)
    ReDim arrRngResults(1 To LRComp - 1, 1 To 1)
 
  ' For each element in each array, remove punctuation, and duplicates
    For r = 1 To UBound(arrRngComp, 1)
        For c = 1 To UBound(arrRngComp, 2)
            If Not IsEmpty(arrRngComp(r, c)) And Not IsEmpty(arrRngComp(r, c)) Then
                sMerged = arrRngComp(r, c)
                Standardize
                GenUnique
                arrRngComp(r, c) = Arr()
            End If
        Next c
    Next r
  ' Perform Jaccard Similarity evaluation
    For r = LBound(arrRngComp, 1) To (UBound(arrRngComp, 1) - 1)
        For c = LBound(arrRngComp, 2) To UBound(arrRngComp, 2)
            For z = r + 1 To UBound(arrRngComp, 1)
                If Not IsEmpty(arrRngComp(r, c)) And Not IsEmpty(arrRngComp(z, c)) Then
                  ' Convert citation strings to arrays
                    sComp_1 = arrRngComp(r, c)
                    sComp_2 = arrRngComp(z, c)
                  ' Create one (common) array from the two arrays
                    sMerged = Join(sComp_1) & " " & Join(sComp_2)
                    GenUnique
                    sCommonComp = Arr()
                  ' Check percentage of similarities between two strings
                    arrRngResults(z, c) = ((UBound(sComp_1) + 1) + (UBound(sComp_2) + 1) - (UBound(sCommonComp) + 1)) / (UBound(sCommonComp) + 1)
                End If
            Next z
        Next c
    ' Print results to worksheet
      Primary = False                   ' Resest primary string
      LRresults = Range("F" & Rows.Count).End(xlUp).Row + 1
      For x = LBound(arrRngResults, 1) To UBound(arrRngResults, 1)
        If arrRngResults(x, 1) > 0.5 Then
            If Primary = False Then
                Range("J" & LRresults) = "Primary"
                    Range("F" & LRresults & ":I" & LRresults).Value = Range("A" & x & ":D" & x).Value
                        LRresults = LRresults + 1
                    Range("F" & LRresults & ":I" & LRresults).Value = Range("A" & x + 1 & ":D" & x + 1).Value
                        LRresults = LRresults + 1
                    Primary = True      ' Primary string has been identified
            Else
                    Range("F" & LRresults & ":I" & LRresults).Value = Range("A" & x + 1 & ":D" & x + 1).Value
                        LRresults = LRresults + 1
            End If
        End If
      Next x
      ReDim arrRngResults(1 To UBound(arrRngResults), 1 To 1)
    Next r
  ' Turn functions back on
    With Application
      .Calculation = xlCalculationAutomatic
      .DisplayStatusBar = True
      .EnableEvents = True
    End With
End Sub
Function Standardize()
  ' Remove punctuation and normalize case
    sMerged = StrConv(Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace( _
                        Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace(sMerged, _
                            ".", ""), ",", ""), "?", ""), "!", ""), ";", ""), ":", ""), """", ""), "'", ""), "<", ""), _
                                ">", ""), "(", ""), ")", ""), "[", ""), "]", ""), "_", " "), "-", " "), "/", " "), vbLowerCase)
End Function
Function GenUnique()
Dim Word
Const Nul As Long = 0
  ' Use Scripting Dictionary to remove duplicates
    With Dict
        For Each Word In Split(sMerged)
            .Item(Trim(Word)) = Nul
        Next
        Arr = .Keys
        .RemoveAll
    End With
End Function

Please feel free to add it to your code as an option or improve on my coding. It simply takes one column and finds the similarity between each cell and the cells below it. It then generates a report of all the similar findings. I am working on adding tokenization to it to improve the results giving weight to rare words and less weight to more common words.

Please let me know what you think.

AMAS
 
Upvote 0
Hi amas, love it! (that's an in joke for all the Latin speakers on the board!)
I'll certainly have a closer look and incorporate it into FuzzyVLookup.
Thanks :)

NPC:
Well Soundex and Metaphone now coded, but not too sure of Metaphone - the coding's working, it's just that the sequence of which rule takes precedence is the issue, for instance, rule 7 must presumably precede rule 1. I eventually went with rule order:
5, 2, 7, 1, 3, 9, 4, 6, 13, 8, 10, 11, 12, 14, 15, 16, 17, 18, 19
Who knows if its right :-?

To implement these algorithms, just set Algorithm parameter to "Soundex" or "Metaphone" as appropriate, here's the modified code, with the comments suitably updated.
Code:
Option Explicit

'************************************************************************************************************
'** Functions available are:

'** FuzzyCount
'** ==========
'**    Utility giving a simple count of (Fuzzy) Matching strings equal to or above NFPercent threshold
'**    Parameters are
'**      LookupValue: Mandatory String
'**      TableArray : Mandatory Range
'**      NFPercent  : Minimum %age match for the entry to be deemed as matching. Default 5%
'**      Algorithm  : Algorithm to be used, default 3
'**
'** FuzzyPercent
'** ============
'**    Return a percentage match on two strings
'**    Parameters are
'**      String1   : Mandatory first string to be compared
'**      String2   : Mandatory second string to be compared
'**      Algorithm : Algorithm to be used for comparison, default 3
'**                  This parameter can take one of the following values:
'**                  Algorithm = 1:
'**                    This algorithm is best suited for matching mis-spellings.
'**                    For each character in 'String1', a search is performed on 'String2'.
'**                    The search is deemed successful if a character is found in 'String2'
'**                    within 3 characters of the current position.
'**                    A score is kept of matching characters which is returned as a
'**                    percentage of the total possible score.
'**                  Algorithm = 2:
'**                    This algorithm is best suited for matching sentences, or
'**                    'firstname lastname' compared with 'lastname firstname' combinations
'**                    A count of matching pairs, triplets, quadruplets etc. in 'String1' and
'**                    'String2' is returned as a percentage of the total possible.
'**                  Algorithm = 3:
'**                    Both Algorithms 1 and 2 are performed.                      **
'**                  Algorithm = 4:
'**                    Levenshtein Distance algorithm is used.
'**                  Algorithm = 'Soundex'
'**                    Soundex rules are applied, see http://www.j-walk.com/ss/excel/tips/tip77.htm
'**                    If the 'normalised' strings exactly match, 100% is returned,
'**                    If the strings converted to Soundex code match, (an arbitrary) 95% match is returned
'**                    If the Soundex strings do not match, 0% is returned.
'**                  Algorithm = 'Metaphone'
'**                    Basic Metaphone rules are applied, see
'**                    If the 'normalised' strings exactly match, 100% is returned,
'**                    If the strings converted to Basic Metaphone code match, (an arbitrary) 95% match is returned
'**                    If the Basic Metaphone strings do not match, 0% is returned.
'**      Normalised: For efficiency set this boolean to True if both strings have been 'Normalised'
'**                  A Normalised string has been converted to all lower or all upper case,
'**                  and has all leading, trailing and multiple internal spaces removed.
'**                  Default False.

'** FuzzyVLookup
'** ============
'** Function to Fuzzy match LookupValue with entries in specified column (default 1)
'** of lookup table specified by TableArray.
'**    Parameters are
'**      LookupValue:    Value to be matched
'**      TableArray :    Look up table
'**      IndexNum   :    The column number of the entry in TableArray required to be
'**                      returned, as follows:
'**                      If IndexNum > 0 and the 'Rank' percentage match is >= NFPercent
'**                      (Default 5%) the column entry indicated by IndexNum is returned.
'**                      if IndexNum = 0 and the 'Rank' percentage match is >= NFPercent
'**                      (Default 5%) the corresponding value in the offset row (starting at 1
'**                      is returned. This returned value can be used to directly feed the 'Index' function.
'**      NFPercent  :    This parameter defines the percentage below which two strings are
'**                      defined as not matching, and can be expressed as either a value between 0.001 to 1
'**                      or an integer between 2 and 100 optionally followed by '%'
'**                      The parameter can optionally be followed by a seperator '~' and then
'**                      a 'high' percentage value (default 100%), above which a string is not considered
'**                      a candidate for matching.
'**                      Valid examples of this parametert are:
'**                      .5           - Entries below 50% match are ignored
'**                      50           - Entries below 50% match are ignored
'**                      50%          - Entries below 50% match are ignored
'**                      50~99        - Only entries between 50% and 99% are candidates for matching
'**                      50%~.99      - Only entries between 50% and 99% are candidates for matching
'**                      If no entries are equal to or above NFPercent, #N/A is returned.
'**      Rank          : Defines the value(s) to be returned within the NFPercent range.
'**                      This parameter is specified as an integer defining which value is to be returned.
'**                      The integer can optiolnally be followed by a string and another rank integer.
'**                      Ifd this is the case, the function will return each value within the rank range,
'**                      seperated by the non-numeric string
'**                      For example the value "1, 10" (without the quotes) will return the best to the 10th
'**                      best matching values, seperated by comma and space, with highest ranking to lowest ranking.
'**                      And the value "10 1" (without the quotes) will return the 10th
'**                      best matching value to the best matching value, seperated by a space.
'**                      If the 'Rank' match percentage is lower than NFPercent #N/A is returned.
'**      Algorithm     : Described in FuzzyPercent
'**      AdditionalCols: Defines additionbal columns within the lookup table which are to be concatenated
'**                      before matching the lookup value. Default 0.
'**      LookupColOffset: Defines offset position within the lookup table of the value(s) to be compared
'**                      Default 0.
'**    The following parameters allow matching by group, and only those values
'**    which are in the group specified will be considered for matching.
'**      GroupColOffset: This parameter specifies the offset column which contains the group values.
'**                      To trigger group matching, this must be a non-zero integer
'**      GroupValue    : This parameter specifies the Group to be considered for matching

'** GetLevenshteinPercentMatch
'** ==========================
'**    Return a percentage match on two strings using the Levenshtein distance method.
'**    This is the function called by FuzzyVLookup if Algorithm is set to 4.
'**    Parameters are
'**      String1   : Mandatory first string to be compared
'**      String2   : Mandatory second string to be compared
'**      Normalised: For efficiency set this boolean to True if both strings have been 'Normalised'
'**                  A Normalised string has been converted to all lower or all upper case,
'**                  and has all leading, trailing and multiple internal spaces removed.
'**                  Default False.

'** LevenshteinDistance
'** ===================
'** Compute Levenshtein Distance between two strings.
'**    Parameters are
'**      String1   : Mandatory first string to be compared
'**      String2   : Mandatory second string to be compared
'************************************************************************************************************


Type RankInfo
    Offset As Long
    Percentage As Single
End Type

Dim mudRankData() As RankInfo
Dim mlBestMatchPtr As Long

Const msngSoundexMatchPercent As Single = 0.95  'Return 95% match for strings which Soundex match
Const msngMetaphoneMatchPercent As Single = 0.95  'Return 95% match for strings which match according to Basic Metaphone rules

'Dim TopMatch         As Long
'Dim strCompare       As String

Function FuzzyCount(ByVal LookupValue As String, _
                      ByVal TableArray As Range, _
                      Optional NFPercent As Single = 0.05, _
                      Optional Algorithm As Variant = 3) As Long
'**********************************************************************
'** Simple count of (Fuzzy) Matching strings >= NFPercent threshold  **
'**********************************************************************
Dim lMatchCount As Long

Dim rCur As Range

Dim sString1 As String
Dim sString2 As String

'** Normalise lookup value **
sString1 = LCase$(Application.Trim(LookupValue))

For Each rCur In Intersect(TableArray.Resize(, 1), Sheets(TableArray.Parent.Name).UsedRange)

    '** Normalise current Table entry **
    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
'*************************************
'** Return a % match on two strings **
'*************************************
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 strings havent been normalised, normalise them --
'-------------------------------------------------------
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

'----------------------------------------------
'-- Give 100% match if strings exactly equal --
'----------------------------------------------
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

'----------------------------------------
'-- Give 0% match if string length < 2 --
'----------------------------------------
If intLen1 < 2 Then
    FuzzyPercent = 0
    Exit Function
End If

intTotScore = 0                   'initialise total possible score
intScore = 0                      'initialise current score

lngAlgorithm = Val(Algorithm)

'--------------------------------------------------------
'-- If Algorithm = 1 or 3, Search for single characters --
'--------------------------------------------------------
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 Algorithm = 2 or 3, Search for pairs, triplets etc. --
'-----------------------------------------------------------
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 Algorithm = 4,5,6,7, use Levenstein Distance method  --
'-- (Algorithm 4 was Dan Ostrander's code)                  --
'-------------------------------------------------------------
If (lngAlgorithm And 4) <> 0 Then
    If intLen1 < intLen2 Then
'        sngScore = FuzzyAlg4(String1, String1)
        sngScore = GetLevenshteinPercentMatch(String1:=String1, _
                                              String2:=String2, _
                                              Normalised:=True)
    Else
'        sngScore = FuzzyAlg4(String2, String1)
        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              'update total possible score
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     'No match if char is > 3 bytes away
            intPos = intStartPos
        Else
            Score = Score + 1          'Update current score
        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                          'Get a copy of String2
    intTo = intLen1 - intCurLen + 1
    TotScore = TotScore + Int(intLen1 / intCurLen)  'Update total possible score
    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) 'corrupt found string
            Score = Score + 1     'Update current score
        End If
    Next intPtr
Next intCurLen

End Sub
'Private Function FuzzyAlg4(strIn1 As String, strIn2 As String) As Single
'
'Dim L1               As Integer
'Dim In1Mask(1 To 24) As Long     'strIn1 is 24 characters max
'Dim iCh              As Integer
'Dim N                As Long
'Dim strTry           As String
'Dim strTest          As String
'
'TopMatch = 0
'L1 = Len(strIn1)
'strTest = UCase(strIn1)
'strCompare = UCase(strIn2)
'For iCh = 1 To L1
'    In1Mask(iCh) = 2 ^ iCh
'Next iCh      'Loop thru all ordered combinations of characters in strIn1
'For N = 2 ^ (L1 + 1) - 1 To 1 Step -1
'    strTry = ""
'    For iCh = 1 To L1
'        If In1Mask(iCh) And N Then
'            strTry = strTry & Mid(strTest, iCh, 1)
'        End If
'    Next iCh
'    If Len(strTry) > TopMatch Then FuzzyAlg4Test strTry
'Next N
'FuzzyAlg4 = TopMatch / CSng(L1)
'End Function
'Sub FuzzyAlg4Test(strIn As String)
'
'Dim l          As Integer
'Dim strTry   As String
'Dim iCh        As Integer
'
'l = Len(strIn)
'If l <= TopMatch Then Exit Sub
'strTry = "*"
'For iCh = 1 To l
'    strTry = strTry & Mid(strIn, iCh, 1) & "*"
'Next iCh
'If strCompare Like strTry Then
'    If l > TopMatch Then TopMatch = l
'End If
'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

''*******************************
''*** Get minimum of three values
''*******************************
'
'Private Function Minimum(ByVal a As Integer, _
'                         ByVal b As Integer, _
'                         ByVal c As Integer) As Integer
'Dim mi As Integer
'
'  mi = a
'  If b < mi Then
'    mi = b
'  End If
'  If c < mi Then
'    mi = c
'  End If
'
'  Minimum = mi
'
'End Function

'********************************
'*** Compute Levenshtein Distance
'********************************

Public Function LevenshteinDistance(ByVal s As String, ByVal t As String) As Integer
Dim d() As Integer ' matrix
Dim m As Integer ' length of t
Dim N As Integer ' length of s
Dim I As Integer ' iterates through s
Dim j As Integer ' iterates through t
Dim s_i As String ' ith character of s
Dim t_j As String ' jth character of t
Dim cost As Integer ' cost

  ' Step 1

  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

  ' Step 2

  For I = 0 To N
    d(I, 0) = I
  Next I

  For j = 0 To m
    d(0, j) = j
  Next j

  ' Step 3

  For I = 1 To N

    s_i = Mid$(s, I, 1)

    ' Step 4

    For j = 1 To m

      t_j = Mid$(t, j, 1)

      ' Step 5

      If s_i = t_j Then
        cost = 0
      Else
        cost = 1
      End If

      ' Step 6

      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

  ' Step 7

  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 Integer = 0, _
                      Optional GroupValue As Variant = "") As Variant
'********************************************************************************
'** Function to Fuzzy match LookupValue with entries in                        **
'** table specified by TableArray.                                            **
'********************************************************************************
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 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 vCurValue As Variant
Dim wsTable As Worksheet

'--------------------------------------------------------------
'--    Validation                                            --
'--------------------------------------------------------------

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
'    sngMinPercent = 0.05
'    sngMaxPercent = 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 mudRankData(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


    '---------------
    '-- Main loop --
    '---------------
    
    If Len(GroupValue) <> 0 Then
        With rSearchRange.Offset(, GroupColOffset)
            For Each rCur In rSearchRange.Offset(, GroupColOffset)
                vCurValue = rCur.Value
                If VarType(vCurValue) = vbString Then
                    bWanted = LCase$(CStr(vCurValue)) = LCase$(CStr(GroupValue))
                Else
                    bWanted = Val(vCurValue) = Val(GroupValue)
                End If
                If bWanted Then
                    FuzzyVlookupMain LookupValue:=LookupValue, _
                                    TableArray:=rCur.Offset(, GroupColOffset * -1), _
                                    IndexNum:=IndexNum, _
                                    NFPercent:=sngNFPercent, _
                                    Rank:=iRankHigh, _
                                    Algorithm:=Algorithm, _
                                    AdditionalCols:=AdditionalCols, _
                                    LookupColOffset:=LookupColOffset
                End If
            Next rCur
        End With
    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 (mudRankData(iaRank(1)).Percentage < sngNFPercent) _
    And (mudRankData(iaRank(2)).Percentage < sngNFPercent) Then
        '--------------------------------------
        '-- Return '#N/A' if below NFPercent --
        '--------------------------------------
        FuzzyVLookup = CVErr(xlErrNA)
    Else
        sResult = ""
        For iPtr2 = iaRank(1) To iaRank(2) Step iRankStep
            If (mudRankData(iPtr2).Percentage >= sngaNFPercent(0)) _
            And (mudRankData(iPtr2).Percentage <= sngaNFPercent(1)) Then
                mlBestMatchPtr = mudRankData(iPtr2).Offset - TableArray.Cells(1, 1).Row + 1
                If sResult <> "" Then sResult = sResult & sRankDelimiter
            
                If IndexNum > 0 Then
                    '-----------------------------------
                    '-- Return column entry specified --
                    '-----------------------------------
                    sResult = sResult & CStr(TableArray.Cells(mlBestMatchPtr, IndexNum))
                Else
                    '-----------------------
                    '-- Return offset row --
                    '-----------------------
                    sResult = sResult & CStr(mlBestMatchPtr)
                End If
            End If
        Next iPtr2
        FuzzyVLookup = sResult
    End If
    
End If          'If rSearchRange Is Nothing Then
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))
   
    '------------------------------------------------
    '-- Fuzzy match strings & get percentage match --
    '------------------------------------------------
    sngCurPercent = FuzzyPercent(String1:=LookupValue, _
                                 String2:=strListString, _
                                 Algorithm:=Algorithm, _
                                 Normalised:=True)

    If sngCurPercent >= NFPercent Then
        '---------------------------
        '-- Store in ranked array --
        '---------------------------
        For intRankPtr = 1 To Rank
            If sngCurPercent > mudRankData(intRankPtr).Percentage Then
                For intRankPtr1 = Rank To intRankPtr + 1 Step -1
                    With mudRankData(intRankPtr1)
                        .Offset = mudRankData(intRankPtr1 - 1).Offset
                        .Percentage = mudRankData(intRankPtr1 - 1).Percentage
                    End With
                Next intRankPtr1
                With mudRankData(intRankPtr)
                    .Offset = TableArray.Row
                    .Percentage = sngCurPercent
                End With
                Exit Sub
            End If
        Next intRankPtr
    End If
    
End If
End Sub

Function Soundex(Surname As String) As String
' Developed by Richard J. Yanco
' This function follows the Soundex rules given at
' http://home.utah-inter.net/kinsearch/Soundex.html

    Dim Result As String, c As String * 1
    Dim Location As Integer

    Surname = UCase(Surname)
    If Surname = "" Then
        Soundex = ""
        Exit Function
    End If
    
'   First character must be a letter
    If Asc(Left(Surname, 1)) < 65 Or Asc(Left(Surname, 1)) > 90 Then
        Soundex = ""
        Exit Function
    Else
'       St. is converted to Saint
        If Left(Surname, 3) = "ST." Then
            Surname = "SAINT" & Mid(Surname, 4)
        End If

'       Convert to Soundex: letters to their appropriate digit,
'                     A,E,I,O,U,Y ("slash letters") to slashes
'                     H,W, and everything else to zero-length string

        Result = Left(Surname, 1)
        For Location = 2 To Len(Surname)
            Result = Result & SoundexCategory(Mid(Surname, Location, 1))
        Next Location
      
'       Remove double letters
        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 of 1st letter equals 2nd character, remove 2nd character
        If SoundexCategory(Left(Result, 1)) = Mid(Result, 2, 1) Then
            Result = Left(Result, 1) & Mid(Result, 3)
        End If
    
'       Remove slashes
        For Location = 2 To Len(Result)
            If Mid(Result, Location, 1) = "/" Then
                Result = Left(Result, Location - 1) & Mid(Result, Location + 1)
            End If
        Next
    
'       Trim or pad with zeroes as necessary
        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
'   Returns a Soundex code for a letter
    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 'This includes H and W, spaces, punctuation, etc.
            SoundexCategory = ""
    End Select
End Function

Private Function NormaliseStringAtoZ(ByVal String1 As String) As String
'---------------------------------------------------------
'-- Remove all but alpha chars and convert to lowercase --
'---------------------------------------------------------
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
'-- Metaphone Basic Rules
'-- ** NOTE ** Depending on the order that they are obeyed, these rules can cancel each other out
'--            I have amended the order that the rules are obeyed as I feel appropriate, but uncertain that it is as the
'--            author intended.
'--   These are the rules as specified in Wikipedia entry:
'-- 1.***** Drop duplicate adjacent letters, except for C.
'-- 2.***** If the word begins with 'KN', 'GN', 'PN', 'AE', 'WR', drop the first letter.
'-- 3.***** Drop 'B' if after 'M' at the end of the word.
'-- 4.***** 'C' transforms to 'X' if followed by 'IA' or 'H' (unless in latter case, it is part of '-SCH-', in which case it transforms to 'K'). 'C' transforms to 'S' if followed by 'I', 'E', or 'Y'. Otherwise, 'C' transforms to 'K'.
'-- 5.***** 'D' transforms to 'J' if followed by 'GE', 'GY', or 'GI'. Otherwise, 'D' transforms to 'T'.
'-- 6.***** Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel. Drop 'G' if followed by 'N' or 'NED' and is at the end.
'-- 7.***** 'G' transforms to 'J' if before 'I', 'E', or 'Y', and it is not in 'GG'. Otherwise, 'G' transforms to 'K'.
'-- 8.***** Drop 'H' if after vowel and not before a vowel.
'-- 9.***** 'CK' transforms to 'K'.
'-- 10.*** 'PH' transforms to 'F'.
'-- 11.*** 'Q' transforms to 'K'.
'-- 12.*** 'S' transforms to 'X' if followed by 'H', 'IO', or 'IA'.
'-- 13.*** 'T' transforms to 'X' if followed by 'IA' or 'IO'. 'TH' transforms to '0'. Drop 'T' if followed by 'CH'.
'-- 14.*** 'V' transforms to 'F'.
'-- 15.*** 'WH' transforms to 'W' if at the beginning. Drop 'W' if not followed by a vowel.
'-- 16.*** 'X' transforms to 'S' if at the beginning. Otherwise, 'X' transforms to 'KS'.
'-- 17.*** Drop 'Y' if not followed by a vowel.
'-- 18.*** 'Z' transforms to 'S'.
'-- 19.*** Drop all vowels unless it is the beginning.

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

'-- Remove non alphabetics and convert to lowercase --
sString = NormaliseStringAtoZ(String1)

'-- 5.***** 'D' transforms to 'J' if followed by 'GE', 'GY', or 'GI'. Otherwise, 'D' transforms to 'T'.
sString = Replace(sString, "dge", "j")
sString = Replace(sString, "dgy", "j")
sString = Replace(sString, "dgi", "j")
sString = Replace(sString, "d", "t")

'-- 2.*If the word begins with 'KN', 'GN', 'PN', 'AE', 'WR', drop the first letter.
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

'-- 7.***** 'G' transforms to 'J' if before 'I', 'E', or 'Y', and it is not in 'GG'. Otherwise, 'G' transforms to 'K'.
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

'-- 1. Drop duplicate adjacent letters, except for C.
sAtoZ = "abdefghijklmnopqrstuvwxyz"
For iPtr = 1 To Len(sAtoZ)
    sChar = Mid$(sAtoZ, iPtr, 1)
    sString = Replace(sString, sChar & sChar, sChar)
Next iPtr

'-- 3.***** Drop 'B' if after 'M' at the end of the word.
If Right$(sString, 2) = "mb" Then sString = Left$(sString, Len(sString) - 1)

'-- 9.***** 'CK' transforms to 'K'.
sString = Replace(sString, "ck", "k")

'-- 4.***** 'C' transforms to 'X' if followed by 'IA' or 'H' (unless in latter case, it is part of '-SCH-',
'--         in which case it transforms to 'K'). 'C' transforms to 'S' if followed by 'I', 'E', or 'Y'.
'--         Otherwise, 'C' transforms to '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")

'-- 6.***** Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel.
'--         Drop 'G' if followed by 'N' or 'NED' and is at the end.
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

'-- 13.*** 'T' transforms to 'X' if followed by 'IA' or 'IO'. 'TH' transforms to '0'. Drop 'T' if followed by 'CH'.
sString = Replace(sString, "tio", "xio")
sString = Replace(sString, "tia", "xia")
sString = Replace(sString, "tch", "ch")
sString = Replace(sString, "th", "0")

'-- 8.***** Drop 'H' if after vowel and not before a vowel.
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

'-- 10.*** 'PH' transforms to 'F'.
sString = Replace(sString, "ph", "f")

'-- 11.*** 'Q' transforms to 'K'.
sString = Replace(sString, "q", "k")

'-- 12.*** 'S' transforms to 'X' if followed by 'H', 'IO', or 'IA'.
sString = Replace(sString, "sio", "xio")
sString = Replace(sString, "sia", "xia")
sString = Replace(sString, "sh", "xh")

'-- 14.*** 'V' transforms to 'F'.
sString = Replace(sString, "v", "f")

'-- 15.*** 'WH' transforms to 'W' if at the beginning. Drop 'W' if not followed by a vowel.
If Left$(sString, 2) = "wh" Then sString = "w" & Mid$(sString, 3)

'-- 16.*** 'X' transforms to 'S' if at the beginning. Otherwise, 'X' transforms to 'KS'.
If Left$(sString, 1) = "x" Then sString = "s" & Mid$(sString, 2)
sString = Replace(sString, "x", "ks")

'-- 17.*** Drop 'Y' if not followed by a vowel.
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

'-- 18.*** 'Z' transforms to 'S'.
sString = Replace(sString, "z", "s")

'-- 19.*** Drop all vowels unless it is the beginning.
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

Continued ...
 
Upvote 1
Solution
...Continued

Here's an example of the Metaphone function results:

Excel 2003
CDE
135. 'D' transforms to 'J' if followed by 'GE', 'GY', or 'GI'. Otherwise, 'D' transforms to 'T'.edgepedgypdgipdgpejpjpjptkp
142. If the word begins with 'KN', 'GN', 'PN', 'AE', 'WR', drop the first letter.gnawnw
157. 'G' transforms to 'J' if before 'I', 'E', or 'Y', and it is not in 'GG'. Otherwise, 'G' transforms to 'K'.ignorexgixgexgyxggixggexggyxggaxgsiknrksjksjksjksgjksgjksgjksgkksks
161. Drop duplicate adjacent letters, except for C.aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzzabkktfgkhjklmnpkrstfwkss
173. Drop 'B' if after 'M' at the end of the word.dumbtm
189. 'CK' transforms to 'K'.back packbkpk
194. 'C' transforms to 'X' if followed by 'IA' or 'H' (unless in latter case, it is part of '-SCH-', in which case it transforms to 'K'). 'C' transforms to 'S' if followed by 'I', 'E', or 'Y'. Otherwise, 'C' transforms to 'K'.ciamchmschmclsmksmkmkl
20 
216. Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel. Drop 'G' if followed by 'N' or 'NED' and is at the end.mahxmmhomksmh
2213. 'T' transforms to 'X' if followed by 'IA' or 'IO'. 'TH' transforms to '0'. Drop 'T' if followed by 'CH'.tiamtiomthismtchsmksm0smtks
238. Drop 'H' if after vowel and not before a vowel.ahpap
2410. 'PH' transforms to 'F'.phishfksh
2511. 'Q' transforms to 'K'.equalekl
2612. 'S' transforms to 'X' if followed by 'H', 'IO', or 'IA'.shmsiomsiashmksmks
27 
2814. 'V' transforms to 'F'.raverf
2915. 'WH' transforms to 'W' if at the beginning. Drop 'W' if not followed by a vowel.whowlwwl
3016. 'X' transforms to 'S' if at the beginning. Otherwise, 'X' transforms to 'KS'.xabcxdefsbkkstf
3117. Drop 'Y' if not followed by a vowel.yaysys
3218. 'Z' transforms to 'S'.azbzasbs
3319. Drop all vowels unless it is the beginning.apepipopupaappppp
Sheet1
Cell Formulas
RangeFormula
E13=metaphone1(D13)
E14=metaphone1(D14)
E15=metaphone1(D15)
E16=metaphone1(D16)
E17=metaphone1(D17)
E18=metaphone1(D18)
E19=metaphone1(D19)
E20=metaphone1(D20)
E21=metaphone1(D21)
E22=metaphone1(D22)
E23=metaphone1(D23)
E24=metaphone1(D24)
E25=metaphone1(D25)
E26=metaphone1(D26)
E27=metaphone1(D27)
E28=metaphone1(D28)
E29=metaphone1(D29)
E30=metaphone1(D30)
E31=metaphone1(D31)
E32=metaphone1(D32)
E33=metaphone1(D33)


Continued ...
 
Upvote 0
Heres an example of Metaphone algorithm via FuzzyVLookup:

Excel 2003
ABCDEFGHIJKLMNO
1Algorithm:Metaphone
2Lookup TableLookup ValueRank:123456789101 1010, 1
3AlphaExcelResult:Excelexcell#N/A#N/A#N/A#N/A#N/A#N/A#N/A#N/AExcel excellexcell, Excel
4Beta% match:100%95%
5Gamma
6Delta
7Epsilon
8Zeta
9excell
10Theta
11Iota
12Kappa
13Lambda
14Ecsel
15Lambda
16Excel
17Eggshell
18omicron
Sheet1
Cell Formulas
RangeFormula
D3=fuzzyvlookup($B3,$A:$A,1,50,D$2,$D$1)
D4=FuzzyPercent($B$3,D3,$D$1)
E3=fuzzyvlookup($B3,$A:$A,1,50,E$2,$D$1)
E4=FuzzyPercent($B$3,E3,$D$1)
F3=fuzzyvlookup($B3,$A:$A,1,50,F$2,$D$1)
G3=fuzzyvlookup($B3,$A:$A,1,50,G$2,$D$1)
H3=fuzzyvlookup($B3,$A:$A,1,50,H$2,$D$1)
I3=fuzzyvlookup($B3,$A:$A,1,50,I$2,$D$1)
J3=fuzzyvlookup($B3,$A:$A,1,50,J$2,$D$1)
K3=fuzzyvlookup($B3,$A:$A,1,50,K$2,$D$1)
L3=fuzzyvlookup($B3,$A:$A,1,50,L$2,$D$1)
M3=fuzzyvlookup($B3,$A:$A,1,50,M$2,$D$1)
N3=fuzzyvlookup($B3,$A:$A,1,50,N$2,$D$1)
O3=fuzzyvlookup($B3,$A:$A,1,50,O$2,$D$1)


and this shows the same but using the Soundex Algorithm:

Excel 2003
ABCDEFGHIJKLMNO
1Algorithm:Soundex
2Lookup TableLookup ValueRank:123456789101 1010, 1
3AlphaExcelResult:ExcelexcellEcselEggshell#N/A#N/A#N/A#N/A#N/A#N/AExcel excell Ecsel EggshellEggshell, Ecsel, excell, Excel
4Beta% match:100%95%95%95%
5Gamma
6Delta
7Epsilon
8Zeta
9excell
10Theta
11Iota
12Kappa
13Lambda
14Ecsel
15Lambda
16Excel
17Eggshell
18omicron
Sheet1
Cell Formulas
RangeFormula
D3=fuzzyvlookup($B3,$A:$A,1,50,D$2,$D$1)
D4=FuzzyPercent($B$3,D3,$D$1)
E3=fuzzyvlookup($B3,$A:$A,1,50,E$2,$D$1)
E4=FuzzyPercent($B$3,E3,$D$1)
F3=fuzzyvlookup($B3,$A:$A,1,50,F$2,$D$1)
F4=FuzzyPercent($B$3,F3,$D$1)
G3=fuzzyvlookup($B3,$A:$A,1,50,G$2,$D$1)
G4=FuzzyPercent($B$3,G3,$D$1)
H3=fuzzyvlookup($B3,$A:$A,1,50,H$2,$D$1)
I3=fuzzyvlookup($B3,$A:$A,1,50,I$2,$D$1)
J3=fuzzyvlookup($B3,$A:$A,1,50,J$2,$D$1)
K3=fuzzyvlookup($B3,$A:$A,1,50,K$2,$D$1)
L3=fuzzyvlookup($B3,$A:$A,1,50,L$2,$D$1)
M3=fuzzyvlookup($B3,$A:$A,1,50,M$2,$D$1)
N3=fuzzyvlookup($B3,$A:$A,1,50,N$2,$D$1)
O3=fuzzyvlookup($B3,$A:$A,1,50,O$2,$D$1)
 
Upvote 0
That is, quite simply, amazing! Thank you so much for building those two options into the code! I've updated my module and am already having a great time playing around with it. I hope it will prove helpful to others in the future, as well. This is such an unbelievably great help and is going to work great--what a huge leap forward from going through a list of thousands of entries manually! Thanks again!
 
Upvote 0
Hi,

I found a few bugs in the original code I posted above. These should now be resolved. Input comes from Column A and Output is in Column B with the similarities placed in Column C.

Code:
Option Explicit
Option Base 1
' New Scripting Dictionary (Early Binding - Microsoft Scripting Runtime required)
  Dim Dict As New Dictionary
' Declare variables
  Dim Arr(), arrCommonComp, r As Long, c%, arrRngComp, Complete As Boolean

Sub Jaccard_similarity()

Dim z As Long, LRComp As Long, LRresults As Long, Primary As Boolean, arrRngResults, arrPrimary

  ' Turn off some Excel functions that are not needed
    With Application
        .ScreenUpdating = False
        .EnableEvents = False
        .DisplayStatusBar = False
        .Calculation = xlCalculationManual
    End With

    Complete = False
  
  ' Create arrays
    LRComp = Range("A" & Rows.Count).End(xlUp).Row
    arrRngComp = Range("A2:A" & LRComp)
    ReDim arrRngResults(1 To LRComp - 1, 1 To 1)

  ' For each element in each array, remove punctuation, and duplicates
    For r = LBound(arrRngComp, 1) To UBound(arrRngComp, 1)
        For c = LBound(arrRngComp, 2) To UBound(arrRngComp, 2)
            If Not IsEmpty(arrRngComp(r, c)) Then
                Standardize
                GenUnique
                arrRngComp(r, c) = Arr()
            End If
            DoEvents
        Next c
    Next r

    Complete = True

  ' Perform Jaccard Similarity evaluation
    For r = LBound(arrRngComp, 1) To (UBound(arrRngComp, 1) - 1)
        For c = LBound(arrRngComp, 2) To UBound(arrRngComp, 2)
            For z = r + 1 To UBound(arrRngComp, 1)
                Select Case True
                    Case IsEmpty(arrRngComp(r, c)), IsEmpty(arrRngComp(z, c)), Len(Trim(Join(arrRngComp(r, c)))) < 1, Len(Trim(Join(arrRngComp(z, c)))) < 1
                        arrRngResults(z, c) = 0
                    Case Join(arrRngComp(r, c)) = Join(arrRngComp(z, c))
                        arrRngResults(z, c) = 1
                    Case Else
                      ' Create one (common) array from the two arrays
                        arrCommonComp = Split(Join(arrRngComp(r, c)) & " " & Join(arrRngComp(z, c)))
                        GenUnique
                        arrCommonComp = Arr()
                      ' Check percentage of similarities between two strings
                        arrRngResults(z, c) = ((UBound(arrRngComp(r, c)) + 1) + (UBound(arrRngComp(z, c)) + 1) _
                                                - (UBound(arrCommonComp) + 1)) / (UBound(arrCommonComp) + 1)
                    DoEvents
                End Select
            Next z
        Next c

    ' Print results to worksheet
      Primary = False                   ' Resest primary string
      LRresults = Range("G" & Rows.Count).End(xlUp).Row + 1
      For z = LBound(arrRngResults, 1) To UBound(arrRngResults, 1)
        arrPrimary = Range("A" & r + 1)
            If arrRngResults(z, 1) > 0.5 Then
                If Primary = False Then
                    Primary = True      ' Primary string has been identified
                    Range("C" & LRresults).Value = arrPrimary
                    Range("D" & LRresults) = "Primary"
                    LRresults = LRresults + 1
                End If
              Range("C" & LRresults).Value = Range("A" & z + 1).Value
              Range("D" & LRresults) = arrRngResults(z, 1)
              LRresults = LRresults + 1
            End If
        DoEvents
      Next
      ReDim arrRngResults(1 To UBound(arrRngResults), 1 To 1)
      Erase arrPrimary
    Next r

  ' Turn functions back on
    With Application
      .Calculation = xlCalculationAutomatic
      .DisplayStatusBar = True
      .EnableEvents = True
    End With

End Sub

Function Standardize()
  ' Remove punctuation and normalize case
    arrRngComp(r, c) = Split(StrConv(Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace( _
                        Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace(Replace(arrRngComp(r, c), _
                            ".", ""), ",", ""), "?", ""), "!", ""), ";", ""), ":", ""), """", ""), "'", ""), "<", ""), _
                                ">", ""), "(", ""), ")", ""), "[", ""), "]", ""), "_", " "), "-", " "), "/", " "), vbLowerCase))

End Function

Function GenUnique()
Dim Word
Const Nul As Long = 0
  ' Use Scripting Dictionary to remove duplicates
    With Dict
        If Complete = False Then
            For Each Word In arrRngComp(r, c): .Item(Trim(Word)) = Nul: Next
          Else
            For Each Word In arrCommonComp: .Item(Trim(Word)) = Nul: Next
        End If
          Arr = .Keys
          .RemoveAll
    End With
End Function

Still working on the tokinization but hope to have it ready soon.

AMAS
 
Upvote 0
Hey,

Registered just to say thanks. This macro has saved me undoubted amounts of stress induced alopecia and bucket loads of time!

So, thanks!

juke
 
Upvote 0

Forum statistics

Threads
1,223,250
Messages
6,171,036
Members
452,374
Latest member
keccles

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top