Filter Array 2D theo nhiều điều kiện

Liên hệ QC

Ngô Hải Đăng

Thành viên hoạt động
Tham gia
31/8/17
Bài viết
180
Được thích
244
Điểm
398
Donate (Paypal)
Donate
Donate (Momo)
Donate
Giới tính
Nam
Hàm nhận 2 tham số iArray (Mảng hoặc Range) và iFilter as String (điều kiện Filter)
Các điều kiện được nối với nhau bởi OR, AND, ]OR[, ]AND[ theo mức độ ưu tiên từ cao đến thấp như sau: ]AND[ >> ]OR[ >> AND >> OR.
Các toán tử so sánh gồm: LIKE, !LIKE, <, <=, >, >=, =, <>. Trong đó LIKE và !LIKE dùng cho Text và Text phải được đặt trong 2 dấu nháy đơn (Ví dụ: 'Giải pháp Excel')
Chuỗi Filter được viết theo cú pháp như sau: [5] > 100 AND [3] LIKE '*GPE*'
Trong đó[5] và [3] là cột cần Filter, các toán tử so sánh và các phép nối OR AND đều phải cách ra 1 khoảng trắng.
Trường hợp cần so sánh ngày thì ta phải thêm # trước ngày cần so sánh (Ví dụ: [1] > #21/12/2020)
Trường hợp có sử dụng công thức thì phải thêm ? đứng trước (Ví dụ: ?[3]*[4] > 1000000 hoặc là ?Year([1]) = 2020 hoặc là ?Left([2],3) = 'GPE')

Code trong Module
PHP:
Function FilterArray2D(ByVal iArray, ByVal iFilter As String)
  Dim x&, y&, z&, minY&, maxY&, minX&, maxX&, sF$
  Dim yI(), xF(), aX

  ReDim aX(1 To 1, 1 To 1)
  If iFilter = "" Then
    aX(1, 1) = "Không có Filter..."
  Else
    If TypeName(iArray) = "Range" Then iArray = iArray.Value
    MakeFilter iFilter
    minX = LBound(iArray, 2): maxX = UBound(iArray, 2)
    For x = 1 To maxX - minX + 1
      If InStr(1, iFilter, "[" & vbBack & vbBack & x & vbBack & vbBack & "]", vbTextCompare) > 0 Then
        z = z + 1: ReDim Preserve xF(1 To z): xF(z) = x
      End If
    Next x
    If z = 0 Then
      aX(1, 1) = "Filter sai..."
    Else
      z = 0
      minY = LBound(iArray, 1): maxY = UBound(iArray, 1)
      For y = minY To maxY
        sF = iFilter
        For x = 1 To UBound(xF)
          sF = Replace(sF, "[" & vbBack & vbBack & xF(x) & vbBack & vbBack & "]", GetValueArray(iArray(y, xF(x) - 1 + minX)), , , vbTextCompare)
        Next x
        If CheckFilter(sF) Then z = z + 1: ReDim Preserve yI(1 To z): yI(z) = y
      Next y
      If z = 0 Then
        aX(1, 1) = "Không tìm ra..."
      Else
        ReDim aX(1 To z, 1 To maxX - minX + 1)
        For y = 1 To z
          For x = 1 To maxX - minX + 1
            aX(y, x) = iArray(yI(y), x - 1 + minX)
          Next x
        Next y
      End If
    End If
  End If
  FilterArray2D = aX
End Function

Private Sub MakeFilter(iFilter As String)
  Dim xBeginText&, xEndText&, sText$, x&, aCompare

  iFilter = WorksheetFunction.Trim(iFilter)
  iFilter = Replace(iFilter, "[", "[" & vbBack & vbBack)
  iFilter = Replace(iFilter, "]", vbBack & vbBack & "]")
  iFilter = Replace(iFilter, " ", vbBack, , , vbTextCompare)
  iFilter = Replace(iFilter, vbBack & vbBack & vbBack, vbBack)

  xBeginText = InStr(1, iFilter, "'") + 1
  While xBeginText > 1
    xEndText = InStr(xBeginText, iFilter, "'") + 1
    sText = Mid(iFilter, xBeginText, xEndText - xBeginText)
    iFilter = Replace(iFilter, sText, Replace(sText, vbBack & vbBack, ""))
    iFilter = Replace(iFilter, sText, Replace(sText, vbBack, " "))
    xBeginText = InStr(xEndText, iFilter, "'") + 1
  Wend

  aCompare = Array("!like", "<=", ">=", "<>", "like", "<", ">", "=")
  For x = 0 To UBound(aCompare)
    iFilter = Replace(iFilter, vbBack & aCompare(x) & vbBack, vbBack & x & vbBack, , , vbTextCompare)
  Next x
End Sub

Private Function GetValueArray(iValue)
  Select Case TypeName(iValue)
    Case Is = "String", "Null", "Empty", "Error": GetValueArray = "'" & CStr(iValue) & "'"
    Case Else: GetValueArray = CDbl(iValue)
  End Select
End Function

Private Function CheckFilter(ByVal iFilter As String) As Boolean
  Dim aF: aF = Split(iFilter, vbBack & "]AND[" & vbBack, , vbTextCompare)
  Dim x&
  For x = LBound(aF) To UBound(aF)
    If Not CheckFilter1(aF(x)) Then Exit Function
  Next x
  CheckFilter = True
End Function

Private Function CheckFilter1(ByVal iFilter As String) As Boolean
  Dim aF: aF = Split(iFilter, vbBack & "]OR[" & vbBack, , vbTextCompare)
  Dim x&
  For x = LBound(aF) To UBound(aF)
    If CheckFilter2(aF(x)) Then CheckFilter1 = True: Exit Function
  Next x
End Function

Private Function CheckFilter2(ByVal iFilter As String) As Boolean
  Dim aF: aF = Split(iFilter, vbBack & "AND" & vbBack, , vbTextCompare)
  Dim x&
  For x = LBound(aF) To UBound(aF)
    If Not CheckFilter3(aF(x)) Then Exit Function
  Next x
  CheckFilter2 = True
End Function

Private Function CheckFilter3(ByVal iFilter As String) As Boolean
  Dim aF: aF = Split(iFilter, vbBack & "OR" & vbBack, , vbTextCompare)
  Dim x&
  For x = LBound(aF) To UBound(aF)
    If CheckFilter4(aF(x)) Then CheckFilter3 = True: Exit Function
  Next x
End Function

Private Function CheckFilter4(ByVal iFilter As String) As Boolean
  Dim aF: aF = Split(iFilter, vbBack)
  Dim u1, u2
  u1 = GetValueFilter(aF(0)): If IsArray(u1) Then Exit Function
  u2 = GetValueFilter(aF(2)): If IsArray(u2) Then Exit Function
  Select Case aF(1)
    Case Is = 0: CheckFilter4 = Not u1 Like u2
    Case Is = 1: CheckFilter4 = u1 <= u2
    Case Is = 2: CheckFilter4 = u1 >= u2
    Case Is = 3: CheckFilter4 = u1 <> u2
    Case Is = 4: CheckFilter4 = u1 Like u2
    Case Is = 5: CheckFilter4 = u1 < u2
    Case Is = 6: CheckFilter4 = u1 > u2
    Case Is = 7: CheckFilter4 = u1 = u2
  End Select
End Function

Private Function GetValueFilter(ByVal iString As String)
  On Error GoTo HError
  Select Case Left$(iString, 1)
    Case Is = "'": GetValueFilter = Mid(iString, 2, Len(iString) - 2)
    Case Is = "?": GetValueFilter = Evaluate("=" & Mid(iString, 2))
    Case Is = "#": GetValueFilter = CDbl(CDate(Mid(Replace(iString, "'", ""), 2)))
    Case Else: GetValueFilter = Val(iString)
  End Select
  Exit Function
HError:
GetValueFilter = Array(1)
End Function

Code Test
PHP:
Sub TestFilterArray2D()
  Dim sFilter$, rData As Range, rResult As Range, aX, xTimer!
  xTimer = Timer
  'Dieu kien Filter
  sFilter = "[2] like 'KH*' and [5] like 'Kg' and [8] > 0"
  'Chon vung Du lieu can Filter
  Set rData = Sheet3.[A3:N9601]
  'Chon Cell de gan ket qua
  Set rResult = Sheet3.[P3]
  aX = FilterArray2D(rData.Value, sFilter)
  Application.ScreenUpdating = False
  rResult.Resize(rData.Rows.Count, rData.Columns.Count).ClearContents
  rResult.Resize(UBound(aX, 1), UBound(aX, 2)) = aX
  Application.ScreenUpdating = True
  MsgBox "Time is: " & Timer - xTimer
End Sub
 

File đính kèm

  • ViDu.xlsb
    361.6 KB · Đọc: 58
Lần chỉnh sửa cuối:
Thứ nhất là vì Recordset đã có nhiều bài hướng dẫn trên diễn đàn, Advanced Filter cũng vậy nên nếu em viết lại thì xem như thừa vì cũng không có gì mới so với các bài viết trước. Thứ hai là giải thuật Filter Array trên diễn đàn cũng có nhưng đã rất lâu và em muốn viết 1 giải thuật khác tốt hơn. Chỉ như vậy thôi.
Theo tôi thì bạn nên khai thác cái Recordset thì sẽ đáp ứng được yêu cầu.
 
Upvote 0
Cách viết code của bạn khiến giải thuật dính liền với giao diện.
Đối với các code mì ăn liền như hầu hết các code viết giúp người khác ở đây thì làm như vậy rất nhanh và hiệu quả.
Với code viết để dùng trong thư viện thì cách sắp xếp code cũng là một công việc quan trọng cần thiết kế.

Ở bài #19, tôi cố tình hỏi như thế là để xem bạn có cách tách rời giải thuật ra. Điển hình là có cái hàm chính, gọi hàm A để phân tích giao diện, gọi hàm B để lấy dữ liệu và làm việc, gọi hàm C để trình bày đầu ra. Kiểu như MVC của dot net vậy. Với thiết kế này, hàm B có đủ tự do để chọn lựa giải thuật tốt nhất hoặc tích nghi với hoàn cảnh nhất. (toi chỉ quan trọng ở A,B,C thôi. Chuyện B gọi b2 là chuyện riêng của nó)

Trên diễn đàn này, tôi chỉ thấy có một bạn làm chuyện này. Nhưng rất tiếc là code mang tính chất biểu diễn nhiều hơn thực dụng cho nên nhét một đống options vào làm rối cả code.
 
Upvote 0
Theo tôi thì bạn nên khai thác cái Recordset thì sẽ đáp ứng được yêu cầu.
Cho mình hỏi thêm là trong Recordset mình có thể Filter với điều kiện Field1 <>= Field2 hay chỉ Filter với 1 giá trị cố định.

Cách viết code của bạn khiến giải thuật dính liền với giao diện.
Đối với các code mì ăn liền như hầu hết các code viết giúp người khác ở đây thì làm như vậy rất nhanh và hiệu quả.
Với code viết để dùng trong thư viện thì cách sắp xếp code cũng là một công việc quan trọng cần thiết kế.

Ở bài #19, tôi cố tình hỏi như thế là để xem bạn có cách tách rời giải thuật ra. Điển hình là có cái hàm chính, gọi hàm A để phân tích giao diện, gọi hàm B để lấy dữ liệu và làm việc, gọi hàm C để trình bày đầu ra. Kiểu như MVC của dot net vậy. Với thiết kế này, hàm B có đủ tự do để chọn lựa giải thuật tốt nhất hoặc tích nghi với hoàn cảnh nhất. (toi chỉ quan trọng ở A,B,C thôi. Chuyện B gọi b2 là chuyện riêng của nó)

Trên diễn đàn này, tôi chỉ thấy có một bạn làm chuyện này. Nhưng rất tiếc là code mang tính chất biểu diễn nhiều hơn thực dụng cho nên nhét một đống options vào làm rối cả code.
Code của em có thể tách ra được theo như gợi ý của anh, em sẽ viết lại theo gợi ý này.
 
Upvote 0
Upvote 0
@Ngô Hải Đăng sử dụng ado lấy nó vào 1 array xong duyệt nó mà tính toán đi còn dễ làm ăn đấy
SQL thuần Ado cà chua lắm ... mặc dù tôi viết ra cái SQL úp GPE đó mà nhiều câu lệnh SQL thuần ado cũng tịt toàn tập luôn những lúc như thế tôi cho vào 1 array xong xử lý nó quá ngon lành và nhẹ nhàng

thêm 2 dòng lặp tốn thêm mấy giây mà mình làm chủ nó .... còn hơn nó làm chủ mình !?

Thế thôi cho nhẹ đầu
 
Upvote 0
@Ngô Hải Đăng sử dụng ado lấy nó vào 1 array xong duyệt nó mà tính toán đi còn dễ làm ăn đấy
SQL thuần Ado cà chua lắm ... mặc dù tôi viết ra cái SQL úp GPE đó mà nhiều câu lệnh SQL thuần ado cũng tịt toàn tập luôn những lúc như thế tôi cho vào 1 array xong xử lý nó quá ngon lành và nhẹ nhàng

thêm 2 dòng lặp tốn thêm mấy giây mà mình làm chủ nó .... còn hơn nó làm chủ mình !?

Thế thôi cho nhẹ đầu
Không vòng lặp và nhìn cũng không mấy cà chua. Chạy trên 365

1608623203849.png
 
Upvote 0
Upvote 0
Cho mình hỏi thêm là trong Recordset mình có thể Filter với điều kiện Field1 <>= Field2 hay chỉ Filter với 1 giá trị cố định.
Sau khi đưa vào Recordset nếu không dùng vòng lặp để xử lý tiếp thì chỉ có thể như sau (*):

1608623671165.png

(*) Kết quả được chạy ở Office 365
 
Upvote 0
Recordset cần sử dụng là của DAO chứ không phải của ADO.
ADO chỉ có hiệu quả khi được dùng để lấy dữ liệu ở nơi khác. Ngày nay, với sự phát triển của Power Query thì vị trí của ADO càng ít đi.
Ở đây, khi nói chuyện "giải thuật" tôi đã giả sử giải thuật đã được đưa vào sub riêng, chỉ làm việc với Array. Việc đưa dữ liệu từ range vào Array là chuyện của sub tiếp xúc với giao diện, lấy unput đưa vào cho sub làm việc với 2D array (đúng theo tiêu đề).
 
Upvote 0
Recordset cần sử dụng là của DAO chứ không phải của ADO.
ADO chỉ có hiệu quả khi được dùng để lấy dữ liệu ở nơi khác. Ngày nay, với sự phát triển của Power Query thì vị trí của ADO càng ít đi.
Ở đây, khi nói chuyện "giải thuật" tôi đã giả sử giải thuật đã được đưa vào sub riêng, chỉ làm việc với Array. Việc đưa dữ liệu từ range vào Array là chuyện của sub tiếp xúc với giao diện, lấy unput đưa vào cho sub làm việc với 2D array (đúng theo tiêu đề).
Bài #28 em dùng cách là lấy 1 vùng dữ liệu nguồn chuyển thành mảng rồi từ mảng đó đưa nó vào Recordset. Sau đó thực hiện lọc, sắp xếp và trả về kết quả.
 
Upvote 0
Web KT
Back
Top Bottom