Column based on header in excel vba - excel

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

Related

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

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

Copying cell values from one sheet to another, and paste it near a cell with specific value

I have a constant task at work where I need to copy a list of numbers to another sheet. In that sheet, I need to paste those numbers one by one, in a cell to the right of cells with a certain value(that repeats in a column). (notice that the target table is sorted by that value -"מודל תגובה" and there are hidden rows.
It's hard to explain so I hope the images will do.
I tried to write suitable code but I kept getting different errors.
It seems that problems occur when copying the cell values to the target cells.
Dim i As Integer
i = 4
Do While IsEmpty(Cells(i, 1).Value) = False
Worksheets(1).Select
Cells(i, 1).Copy
Worksheets(2).Select
Cells.Find(What:="מודל תגובה", After:=ActiveCell, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False).Activate
ActiveCell.Offset(0, -1).Activate
If IsEmpty(ActiveCell.Value) = False Then
Selection.FindNext(After:=ActiveCell).Activate
ActiveCell.Offset(0, -1).Paste
Else
ActiveCell.Offset(0, -1).Select
ActiveCell.Paste
End If
i = i + 1
Loop
sorry for the shitty code(literally my first macro).
The solution would be to loop through the visible cells of the filtered range only.
Make sure the destination is filtered for "מודל תגובה" before running this code. It needs to look like your second image before running this code.
Dim SourceSheet As Worksheet
Set SourceSheet = Worksheets(1)
Dim DestinationSheet As Worksheet
Set DestinationSheet = Worksheets(2)
Dim LastRow As Long
LastRow = DestinationSheet.Cells(DestinationSheet.Rows.Count, "B").End(xlUp).Row
Dim VisibleCells As Range
On Error Resume Next 'next line errors if no visible cells so we turn error reporting off
Set VisibleCells = DestinationSheet.Range("A2", "A" & LastRow).SpecialCells(xlCellTypeVisible)
On Error Goto 0 'turn error reporting on or you won't see if other errors occur
If VisibleCells Is Nothing Then 'abort if no cells are visible in the filter
MsgBox "No cells to paste at"
Exit Sub
End If
Dim SourceRow As Long
SourceRow = 4 'start row in your source sheet
Dim Cell As Range
For Each Cell In VisibleCells.Cells 'loop through visible cells
Cell.Value = SourceSheet.Cells(SourceRow, "A").Value 'copy value
SourceRow = SourceRow + 1 'incerease source row
Next Cell
Make sure to define DestinationSheet and SourceSheet with your sheets names.
Try this:
Dim i As Integer
Dim Last_Row as Long
Worksheets(1).Select
'The "1" Of the line below means that the variable gonna count the rows of the first column (A)
Last_Row = Application.ActiveSheet.Cells(Rows.Count, 1).End(xlUp).Row
Range("A1:A" & Last_Row).Copy
Worksheets(2).Select
Range("A1").Select
ActiveSheet.Paste

Iterate through a column and if there is a match in the header of another sheet, then copy and transpose data into original sheet

I'm trying to do something which appears to be simple but proving a little too difficult for me.
I have two sheets; master and data.
master has a set of field names in column A
data has the field name across the top header (row 1)
What I want to do is:
iterate through column A of master and for each field, check if the field exists in row 1 of data
if it does, copy all the data from that column in data where the match exists (excluding the header) and paste transpose the data into the corresponding row in master.
To make is easier to visualize, master looks like this:
id |
total|
...and data looks like this:
id | name | total
-------------------------
1 | Khar | 5
2 | SantaCruz | 3
3 | Sion | 2
4 | VT | 1
5 | newFort | 3
The end result in master would look like this:
id | 1 | 2 | 3 | 4 | 5
total| 5 | 3 | 2 | 1 | 3
These are simplistic examples. The actual sheets have hundreds of rows and columns and they can change so hard coding field names into any solution is not really an option.
The code I have so far is shown below.
Sub CopyTranspose()
Dim x As Integer
Dim whatToFind As String
Dim NumRows As Range
Dim rngFound As Range
Dim rgCopy As Range
Dim LastRow As Long
Dim LastRowMaster As Long
Dim LastCol As Integer
Sheets("master").Select
' Select cell BR13, *first line of data*.
Range("A1").Select
' Set Do loop to stop when an empty cell is reached.
Do Until IsEmpty(ActiveCell)
whatToFind = ActiveCell.Value
'Find name and copy
Sheets("data").Select
With ActiveSheet
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
LastCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
End With
With Sheets("data").Range("A1:ZZZ" & LastRow)
Set rngFound = Cells.Find(What:=whatToFind, After:=Range("A1"), LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlNext, _
MatchCase:=False)
If Not rngFound Is Nothing Then
rngFound.Select
ActiveCell.Offset(1, 0).Copy
End If
End With
'find name then offset and paste
Sheets("master").Select
With ActiveSheet
LastRowMaster = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
With Sheets("master").Range("A1:A" & LastRowMaster)
Set rngFound = Cells.Find(What:=whatToFind, After:=Range("A1"), LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False)
If Not rngFound Is Nothing Then
rngFound.Select
ActiveCell.Offset(0, 1).Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=True
ActiveCell.Offset(1, -2).Select
End If
End With
' Step down 1 row from present location.
ActiveCell.Offset(1, 0).Select
Loop
End Sub
The error I'm getting is
'1004': Application-defined or object-defined error
on line With Sheets("data").Range("A1:ZZZ" & LastRow)
I've tried to butcher something together from the questions already answered here so I don't even know if the above is the best option to use for this particular task.
Any help would really be appreciated. Many thanks
Edit 1:
Thanks to #CATSandCATSandCATS I was able to resolve the above issue by reducing the range. i.e.
With Sheets("data").Range("A1:SA" & LastRow)
However, I'm getting another error now - "'1004': PasteSpecial method of Range class failed" on line Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=True
I suggest the following:
Read headers of master and data sheets into arrays for faster matching!
Loop through master "header" column A
Match each header with the data headers (row 1)
If they match transpos data
So outging from this data …
you will end up with the following master …
Option Explicit
Public Sub CopyTranspose()
Dim wsMaster As Worksheet 'define master sheet
Set wsMaster = ThisWorkbook.Worksheets("master")
Dim wsData As Worksheet 'define data sheet
Set wsData = ThisWorkbook.Worksheets("data")
'read master headers (column A) into array
Dim MasterHeaders() As Variant
MasterHeaders = wsMaster.Range("A1", wsMaster.Cells(wsMaster.Rows.Count, "A").End(xlUp)).Value
'read data headers (row 1) into array
Dim DataHeaders() As Variant
DataHeaders = wsData.Range("A1", wsData.Cells(1, wsData.Columns.Count).End(xlToLeft)).Value
Dim MatchedColumn As Long
Dim MatchedColumnData As Range
Dim iRow As Long
For iRow = LBound(MasterHeaders, 1) To UBound(MasterHeaders, 1)
MatchedColumn = 0 'initialize
On Error Resume Next 'next line throws error if headers do not match (hide it)
MatchedColumn = Application.WorksheetFunction.Match(MasterHeaders(iRow, 1), DataHeaders, 0)
On Error GoTo 0 'always re-enable error reporting!!!
If MatchedColumn > 0 Then 'a matching header was found
'find last used row in matched column to get all data
Set MatchedColumnData = wsData.Range(wsData.Cells(2, MatchedColumn), wsData.Cells(wsData.Rows.Count, MatchedColumn).End(xlUp))
'transpos data to master sheet
wsMaster.Cells(iRow, 2).Resize(columnsize:=MatchedColumnData.Rows.Count).Value = Application.WorksheetFunction.Transpose(MatchedColumnData)
End If
Next iRow
End Sub
Note that there is a limitation: If there are more rows in the data sheet than columns are available in the master sheet then you cannot transpose the data because it doesn't fit into one row (Excel has more rows than columns).
Would a SUMIF function work for you?
The two sheets are in the same book, right?
=SUMIF($A$6:$A$10, B$1,$C$6:$C$10)
=
Regarding your particular error, I am pretty sure excel does not go to ZZZ. It only goes up to XFD (16,384).
On the new error, it does not look like you are copying anything before trying to paste. Try this:
If Not rngFound Is Nothing Then
rngFound.Copy
ActiveCell.Offset(0, 1).Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=True
ActiveCell.Offset(1, -2).Select
End If

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

Getting the actual usedrange

I have a Excel worksheet that has a button.
When I call the usedRange() function, the range it returns includes the button part.
Is there anyway I can just get actual used range that contains data?
What sort of button, neither a Forms Control nor an ActiveX control should affect the used range.
It is a known problem that excel does not keep track of the used range very well. Any reference to the used range via VBA will reset the value to the current used range. So try running this sub procedure:
Sub ResetUsedRng()
Application.ActiveSheet.UsedRange
End Sub
Failing that you may well have some formatting hanging round. Try clearing/deleting all the cells after your last row.
Regarding the above also see:
Excel Developer Tip
Another method to find the last used cell:
Dim rLastCell As Range
Set rLastCell = ActiveSheet.Cells.Find(What:="*", After:=.Cells(1, 1), LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False)
Change the search direction to find the first used cell.
Readify made a very complete answer. Yet, I wanted to add the End statement, you can use:
Find the last used cell, before a blank in a Column:
Sub LastCellBeforeBlankInColumn()
Range("A1").End(xldown).Select
End Sub
Find the very last used cell in a Column:
Sub LastCellInColumn()
Range("A" & Rows.Count).End(xlup).Select
End Sub
Find the last cell, before a blank in a Row:
Sub LastCellBeforeBlankInRow()
Range("A1").End(xlToRight).Select
End Sub
Find the very last used cell in a Row:
Sub LastCellInRow()
Range("IV1").End(xlToLeft).Select
End Sub
See here for more information (and the explanation why xlCellTypeLastCell is not very reliable).
Here's a pair of functions to return the last row and col of a worksheet, based on Reafidy's solution above.
Function LastRow(ws As Object) As Long
Dim rLastCell As Object
On Error GoTo ErrHan
Set rLastCell = ws.Cells.Find("*", ws.Cells(1, 1), , , xlByRows, _
xlPrevious)
LastRow = rLastCell.Row
ErrExit:
Exit Function
ErrHan:
MsgBox "Error " & Err.Number & ": " & Err.Description, _
vbExclamation, "LastRow()"
Resume ErrExit
End Function
Function LastCol(ws As Object) As Long
Dim rLastCell As Object
On Error GoTo ErrHan
Set rLastCell = ws.Cells.Find("*", ws.Cells(1, 1), , , xlByColumns, _
xlPrevious)
LastCol = rLastCell.Column
ErrExit:
Exit Function
ErrHan:
MsgBox "Error " & Err.Number & ": " & Err.Description, _
vbExclamation, "LastRow()"
Resume ErrExit
End Function
Public Sub FindTrueUsedRange(RowLast As Long, ColLast As Long)
Application.EnableEvents = False
Application.ScreenUpdating = False
RowLast = 0
ColLast = 0
ActiveSheet.UsedRange.Select
Cells(1, 1).Activate
Selection.End(xlDown).Select
Selection.End(xlDown).Select
On Error GoTo -1: On Error GoTo Quit
Cells.Find(What:="*", LookIn:=xlFormulas, LookAt:=xlWhole, SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Activate
On Error GoTo -1: On Error GoTo 0
RowLast = Selection.Row
Cells(1, 1).Activate
Selection.End(xlToRight).Select
Selection.End(xlToRight).Select
Cells.Find(What:="*", LookIn:=xlFormulas, LookAt:=xlWhole, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Activate
ColLast = Selection.Column
Quit:
Application.ScreenUpdating = True
Application.EnableEvents = True
On Error GoTo -1: On Error GoTo 0
End Sub
This function returns the actual used range to the lower right limit. It returns "Nothing" if the sheet is empty.
'2020-01-26
Function fUsedRange() As Range
Dim lngLastRow As Long
Dim lngLastCol As Long
Dim rngLastCell As Range
On Error Resume Next
Set rngLastCell = ActiveSheet.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious)
If rngLastCell Is Nothing Then 'look for data backwards in rows
Set fUsedRange = Nothing
Exit Function
Else
lngLastRow = rngLastCell.Row
End If
Set rngLastCell = ActiveSheet.Cells.Find("*", searchorder:=xlByColumns, searchdirection:=xlPrevious)
If rngLastCell Is Nothing Then 'look for data backwards in columns
Set fUsedRange = Nothing
Exit Function
Else
lngLastCol = rngLastCell.Column
End If
Set fUsedRange = ActiveSheet.Range(Cells(1, 1), Cells(lngLastRow, lngLastCol)) 'set up range
End Function
I use the following vba code to determine the entire used rows range for the worksheet to then shorten the selected range of a column:
Set rUsedRowRange = Selection.Worksheet.UsedRange.Columns( _
Selection.Column - Selection.Worksheet.UsedRange.Column + 1)
Also works the other way around:
Set rUsedColumnRange = Selection.Worksheet.UsedRange.Rows( _
Selection.Row - Selection.Worksheet.UsedRange.Row + 1)
This function gives all 4 limits of the used range:
Function FindUsedRangeLimits()
Set Sheet = ActiveSheet
Sheet.UsedRange.Select
' Display the range's rows and columns.
row_min = Sheet.UsedRange.Row
row_max = row_min + Sheet.UsedRange.Rows.Count - 1
col_min = Sheet.UsedRange.Column
col_max = col_min + Sheet.UsedRange.Columns.Count - 1
MsgBox "Rows " & row_min & " - " & row_max & vbCrLf & _
"Columns: " & col_min & " - " & col_max
LastCellBeforeBlankInColumn = True
End Function
Timings on Excel 2013 fairly slow machine with a big bad used range million rows:
26ms Cells.Find xlPrevious method (as above)
0.4ms Sheet.UsedRange (just call it)
0.14ms Counta binary search + 0.4ms Used Range to start search (12 CountA calls)
So the Find xlPrevious is quite slow if that is of concern.
The CountA binary search approach is to first do a Used Range. Then chop the range in half and see if there are any non-empty cells in the bottom half, and then halve again as needed. It is tricky to get right.
Here's another one. It looks for the first and last non empty cell and builds are range from those. This also handles cases where your data is not rectangular and does not start in A1. Furthermore it handles merged cells as well, which .Find skips when executed from a macro, used on .Cells on a worksheet.
Function getUsedRange(ByRef sheet As Worksheet) As Range
' finds used range by looking for non empty cells
' works around bug in .Find that skips merged cells
' by starting at with the UsedRange (that may be too big)
' credit to https://contexturesblog.com/archives/2012/03/01/select-actual-used-range-in-excel-sheet/
' for the .Find commands
Dim excelsUsedRange As Range
Dim lastRow As Long
Dim lastCol As Long
Dim lastCell As Range
Dim firstRow As Long
Dim firstCol As Long
Dim firstCell As Range
Set excelsUsedRange = ActiveSheet.UsedRange
lastRow = excelsUsedRange.Find(What:="*", _
LookIn:=xlValues, SearchOrder:=xlRows, _
SearchDirection:=xlPrevious).Row
lastCol = excelsUsedRange.Find(What:="*", _
LookIn:=xlValues, SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious).Column
Set lastCell = sheet.Cells(lastRow, lastCol)
firstRow = excelsUsedRange.Find(What:="*", After:=lastCell, _
LookIn:=xlValues, SearchOrder:=xlRows, _
SearchDirection:=xlNext).Row
firstCol = excelsUsedRange.Find(What:="*", After:=lastCell, _
LookIn:=xlValues, SearchOrder:=xlByColumns, _
SearchDirection:=xlNext).Row
Set firstCell = sheet.Cells(firstRow, firstCol)
Set getUsedRange = sheet.Range(firstCell, lastCell)
End Function
This is a different approach to the other answers, which will give you all the regions with data - a Region is something enclosed by an empty row and column and or the the edge of the worksheet. Basically it gives all the rectangles of data:
Public Function ContentRange(ByVal ws As Worksheet) As Range
'First, identify any cells with data, whose neighbourhood we will inspect
' to identify contiguous regions of content
'For efficiency, restrict our search to only the UsedRange
' NB. This may be pointless if .SpecialCells does this internally already, it probably does...
With ws.UsedRange 'includes data and cells that have been formatted
Dim cellsWithContent As Range
On Error Resume Next '.specialCells will error if nothing found, we can ignore it though
Set cellsWithContent = .SpecialCells(xlCellTypeConstants)
Set cellsWithContent = Union(cellsWithContent, .SpecialCells(xlCellTypeFormulas))
On Error GoTo 0
End With
'Early exit; return Nothing if there is no Data
If cellsWithContent Is Nothing Then Exit Function
'Next, loop over all the content cells and group their currentRegions
' This allows us to include some blank cells which are interspersed amongst the data
' It is faster to loop over areas rather than cell by cell since we merge all the CurrentRegions either way
Dim item As Range
Dim usedRegions As Range
For Each item In cellsWithContent.Areas
'Debug.Print "adding: "; item.Address, item.CurrentRegion.Address
If usedRegions Is Nothing Then
Set usedRegions = item.CurrentRegion 'expands "item" to include any surrounding non-blank data
Else
Set usedRegions = Union(usedRegions, item.CurrentRegion)
End If
Next item
'Debug.Print cellsWithContent.Address; "->"; usedRegions.Address
Set ContentRange = usedRegions
End Function
Used like:
Debug.Print ContentRange(Sheet1).Address '$A$1:$F$22
Debug.Print ContentRange(Sheet2).Address '$A$1:$F$22,$N$5:$M$7
The result is a Range object containing 1 or more Areas, each of it which will represent a data/formula containing region on the sheet.
It is the same technique as clicking in all the cells in your sheet and pressing Ctrl+T, merging all those areas. I'm using it to find potential tables of data

Resources