VBA, find last used column in the whole sheet - excel

I googled a lot and found a lot of different solutions, but I need to improve the one I'm using now.
I want to find the last used column in the sheet using the find method not to consider the deleted cells.
All I want is to get the last column used, including the one in the row of the starting cell. In the image below if I use my code it will give last column = 4, because in the 2nd row data stops at column 4. Why isn't it giving 5 (header column) as result?
Thank you!!
With ActiveSheet
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
findlastcol = .Cells.Find(What:="*", _
After:=.Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column
Else
findlastcol = 1
End If
End With
Example Table screenshot
+---------+---------+---------+---------+---------+
| Header1 | Header2 | Header3 | Header4 | Header5 |
+---------+---------+---------+---------+---------+
| Data | Data | Data | Data | |
+---------+---------+---------+---------+---------+

AutoFilter Kicks the Find Method
The Find method with xlFormulas is pretty much 'bullet proof', unless there is a filter involved which is happening in your case.
The following example shows how to do it by turning the AutoFilter off, which is not quite what one wants. It also shows how there were three not needed arguments. Additionally it is a different approach which does not need CountA.
A proper solution would be to copy the current filter into a Filter object and then apply it later back. Here is an example of how to do it.
The Code
Sub testBulletProof()
Dim LastCol As Long
Dim rng As Range
With ActiveSheet
If .AutoFilterMode Then
.AutoFilterMode = False
End If
Set rng = .Cells.Find(What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious)
End With
If Not rng Is Nothing Then
LastCol = rng.Column
Else
LastCol = 1
End If
Debug.Print LastCol
End Sub
Since you might know the row where the headers are and the data will not have more columns then the header does, you could use this:
The Code
Sub testFindInRow()
Dim LastCol As Long
Dim rng As Range
With ActiveSheet
Set rng = .Rows(1).Find(What:="*", _
LookIn:=xlFormulas, _
SearchDirection:=xlPrevious)
End With
If Not rng Is Nothing Then
LastCol = rng.Column
Else
LastCol = 1
End If
Debug.Print LastCol
End Sub

You could try the following code:
Sub FindLastColumn()
Dim iLastCol As Integer
ActiveSheet.UsedRange 'Refreshing used range (may need to save wb also)
iLastCol = ActiveSheet.UsedRange.Columns(ActiveSheet.UsedRange.Columns.Count).Column
End Sub

Alternatively, you can try:
findlastcol = Selection.SpecialCells(xlCellTypeLastCell).Column

Related

How to Select All or (Ctrl + A) dynamic table doesn't matter how big it is

This is my sample data.
And this is the VBA code produced with macro.
Sub Macro1()
'
' Macro1 Macro
'
'
Range("A1:C3").Select
Application.CutCopyMode = False
ActiveSheet.ListObjects.Add(xlSrcRange, Range("$A$1:$C$3"), , xlYes).Name = _
"Table1"
Range("Table1[#All]").Select
ActiveSheet.ListObjects("Table1").TableStyle = "TableStyleMedium9"
End Sub
How do I get the macro code?
Developer > Record Macro
Select all (ctrl + A) inside any cells within A1:C3
Home > Format as Table
Output
The problem is my data is dynamic and not necessarily stay at A1:C3.
It could be bigger or smaller.
E.g., let say I've bigger within range A1:C4 in different Ms Excel file.
The code above won't select all, instead it selects only A1:C3.
Last row A4:C4 won't be affected with this code.
How do I change this doesn't matter how big the table is?
Solution 1
You can use Range("A1").CurrentRegion to get the area of continous data (which is the same as Ctrl + A):
Option Explicit
Public Sub SelectCurrentRegion()
Dim MyData As Range
Set MyData = Worksheets("Sheet1").Range("A1").CurrentRegion
'don't use .select this is just for illustrating
MyData.Select
End Sub
Note that the number in cell D6 is not vertically nor horizontally connected with the other data. Therefore it is not selected by Ctrl + A or .CurrentRegion.
Solution 2
Or Worksheets("Sheet1").UsedRange to get the area of all data:
Option Explicit
Public Sub SelectCurrentRegion()
Dim MyData As Range
Set MyData = Worksheets("Sheet1").UsedRange
'don't use .select this is just for illustrating
MyData.Select
End Sub
You might benefit from reading
How to avoid using Select in Excel VBA.
#Pᴇʜ has already given you 2 solutions. Here is Solution 3. I would prefer finding last row and column over UsedRange and then construct the range. I have explained here why I do not prefer using UsedRange
Solution 3
Find the last row and last column and then create your range
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim LastRow As Long
Dim LastColumn As Long
Dim rng As Range
'~~> Change this to relevant sheet
Set ws = Sheet1
With ws
LastRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
LastColumn = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column
Set rng = .Range(.Cells(1, 1), .Cells(LastRow, LastColumn))
Debug.Print rng.Address
End With
End Sub
If you are using Excel tables then you can use DataBodyRange or Range Properties to select what you need to select.
Dim lstObj As ListObject
For Each lstObj In ActiveSheet.ListObjects
lstObj.DataBodyRange.Select ' Will select only data without headers
lstObj.Range.Select ' Will select complete table
Next lstObj

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

Using Range.Find in VBA to find only the previous value x?

I am trying to use the Range.Find method in VBA to locate the closest previous row number that has a "true" value.
For example, in Column X, there will be a "true" value (row 35), 10 rows with "false," and then "true" again (row 46).
When I get to row 46 in my loop, I need to do a range.find and return row 35.
The code I am using is this:
Worksheets("Data").Cells.Find(True, searchorder:=xlByColumns, searchdirection:=xlNext).Row
What is happening is that I am only finding either the very first "true" value (in this case, row 2), or the very last "true" value (row 24,xxx), as I vary search direction.
What can I do to find only the previous-most "true" value?
You can find the previous row with True by using the After argument in the Find method combined with xlPrevious as the SearchDirection. I have updated the code to add it into a loop, based on your comments.
Since you posted your code, I have edited my answer into your code.
Sub Main()
Dim iCurRow As Long
Dim iCounter As Long
Dim iLastRow As Long
Dim iTempRow As Long
Dim iPreviousRow As Long
Dim iChangeCol As Long
Dim ws As Worksheet
Set ws = Worksheets("Data")
With ws
iChangeCol = .Cells.Find(what:="Change Over?", searchorder:=xlByColumns, searchdirection:=xlNext).Column
iLastRow = .Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row
iPreviousRow = 2
For iCounter = 3 To iLastRow
If .Cells(iCounter, iChangeCol).Value = True Then
iTempRow = .Cells.Find(what:=True, After:=.Cells(iCounter, iChangeCol), searchorder:=xlByColumns, searchdirection:=xlPrevious).Row
iPreviousRow = iTempRow
End If
Next iCounter
End With
End Sub
This short snippet uses both the Range.Find method and Range.FindNext method to cycle through all matching cells in column X.
Sub rings_true()
Dim fnd As Range
With Worksheets("Sheet1") `<~~ set this worksheet reference properly
With .Columns(24)
Set fnd = .Find(What:="TRUE", after:=.Cells(.Rows.Count), _
LookIn:=xlValues, LookAt:=xlWhole, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False)
Do While Not fnd Is Nothing
If MsgBox("Currently at " & fnd.Address(0, 0) & Chr(10) & "exit now...?", vbYesNo + vbQuestion) = vbYes Then
Exit Do
Else
Set fnd = .FindNext(after:=fnd)
End If
Loop
End With
End With
End Sub
The current cell address is reported through a MsgBox function. The same MsgBox offers the user the opportunity to break the loop.
Additional error control might include confirming at least one matching value in column X before entering into the loop.
There are multiple arguments to put into the Find method, regarding what you told us, I suggest that you use :
After:=.Cells(.Rows.Count, 1) to start from the bottom of the column
LookIn:=xlValues
LookAt:=xlWhole
SearchOrder:=xlByRows to look row by row (instead of column by column)
SearchDirection:=xlPrevious to look "back", from bottom to top
MatchCase:=False
SearchFormat:=False
And furthermore, you can use the .Find method into a specific range, so rather than Worksheets("Data").Cells.Find(..., you should use Worksheets("Data").Range("X:X").Find(... to look only in the column X.
Here is your amended code :
Sub test_ilarson007()
Dim FirstAddress As String, PreviousMatch As Range, cF As Range
Worksheets("Data").Activate
With Worksheets("Data").Range("X:X")
'First, define properly the Find method
Set cF = .Find(What:=True, _
After:=ActiveCell, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False, _
SearchFormat:=False)
'If there is a result,
If Not cF Is Nothing Then
FirstAddress = cF.Address
MsgBox "The row containing the previous 'True' in Column X is : " & cF.Row
'keep looking with FindNext method : Not usefull for your example
Do
Set PreviousMatch = cF
Set cF = .FindNext(cF)
'-------------------------------------------------------------
'----Place instructions to execute on the matched cell/row/...
'First match (i.e. Row 46 in your example)
MsgBox PreviousMatch.Row 'Should display 46 (then 35, then ??)
'Second match (i.e. Row 35 in your example)
MsgBox cf.Row 'Should display 35 (then ??, then ??)
'-------------------------------------------------------------
'Look until you find again the first result
Loop While Not cF Is Nothing And cF.Address <> FirstAddress
End If
End With
End Sub

Set VBA Range with Variable End

I'm kind of new to VBA and am struggling to understand some of the syntax.
I have a range from a3:c13, for example, and I'd like to set it as a variable so I can pass it to vlookup later as a the table array. However, the range is defined by user input in terms of its size. It will always start in A3, it will always include columns A:C, but I don't know how far down it would go. In that case, I think I'd set it as:
With range("a3")
table_array = range(.cells(0,0), .End(xlDown).End(xlToRight)).Select
End With
However, that doesn't seem to work. I get a runtime error:
Run-time Error '1004': Method '_Default' of object 'Range' failed.
Assuming cols A, B, and C have the same number of rows:
Sub Macro1()
Set r = Range("A3")
Set table_array = Range(r, r.End(xlDown)).Resize(, 3)
End Sub
You can find the last row in Col A:C and then construct your range?
Sub Sample()
Dim ws As Worksheet
Dim LastRow As Long
Dim Rng As Range
'~~> Change this to the relevant sheet
Set ws = ThisWorkbook.Sheets("Sheet1")
With ws
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
LastRow = .Range("A:C").Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
LastRow = 1
End If
If Not LastRow < 3 Then
Set Rng = .Range("A3:C" & LastRow)
Debug.Print Rng.Address
Else
MsgBox "No Data found beyond A3"
End If
End With
End Sub

How do I find the last column with data?

I've found this method for finding the last data containing row in a sheet:
ws.Range("A65536").End(xlUp).row
Is there a similar method for finding the last data containing column in a sheet?
Lots of ways to do this. The most reliable is find.
Dim rLastCell As Range
Set rLastCell = ws.Cells.Find(What:="*", After:=ws.Cells(1, 1), LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious, MatchCase:=False)
MsgBox ("The last used column is: " & rLastCell.Column)
If you want to find the last column used in a particular row you can use:
Dim lColumn As Long
lColumn = ws.Cells(1, Columns.Count).End(xlToLeft).Column
Using used range (less reliable):
Dim lColumn As Long
lColumn = ws.UsedRange.Columns.Count
Using used range wont work if you have no data in column A. See here for another issue with used range:
See Here regarding resetting used range.
I know this is old, but I've tested this in many ways and it hasn't let me down yet, unless someone can tell me otherwise.
Row number
Row = ws.Cells.Find(What:="*", After:=[A1] , SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
Column Letter
ColumnLetter = Split(ws.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Cells.Address(1, 0), "$")(0)
Column Number
ColumnNumber = ws.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
Try using the code after you active the sheet:
Dim J as integer
J = ActiveSheet.UsedRange.SpecialCells(xlCellTypeLastCell).Row
If you use Cells.SpecialCells(xlCellTypeLastCell).Row only, the problem will be that the xlCellTypeLastCell information will not be updated unless one do a "Save file" action. But use UsedRange will always update the information in realtime.
I think we can modify the UsedRange code from #Readify's answer above to get the last used column even if the starting columns are blank or not.
So this lColumn = ws.UsedRange.Columns.Count modified to
this lColumn = ws.UsedRange.Column + ws.UsedRange.Columns.Count - 1 will give reliable results always
?Sheet1.UsedRange.Column + Sheet1.UsedRange.Columns.Count - 1
Above line Yields 9 in the immediate window.
Here's something which might be useful. Selecting the entire column based on a row containing data, in this case i am using 5th row:
Dim lColumn As Long
lColumn = ActiveSheet.Cells(5, Columns.Count).End(xlToLeft).Column
MsgBox ("The last used column is: " & lColumn)
I have been using #Reafidy method/answer for a long time, but today I ran into an issue with the top row being merged cell from A1-->N1 and my function returning the "Last Column" as 1 not 14.
Here is my modified function now account for possibly merged cells:
Public Function Get_lRow(WS As Worksheet) As Integer
On Error Resume Next
If Not IsWorksheetEmpty(WS) Then
Get_lRow = WS.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
Dim Cell As Range
For Each Cell In WS.UsedRange
If Cell.MergeCells Then
With Cell.MergeArea
If .Cells(.Cells.Count).Row > Get_lRow Then Get_lRow = .Cells(.Cells.Count).Row
End With
End If
Next Cell
Else
Get_lRow = 1
End If
End Function
Public Function Get_lCol(WS As Worksheet) As Integer
On Error Resume Next
If Not IsWorksheetEmpty(WS) Then
Get_lCol = WS.Cells.Find(What:="*", after:=[A1], SearchOrder:=xlByColumns, SearchDirection:=xlPrevious).Column
Dim Cell As Range
For Each Cell In WS.UsedRange
If Cell.MergeCells Then
With Cell.MergeArea
If .Cells(.Cells.Count).Column > Get_lCol Then Get_lCol = .Cells(.Cells.Count).Column
End With
End If
Next Cell
Else
Get_lCol = 1
End If
End Function
Here's a simple option if your data starts in the first row.
MsgBox "Last Row: " + CStr(Application.WorksheetFunction.CountA(ActiveSheet.Cells(1).EntireRow))
It just uses CountA to count the number of columns with data in the entire row.
This has all sorts of scenarios where it won't work, such as if you have multiple tables sharing the top row, but for a few quick & easy things it works perfect.

Resources