How can I select the exact number of items after applying the filter? - excel

I need to select a group of items after applying the filter and it works until a certain number of items.
I use 'Areas' to select the set of items because sometimes it needs to select the first one, two, three... until ten items. The variable which determines how long the selection will be is called rangeA, rangeB and rangeC for SELECTION A, SELECTION B and SELECTION C, respectively. Also, the number of columns for each selection is always the same. After the selection is done it's copied and pasted for each selection. It works this way:
the filter is applied
the selection (A, B and C one per time) is copied
the selection is pasted on the "Worksheet 2"
for selection A, B and C.
One observation is that I will always have items to select because "DATA" is too big, it has over 13 thousand items.
Sub SELECT()
Dim area As Range
Dim CellCount As Integer
Dim firstCell As Range 'firstCell and lastCell determines how big the selection will be.
Dim lastCell As Range
Dim rangeA, rangeB, rangeC As Variant
rangeA = Range("v20").Value 'this is the cell where the number of rows I want (one to ten)
rangeB = Range("v21").Value
rangeC = Range("v22").Value
'############# SELECTION A #################'
'##########################################
Application.Goto ActiveWorkbook.Sheets("DATA").Cells(11, 3)
ActiveSheet.Range("$A$11:$P$65").AutoFilter Field:=10, Criteria1:= _
"FILTER X"
ActiveSheet.Range("$A$11:$P$74").AutoFilter Field:=7, Criteria1:="A"
With ActiveSheet.Range("B11").Offset(1, 0).Resize(Rows.Count - ActiveSheet.Range("B11").Row, 1)
'first cell will be the the first cell of Areas(1)
Set firstCell = .SpecialCells(xlCellTypeVisible).Areas(1).Cells(1, 7)
'Get last cell by looping through areas until their total cell count reaches 4.
For Each area In .SpecialCells(xlCellTypeVisible).Areas
'first area may already contain more than N cells, in which case we just get its Nth cell and exit. "N" is rangeA, rangeB or rangeC
'If this is not the case, we add up rows.Count of each area until we get more than N, and when that happens,
'we get the cell of last area which is needed to get to N.
If CellCount + area.Rows.Count >= Range("v20").Value Then
Set lastCell = area.Cells(Range("v20").Value - CellCount, 0)
Exit For
End If
CellCount = CellCount + area.Rows.Count
Next
End With
'finally, from the firstCell and lastCell we can get the range of first N visible cells.
ActiveSheet.Range(firstCell, lastCell).Select
Selection.Copy
Application.Goto ActiveWorkbook.Sheets("Worksheet 2").Cells(8, 2)
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
''############# SELECTION B #################'
'##############################################
Application.Goto ActiveWorkbook.Sheets("DATA").Cells(11, 3)
ActiveSheet.Range("$A$11:$P$65").AutoFilter Field:=10, Criteria1:= _
"FILTER X"
ActiveSheet.Range("$A$11:$P$74").AutoFilter Field:=7, Criteria1:="B"
With ActiveSheet.Range("B11").Offset(1, 0).Resize(Rows.Count - ActiveSheet.Range("B11").Row, 1)
Set firstCell = .SpecialCells(xlCellTypeVisible).Areas(1).Cells(1, 7)
For Each area In .SpecialCells(xlCellTypeVisible).Areas
If CellCount + area.Rows.Count >= Range("V21").Value Then
Set lastCell = area.Cells(Range("V21").Value - CellCount, 0)
Exit For
End If
CellCount = CellCount + area.Rows.Count
Next
'End If
End With
ActiveSheet.Range(firstCell, lastCell).Select
Selection.Copy
Application.Goto ActiveWorkbook.Sheets("Worksheet 2").Cells(Range("v20").Value + 8, 2)
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Worksheets("BASE DE DADOS").Activate
ActiveSheet.ShowAllData
'############# SELECTION C #######################'
'################################################
Application.Goto ActiveWorkbook.Sheets("DATA").Cells(11, 3)
ActiveSheet.Range("$A$11:$P$65").AutoFilter Field:=10, Criteria1:= _
"FILTER X"
ActiveSheet.Range("$A$11:$P$74").AutoFilter Field:=7, Criteria1:="C"
With ActiveSheet.Range("B11").Offset(1, 0).Resize(Rows.Count - ActiveSheet.Range("B11").Row, 1)
Set firstCell = .SpecialCells(xlCellTypeVisible).Areas(1).Cells(1, 7)
For Each area In .SpecialCells(xlCellTypeVisible).Areas
If CellCount + area.Rows.Count >= Range("V22").Value Then
Set lastCell = area.Cells(Range("V22").Value - CellCount, 0)
Exit For
End If
CellCount = CellCount + area.Rows.Count
Next
End With
ActiveSheet.Range(firstCell, lastCell).Select
Selection.Copy
Application.Goto ActiveWorkbook.Sheets("Worksheet 2").Cells(Range("v21").Value + Range("v20").Value + 8, 2)
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Worksheets("BASE DE DADOS").Activate
ActiveSheet.ShowAllData
End Sub
Another observation is that when I run one selection alone it works perfectly fine (it can be one, two, three,... ten items). But when I run two selections (any combinations) it works until certain number of items, for rangeA and rangeB it works for 1 and 3 but it doesn't work for 3 and 3. In this last case it selects 3 items of A (correct) but only 1 item of B.
Also, it works for the three selections when the rangeA, rangeB and rangeC are respectively 3, 1, 1.
Any ideas?

Make the selection a function so you can re-use same code for each selection.
Option Explicit
Sub selectABC()
Dim wsData As Worksheet, ws2 As Worksheet, rngTarget As Range
Dim countA As Long, countB As Long, countC As Long, n As Long
With ThisWorkbook
Set wsData = .Sheets("DATA")
Set ws2 = .Sheets("worksheet 2")
End With
With wsData
'these are the cells where the number of rows I want
countA = 1 '.Range("v20").Value
countB = 4 ' .Range("v21").Value
countC = 4 ' .Range("v22").Value
End With
'selection A
Set rngTarget = ws2.Range("B8")
n = myselect(wsData, rngTarget, "FILTER X", "A", countA)
'selection B
Set rngTarget = rngTarget.Offset(n)
n = myselect(wsData, rngTarget, "FILTER X", "B", countB)
'selection C
Set rngTarget = rngTarget.Offset(n)
Call myselect(wsData, rngTarget, "FILTER X", "C", countC)
End Sub
Function myselect(wsData, rngTarget, f1, f2, maxrows) As Long
Dim rng As Range, a As Range, rngVisible As Range, rngCopy As Range
Dim lastrow As Long, n As Long, m As Long
With wsData
lastrow = .UsedRange.Row + .UsedRange.Rows.Count - 1
With .Range("A11:P" & lastrow)
.AutoFilter Field:=10, Criteria1:=f1 ' col J
.AutoFilter Field:=7, Criteria1:=f2 ' col G
Set rngVisible = .SpecialCells(xlCellTypeVisible)
If rngVisible Is Nothing Then
MsgBox " Error no data", vbCritical
Exit Function
End If
.AutoFilter
End With
'Debug.Print rngVisible.Address, f1, f2, lastrow
n = 0
m = 0
For Each a In rngVisible.Areas
For Each rng In a.Rows
' skip first headers
If n > 0 Then
If rngCopy Is Nothing Then
Set rngCopy = rng
Else
Set rngCopy = Union(rngCopy, rng)
End If
m = m + 1 ' row count
End If
n = n + 1
If n > maxrows Then Exit For
Next
If n > maxrows Then Exit For
Next
If rngCopy Is Nothing Then
' no data
Else
'Debug.Print rngCopy.Address
rngCopy.Copy
rngTarget.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, _
SkipBlanks:=False, Transpose:=False
Application.CutCopyMode = False
' remove selection shading
rngTarget.Parent.Activate
rngTarget.Select
End If
End With
myselect = m
MsgBox m & " rows copied for J=" & f1 & " G=" & f2
End Function

Related

How to work with the range method with only one specific cell?

I would like the cells I have selected in the spreadsheet to receive the +1 increment. The code below works fine when I have a range, but when I have only one cells selected the code adds +1 to every cell in the spreadsheet.
Sub Macro_MAIS_1()
'
' Macro_MAIS_1 Macro
'
'
Dim AlocationWorksheet As Worksheet
Dim ActSheet As Worksheet
Dim SelRange As Range
Dim iCells As Integer
On Error GoTo Fim
Set AlocationWorksheet = Worksheets("ALOCAÇÃO")
AlocationWorksheet.Unprotect
Set ActSheet = ActiveSheet
Set SelRange = Selection.SpecialCells(xlCellTypeVisible)
iCells = SelRange.Cells.Count
Range("O7").Select
Selection.Copy
SelRange.PasteSpecial Paste:=xlPasteValues, Operation:=xlAdd, SkipBlanks _
:=False, Transpose:=False
Exit Sub
Fim:
MsgBox Selection.Address
Range("O7").Select
Selection.Copy
SelRange.PasteSpecial Paste:=xlPasteValues, Operation:=xlAdd, SkipBlanks _
:=False, Transpose:=False
End Sub
I would avoid using a selection, but this should work. If you have text you'll run into trouble and need to write out some checks. You also should not be counting all cells, as you might have an overflow of values. Check rows and columns, but not both.
Sub addPlusOne()
Dim aRange As Range, i As Long, j As Long
Set aRange = Selection
If aRange.Rows.Count > 1 Or aRange.Columns.Count > 1 Then
Dim zRng()
zRng = aRange.Value
For i = LBound(zRng) To UBound(zRng)
For j = LBound(zRng, 2) To UBound(zRng, 2)
zRng(i, j) = zRng(i, j) + 1
Next j
Next i
aRange.Value = zRng
Else
aRange.Value = aRange.Value + 1
End If
End Sub
EDIT: OP commented that they want to use visible selection. While this isn't best practice, this will work.
Sub plusOneOnSelection()
Dim aCell As Range
For Each aCell In Selection.SpecialCells(xlCellTypeVisible).Cells
If IsNumeric(aCell) Then aCell.Value = aCell.Value + 1
Next aCell
End Sub

Column based on header in excel vba

The formula:
=IF(RC[2]=""Debit"",RC[-1],IF(RC[2]=""Credit"",-RC[-1]))
Says it will check if the value is debit/credit in col M and put (-) in col K.
My question is what if we don't know that debit/credit is in col M only? What we will give instead of RC[2]? We Only know that header of that column will be "Debit or Credit".
My full code:
Rows("1:1").Select
Selection.Find(What:="AMNT", After:=ActiveCell, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False).Select
ActiveCell.Offset(1, 0).Select 'Noting but K2
Range(Cells(2, ActiveCell.Column), Cells(lastRow, ActiveCell.Column)).FormulaR1C1 = _
"=IF(RC[2]=""Debit"",RC[-1],IF(RC[2]=""Credit"",-RC[-1]))"
ActiveCell.EntireColumn.Select
Selection.Copy
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Application.CutCopyMode = False
Your code does a lot of selecting, and nothing with those selections. At the best of times it is best to avoid using select as it slows down your code (and in most cases is unnecessary).
This code assumes the worksheet is Sheet1, if not change the sheet reference to suit your own Worksheet.
I create variables for all the numbers and ranges I want to use which makes the code easier to read and follow (as the variable can be descriptively named).
I find the last column in row 1 (assuming this is the header row) which means the code will work all the same if columns are added or removed.
Once the column header is found we assign the credit or debit column number to DebtCreditColumn and use that to define our HeaderRange.
We then do the same for AMNTColumn.
I added a couple of If...Then statements to display a MsgBox and abort the code if either values are 0 (which means the headers weren't found).
Then minus AMNTColumn from DebtCreditColumn to get the difference and assign to FormulaReferenceColumn.
Then find the LastRow in the Debit or Credit and set our TargetRange for the 'AMNT Column' from row 2 to the LastRow (LastRow wasn't defined in your code so I assumed it was the 'Debit or Credit' column).
Finally incorporate the FormulaReferenceColumn into our formula to be written to our TargetRange.
Like so:
Sub ParanTest()
Dim DebtCreditColumn As Long
Dim AMNTColumn As Long
Dim LastColumn As Long
Dim FormulaReferenceColumn As Long
Dim LastRow As Long
Dim HeaderRange As Range
Dim TargetCell As Range
Dim TargetRange As Range
With Sheet1
LastColumn = .Cells(1, Columns.Count).End(xlToLeft).Column
Set HeaderRange = .Range(.Cells(1, 1), .Cells(1, LastColumn))
End With
For Each TargetCell In HeaderRange
If TargetCell.Value Like "Debit or Credit" Then
DebtCreditColumn = TargetCell.Column
Exit For
Else
'Go To Next Cell
End If
Next TargetCell
For Each TargetCell In HeaderRange
If TargetCell.Value Like "AMNT" Then
AMNTColumn = TargetCell.Column
Exit For
Else
'Go To Next Cell
End If
Next TargetCell
'In case the column can't be found, this will notify you and abort the code to avoid errors.
If DebtCreditColumn = 0 Then
MsgBox "A column header 'Debit or Credit' could not be found.", vbOKOnly, "No column found!"
Exit Sub
End If
'In case the column can't be found, this will notify you and abort the code to avoid errors.
If AMNTColumn = 0 Then
MsgBox "A column header 'AMNT' could not be found.", vbOKOnly, "No column found!"
Exit Sub
End If
FormulaReferenceColumn = DebtCreditColumn - AMNTColumn
With Sheet1
LastRow = .Cells(Rows.Count, DebtCreditColumn).End(xlUp).Row 'You can define whatever column works best for you
Set TargetRange = .Range(.Cells(2, AMNTColumn), .Cells(LastRow, AMNTColumn))
End With
TargetRange.FormulaR1C1 = "=IF(RC[" & FormulaReferenceColumn & "]=""Debit"",RC[-1],IF(RC[" & FormulaReferenceColumn & "]=""Credit"",-RC[-1]))"
End Sub

Macro to copy and paste (transpose) data from column to row - Scalable

I am looking to create a macro which would allow me to copy and paste data from one column and then transpose that data over 2 columns in the right order
I have recorded a macro while doing the process manually
Range("G3").Select
Application.CutCopyMode = False
Selection.Copy
Range("G2:G7").Select ' (The column range I want to copy)
Application.CutCopyMode = False
Selection.Copy
Range("I1").Select ' (Row where the range of G2:G7) is now transposed)
Selection.PasteSpecial Paste:=xlPasteAll, Operation:=xlNone, SkipBlanks:= _
False, Transpose:=True
Range("H2:H7").Select ' (The second column range I want to copy)
Application.CutCopyMode = False
Selection.Copy
Range("I2").Select ' (Second Row where the range of H2:H7) is now transposed)
Selection.PasteSpecial Paste:=xlPasteAll, Operation:=xlNone, SkipBlanks:= _
False, Transpose:=True
Range("H8:H13").Select ' (The third column range I want to copy)
Application.CutCopyMode = FalseSelection.Copy
Range("I3").Select' ( Third Row where the range of H8:H13) is now transposed)
Selection.PasteSpecial Paste:=xlPasteAll, Operation:=xlNone, SkipBlanks:= _
False, Transpose:=True
The problem is that this code only works up to certain number of rows (up till H13 for example), but if I want to this repeat this process up to row H600 (range of H600:H605) and pasting to I31 for example without copying and pasting this code hundreds of times, is there a way I can do this?
This is what I mean by example
Column H
Star
Greenwood
Titon
Humford
converted to
Column I | Column J**
Star | Greenwood
titon | Humford
Here's an alternative to Copy/Paste - using Variant Arrays. This will be much faster for large data sets.
Sub Demo()
Dim rng As Range
Dim Src As Variant
Dim Dst As Variant
Dim GroupSize As Long
Dim Groups As Long
Dim iRow As Long
Dim iCol As Long
Dim iDst As Long
Dim SrcStartRow As Long
Dim SrcColumn As Long
Dim DstStartRow As Long
Dim DstColumn As Long
' Set up Parameters
GroupSize = 2
SrcStartRow = 2
SrcColumn = 8 'H
DstStartRow = 1
DstColumn = 9 'I
With ActiveSheet 'or specify a specific sheet
' Get Reference to source data
Set rng = .Range(.Cells(SrcStartRow, SrcColumn), .Cells(.Rows.Count, SrcColumn).End(xlUp))
' Account for possibility there is uneven amount of data
Groups = Application.RoundUp(rng.Rows.Count / GroupSize, 0)
If rng.Rows.Count <> Groups * GroupSize Then
Set rng = rng.Resize(Groups * GroupSize, 1)
End If
'Copy data to Variant Array
Src = rng.Value2
'Size the Destination Array
ReDim Dst(1 To UBound(Src, 1) / GroupSize, 1 To GroupSize)
'Loop the Source data and split into Destination Array
iDst = 0
For iRow = 1 To UBound(Src, 1) Step GroupSize
iDst = iDst + 1
For iCol = 1 To GroupSize
Dst(iDst, iCol) = Src(iRow + iCol - 1, 1)
Next
Next
' Move result to sheet
.Cells(DstStartRow, DstColumn).Resize(UBound(Dst, 1), UBound(Dst, 2)).Value = Dst
End With
End Sub
Before
Well, you are not really transposing, but I would use this method. I start at 2 to leave the first in place, then basically move the next one over and delete all the empty spaces at the end.
Sub MakeTwoColumns()
Dim x As Long
For x = 2 To 500 Step 2
Cells(x, 6) = Cells(x, 5)
Cells(x, 5).ClearContents
Next x
Columns(5).SpecialCells(xlCellTypeBlanks).Delete
Columns(6).SpecialCells(xlCellTypeBlanks).Delete
End Sub
After

Auto filter to select just the visible rows

I have this code. It loops through a list for the filtering criteria, then if no data to select it shows all data again and loops to the next criteria. If it shows data it end(slDown) and selects all the data showing, copies it and pastes it into another worksheet.
The cleanup script cleans any blank rows and columns and then returns to the original data sheet and deletes the data selected for the copy paste.
The problem is when there is just one row. It moves to the row with data, but when I End(xlDown), it shoots all the way to the bottom and the paste then causes the macro to freeze up.
I nested another if statement to capture if there is only one line of data visible, but I cannot get it to function correctly. Any Suggestions on the nested if statement?
Dim criteria As String
Dim F As Range
Set Rng = Sheets("Reference").Range("W2:W36")
For Each F In Rng
criteria = F
ActiveSheet.Range("$AV$1").AutoFilter Field:=48, Criteria1:="=*BULK SUBSERVIENT*", Operator:=xlAnd
ActiveSheet.Range("$K$1").AutoFilter Field:=11, Criteria1:=criteria
Range("A2:CM" & ActiveSheet.UsedRange.Rows.Count + 1) _
.Cells.SpecialCells(xlCellTypeVisible).Rows(1).Select
If ActiveCell.Value = vbNullString Then
ActiveSheet.ShowAllData
Else
If (ActiveSheet.UsedRange.SpecialCells(xlCellTypeLastCell)) = 2 Then
'Range(Selection).Select
Selection.Copy
Sheets("Bulk Subservient").Select
ActiveSheet.Range("A" & Rows.Count).End(xlUp).Offset(1).Select
ActiveSheet.Paste
Call cleanup
Else
Range(Selection, Selection.End(xlDown)).Select
Selection.Copy
Sheets("Bulk Subservient").Select
ActiveSheet.Range("A" & Rows.Count).End(xlUp).Offset(1).Select
ActiveSheet.Paste
Call cleanup
End If
End If
Next F
I figured it out.... Here is what I did. Thanks all!
I used this If ActiveSheet.UsedRange.SpecialCells(xlCellTypeVisible).Areas.Count <= 2
instead of this (ActiveSheet.UsedRange.SpecialCells(xlCellTypeLastCell)) = 2
Dim criteria As String
Dim F As Range
Set Rng = Sheets("Reference").Range("W2:W36")
For Each F In Rng
criteria = F
ActiveSheet.Range("$AV$1").AutoFilter Field:=48, Criteria1:="=*BULK SUBSERVIENT*", Operator:=xlAnd
ActiveSheet.Range("$K$1").AutoFilter Field:=11, Criteria1:=criteria
Range("A2:CM" & ActiveSheet.UsedRange.Rows.Count + 1) _
.Cells.SpecialCells(xlCellTypeVisible).Rows(1).Select
If ActiveCell.Value = vbNullString Then
ActiveSheet.ShowAllData
Else
If ActiveSheet.UsedRange.SpecialCells(xlCellTypeVisible).Areas.Count <= 2 Then
'Range(Selection).Select
Selection.Copy
Sheets("Bulk Subservient").Select
ActiveSheet.Range("A" & Rows.Count).End(xlUp).Offset(1).Select
ActiveSheet.Paste
Call cleanup
Else
Range(Selection, Selection.End(xlDown)).Select
Selection.Copy
Sheets("Bulk Subservient").Select
ActiveSheet.Range("A" & Rows.Count).End(xlUp).Offset(1).Select
ActiveSheet.Paste
Call cleanup
End If
End If
Next F
I think your code could be much cleaner than this. I prefer to use an auxiliar funcion to make this filter. Something like this:
Function MyFilter(criteria as string) as Range
Set tableRange = ActiveSheet.UsedRange
' Filter
With tableRange
Call .AutoFilter(48, "*BULK SUBSERVIENT*")
Call .AutoFilter(11, criteria)
End With
On Error Resume Next
'This...
Set selectedRange = tableRange.SpecialCells(xlCellTypeVisible)
'...Or (how to remover title).
Set selectedRange = Intersect(tableRange.SpecialCells(xlCellTypeVisible), .[2:1000000])
On Error GoTo 0
With tableRange
Call .AutoFilter(11)
Call .AutoFilter(48)
End With
'Empty Criteria
If WorksheetFunction.CountA(selectedRange) < 2 Then
Exit Sub
End If
Set MyFilter = selectedRange
End Sub
Here is your original code rewritten using the Range.CurrentRegion property to define the range of cells to be filtered.
Dim criteria As String
Dim F As Range, rng As Range
With Worksheets("Reference")
Set rng = .Range(.Cells(2, 23), .Cells(Rows.Count, 23).End(xlUp))
End With
With ActiveSheet '<~~ set this to worksheets("Sheet1") as appropriate
If .AutoFilterMode Then .AutoFilterMode = False
With .Cells(1, 1).CurrentRegion
For Each F In rng
criteria = F
.AutoFilter Field:=48, Criteria1:="*BULK SUBSERVIENT*"
.AutoFilter Field:=11, Criteria1:=criteria
With .Resize(.Rows.Count - 1, .Columns.Count).Offset(1, 0)
If CBool(Application.Subtotal(103, .Cells)) Then
.Copy Destination:=Sheets("Bulk Subservient").Cells(Rows.Count, 1).End(xlUp).Offset(1)
End If
End With
Next F
End With
If .AutoFilterMode Then .AutoFilterMode = False
End With
Here is the same thing that collects all of the criteria terms from the Reference worksheet into a variant array and uses that to filter for all terms at once.
Dim rng As Range
Dim vCRITERIA As Variant, v As Long
With Worksheets("Reference")
ReDim vCRITERIA(1 To 1) '<~~for alternate method
For Each rng In .Range(.Cells(2, 23), .Cells(Rows.Count, 23).End(xlUp))
vCRITERIA(UBound(vCRITERIA)) = rng.Value2
ReDim Preserve vCRITERIA(UBound(vCRITERIA) + 1)
Next rng
ReDim Preserve vCRITERIA(UBound(vCRITERIA) - 1)
End With
With ActiveSheet '<~~ set this to worksheets("Sheet1") as appropriate
If .AutoFilterMode Then .AutoFilterMode = False
With .Cells(1, 1).CurrentRegion
.AutoFilter Field:=48, Criteria1:="*BULK SUBSERVIENT*"
.AutoFilter Field:=11, Criteria1:=(vCRITERIA), Operator:=xlFilterValues
With .Resize(.Rows.Count - 1, .Columns.Count).Offset(1, 0)
If CBool(Application.Subtotal(103, .Cells)) Then
.Copy Destination:=Sheets("Bulk Subservient").Cells(Rows.Count, 1).End(xlUp).Offset(1)
End If
End With
End With
If .AutoFilterMode Then .AutoFilterMode = False
End With
The latter is likely a few milli-seconds faster than the first.
The worksheet's SUBTOTAL function never includes filtered or hidden rows so asking for a count will determine if there is anything to copy. Resizing and offsetting moves to the filtered range.
You will need to reincorporate the Cleanup subroutine.

Excel VBA: Filter and copy from top 5 rows/cells

I have a data table which is sorted on descending order in column F. I then need to copy the top 5 rows, but only data from Column A, B, D, and F (not the headers). See pictures.
Sub top5()
Sheets("Sheet1").Select
If (ActiveSheet.AutoFilterMode And ActiveSheet.FilterMode) Or ActiveSheet.FilterMode Then
ActiveSheet.ShowAllData
End If
ActiveSheet.Range("$A$4:$T$321").AutoFilter Field:=3, Criteria1:="Dave"
ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort.SortFields. _
Clear
ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort.SortFields.Add _
Key:=Range("F4:F321"), SortOn:=xlSortOnValues, Order:=xlDescending, _
DataOption:=xlSortTextAsNumbers
With ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
' This copy-paste part does what its supposed to, but only for the specific
' cells. Its not generalised and I will have to repeat this operation
' several times for different people
Sheets("Sheet1").Select
Range("A3:B15").Select
Selection.Copy
Sheets("Sheet2").Select
Range("A3").Select
ActiveSheet.Paste
Sheets("Sheet1").Select
Range("D3:D15").Select
Application.CutCopyMode = False
Selection.Copy
Sheets("Sheet2").Select
Range("C3").Select
ActiveSheet.Paste
Sheets("Sheet1").Select
Range("F3:F15").Select
Application.CutCopyMode = False
Selection.Copy
Sheets("Sheet2").Select
Range("D3").Select
ActiveSheet.Paste
Application.CutCopyMode = False
End Sub
I thought about trying to adapt this snippet of code below using visible cells function, but I'm stuck and I can't find anything on the net which fits.
' This selects all rows (plus 1, probably due to offset), I only want parts of from the top 5.
Sheets("Sheet1").Select
ActiveSheet.Range("$A$4:$B$321").Offset(1, 0).SpecialCells(xlCellTypeVisible).Select
Selection.Copy
Sheets("Sheet2").Select
Range("A3").Select
ActiveSheet.Paste
Sheets("Sheet1").Select
ActiveSheet.Range("$D$4:$D$321").Offset(1, 0).SpecialCells(xlCellTypeVisible).Select
Selection.Copy
Sheets("Sheet2").Select
Range("C3").Select
ActiveSheet.Paste
I hope my example makes sense and I really appreciate your help!
Note: The heading names are only the same in the two tables to show that the data is the same. The headers are NOT supposed to be copied. In addition, there is an extra column/white space in the second table. A solution should include this.
Firstly a few helpful points:
You should refer to worksheets by there Code Name to avoid renaming issues.
If you want to work with VBA then my advice is to avoid merged cells like the plague. They cause havoc with code. If possible use format cells - alignment - horizontal - centre accross selection
I also advise avoiding loops wherever possible and take advantage of excels built in functions instead as a good practice exercise.
Here is my solution. Keep it simple. If you need further help let me now.
Sub HTH()
Dim rCopy As Range
With Sheet1.AutoFilter.Range
'// Set to somewhere blank and unused on your worksheet
Set rCopy = Sheet1.Range("A" & Rows.Count - (.Rows.Count))
.SpecialCells(xlCellTypeVisible).Copy rCopy
End With
With rCopy.Offset(1).Resize(5) '// Offset to avoid the header
.Resize(, 2).Copy Sheet2.Range("A5")
.Offset(, 3).Resize(, 1).Copy Sheet2.Range("D5")
.Offset(, 5).Resize(, 1).Copy Sheet2.Range("F5")
.CurrentRegion.Delete xlUp '// Delete the tempory area
End With
Set rCopy = Nothing
End Sub
A quick way to do this is to use Union and Intersect to only copy the cells that you want. If you are pasting values (or the data is not a formula to start), this works well. Thinking about it, it builds a range of columns to keep using Union and then Intersect that with the first 5 rows of data with 2 header rows. The result is a copy of only the data you want with formatting intact.
Edit only process visible rows, grabbing the header, and then the first 5 below the header rows
Sub CopyTopFiveFromSpecificColumns()
'set up the headers first to keep
Dim rng_top5 As Range
Set rng_top5 = Range("3:4").EntireRow
Dim int_index As Integer
'start below the headers and keep all the visible cells
For Each cell In Intersect( _
ActiveSheet.UsedRange.Offset(5), _
Range("A:A").SpecialCells(xlCellTypeVisible))
'add row to keepers
Set rng_top5 = Union(rng_top5, cell.EntireRow)
'track how many items have been stored
int_index = int_index + 1
If int_index >= 5 Then
Exit For
End If
Next cell
'copy only certain columns of the keepers
Intersect(rng_top5, _
Union(Range("A:A"), _
Range("B:B"), _
Range("D:D"), _
Range("F:F"))).Copy
'using Sheet2 here, you can set to wherever, works if data is not formulas
Range("Sheet2!A1").PasteSpecial xlPasteAll
'if the data contains formulas, use this route
'Range("Sheet2!A1").PasteSpecial xlPasteValues
'Range("Sheet2!A1").PasteSpecial xlPasteFormats
End Sub
Here is the result I get from some dummy data set up in the same ranges as the picture above.
Sheet1 with copied range visible
Sheet2 with pasted data
The first part of your question, selecting the top5 visible cells, is relatively easy, the copying and pasting is where the trouble are. You see, you cannot paste a range, even if it is not uniform, into non uniform range. So you'll need to write your own Paste function.
Part 1 - Getting the Top5 rows
I used a similar technique to #Byron's. Notice that this is merely a function returning a Range object and accepting a String, which represents your non-uniform range (you can change the parameter type to Range if you wish).
Function GetTop5Range(SourceAddress As String) As Range
Dim rngSource As Range
Dim rngVisible As Range
Dim rngIntersect As Range
Dim rngTop5 As Range
Dim i As Integer
Dim cell As Range
Set rngSource = Range(SourceAddress)
Set rngVisible = rngSource.SpecialCells(xlCellTypeVisible).Cells
Set rngIntersect = Intersect(rngVisible, rngVisible.Cells(1, 1).EntireColumn)
i = 1
For Each cell In rngIntersect
If i = 1 Then
Set rngTop5 = cell.EntireRow
i = i + 1
ElseIf i > 1 And i < 6 Then
Set rngTop5 = Union(rngTop5, cell.EntireRow)
i = i + 1
Else
Exit For
End If
Next cell
Set GetTop5Range = Intersect(rngTop5, rngVisible)
End Function
Part 2 - Creating your own pasting function
Because Excel always pastes your copied range as uniform, you need to do it yourself. This method essentially breaks down your source region to columns and pastes them individually. The method accepts parameter SourceRange of type Range , which is meant to by your Top5 range, and a TopLeftCornerRange of type Range, which represents the target cell of your pasting.
Sub PasteRange(SourceRange As Range, TopLeftCornerRange As Range)
Dim rngColumnRange As Range
Dim cell As Range
Set rngColumnRange = Intersect(SourceRange, SourceRange.Cells(1, 1).EntireRow)
For Each cell In rngColumnRange
Intersect(SourceRange, cell.EntireColumn).Copy
TopLeftCornerRange.Offset(0, cell.Column - 1).PasteSpecial xlPasteValuesAndNumberFormats
Next cell
Application.CutCopyMode = False
End Sub
Part 3 - Running the procedure
Sub Main()
PasteRange GetTop5Range("A2:B33,D2:D33"), Range("A35")
End Sub
That's it.
In my project, I had source data in Columns A, B and D like you did and the results are pasted to range beginning at A35.
Result:
Hope this helps!
While it may simply be easier to loop through the first five visible rows, I used application.evaluate to process a worksheet-style formula that returned the row number of the fifth visible record.
Sub sort_filter_copy()
Dim lr As Long, lc As Long, flr As Long, rws As Long, v As Long
Dim sCRIT As String
Dim vCOLs As Variant, vVALs As Variant
Dim bCopyFormulas As Boolean, bSort2Keys As Boolean
bCopyFormulas = True
bSort2Keys = False
sCRIT = "dave"
vCOLs = Array(1, 2, 4, 6)
With Sheet1
lr = .Cells(Rows.Count, 1).End(xlUp).Row
lc = .Cells(4, Columns.Count).End(xlToLeft).Column
With .Cells(5, 1).Resize(lr - 4, lc)
'sort on column F as if there was no header
If bSort2Keys Then
.Cells.Sort Key1:=.Columns(6), Order1:=xlDescending, _
Key2:=.Columns(7), Order2:=xlDescending, _
Orientation:=xlTopToBottom, Header:=xlNo
Else
.Cells.Sort Key1:=.Columns(6), Order1:=xlDescending, _
Orientation:=xlTopToBottom, Header:=xlNo
End If
With .Offset(-1, 0).Resize(.Rows.Count + 1, .Columns.Count)
.AutoFilter
.AutoFilter field:=3, Criteria1:=sCRIT
With .Offset(1, 0).Resize(.Rows.Count - 1, .Columns.Count)
rws = Application.Min(5, Application.Subtotal(103, .Columns(3)))
If CBool(rws) Then
flr = Application.Evaluate("=small(index(rows(5:" & lr & ") + ('" & Sheet1.Name & "'!C5:C" & lr & "<>" & Chr(34) & sCRIT & Chr(34) & ")*1e99, , ), " & rws & ")")
For v = LBound(vCOLs) To UBound(vCOLs)
If .Columns(vCOLs(v)).Cells(1).HasFormula And bCopyFormulas Then
Sheet2.Cells(3, vCOLs(v)).Resize(5, 1).FormulaR1C1 = _
.Columns(vCOLs(v)).Cells(1).FormulaR1C1
Else
.Columns(vCOLs(v)).Resize(flr - 4, 1).Copy _
Destination:=Sheet2.Cells(3, vCOLs(v))
End If
Next v
End If
End With
.AutoFilter
End With
'uncomment the next line if you want to return to a standard ascending sort on column A
'.Cells.Sort Key1:=.Columns(1), Order1:=xlAscending, _
Orientation:=xlTopToBottom, Header:=xlNo
End With
End With
End Sub
All options are set just below the variable declarations. Your sample images seemed to indicate that you used a two key sort so I coded for that optionally. If you want to bring in any formulas as formulas, that option is there. The filter criteria and the columns to copy are assigned to their respective vars as well.
        
My sample workbook is available on my public DropBox at:
      Sort_Filter_Copy_from_Top_5.xlsb
Try this:
Sub GetTopFiveRows()
Dim table As Range, cl As Range, cnt As Integer
Set table = Worksheets("Sheet1").Range("A2:A10").SpecialCells(xlCellTypeVisible)
cnt = 1
With Worksheets("Sheet2")
For Each cl In table
If cnt <= 5 Then
.Range("A" & cnt) = cl
.Range("B" & cnt) = cl.Offset(0, 1)
.Range("D" & cnt) = cl.Offset(0, 3)
.Range("F" & cnt) = cl.Offset(0, 5)
cnt = cnt + 1
Else
Exit Sub
End If
Next cl
End With
End Sub
First a reference is set to only visible rows in the entire table (you'll need to update the range reference)
Then we loop over the visible range, copy to sheet 2, and stop when 5 records (i.e. the top five) have been copied
First Unmerge the cells then use this code, very similar to some of the other suggestions.
Sub Button1_Click()
Dim sh As Worksheet
Dim Rws As Long, Rng As Range, fRng As Range, c As Range, fRw As Long
Set sh = Sheets("Sheet2")
Rws = Cells(Rows.Count, "A").End(xlUp).Row
Set Rng = Range(Cells(4, 1), Cells(Rws, "T")) 'unmerge all the headers
Rng.AutoFilter Field:=3, Criteria1:="Dave"
ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort.SortFields. _
Clear
ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort.SortFields.Add _
Key:=Range("F4:F321"), SortOn:=xlSortOnValues, Order:=xlDescending, _
DataOption:=xlSortTextAsNumbers
With ActiveWorkbook.Worksheets("Sheet1").AutoFilter.Sort
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
Set fRng = Range(Cells(5, 1), Cells(Rws, 1)).SpecialCells(xlCellTypeVisible)
x = 0
For Each c In fRng.Cells
If x = 5 Then Exit Sub
fRw = sh.Cells(Rows.Count, "A").End(xlUp).Row + 1
sh.Range(sh.Cells(fRw, 1), sh.Cells(fRw, 2)).Value = Range(Cells(c.Row, 1), Cells(c.Row, 2)).Value
sh.Cells(fRw, 4).Value = Cells(c.Row, 4).Value
sh.Cells(fRw, 6).Value = Cells(c.Row, 6).Value
x = x + 1
Next c
End Sub

Resources