This question is now answered elegantly, thanks to Chris Neilsen, see the answer below. It is the one I will use from now on. The solution reliably finds the last cell in
Based on @Gary's method, but optimised to work fast when the UsedRange
is Large but not reflective of the True Last Cell (as can happen when a cell on the extreames of a worksheet is inadvertently formatted)
It works by, starting with the UsedRange, counting cells in half the range and halving the referenced test range above or below the split point depending on the count result, and repeating until it reaches < 5 rows/columns, then uses a linear search from there.
Function TrueLastCell( _
ws As Excel.Worksheet, _
Optional lRealLastRow As Long, _
Optional lRealLastColumn As Long _
) As Range
Dim lrTo As Long, lcTo As Long, i As Long
Dim lrFrom As Long, lcFrom As Long
Dim wf As WorksheetFunction
Set wf = Application.WorksheetFunction
With ws.UsedRange
lrTo = .Rows.Count
lcTo = .Columns.Count
lrFrom = lrTo \ 2
Do While (lrTo - lrFrom) > 2
If wf.CountA(.Rows(lrFrom & ":" & lrTo)) = 0 Then
lrTo = lrFrom - 1
lrFrom = lrFrom \ 2
Else
lrFrom = (lrTo + lrFrom) \ 2
End If
Loop
If wf.CountA(.Rows(lrFrom & ":" & lrTo)) = 0 Then
lrTo = lrFrom - 1
Else
For i = lrTo To lrFrom Step -1
If wf.CountA(.Rows(i)) <> 0 Then
Exit For
End If
Next i
lrTo = i
End If
lcFrom = lcTo \ 2
Do While (lcTo - lcFrom) > 2
If wf.CountA(Range(.Columns(lcFrom), .Columns(lcTo))) = 0 Then
lcTo = lcFrom - 1
lcFrom = lcFrom \ 2
Else
lcFrom = (lcTo + lcFrom) \ 2
End If
Loop
If wf.CountA(Range(.Columns(lcFrom), .Columns(lcTo))) = 0 Then
lcTo = lcFrom - 1
Else
For i = lcTo To 1 Step -1
If wf.CountA(.Columns(i)) <> 0 Then
Exit For
End If
Next i
lcTo = i
End If
Set TrueLastCell = .Cells(lrTo, lcTo)
lRealLastRow = lrTo + .Row - 1
lRealLastColumn = lcTo + .Column - 1
End With
End Function
On my hardware it runs in about 2ms on a sheet with UsedRange
extending to the sheet limits and True Last Cell at F5
, and 0.1ms when UsedRange reflects the True Last Cell at F5
Edit: slightly more optimised search
Great question.
As you note, Find
failes with AutoFilter. As an alternative to looping through the filters, or the range loop used by another answer you could
AutoFilter
Find
routine which caters to hidden cellsSo something lke this:
Sub GetRange()
'by Brettdj, http://stackoverflow.com/questions/8283797/return-a-range-from-a1-to-the-true-last-used-cell
Dim rng1 As Range
Dim rng2 As Range
Dim rng3 As Range
Dim ws As Worksheet
With Application
.EnableEvents = False
.ScreenUpdating = False
End With
ActiveSheet.Copy
Set ws = ActiveSheet
With ws
.AutoFilterMode = False
Set rng1 = ws.Cells.Find("*", ws.[a1], xlFormulas, , xlByRows, xlPrevious)
Set rng2 = ws.Cells.Find("*", ws.[a1], xlFormulas, xlPart, xlByColumns, xlPrevious)
If Not rng1 Is Nothing Then
Set rng3 = Range([a1], Cells(rng1.Row, rng2.Column))
MsgBox "Range is " & rng3.Address(0, 0)
Debug.Print "Brettdj's GetRange gives: Range is " & rng3.Address(0, 0) 'added for this test by ND
'if you need to actual select the range (which is rare in VBA)
Application.GoTo rng3
Else
MsgBox "sheet is blank", vbCritical
End If
.Parent.Close False
End With
With Application
.EnableEvents = True
.ScreenUpdating = True
End With
End Sub
I think you can utilize the .UsedRange
property from the Worksheet
object. Try below:
Option Explicit
Function GetTrueLastCell(WS As Worksheet) As Range
With WS
If .UsedRange.Count = 1 Then
Set GetTrueLastCell = .UsedRange
Else
Set GetTrueLastCell = .Range(Split(.UsedRange.Address, ":")(1))
End If
End With
End Function
Best way I know to find "true Last Cell" is to use 2 steps:
UsedRange
(i.e. UsedRange.Cells.CountLarge
)CountA
(i.e. WorksheetFunction.CountA(Range)
), as it is fast, and works with Hidden / AutoFiltered / Grouped ranges.This takes some time, so I've written an optimized code for the second step. Then I found @Chris' code edited on Nov 30, 2019, and it looked similar, though I was wondering why so different. I compared (...did my best to do apple v apple), and was surprised by the results.
If my tests are reliable, then all what matters is how many searches you do with CountA
. I call it cycle - it is actually the number of CountA
functions!
My routine does up to 34 cycles, and @Chris' routine seems to do up to 32..80+ cycles. His code seems to test the same ranges repeatedly.
Please have a look at the test table Link, see my test results in VBA notes, and watch Immediate for your live results. You may test with any content, or even use an ActiveSheet in your own WorkBook. Play with parameters in VBA at "==== PARAMETERS TO BE CHANGED ====". You may zoom to 10%-15% to see painted cells showing the search ranges for each cycle. That's where the number of cycles becomes visible.
Note: I have not found any side-effects or errors with this so far. I avoid using Range.Find
, and changing its parameters behind the scenes. Some users will learn it the hard way... - like I did, when I then replaced text in the entire workbook, just to find it out days later.
Note2: This is my first post, please excuse possible glitches here.
Function GetLastSheetCellRng(ws As Excel.Worksheet) As Range
'Returns the [Range] of last used cell of the specified [Worksheet], located in the cross-section of the bottom row and right column with non-empty cells
Dim wf As Excel.WorksheetFunction: Set wf = Application.WorksheetFunction
Dim Xfound&, Yfound&, Xfirst&, Yfirst&, Xfrom&, Yfrom&, Xto&, Yto As Long
With ws
'1. step: UsedRange last cell
Set GetLastSheetCellRng = .UsedRange.Cells(.UsedRange.Cells.CountLarge) 'Getting UsedRange last cell
Yfound = GetLastSheetCellRng.Row: Xfound = GetLastSheetCellRng.Column
'2. step: Check non-empty cells in UsedRange last cell row & column
'If not found, then search up for last non-empty row, and search left for last non-empty column
If (wf.CountA(.Rows(Yfound)) = 0) And (Yfound > 1) Then
Yto = Yfound
Yfrom = Yto \ 2
Yfirst = 0
Do
If wf.CountA(.Range(.Rows(Yfrom), .Rows(Yto))) <> 0 Then
Yfirst = Yfrom
Yfrom = (Yfirst + Yto + 0.5) \ 2
Else
Yto = Yfrom - 1
Yfrom = (Yfrom + Yfirst) \ 2
End If
Loop Until Yfirst = Yfrom
If Yfirst = 0 Then
Yfound = 1 'If no cell found, then 1st row returned
Else
Yfound = Yfirst
End If
End If
If (wf.CountA(.Columns(Xfound)) = 0) And (Xfound > 1) Then
Xto = Xfound
Xfrom = Xto \ 2
Xfirst = 0
Do
If wf.CountA(.Range(.Columns(Xfrom), .Columns(Xto))) <> 0 Then
Xfirst = Xfrom
Xfrom = (Xfirst + Xto + 0.5) \ 2
Else
Xto = Xfrom - 1
Xfrom = (Xfrom + Xfirst) \ 2
End If
Loop Until Xfirst = Xfrom
If Xfirst = 0 Then
Xfound = 1 'If no cell found, then 1st column returned
Else
Xfound = Xfirst
End If
End If
Set GetLastSheetCellRng = .Cells(Yfound, Xfound)
End With
End Function
UsedRange
may be erroneous, (it may be too large), but we can start with its outer limits and work inwards:
Sub TrueLastCell()
Dim lr As Long, lc As Long, i As Long
Dim wf As WorksheetFunction
Set wf = Application.WorksheetFunction
ActiveSheet.UsedRange
With ActiveSheet.UsedRange
lr = .Rows.Count + .Row - 1
lc = .Columns.Count + .Column - 1
End With
For i = lr To 1 Step -1
If wf.CountA(Rows(i)) <> 0 Then
Exit For
End If
Next i
For i = lc To 1 Step -1
If wf.CountA(Cells(lr, i)) <> 0 Then
MsgBox "The TRUE last cell is " & Cells(lr, i).Address(0, 0)
Exit Sub
End If
Next i
End Sub