So sánh sự giống/khác nhau giữa 2 chuỗi bằng hàm tự tạo

  • Thread starter vothanhthu
  • Ngày gửi
V

vothanhthu

Guest
1. Giới thiệu hàm
Trong lúc lập hàm sử dụng cho các bài toán, chúng ta không ít lần muốn có một hàm có thể so sánh sự giống/khác nhau giữa 2 chuỗi kí tự, mình cũng đã gặp rất nhiều lần như thế, nên hôm nay mình xin chia sẽ lại bài viết của Alain bằng cách áp dụng Levenshtein Distance để so sánh sự giống và khác nhau giữa 2 chuỗi kí tự

Toàn bộ code của Function này:
Mã:
Public Function valuePhrase#(ByRef S1$, ByRef S2$)
    valuePhrase = LevenshteinDistance(S1, S2)
End Function

Public Function valueWords#(ByRef S1$, ByRef S2$)
    Dim wordsS1$(), wordsS2$()
    wordsS1 = SplitMultiDelims(S1, " _-")
    wordsS2 = SplitMultiDelims(S2, " _-")
    Dim Word1%, Word2%, thisD#, wordbest#
    Dim wordsTotal#
    For Word1 = LBound(wordsS1) To UBound(wordsS1)
        wordbest = Len(S2)
        For Word2 = LBound(wordsS2) To UBound(wordsS2)
            thisD = LevenshteinDistance(wordsS1(Word1), wordsS2(Word2))
            If thisD < wordbest Then wordbest = thisD
            If thisD = 0 Then GoTo foundbest
        Next Word2
foundbest:
        wordsTotal = wordsTotal + wordbest
    Next Word1
    valueWords = wordsTotal
End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' SplitMultiDelims
' This function splits Text into an array of substrings, each substring
' delimited by any character in DelimChars. Only a single character
' may be a delimiter between two substrings, but DelimChars may
' contain any number of delimiter characters. It returns a single element
' array containing all of text if DelimChars is empty, or a 1 or greater
' element array if the Text is successfully split into substrings.
' If IgnoreConsecutiveDelimiters is true, empty array elements will not occur.
' If Limit greater than 0, the function will only split Text into 'Limit'
' array elements or less. The last element will contain the rest of Text.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function SplitMultiDelims(ByRef Text As String, ByRef DelimChars As String, _
        Optional ByVal IgnoreConsecutiveDelimiters As Boolean = False, _
        Optional ByVal Limit As Long = -1) As String()
    Dim ElemStart As Long, N As Long, M As Long, Elements As Long
    Dim lDelims As Long, lText As Long
    Dim Arr() As String

    lText = Len(Text)
    lDelims = Len(DelimChars)
    If lDelims = 0 Or lText = 0 Or Limit = 1 Then
        ReDim Arr(0 To 0)
        Arr(0) = Text
        SplitMultiDelims = Arr
        Exit Function
    End If
    ReDim Arr(0 To IIf(Limit = -1, lText - 1, Limit))

    Elements = 0: ElemStart = 1
    For N = 1 To lText
        If InStr(DelimChars, Mid(Text, N, 1)) Then
            Arr(Elements) = Mid(Text, ElemStart, N - ElemStart)
            If IgnoreConsecutiveDelimiters Then
                If Len(Arr(Elements)) > 0 Then Elements = Elements + 1
            Else
                Elements = Elements + 1
            End If
            ElemStart = N + 1
            If Elements + 1 = Limit Then Exit For
        End If
    Next N
    'Get the last token terminated by the end of the string into the array
    If ElemStart <= lText Then Arr(Elements) = Mid(Text, ElemStart)
    'Since the end of string counts as the terminating delimiter, if the last character
    'was also a delimiter, we treat the two as consecutive, and so ignore the last elemnent
    If IgnoreConsecutiveDelimiters Then If Len(Arr(Elements)) = 0 Then Elements = Elements - 1

    ReDim Preserve Arr(0 To Elements) 'Chop off unused array elements
    SplitMultiDelims = Arr
End Function
'Calculate the Levenshtein Distance between two strings (the number of insertions,
'deletions, and substitutions needed to transform the first string into the second)
Public Function LevenshteinDistance(ByRef S1 As String, ByVal S2 As String) As Long
    Dim L1 As Long, L2 As Long, D() As Long 'Length of input strings and distance matrix
    Dim i As Long, j As Long, cost As Long 'loop counters and cost of substitution for current letter
    Dim cI As Long, cD As Long, cS As Long 'cost of next Insertion, Deletion and Substitution
    L1 = Len(S1): L2 = Len(S2)
    ReDim D(0 To L1, 0 To L2)
    For i = 0 To L1: D(i, 0) = i: Next i
    For j = 0 To L2: D(0, j) = j: Next j

    For j = 1 To L2
        For i = 1 To L1
            cost = Abs(StrComp(Mid$(S1, i, 1), Mid$(S2, j, 1), vbTextCompare))
            cI = D(i - 1, j) + 1
            cD = D(i, j - 1) + 1
            cS = D(i - 1, j - 1) + cost
            If cI <= cD Then 'Insertion or Substitution
                If cI <= cS Then D(i, j) = cI Else D(i, j) = cS
            Else 'Deletion or Substitution
                If cD <= cS Then D(i, j) = cD Else D(i, j) = cS
            End If
        Next i
    Next j
    LevenshteinDistance = D(L1, L2)
End Function
2. Một số ví dụ minh họa cho dễ hiểu
2.1. Hàm valuePhrase

Hàm sẽ có dạng valuePhrase(Chuỗi 1,Chuỗi 2)
Hàm này sẽ so sánh khác nhau giữa 2 chuỗi kí tự, kết quả trả về sẽ là số kí tự khác nhau trong 2 chuỗi, xem ví dụ minh họa cho dễ hiểu nha.
Bạn cần đăng nhập để thấy hình ảnh

2.2. Hàm valueWords
Hàm sẽ có dạng valueWords(Chuỗi 1,Chuỗi 2)
Hàm này sẽ so sánh khác nhau giữa các kí tự trong một chuỗi kí tự liền nhau nằm giữa các khoảng trống. kết quả trả về sẽ là số kí tự khác nhau giữa 2 chuỗi này. Xem ví dụ mình sẽ giải thích kỹ hơn.
Bạn cần đăng nhập để thấy hình ảnh

Ở ví vụ, mình tô màu xanh là 2 sự giống nhau của 2 chuỗi, màu đỏđen là sự khác nhau giữa 2 chuỗi.
Mình ví dụ công thức ở ô D3, Bạn thấy 2 chuỗi này có chuỗi tuhoc là giống nhau, và có 3 ki tự khác nhau (vba và _vb), kết quả là 3.
Ở ví dụ ô D4, dù trong chuỗi 2 có .net là khác chuỗi 1, nhưng chuỗi tuhocvba là chuỗi nhiều kí tự liên tiếp giống nhau nhất (không tính khoảng trống). Có nghĩ là, bên chuỗi 2 CÓ CHỨA chuỗi tuhocvba giống hoàn toàn với chuỗi tuhocvba bên chuỗi 1, nên kết quả =0.

Tất cả giải thích về nguyên lý hoạt động code nằm hết trong nguồn, các bạn đọc tham khảo nhé.
Nguồn:
 
Sửa lần cuối bởi điều hành viên:
H

haokira

Guest
Cái này hay quá (y)
Ngày trước mình đọc được bài này:
Mình tìm hiểu thì nó gọi là dò tìm mờ (Fuzzy) thì phải.
Mình nghĩ nếu viết nó thành 1 udf khác kiểu dò tìm theo kết quả có độ giống gần nhất thì tuyệt. Ai viết được cho mình ké với nhé :D
 
V

vothanhthu

Guest
Cái này hay quá (y)
Ngày trước mình đọc được bài này:
Mình tìm hiểu thì nó gọi là dò tìm mờ (Fuzzy) thì phải.
Mình nghĩ nếu viết nó thành 1 udf khác kiểu dò tìm theo kết quả có độ giống gần nhất thì tuyệt. Ai viết được cho mình ké với nhé :D
HocExcelOnline họ giấu nghề cho học viên vào học ^^!
Lúc trước mình nhớ cũng lâu lắm rồi, mình cũng làm dò tìm giống nhất. Mình lập cột phụ, cho hàm này vào xác định giống/khác rồi dùng hàm Min. Viết ra hẳn một hàm riêng thì mình chưa thử !
 
H

haokira

Guest
Mình đã vào học nhưng cũng k viết được, 1 phần do học kiểu đó có thể k hợp vs mình hoặc mình dốt quá =))
Đành chờ xem có cao thủ nào có hứng thú và cho ké vậy :D
 
V

vothanhthu

Guest
Mình đã vào học nhưng cũng k viết được, 1 phần do học kiểu đó có thể k hợp vs mình hoặc mình dốt quá =))
Đành chờ xem có cao thủ nào có hứng thú và cho ké vậy :D
Có phải dạng như thế này không nhỉ?
 
Sửa lần cuối bởi điều hành viên:
H

haokira

Guest
Nó mà có thêm % giống như trang này

thì ngon nhể :D
 
V

vothanhthu

Guest
Rảnh mình mò cho !
 
Sửa lần cuối bởi điều hành viên:
V

vothanhthu

Guest
Nó mà có thêm % giống như trang này

thì ngon nhể :D
Của bác đây, Hàm trả về % giống nhau giữa 2 chuỗi, Hehe !
Bạn cần đăng nhập để thấy hình ảnh

Và đây là code:
Mã:
Option Explicit
Type RankInfo
    Offset As Integer
    Percentage As Single
End Type


Function FuzzyPercent(ByVal String1 As String, _
                      ByVal String2 As String, _
                      Optional Algorithm As Integer = 3, _
                      Optional Normalised As Boolean = False) As Single
'*************************************

'*************************************
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 strWork As String


'-------------------------------------------------------
'-- Chuẩn hóa trim
'-------------------------------------------------------
If Normalised = False Then
    String1 = LCase$(Application.Trim(String1))
    String2 = LCase$(Application.Trim(String2))
End If


'----------------------------------------------
'-- Set 100% nếu 2 chuỗi giống nhau
'----------------------------------------------
If String1 = String2 Then
    FuzzyPercent = 1
    Exit Function
End If


intLen1 = Len(String1)
intLen2 = Len(String2)


'----------------------------------------
'Gán 0% nếu độ dài chuỗi <2
'----------------------------------------
If intLen1 < 2 Then
    FuzzyPercent = 0
    Exit Function
End If
intTotScore = 0
intScore = 0
'--------------------------------------------------------
'Nếu Algorithm = 1 hoặc 3 thì tìm kiếm những kí tự đơn
'--------------------------------------------------------
If (Algorithm And 1) <> 0 Then
    FuzzyAlg1 String1, String2, intScore, intTotScore
    If intLen1 < intLen2 Then FuzzyAlg1 String2, String1, intScore, intTotScore
End If
'-----------------------------------------------------------
'-- Nếu Algorithm = 2 hoặc 3 thì tìm kiếm những kí tự tiếp theo
'-----------------------------------------------------------
If (Algorithm And 2) <> 0 Then
    FuzzyAlg2 String1, String2, intScore, intTotScore
    If intLen1 < intLen2 Then FuzzyAlg2 String2, String1, intScore, intTotScore
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             'Cập nhật tổng
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          'Cập nhật %
        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 = 2 To intLen1
    strWork = String2                          'Get thêm 1 bản sao của String2
    intTo = intLen1 - intCurLen + 1
    TotScore = TotScore + Int(intLen1 / intCurLen)  'Cập nhật %
    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) 'Chuoi tim thay bi loi
            Score = Score + 1    'Cập nhật %
        End If
    Next intPtr
Next intCurLen
End Sub
'Sourse: https://www.mrexcel.com
 
Sửa lần cuối bởi điều hành viên:

PeterVu

Yêu THVBA nhất
1. Giới thiệu hàm
Trong lúc lập hàm sử dụng cho các bài toán, chúng ta không ít lần muốn có một hàm có thể so sánh sự giống/khác nhau giữa 2 chuỗi kí tự, mình cũng đã gặp rất nhiều lần như thế, nên hôm nay mình xin chia sẽ lại bài viết của Alain bằng cách áp dụng Levenshtein Distance để so sánh sự giống và khác nhau giữa 2 chuỗi kí tự

Toàn bộ code của Function này:
Mã:
Public Function valuePhrase#(ByRef S1$, ByRef S2$)
    valuePhrase = LevenshteinDistance(S1, S2)
End Function

Public Function valueWords#(ByRef S1$, ByRef S2$)
    Dim wordsS1$(), wordsS2$()
    wordsS1 = SplitMultiDelims(S1, " _-")
    wordsS2 = SplitMultiDelims(S2, " _-")
    Dim Word1%, Word2%, thisD#, wordbest#
    Dim wordsTotal#
    For Word1 = LBound(wordsS1) To UBound(wordsS1)
        wordbest = Len(S2)
        For Word2 = LBound(wordsS2) To UBound(wordsS2)
            thisD = LevenshteinDistance(wordsS1(Word1), wordsS2(Word2))
            If thisD < wordbest Then wordbest = thisD
            If thisD = 0 Then GoTo foundbest
        Next Word2
foundbest:
        wordsTotal = wordsTotal + wordbest
    Next Word1
    valueWords = wordsTotal
End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' SplitMultiDelims
' This function splits Text into an array of substrings, each substring
' delimited by any character in DelimChars. Only a single character
' may be a delimiter between two substrings, but DelimChars may
' contain any number of delimiter characters. It returns a single element
' array containing all of text if DelimChars is empty, or a 1 or greater
' element array if the Text is successfully split into substrings.
' If IgnoreConsecutiveDelimiters is true, empty array elements will not occur.
' If Limit greater than 0, the function will only split Text into 'Limit'
' array elements or less. The last element will contain the rest of Text.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function SplitMultiDelims(ByRef Text As String, ByRef DelimChars As String, _
        Optional ByVal IgnoreConsecutiveDelimiters As Boolean = False, _
        Optional ByVal Limit As Long = -1) As String()
    Dim ElemStart As Long, N As Long, M As Long, Elements As Long
    Dim lDelims As Long, lText As Long
    Dim Arr() As String

    lText = Len(Text)
    lDelims = Len(DelimChars)
    If lDelims = 0 Or lText = 0 Or Limit = 1 Then
        ReDim Arr(0 To 0)
        Arr(0) = Text
        SplitMultiDelims = Arr
        Exit Function
    End If
    ReDim Arr(0 To IIf(Limit = -1, lText - 1, Limit))

    Elements = 0: ElemStart = 1
    For N = 1 To lText
        If InStr(DelimChars, Mid(Text, N, 1)) Then
            Arr(Elements) = Mid(Text, ElemStart, N - ElemStart)
            If IgnoreConsecutiveDelimiters Then
                If Len(Arr(Elements)) > 0 Then Elements = Elements + 1
            Else
                Elements = Elements + 1
            End If
            ElemStart = N + 1
            If Elements + 1 = Limit Then Exit For
        End If
    Next N
    'Get the last token terminated by the end of the string into the array
    If ElemStart <= lText Then Arr(Elements) = Mid(Text, ElemStart)
    'Since the end of string counts as the terminating delimiter, if the last character
    'was also a delimiter, we treat the two as consecutive, and so ignore the last elemnent
    If IgnoreConsecutiveDelimiters Then If Len(Arr(Elements)) = 0 Then Elements = Elements - 1

    ReDim Preserve Arr(0 To Elements) 'Chop off unused array elements
    SplitMultiDelims = Arr
End Function
'Calculate the Levenshtein Distance between two strings (the number of insertions,
'deletions, and substitutions needed to transform the first string into the second)
Public Function LevenshteinDistance(ByRef S1 As String, ByVal S2 As String) As Long
    Dim L1 As Long, L2 As Long, D() As Long 'Length of input strings and distance matrix
    Dim i As Long, j As Long, cost As Long 'loop counters and cost of substitution for current letter
    Dim cI As Long, cD As Long, cS As Long 'cost of next Insertion, Deletion and Substitution
    L1 = Len(S1): L2 = Len(S2)
    ReDim D(0 To L1, 0 To L2)
    For i = 0 To L1: D(i, 0) = i: Next i
    For j = 0 To L2: D(0, j) = j: Next j

    For j = 1 To L2
        For i = 1 To L1
            cost = Abs(StrComp(Mid$(S1, i, 1), Mid$(S2, j, 1), vbTextCompare))
            cI = D(i - 1, j) + 1
            cD = D(i, j - 1) + 1
            cS = D(i - 1, j - 1) + cost
            If cI <= cD Then 'Insertion or Substitution
                If cI <= cS Then D(i, j) = cI Else D(i, j) = cS
            Else 'Deletion or Substitution
                If cD <= cS Then D(i, j) = cD Else D(i, j) = cS
            End If
        Next i
    Next j
    LevenshteinDistance = D(L1, L2)
End Function
2. Một số ví dụ minh họa cho dễ hiểu
2.1. Hàm valuePhrase

Hàm sẽ có dạng valuePhrase(Chuỗi 1,Chuỗi 2)
Hàm này sẽ so sánh khác nhau giữa 2 chuỗi kí tự, kết quả trả về sẽ là số kí tự khác nhau trong 2 chuỗi, xem ví dụ minh họa cho dễ hiểu nha.
Bạn cần đăng nhập để thấy hình ảnh

2.2. Hàm valueWords
Hàm sẽ có dạng valueWords(Chuỗi 1,Chuỗi 2)
Hàm này sẽ so sánh khác nhau giữa các kí tự trong một chuỗi kí tự liền nhau nằm giữa các khoảng trống. kết quả trả về sẽ là số kí tự khác nhau giữa 2 chuỗi này. Xem ví dụ mình sẽ giải thích kỹ hơn.
Bạn cần đăng nhập để thấy hình ảnh

Ở ví vụ, mình tô màu xanh là 2 sự giống nhau của 2 chuỗi, màu đỏđen là sự khác nhau giữa 2 chuỗi.
Mình ví dụ công thức ở ô D3, Bạn thấy 2 chuỗi này có chuỗi tuhoc là giống nhau, và có 3 ki tự khác nhau (vba và _vb), kết quả là 3.
Ở ví dụ ô D4, dù trong chuỗi 2 có .net là khác chuỗi 1, nhưng chuỗi tuhocvba là chuỗi nhiều kí tự liên tiếp giống nhau nhất (không tính khoảng trống). Có nghĩ là, bên chuỗi 2 CÓ CHỨA chuỗi tuhocvba giống hoàn toàn với chuỗi tuhocvba bên chuỗi 1, nên kết quả =0.

Tất cả giải thích về nguyên lý hoạt động code nằm hết trong nguồn, các bạn đọc tham khảo nhé.
Nguồn:
Cám ơn bạn chia sẻ, đúng cái mình đang cần. Để test thử xem nó hoạt động có đúng như mình mong đợi không.
 

lethanh1

Yêu THVBA
Nếu có thể bạn viết thêm để tô màu đoạn ký tự khác nhau thì thì tốt quá!
 
B

bvtvba

Guest
Đã làm hàm (Function) thì theo tôi biết là không tô màu được.
Là Sub thì còn có thể làm được.
 
Top