Improve performance getting or setting values in cells through VBA - vba

I've managed to connect to a web service and retrieve data to finally insert it into a sheet.
Looks like when I perform this final operation, the sheet gets the focus and the screen blinks constantly until this ends, which is a little awkward for the user and it depletes performance. We are talking that inserting about 1000 rows with 4 different parsed columns lasts for about 5 seconds.
Is there a way to do this "on the background"? I noted that basically "every action" I performed (like reading values or setting them) requests the focus on that concrete sheet, so I basically return the focus to the sheet who called the procedure once everything ended.
My code is the following:
For i = 3 To UBound(Data) - 1
If (IsNullOrWhiteSpace(Data(i))) Then
Exit Sub
End If
splitted = Split(Data(i), ";")
For j = 0 To UBound(splitted)
Cells(i - 1, j + 1).Value = splitted(j)
Next
Next
Many thanks, I'm new into this VBA world.

for starter, place:
Application.Calculation = xlCalculationManual
at the beginning of your Sub and:
Application.Calculation =xlCalculationAutomatic
right before End Sub
then let's see further three steps to speed things up:
a first step could be replacing:
For j = 0 To UBound(splitted)
Cells(i - 1, j + 1).Value = splitted(j)
Next
with:
Cells(i - 1, 1).Resize(, UBound(splitted) + 1).Value = Application.Transpose(Application.Transpose(splitted))
so that you write a whole row in one shot
a second step could be working with arrays and write the content of a 2D array in one shot:
Dim Data As Variant
Dim nRows As Long, nCols As Long, i As Long, j As Long
' >>>> here your code code to fill 'Data' variant array <<<<
ReDim notNullData(1 To UBound(Data) - 3) As Variant '<--| size 'notNullData' 1D Variant array to the maximum possible rows
For i = 3 To UBound(Data) - 1
If IsNullOrWhiteSpace(Data(i)) Then Exit For '<--| exit loop at the first null or empty 'Data' value
nRows = nRows + 1 '<--| update valid rows counter
notNullData(nRows) = Split(Data(i), ";") '<--| fill 'notNullData' array with an array from current 'Data' row content
If UBound(notNullData(nRows)) > nCols Then nCols = UBound(notNullData(nRows)) '<--| update maximum n° of columns to be written
Next
ReDim dataToWrite(1 To nRows, 1 To nCols + 1) As Variant '<--| size 'dataToWrite' 2D Variant array to 'Data' array valid rows number and calculated maximum nr of columns
'fill 2D 'dataToWrite' array processing 'notNullData' 1D array
For i = 1 To nRows
For j = 0 To UBound(notNullData(i))
dataToWrite(i, j + 1) = notNullData(i)(j)
Next
Next
'write 2D 'dataToWrite' array in one shoit
Cells(1, 1).Resize(nRows, nCols + 1).Value = dataToWrite
a third step would involve IsNullOrWhiteSpace() function

Related

VBA, Code runs fine in excel 2010, slow and not responding error in 2013

Previously when running my code in 2010 excel version (Version 14.0.7165.5000), it runs in 4 minutes. (however if I run it a second time, it doesnt work)
When switching to 2013 I get 'excel not responding' or it just excel hangs.
The code opens a files from a directory, loads them into my excel sheet and compiles and transforms some data.
I stepped through the code, seems to get past this part and now excel isnt responding on my given sub sub , specifically at this part,
'write
For i = 1 To WorksheetFunction.Min(nRows, UBound(arr, 1))
For j = 1 To nCols
If fromTop Then writeVal = arr(i, j) Else writeVal = arr(UBound(arr, 1) - i + 1, j)
thisWS.Cells(startRow + i - 1, startCol + j - 1).value = writeVal
Next j
Next i
Does anyone know why this is? Is there some function I am using that works in 2010 but not in 2013?
this isn't really an answer. But, I'm not sure the code will format in the comments section. If anyone would care to inform me how to handle this situation in the future, let me know!
Anyway, excel guy, I mean this:
'clear
startCell.Resize(nRows, nCols).ClearContents
instead of:
'clear
For i = 1 To nRows
For j = 1 To nCols
thisWS.Cells(startRow + i - 1, startCol + j - 1).value = ""
Next j
Next i
OK... next would be to change the writeArrToWS sub to use an array to write to the whole range at once. I rewrote the sub, incorporating a modified change from my answer above. I think you'd want:
Public Sub writeArrToWS(arr() As Variant, startCell As Range, fromTop As Boolean, nRows As Long, nCols As Long)
Dim i As Long, j As Long, startRow As Long, startCol As Long
Dim thisWS As Worksheet, totalRange As Range
Set thisWS = startCell.Worksheet
'set the write range
Set totalRange = startCell.Resize(nRows, nCols)
'clear
totalRange.ClearContents
'write
If fromTop Then
totalRange.Value2 = arr
Else
Dim reversedArr() As Variant, swappedRow As Long
ReDim reversedArr(1 To nRows, 1 To nCols)
For i = 1 To nRows
swappedRow = nRows - i + 1
For j = 1 To nCols
reversedArr(swappedRow, j) = arr(i, j)
Next j
Next i
totalRange.Value2 = reversedArr
End If
End Sub
A few things:
It seems like if fromTop is true, you just paste the array to the range... did I misunderstand that? If so, it's almost like you just want a "reverseArray" function/subroutine...
We're basically just generating a new array, "reversedArr", which reverses the order of the rows but leaves the columns alone
Is there any particular reason why you're passing nRows and nCols to the function? Do you ever want to use the function when nRows and nCols is not just the number of rows and columns of the passed array? If no, then I don't think there's a reason to pass those variables, and instead just generate them inside the function from the passed array.

Gather data tidy in Excel using VBA

What the case is:
So I got a "results sample" in excel format that needs filtering and reshaping to look nice. It is a result that will be not identical all the time but it follows similar rules. I have to filter it further and make it a little more tidy. I have figured out the filtering part, but I am not sure how to sort the remaining data, in a tidy way.
What the situation is:
There are six columns involved.
Notice: Real deal is not THAT simple, but what I need can be demonstrated using such a simple example and then I can manage more complex stuff myself I suppose.
For our example we use columns from B to G
The data are set as pairs of a "title" and a value.
For instance, if you look the first example picture I provide, The first detais the pair B3 and C3.
As you can see, looking at the same picture, D3 and E3 is an empty pair.
Same goes for D4 - E4 and F4 - G4 and so on until a last one at B11 - C11.
Starting data example:
[
What I want to achieve:
I would like, using Visual Basic for Applications, to sort the data, starting from let's say for our example B3 (see second picture) and fill three SETS of two columns, (BC, DE, FG) if there are no data inside those cells.
Notice: If a cell like D3 is null then SURELY E3 will be null too so there can be just only one check. I mean we can check either value columns or title columns.
Notice2: The B,D,F or C,E,G columns DON'T have to be sorted. I just want all the not-null values of B,D,F and their respective values from C,E,G gathered together neat so printing will not need 30 pages but just a few (too many spaces between is causing it and I try to automate the cleanup)
Here's something to start with. The first double loop populates a VBA Collection with Range variables that refer to the Cells that contain the titles.
The associated values are obtained by using an offset. The middle double loop performs a bubble sort on the latter (highly inefficient - you might want to replace it with something else). The next if statement creates a 2nd sheet if it doesn't exist on which to write out the results (last loop).
Option Explicit
Sub GatherData()
Dim lastRow As Integer, lastCol As Integer
Dim r As Integer, c As Integer
Dim vals As Collection
Set vals = New Collection
With Sheets(1)
lastCol = .UsedRange.Columns(.UsedRange.Columns.Count).Column
lastRow = .UsedRange.Rows(.UsedRange.Rows.Count).row
For c = 1 To lastCol Step 2
For r = 1 To lastRow
If (Trim(Cells(r, c).Value) <> "") Then
vals.Add .Cells(r, c)
End If
Next
Next
End With
' Bubble Sort
Dim i As Integer, j As Integer
Dim vTemp As Range
For i = 1 To vals.Count - 1
For j = i + 1 To vals.Count
If vals(i).Value > vals(j).Value Then
Set vTemp = vals(j)
vals.Remove j
vals.Add vTemp, vTemp, i
End If
Next j
Next i
Dim sht2 As Worksheet
If ThisWorkbook.Worksheets.Count = 1 Then
Set sht2 = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(1))
Else
Set sht2 = Worksheets(2)
End If
With sht2
r = 3
c = 2
For i = 1 To vals.Count
.Cells(r, c).Value = vals(i).Value
.Cells(r, c + 1).Value = vals(i).Offset(, 1).Value
c = c + 2
If c = 8 Then
r = r + 1
c = 2
End If
Next
End With
End Sub
Here is a method using the Dictionary object. I use early binding which requires setting a reference to Microsoft Scripting Runtime. If you are going to be distributing this, you might want to convert this to late-binding.
We assume that your data is properly formed as you show it above. In other words, all the titles are in even numbered columns; and the results are in the adjacent cell.
We create the dictionary using the Title as the Key, and the adjacent cell value for the Dictionary item.
We collect the information
Transfer the Keys to a VBA array and sort alphabetically
create a "Results Array" and populate it in order
write the results to a worksheet.
I will leave formatting and header generation to you.
By the way, there is a constant in the code for the number of Title/Value pair columns. I have set it to 3, but you can vary that.
Enjoy
Option Explicit
Option Compare Text 'If you want the sorting to be case INsensitive
'set reference to Microsoft Scripting Runtime
Sub TidyData()
'Assume Titles are in even numbered columns
'Assume want ColPairs pairs of columns for output
'Use dictionary with Title as key, and Value as the item
Dim dctTidy As Dictionary
Dim arrKeys As Variant
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant
Dim LastRow As Long, LastCol As Long
Dim I As Long, J As Long, K As Long, L As Long
Dim V As Variant
'in Results
Const ColPairs As Long = 3
'Set Source and results worksheet and range
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 2)
'Read source data into variant array
With wsSrc.Cells
LastRow = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByRows, searchdirection:=xlPrevious).Row
LastCol = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByColumns, searchdirection:=xlPrevious).Column
vSrc = .Range(.Cells(1, 1), .Cells(LastRow, LastCol))
End With
'Collect the data into a dictionary
Set dctTidy = New Dictionary
For I = 1 To UBound(vSrc, 1)
For J = 2 To UBound(vSrc, 2) Step 2
If vSrc(I, J) <> "" Then _
dctTidy.Add Key:=vSrc(I, J), Item:=vSrc(I, J + 1)
Next J
Next I
'For this purpose, we can do a simple sort on the dictionary keys,
' and then create our results array in the sorted order.
arrKeys = dctTidy.Keys
Quick_Sort arrKeys, LBound(arrKeys), UBound(arrKeys)
'Create results array
ReDim vRes(1 To WorksheetFunction.RoundUp(dctTidy.Count / ColPairs, 0), 1 To ColPairs * 2)
I = 0
J = 0
For Each V In arrKeys
K = Int(I / ColPairs) + 1
L = (J Mod ColPairs) * 2 + 1
vRes(K, L) = V
vRes(K, L + 1) = dctTidy(V)
I = I + 1
J = J + 1
Next V
'write the results
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.Worksheet.Cells.Clear
.Value = vRes
.HorizontalAlignment = xlCenter
End With
End Sub
Sub Quick_Sort(ByRef SortArray As Variant, ByVal first As Long, ByVal last As Long)
Dim Low As Long, High As Long
Dim Temp As Variant, List_Separator As Variant
Low = first
High = last
List_Separator = SortArray((first + last) / 2)
Do
Do While (SortArray(Low) < List_Separator)
Low = Low + 1
Loop
Do While (SortArray(High) > List_Separator)
High = High - 1
Loop
If (Low <= High) Then
Temp = SortArray(Low)
SortArray(Low) = SortArray(High)
SortArray(High) = Temp
Low = Low + 1
High = High - 1
End If
Loop While (Low <= High)
If (first < High) Then Quick_Sort SortArray, first, High
If (Low < last) Then Quick_Sort SortArray, Low, last
End Sub
Assuming we got all variables set and initialized properly, in this example:
Sheets("sheetname").Select ' because stupid things can happen...
For i = 3 To 13
Let newrangeT = "B" & i '
Let newrangeV = "C" & i '
If Sheets("sheetname").Range(newrangeV) <> "" Then
values(Position) = Sheets("sheetname").Range(newrangeV)
titles(Position) = Sheets("sheetname").Range(newrangeT)
Position = Position + 1
Else
' Don't do anything if the fields are null
End If
Next i
Sheets("sheetname").Range("B1:G13").Clear
' We then get each data from the arrays with a For loop.
' We set a columnset variable to 1.
' We set a currentrow variable to 3.
' If columnset is 1 data will enter in B and C and columnset = columnset +1
' Then if columnset is 2 we set data to DE and columnset = columnset +1
' But if columnset is 2we set data to FG and columnset = 1 and currentrow = currentrow +1
' Iterating the arrays will result in a neat setting of the data, but it will add zeros for all the nulls. Thus we need an If statement that will exclude that values checking the TITLE array (that should contain a title instead). if the value is not 0 then... we run what I describe, otherwise we do nothing.
Putting the data in the array is half of the trick.
Then we clear the area.
We set two string variables to declare ranges (actually cell reference) for every cell iterated in the loop. Here I demonstrated only for column set B,C
but we have to do the same for the rest of the columns.
The If statement here checks for null. You might have different needs, so changing the if statement changes the filtering. Here I check if the cells are not null. If the cells of column C contain data, put those data in values array and the respective B data on titles array but where? Position starts as 1 and we then iterate it +1 each time it adds something.
You can set data from an array using this command:
' current_row is set to the first row of the spreadsheet we wanna fill.
Sheets("sheetname").Select ' because stupid things can happen...
newrangeV = "C" & current_row
Sheets("sheetname").Range(newrangeV) = values(j)
The rest is just putting things together.
In any case, I wanna thank both of the people involved in this question, because I might didn't got the solution, but I got an idea of how to do other stuff, like accidentally learning something new. Cheers.

Sorting by description for groups of rows on a single sheet, concept inquiry

So, I'm a bit baffled on how to move forward, and would like some collaboration to get me started. I'm not asking for someone to code this, but to verify my theoretical path forward.
Background:
I have a single worksheet with 30 activities. Each activity is 75 rows, with the first of the 75 rows having a cell with the description of the activity. Assuming the # of columns is irrelevent to this, the activities A, B, and C, would appear such as:
A1
A...
A75
B1
B...
B75
C1
C...
C75
Theoretical path forward:
Since I have a known row which starts each activity, I was thinking that I could:
.1) Copy the known cell from each row that I intend to sort by to another sheet (this isn't preferred, but is how I can think to do it).
.2) Once in the other sheet, Sort the activity descriptions.
.3) Once sorted, I want to copy each of the activities 75 rows, in order, to the sorted sheet, via Match or Find.
.4) Once completed, I would Copy the Activities from the new sheet, paste back into the original sheet, then delete the new sheet.
Question:
Does this sound appropriate? Is there possibly a better way to do this that immediately comes to mind?
I think you could save a lot of time copying and pasting if you used a 2 dimensional array. This program stops at each 75th row and loads up the 2d array "Activities" from column A. The array gets passed to bubblesort where it is sorted on the first value. Then it is returned and all output to column B. You might have to adjust the constants to match and if there is a blank row between activities there will be some other minor adjustments to the two main loops.
Option Compare Text
Const RowsPerActivity As Integer = 75 'setting
Const NumActivities As Integer = 30 'setting
Const RowtoSortOn As Integer = 1 'setting
Sub SortGroups()
Dim MySheet As Worksheet
Set MySheet = Worksheets("sheet1")
Dim Activities(1 To NumActivities, 1 To RowsPerActivity) As Variant
Dim CurActivity, CurDataRow As Integer
Dim LastRow As Integer
LastRow = MySheet.Cells(MySheet.Rows.Count, "A").End(xlUp).Row
For CurActivity = 1 To LastRow Step RowsPerActivity 'maybe +1 if there is a blank row between activities
For CurDataRow = 1 To RowsPerActivity Step 1
Activities((CurActivity \ 75) + 1, CurDataRow) = MySheet.Cells(CurActivity + CurDataRow - 1, 1).Value
Next CurDataRow
Next CurActivity
Call BubbleSort(Activities)
For CurActivity = 1 To LastRow Step RowsPerActivity 'maybe +1 if there is a blank row between activities
For CurDataRow = 1 To RowsPerActivity Step 1
MySheet.Cells(CurActivity + CurDataRow - 1, 2).Value = Activities((CurActivity \ 75) + 1, CurDataRow)
Next CurDataRow
Next CurActivity
End Sub
Sub BubbleSort(ByRef list() As Variant)
' Sorts an array using bubble sort algorithm
Dim First As Integer, Last As Long
Dim i As Long, j As Long, k As Long
Dim Temp As Variant
First = LBound(list, 1)
Last = UBound(list, 1)
For i = First To Last - 1
For j = i + 1 To Last
If list(i, RowtoSortOn) > list(j, RowtoSortOn) Then
For k = 1 To RowsPerActivity
Temp = list(j, k)
list(j, k) = list(i, k)
list(i, k) = Temp
Next k
End If
Next j
Next i
End Sub

Loop through Column restarting when value changes

I've found a couple threads with similar titles but weren't really what I am looking to do. What I'm trying to do is go through the list of numbers in Col A, and calculate the time difference using NetworkDays for the first instance the number appears in Col B ' Received On ' and the last instance the number appears in Col C ' Processed On '. After the NetworkDays calculation is done I'd like to put that value repeating in Col D on every respective row. The number of times a value will appear in Col A constantly varies, and Col A itself is several thousand lines long and constantly growing. Once that is done I need to loop through all the other different sets of numbers in Col A and repeat the process. As an example, ***39430 first appears on Row 2 and last appears on Row 7. Using Networkdays(B2,C7) gives 11 days, and so forth. After that move onto ***39383. Sample below.
Sample data
Below is the code I have so far. From the sample above I have to put a blank row under ***39430 in order to get the code to work, otherwise it just continues on to the bottom of the list and calculates that difference (not what I want obviously). What I'm stumped on is how to tell the loop to restart whenever the value changes in Col A and then continue on. I suspect it might be something close to Do Until ActiveCell.Value <> Activecell.Offset(-1,0).Value but I can't quite figure it out. Also how to get the Networkdays value to repeat on every respective row.
Dim counter As Integer
Dim CycleTime As Long
counter = 0
Do Until ActiveCell.Value = ""
counter = counter + 1
ActiveCell.Offset(1, 0).Select
Loop
'Gives the number of rows to offset
MsgBox counter
'Shows the correct number of days difference
MsgBox WorksheetFunction.NetworkDays(Range("B2"), Range("B2").Offset(counter - 1, 1))
CycleTime = WorksheetFunction.NetworkDays(Range("B2"), Range("B2").Offset(counter - 1, 1))
Range("D2").Value = CycleTime
Any help would be greatly appreciated. Thanks in advance.
Update
After using the code provided for a couple of weeks I've noticed a complication that I had not thought of before. Previously, I had thought that there was always only one output doc for each input doc (not considered in scope of original question), however as shown in Sample-New image in the top box there can be more than one output doc per input doc. For the new screenshot below I've included two additional columns, Col. C 'Output Doc #' and Col. D 'Output Doc Created On'. What I'd like to be able to do, amending the code that #YowE3K provided below, is to nest another loop that goes through Col. D 'Output Doc #' and uses NetworkDays to calculate the difference from B1 and D1 for the first group, and then B1 and D8 for the second group. As it is now, the code isn't written to handle the change and calculates everything as shown in Column F, with the ideal code resulting in Column G. The second box (in dark blue) shows a typical example where the code performs perfectly. Loops are something I'm struggling with to understand and not really sure how to even take a stab at this. Any comments to the code in a response would be very helpful. Thanks in advance.
Sample - New
The following code loops using endRow as the loop "counter".
startRow is set to the row containing the start of the current "Doc Number", and endRow is incremented until it is pointing at the last row for that "Doc Number".
Once endRow is pointing at the correct place, CycleTime is calculated and written to column D of each row from startRow to endRow. startRow is then set to point to the beginning of the next "Doc Number".
The loop ends when a blank cell is found in column A.
Sub Calc()
Dim startRow As Long
Dim endRow As Long
Dim CycleTime As Long
startRow = 2
endRow = 2
Do
If Cells(startRow, "A").Value <> Cells(endRow + 1, "A").Value Then
CycleTime = WorksheetFunction.NetworkDays(Cells(startRow, "B"), Cells(endRow, "C"))
Range(Cells(startRow, "D"), Cells(endRow, "D")).Value = CycleTime
startRow = endRow + 1
End If
endRow = endRow + 1
If Cells(endRow, "A").Value = "" Then
Exit Do
End If
Loop
End Sub
Edited to keep track of the first and last "Approved" record, and only update column D if one is found:
Sub Calc()
Dim startRow As Long 'Start of the Doc Number
Dim firstRow As Long 'First "approved" row
Dim lastRow As Long 'Last "approved" row
Dim endRow As Long 'End of the Doc Number
Dim CycleTime As Long
startRow = 2
endRow = 2
firstRow = -1
lastRow = -1
Do
If Cells(endRow, "Q").Value = "Approved" Then
'Found an "Approved" record
'Set the first row if not already set
If firstRow = -1 Then
firstRow = endRow
End If
'Set the last row (will replace this if we find another record)
lastRow = endRow
End If
If Cells(startRow, "A").Value <> Cells(endRow + 1, "A").Value Then
If firstRow > 0 Then ' (If it is -1 then we never found an "Approved" record)
CycleTime = WorksheetFunction.NetworkDays(Cells(firstRow, "B"), Cells(lastRow, "C"))
Range(Cells(startRow, "D"), Cells(endRow, "D")).Value = CycleTime
End If
'Set up for next Doc Number
startRow = endRow + 1
firstRow = -1
lastRow = -1
End If
'Go to next row
endRow = endRow + 1
'Exit when we hit a blank Doc Number
If Cells(currentRow, "A").Value = "" Then
Exit Do
End If
Loop
End Sub

Excel VBA selecting cells from filtered range

The code below is part of a larger form. What I'm trying to do is:
On sheet 2 (Data List) filter data based on column 1
Based on user selected items in a listbox pull out data from the filtered range from columns 2 and 3 for specific rows and paste into sheet 1 (JHA)
I know which rows the data is in only within the filtered list because I'm storing it in a 2D array dataArr
Sheets("Data List").Select
With Worksheets("Data List").Range("A1", Sheets("Data List").Range("C1").End(xlDown))
.AutoFilter field:=1, Criteria1:=UserForm1.ListBox3.List(k) 'filter data based on user listbox selection
For j = 0 To UserForm1.ListBox1.ListCount - 1 'Find user selection from LB3 in LB1 to match filtered data order
If UserForm1.ListBox3.List(k) = UserForm1.ListBox1.List(j) Then Exit For
Next j
For h = 0 To UBound(dataArr, 2)
If dataArr(j, h) = 1 Then 'If the user has selected they want this data then add it to the array
Set myRange = Sheets("Data List").AutoFilter.Range().SpecialCells(xlCellTypeVisible)
myRange.Select
arr1(l) = myRange.Cells(h + 2, 2)
arr2(l) = myRange.Cells(h + 2, 3)
l = l + 1
End If
Next h
.AutoFilter
After this bit of code I redimension the array and paste the data on the other sheet. My issue is that myRange.cells is selecting from the unfiltered data. So for example say my filtered data set includes rows 7, 11, 15 and 21. When I filter it and set myRange it highlights 4 rows plus the header. However, when I use cells(2, 2) I get the unfiltered row 2 column 2 data, not for my filtered data set. I'm sure I'm missing something simple but I can't see what it is.
Filtered range can be (well, it almost always is!) a not contiguous one, so you have to iterate through it and pitch the nth value
You may want to use this function:
Function GetFilteredCellsNthValue(filteredRng As Range, nthVal As Long) As Variant
Dim iVal As Long, Dim cell As Range
iVal = 1
ForEach cell in filteredRng
If iVal = nthVal Then Exit For
iVal = iVal + 1
Next
GetFilteredCellsNthValue = Iif(iVal>filteredRng.Count, CVErr(xlErrValue), cell.Value)
End Function
That could be used in your "main" code as follows
arr1(l) = GetFilteredCellsNthValue( .Resize(,1).Offset(.Rows.Count - 1,1).SpecialCells(xlCellTypeVisible)), h + 2)
arr2(l) = GetFilteredCellsNthValue( .Resize(,1).Offset(.Rows.Count - 1,2).SpecialCells(xlCellTypeVisible)), h + 2)