1

I am trying to change an Excel xlookup formula to visual basic to reduce the file size. However, I cannot quite get the code right. There are two criteria to match, when both criteria match, then a value from my return array (in a different worksheet) should be returned.

Both individual xlookups do not give me any errors but the combined lookup does.

This is the formula I am trying to change to visual basic:

=XLOOKUP(1, (AK2=auditdata!A:A)*(F2=auditdata!D:D), auditdata!E:E, "no match")

My code so far:

Sub CheckTimeAudited_Method()
    
    'number of rows to check in main and audit data
    Dim TotaltoCheck, EndofAuditRange As Long 'last row in the column to check
    TotaltoCheck = Worksheets("main").Range("A2").End(xlDown).Row        
    EndofAuditRange = Worksheets("auditdata").Range("A1").End(xlDown).Row
    
    'audit data range
    Dim TableArrayRoom As Range
    Set TableArrayRoom = Worksheets("auditdata").Range(Worksheets("auditdata").Cells(1, 1), Worksheets("auditdata").Cells(EndofAuditRange, 1)) 'column auditdata!A:A room
    
    Dim TableArrayDate As Range
    Set TableArrayDate = Worksheets("auditdata").Range(Worksheets("auditdata").Cells(1, 4), Worksheets("auditdata").Cells(EndofAuditRange, 4)) 'column auditdata!D:D date
    
    Dim TableArrayTime As Range
    Set TableArrayTime = Worksheets("auditdata").Range(Worksheets("auditdata").Cells(1, 5), Worksheets("auditdata").Cells(EndofAuditRange, 5)) 'column auditdata!E:E time
     
    'destination ranges
    Dim CheckTimeCell As Range
    
    Dim CheckTimeColumn As Range
    Set CheckTimeColumn = Worksheets("main").Range(Worksheets("main").Cells(2, 24), Worksheets("main").Cells(TotaltoCheck, 24)) 'column X
        
    For Each CheckTimeCell In CheckTimeColumn 
        CheckTimeCell.Value = Application.XLookup(CheckTimeCell.Offset(0, 13) & CheckTimeCell.Offset(0, -18), TableArrayRoom & TableArrayDate, TableArrayTime, "no match")
        'OR 
        'CheckTimeCell.Value = Application.XLookup(1, CheckTimeCell.Offset(0, 13) & CheckTimeCell.Offset(0, -18), TableArrayRoom & TableArrayDate, TableArrayTime, "no match")

    Next CheckTimeCell

End Sub

I have tried a few different things as above, also using * instead of &, and using () around the individual criteria match. All give me "Run-time error 13 Type mismatch".

Mayukh Bhattacharya
  • 12,541
  • 5
  • 21
  • 32
Saira
  • 15
  • 4
  • 2
    Yoiu should be able to use something like `Worksheets("main").Evaluate("XLOOKUP(1, (AK2=auditdata!A:A)*(F2=auditdata!D:D), auditdata!E:E, ""no match"")")` after substituting `AK2` and `F2` for the addresses of the cells with the actual lookup values. – Tim Williams Jul 31 '23 at 16:51
  • That did not work for me. I get a "#NAME?" error. I think because the code does not pick up my variable names. How are you referencing the cells? I do not want to hard-code cell names that are my lookup_values eg F2, AK2 - the For Each loops through all the cells in the column. Thanks – Saira Aug 01 '23 at 09:50

2 Answers2

3

Following from comment above - tested and works for me

Sub CheckTimeAudited_Method()
    
    Const F_TMPLT As String = "XLOOKUP(1, (<AK>=auditdata!A:A)*(<F>=auditdata!D:D), auditdata!E:E, ""no match"")"
    
    Dim wsMain As Worksheet, cTime As Range, frm As String
    
    Set wsMain = ThisWorkbook.Worksheets("main")
    
    For Each cTime In wsMain.Range("X2:X" & wsMain.Cells(Rows.Count, "A").End(xlUp).row).Cells
        'replace the placeholders in the formula template
        frm = Replace(F_TMPLT, "<AK>", cTime.EntireRow.Columns("AK").Address)
        frm = Replace(frm, "<F>", cTime.EntireRow.Columns("F").Address)
        cTime.Value = wsMain.Evaluate(frm) 'Execute the formula using
                                           ' the context of `wsMain`
    Next cTime

End Sub

EDIT After review of your shared file... It does work , but it's very slow, and you had a typo: For Each cTime In wsMain.Range("X2:X" & wsMain.Cells(Rows.Count, "A").End(xlDow).Row).Cells ...so you were checking a million rows which made it even slower.

Here's a slightly different approach which is much faster, using a temporary lookup column and Match()

Sub CheckTimeAudited2_Method2()
    
    Dim wsMain As Worksheet, wsAudit As Worksheet, cTime As Range, t
    Dim lrAudit As Long, rngMatch As Range, m, k, rngResults As Range
    
    Set wsMain = ThisWorkbook.Worksheets("main")
    Set wsAudit = ThisWorkbook.Worksheets("auditdata")
    
    lrAudit = wsAudit.Cells(Rows.Count, "A").End(xlUp).Row
    
    Set rngResults = wsAudit.Range("E2:E" & lrAudit) 'results come from here
    'create a lookup column using a concatenated key from A and D
    Set rngMatch = wsAudit.Range("X2:X" & lrAudit)   'any unused column
    With rngMatch
        .Formula = "=A2 & ""|"" & D2" 'add key formula
        .Value = .Value               'convert to values
    End With
    
    t = Timer
    'check against the temporary lookup column using Match (much faster)
    For Each cTime In wsMain.Range("X2:X" & wsMain.Cells(Rows.Count, "A").End(xlUp).Row).Cells
        With cTime.EntireRow
            k = .Columns("AK").Value & "|" & CLng(.Columns("F").Value) 'convert date to Long
            If cTime.Row < 5 Then Debug.Print "Key: " & k
        End With
        m = Application.Match(k, rngMatch, 0)
        If Not IsError(m) Then
            If cTime.Row < 5 Then Debug.Print "   Matched row#: " & m
            cTime.Value = rngResults.Cells(m).Value
        Else
            cTime.Value = "No match"
        End If
    Next cTime
    Debug.Print "Done in " & Timer - t & " sec"
    
    rngMatch.ClearContents  'remove the key data

End Sub
Tim Williams
  • 154,628
  • 8
  • 97
  • 125
  • Please flag as "Accepted" if this resolved your problem, to help anyone coming along later with a similar question. – Tim Williams Jul 31 '23 at 23:51
  • Thank you, I will give the evaluate a try. Though I was hoping to understand better why the code I wrote does not work when using multiple criteria. It works for VLookup and XLookup when I use just one lookup_value and one lookup_array. – Saira Aug 01 '23 at 09:22
  • Not all worksheet function syntax can be mapped directly to VBA - things you can do in a worksheet such as multiplying a range by a constant for example are not possible in VBA without extra code. – Tim Williams Aug 01 '23 at 14:59
  • Thank you, Tim, and sorry for the delay. I did try the above code. If I step through the code, it does seem to work. However, when I just run the code, it crashes Excel - no error given. I cannot work out why. I see that you have compressed some of my code and got rid of some of the variables. Though not sure why the "EntireRow" property of the range is used as I am only interested in two columns of the main sheet, writing to a third column, (+the equivalent values in the second sheet). I did wonder whether this was the reason why it breaks but removing it does not fix the problem. – Saira Aug 07 '23 at 15:15
  • I also rechecked both data sheets to see if there is anything strange, I did not pick up before. Again, I cannot find anything. There is always valid data in all used cells in column F (dates) of the main sheet, these can be matched in the second sheet. Column AK of the main sheet has data in some cells and blanks. Some of these cells cannot be found in the second data sheet. In the excel formula, if the cell in AK is blank or not found in auditdata sheet, or not found for a particular date, then the if_not_found "no match" is returned. – Saira Aug 07 '23 at 15:25
  • How much data do you have? – Tim Williams Aug 07 '23 at 15:30
  • In the main sheet, there are 46 columns x 7531 rows, in the second sheet 12 columns x 5641 rows. – Saira Aug 07 '23 at 15:46
  • It worked for me in testing, but I don't have your source data (unless you can share it...) – Tim Williams Aug 07 '23 at 15:50
  • I did also try with removing data from some columns that I do not need for this particular formula to work but did not seem to help at all. It still crashes. – Saira Aug 07 '23 at 15:52
  • There is nothing particularly sensitive in the data but let me edit it a little to anonymise it. Is there a way to attach files here? – Saira Aug 07 '23 at 15:55
  • Thank you. I can share it in github perhaps. I will send you an email. – Saira Aug 08 '23 at 09:44
  • Is this now resolved? – Tim Williams Aug 10 '23 at 17:34
  • Thank you, Tim. Yes, the code works for one column though takes a little while. However, I need to change it slightly. I have 8x one hour time slots and I need to be able to carry out the lookup/match for all eight. There are eight equivalent time columns I am checking against in the "auditdata" sheet. – Saira Aug 15 '23 at 10:46
  • That sounds like a new question? – Tim Williams Aug 15 '23 at 15:27
  • Maybe. Though I am trying to recreate what I was able to do with my original excel formula. I did start with a loop to go through all eight time columns but the main code for the xlookup was throwing an error so I simplified it. I did not get very far as I had to do some other work, but I made an 2x8 array and was going to substitute the relevant column letters through a for-next loop. – Saira Aug 15 '23 at 16:07
  • If you can put a sample in your Github repo I will take a look. Mail me if you do that. – Tim Williams Aug 15 '23 at 17:02
  • Thank you, Tim. The data and with the extra columns are already in my workbook. In the main sheet, they are columns X to AE - these are the columns I am trying to populate with data. The data I am checking against is in the auditdata sheet, columns E to L. – Saira Aug 17 '23 at 14:53
  • `cTime.Resize(1, 8).Value = rngResults.Cells(m).Resize(1, 8).Value` will copy 8 values over from "auditdata" to "main" – Tim Williams Aug 17 '23 at 21:30
  • Please flag as "Accepted" if this resolved your problem, to help others coming along later with similar questions. – Tim Williams Aug 18 '23 at 17:13
0

A VBA Lookup: XLookup

enter image description here

A Quick Fix (Slow) - Main

Sub CheckTimeAudited_Method()
    
    Const JOIN_DELIMITER As String = "@"
    
    Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
    
    ' Source
    
    Dim sws As Worksheet: Set sws = wb.Sheets("AuditData")
    
    Dim slRow As Long: slRow = sws.Cells(sws.Rows.Count, "A").End(xlUp).Row
    
    Dim srgRoom As Range: Set srgRoom = sws.Range("A3", sws.Cells(slRow, "A"))
    Dim srgDate As Range: Set srgDate = srgRoom.EntireRow.Columns("D")
    Dim srgTime As Range: Set srgTime = srgRoom.EntireRow.Columns("E")
    
    Dim sLookup() As String
    sLookup = JoinTwoSingleColumnRanges(srgRoom, srgDate, JOIN_DELIMITER)
    
    ' Destination
    
    Dim dws As Worksheet: Set dws = wb.Sheets("Main")
    
    Dim dlRow As Long: dlRow = dws.Cells(dws.Rows.Count, "AK").End(xlUp).Row
    
    Dim drgRoom As Range: Set drgRoom = dws.Range("AK3", dws.Cells(dlRow, "AK"))
    Dim drgDate As Range: Set drgDate = drgRoom.EntireRow.Columns("F")
    Dim drgTime As Range: Set drgTime = drgRoom.EntireRow.Columns("X")
     
    ' Lookup
     
    Application.ScreenUpdating = False
     
    Dim dTimeCell As Range, r As Long, dLookup As String
        
    For Each dTimeCell In drgTime.Cells
        r = r + 1
        dLookup = CStr(drgRoom.Cells(r)) & JOIN_DELIMITER & CStr(drgDate.Cells(r))
        dTimeCell.Value _
            = Application.XLookup(dLookup, sLookup, srgTime, "no match")
    Next dTimeCell

    Application.ScreenUpdating = True
    
    ' Inform.
    
    MsgBox "Data checked.", vbInformation

End Sub

A Quick Fix (Slow) - Help - JoinTwoSingleColumnRanges

Function JoinTwoSingleColumnRanges( _
    ByVal rg1 As Range, _
    ByVal rg2 As Range, _
    Optional ByVal JoinDelimiter As String = "@") _
As String()
        
    Dim rCount As Long: rCount = rg1.Rows.Count
    Dim sData() As String: ReDim sData(1 To rCount, 1 To 1)
        
    Dim r As Long
    
    For r = 1 To rg1.Rows.Count
        sData(r, 1) = CStr(rg1.Cells(r).Value) & JoinDelimiter _
            & CStr(rg2.Cells(r).Value)
    Next r
    
    JoinTwoSingleColumnRanges = sData

End Function

An Even Quicker Fix (Slow, Not Recommended) - Using the Function

  • Using the function with your code, you could replace the invalid TableArrayRoom & TableArrayDate with

    JoinTwoSingleColumnRanges(TableArrayRoom, TableArrayDate, "")
    

An Improvement (Fast) - Main

Sub CheckTimeAudited()
    
    ' Constants
    
    ' Source
    Const SRC_SHEET As String = "AuditData"
    Const SRC_FIRST_CELL As String = "A3"
    Dim sLookupColumns(): sLookupColumns = VBA.Array(4, 1) ' D, A
    Const SRC_RETURN_COLUMN As Long = 5 ' E
    ' Destination
    Const DST_SHEET As String = "Main"
    Const DST_FIRST_CELL As String = "A3"
    Dim dLookupColumns(): dLookupColumns = VBA.Array(6, 37) ' F, AK
    Const DST_RETURN_COLUMN As Long = 24 ' X
    Const JOIN_DELIMITER As String = "@"
    Const IF_NO_MATCH_VALUE As String = "no match"
    ' Workbook
    Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
    
    ' Source
    
    Dim srg As Range: Set srg = RefRangeUR(wb, SRC_SHEET, SRC_FIRST_CELL)
    Dim slJag(): slJag = GetJaggedColumnsFromRange(srg, sLookupColumns)
    Dim slData() As String: slData = JoinJaggedColumns(slJag, JOIN_DELIMITER)
    Dim srData(): srData = GetSingleColumnRange(srg.Columns(SRC_RETURN_COLUMN))
    Dim sDict As Object: Set sDict = DictColumns(slData, srData)
    
    ' Destination
    
    Dim drg As Range: Set drg = RefRangeUR(wb, DST_SHEET, DST_FIRST_CELL)
    Dim dlJag(): dlJag = GetJaggedColumnsFromRange(drg, dLookupColumns)
    Dim dlData() As String: dlData = JoinJaggedColumns(dlJag, JOIN_DELIMITER)
    Dim drData(): drData = GetLookupData(dlData, sDict, IF_NO_MATCH_VALUE)
    Dim drrg As Range: Set drrg = drg.Columns(DST_RETURN_COLUMN)
    drrg.Value = drData
        
    MsgBox "Data checked.", vbInformation
        
End Sub

An Improvement (Fast) - Help - RefRangeCR

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      In a worksheet ('WorksheetID') of a workbook ('wb'),
'               references the range from a cell ('FirstCellAddress')
'               to the last cell of the given cell's current region.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefRangeCR( _
    ByVal wb As Workbook, _
    ByVal WorksheetID As Variant, _
    ByVal FirstCellAddress As String) _
As Range
    
    Dim ws As Worksheet: Set ws = wb.Worksheets(WorksheetID)
    
    Dim fCell As Range: Set fCell = ws.Range(FirstCellAddress)
    
    Dim lCell As Range:
    With fCell.CurrentRegion
        Set lCell = .Cells(.Cells.CountLarge)
    End With
    
    If lCell.Row < fCell.Row Or lCell.Column < fCell.Column Then Exit Function
    
    Dim rg As Range: Set rg = ws.Range(fCell, lCell)
    
    Set RefRangeCR = rg
    
End Function

An Improvement (Fast) - Help - RefRangeUR

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      In a worksheet ('WorksheetID') of a workbook ('wb'),
'               references the range from a cell ('FirstCellAddress')
'               to the last cell of the worksheet's used range.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefRangeUR( _
    ByVal wb As Workbook, _
    ByVal WorksheetID As Variant, _
    ByVal FirstCellAddress As String) _
As Range
    
    Dim ws As Worksheet: Set ws = wb.Worksheets(WorksheetID)
    
    Dim fCell As Range: Set fCell = ws.Range(FirstCellAddress)
    
    Dim lCell As Range:
    With ws.UsedRange
        Set lCell = .Cells(.Cells.CountLarge)
    End With
    
    If lCell.Row < fCell.Row Or lCell.Column < fCell.Column Then Exit Function
    
    Dim rg As Range: Set rg = ws.Range(fCell, lCell)
    
    Set RefRangeUR = rg
    
End Function

An Improvement (Fast) - Help - GetJaggedColumnsFromRange

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      Returns the values from columns ('ColumnIndices') of a range
'               ('rg') in 2D one-based (single-column) arrays
'               of the resulting jagged array.
' Calls:        GetSingleColumnrange
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetJaggedColumnsFromRange( _
    ByVal rg As Range, _
    ColumnIndices() As Variant) _
As Variant()
    
    Dim JLB As Long: JLB = LBound(ColumnIndices)
    Dim JUB As Long: JUB = UBound(ColumnIndices)
    Dim Jag() As Variant: ReDim Jag(JLB To JUB)
    
    Dim j As Long
    
    For j = JLB To JUB
        Jag(j) = GetSingleColumnRange(rg.Columns(ColumnIndices(j)))
    Next j
     
    GetJaggedColumnsFromRange = Jag

End Function

An Improvement (Fast) - Help - GetSingleColumnRange

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      Returns the values from a column ('ColumnIndex') of a range
'               ('rg') in a 2D one-based (single-column) array.
' In Excel:     '=INDEX(rg,,ColumnIndex)'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetSingleColumnRange( _
    ByVal rg As Range, _
    Optional ByVal ColumnIndex As Long = 1) _
As Variant()
    
    Dim Data() As Variant
    
    With rg.Columns(ColumnIndex)
        Dim rCount As Long: rCount = .Rows.Count
        If rCount = 1 Then
            ReDim Data(1 To 1, 1 To 1): Data(1, 1) = .Value
        Else
            Data = .Value
        End If
    End With
    
    GetSingleColumnRange = Data

End Function

An Improvement (Fast) - Help - JoinJaggedColumns

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      Returns the joined ('JoinDelimiter') strings of each row
'               of each (same-sized) 2D one-based single-column array
'               of a jagged array ('JaggedColumns'),
'               in a 2D one-based single-column String typed array.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function JoinJaggedColumns( _
    JaggedColumns() As Variant, _
    Optional ByVal JoinDelimiter As String = "@") _
As String()
    
    Dim JLB As Long: JLB = LBound(JaggedColumns)
    Dim JUB As Long: JUB = UBound(JaggedColumns)
    Dim RUB As Long: RUB = UBound(JaggedColumns(JLB), 1)
    
    Dim sData() As String: ReDim sData(1 To RUB, 1 To 1)
    
    Dim j As Long, r As Long, Delimiter As String
    
    For j = JLB To JUB
        For r = 1 To RUB
            sData(r, 1) = sData(r, 1) & Delimiter & CStr(JaggedColumns(j)(r, 1))
        Next r
        Delimiter = JoinDelimiter
    Next j
            
    JoinJaggedColumns = sData
                
End Function

An Improvement (Fast) - Help - DictColumns

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      Returns the unique strings from a 2D one-based single-column
'               String typed array ('KeyColumn') in the keys of a dictionary,
'               and returns the values from another same-sized Variant typed
'               array ('ItemColumn') in the associated items of the dictionary.
'               Each associated item is the one that appears in the same row
'               as the row of the first occurence of each key.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function DictColumns( _
    KeyColumn() As String, _
    ItemColumn() As Variant) _
As Object
    
    Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
    dict.CompareMode = vbTextCompare
    
    Dim r As Long
    
    For r = 1 To UBound(KeyColumn, 1)
        If Not dict.Exists(KeyColumn(r, 1)) Then
            dict(KeyColumn(r, 1)) = ItemColumn(r, 1)
        End If
    Next r
    
    Set DictColumns = dict
    
End Function

An Improvement (Fast) - Help - GetLookupData

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      Looks up each string of a 2D one-based single-column
'               string-typed array ('LookupData') in the keys of a dictionary
'               ('LookupDictionary') and for each found string, returns
'               the associated item in a same-sized variant-typed array.
'               If the string is not found,
'               a given value ('IfNoMatchValue') is returned.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetLookupData( _
    LookupData() As String, _
    ByVal LookupDictionary As Object, _
    Optional ByVal IfNoMatchValue As Variant = "") _
As Variant()

    Dim rCount As Long: rCount = UBound(LookupData, 1)
    Dim Data(): ReDim Data(1 To rCount, 1 To 1)
    
    Dim r As Long, rStr As String
    
    For r = 1 To rCount
        rStr = LookupData(r, 1)
        If LookupDictionary.Exists(rStr) Then
            Data(r, 1) = LookupDictionary(rStr)
        Else
            Data(r, 1) = IfNoMatchValue
        End If
    Next r
    
    GetLookupData = Data

End Function
VBasic2008
  • 44,888
  • 5
  • 17
  • 28
  • thank you for responding but the code you have suggested is a little difficult to follow and I am not sure it covers what I am trying to do. – Saira Aug 07 '23 at 15:29