[edit] I realised why everyone does this with loops; my plan was to use .SpecialCells(xlCellTypeVisible).EntireRow.Address to return the row(s) of the results of the results of the filter, but that returns a string. Strings have a limit of 256 characters, so with even moderately fragmented data some will be truncated. However, I think this approach is still more efficient than grabbing the table contents row-by-row.
I recently needed to make an array from a filtered sheet. Because the filtered results will be in multiple areas, the code I found online all referred back to the sheet in loops. I believe I have come up with a much more efficient approach, which grabs all the table in one go, then grabs the information relating to the rows that are visible. It does not have to go row-by-row to grab the data, nor does it have to redim the array after its initial size is set.
Some operations could be combined (sacrificing readability for efficiency) but otherwise I think this is a faster and neater way than any I've yet seen.
I am particularly interested in any efficiency improvements, and any vulnerabilities which could/should be dealt with. Thanks in advance!
Function ArrayFromFilter(Table As Range) As Variant
Dim FullData() As Variant, Trimdata() As Variant, FilterZones() As String, Filters() As String, FilterRows() As Long
Dim numZones As Long, curZone As Long, curRow As Long, curCol As Long, FirstRow As Long
Dim numItems As Long, i As Long, j As Long
Dim Output As String
FirstRow = Table.Rows(1).Row - 1 'this an offset used later
'grab the entire unfiltered table - faster to process it in VBA than to cycle through on the sheet
FullData() = Table
'grab the result row numbers by area
numZones = Table.SpecialCells(xlCellTypeVisible).Areas.Count
ReDim Filters(1 To numZones)
For curZone = 1 To numZones
Filters(curZone) = Table.SpecialCells(xlCellTypeVisible).Areas(curZone).EntireRow.Address
Next
ReDim FilterZones(1 To numZones, 1 To 2)
ReDim FilterRows(1 To numZones, 1 To 2)
'split the zones into start and end rows
For i = LBound(Filters()) To UBound(Filters())
'first split each zone into its first and last rows
FilterZones(i, 1) = Left(Filters(i), InStr(Filters(i), ":") - 1)
FilterZones(i, 2) = Right(Filters(i), InStr(Filters(i), ":") + 1)
'now take just the row from each cell and convert to a number, then remove the offset of the start of the data
FilterRows(i, 1) = (CDbl(Split(FilterZones(i, 1), "$")(1)) - FirstRow)
FilterRows(i, 2) = (CDbl(Split(FilterZones(i, 2), "$")(1)) - FirstRow)
'ta-da we have an array with the first and last row of each zone inside the overall filtered data - now work out how many items there are
numItems = numItems + (FilterRows(i, 2) - FilterRows(i, 1)) + 1
Next
'go through the table, moving only the useful bits to the trimmed data
ReDim Trimdata(1 To numItems, 1 To UBound(FullData, 2))
curRow = 1
For i = 1 To numZones
For j = FilterRows(i, 1) To FilterRows(i, 2)
For curCol = 1 To UBound(FullData, 2)
Trimdata(curRow, curCol) = FullData(j, curCol)
Next
curRow = curRow + 1
Next
Next
'uncomment this block for testing to show that the data has been grabbed
' For i = 1 To UBound(Trimdata(), 1)
' For j = 1 To UBound(Trimdata(), 2)
' If j = 1 Then
' Output = Trimdata(i, j)
' Else
' Output = Output & ", " & Trimdata(i, j)
' End If
' Next
' Debug.Print Output
' Next
ArrayFromFilter = Trimdata
End Function