Preserve null strings when assigning to .Value with VBA - vba

(edit: added 2 more restrictions to solutions)
I have a table (listobject wise) in which I need to duplicate certain rows. I'm using SourceListRow.Range.Value2 = DestListRow.Range.Value2 for efficiency reasons (copying whole ranges in one go). All formula columns after the copied cell ranges auto-expand perfectly into the new rows and act upon the copied data. I'm using Excel 2010 here on Windows.
However, although I've already been using this sort of code for eons, I've only now stumbled upon an oddity when using Range.Value / Range.Value2: if you assign an empty string to it, the final cell value will not be an empty string, but it will be Empty. That is: the data isn't copied over faithfully, and the copy can differ from the source, especially if consecutive formulas use ISBLANK etc. on it. Therefore the same formula will have different results when working on the copy vs. the source.
See below test code. Open a new, blank, Excel workbook, go to VBA, add a new module and add the following code:
Sub Test()
ActiveSheet.Range("a1").Formula = "="""""
ActiveSheet.Range("b1").Formula = "=isblank(a1)"
ActiveSheet.Range("c1").Value2 = TypeName(ActiveSheet.Range("a1").Value2)
ActiveSheet.Range("a2").Value2 = ActiveSheet.Range("a1").Value2
ActiveSheet.Range("b2").Formula = "=isblank(a2)"
ActiveSheet.Range("c2").Value2 = TypeName(ActiveSheet.Range("a2").Value2)
ActiveSheet.Range("a3").Value2 = ""
ActiveSheet.Range("b3").Formula = "=isblank(a3)"
ActiveSheet.Range("c3").Value2 = TypeName(ActiveSheet.Range("a3").Value2)
ActiveSheet.Range("a4").Formula = ActiveSheet.Range("a1").Formula
ActiveSheet.Range("b4").Formula = "=isblank(a4)"
ActiveSheet.Range("c4").Value2 = TypeName(ActiveSheet.Range("a4").Value2)
Call ActiveSheet.Range("a1").Copy
Call ActiveSheet.Range("a5").PasteSpecial(xlPasteValues)
ActiveSheet.Range("b5").Formula = "=isblank(a5)"
ActiveSheet.Range("c5").Value2 = TypeName(ActiveSheet.Range("a5").Value2)
End Sub
Then run it and look at the sheet;
B1 tells FALSE (as it should - the cell is not empty), and C1 tells "String" (the cell value is indeed an empty string);
B2 tells TRUE, even though we just copied over A1's value as-is; C2 tells "Empty", while it should tell "String" if the value was copied over faithfully;
as a test, B3 tells TRUE, even though we just set it explicitly to an empty string; C3 again tells "Empty" to confirm the data mangling;
assigning Range.Formula to OtherRange.Formula works (the String data type is preserved in B4), but I do not want to copy formula's, I want to copy only the values!
as a test I mimic what the GUI would yield in A5, and sure enough, Copy/Paste-As-Values does preserve the is-an-empty-string state...
What to do?
using Range.Copy / Range.PasteSpecial(xlPasteValues) is unacceptable, performance-wise and clipboard-wise;
using .Formula is also not an option, because I want to copy the values;
I could iterate over all values in the array, testing if they're a null string and then setting those cell's formula to ="", but I want to copy whole multi-cell ranges in one go to gain efficiency here...
I can't use autofilters and such because my data lives in a table (a.k.a. ListObject); I could use the table's own autofilter, but people may have it in use already so I'd have to restore it afterwards which is too much of a kludge.
using Range.Find and Range.Replace is not an option either, since that changes the user's Find/Replace dialog settings.
All ideas appreciated!

SpecialCells(xlCellTypeBlank) and AutoFilter treat null strings differently. Is your source data filterable? If so, could you use a placeholder value and then change the cells to have .Formula = "=""""" after copying?
I took your sample code and generated the workbook, then added a row above it for a filter header. The below code would change all null strings to "ChangeMe"; you could then copy over the values and replace all instances of "ChangeMe" in your destination with "=""""" using Range.Replace.
Sub Test2()
ActiveSheet.Range("A2:A6").SpecialCells(xlCellTypeBlanks).Interior.Color = 255 'Just to prove that xlCellTypeBlanks only selects actual blanks
ActiveSheet.Range("A1:A6").AutoFilter Field:=1, Criteria1:="=" 'Shows both actual blanks and ZLS/null strings
ActiveSheet.Range("A1:A6").SpecialCells(xlCellTypeBlanks).EntireRow.Hidden = True 'Be able to use SpecialCells(xlCellTypeVisible) to reference all ZLS
ActiveSheet.Range("A2:A6").SpecialCells(xlCellTypeVisible).Value2 = "ChangeMe"
'Now unfilter and unhide your data, copy using .Value2, then .Replace "ChangeMe" with "="""""
End Sub
I think that accomplishes what you are trying to do - let me know if this is the right direction for a solution.
Edit: misinterpreted original question. Below old answer is incorrect.
While not quite as efficient as your original code, using .AutoFilter is much more efficient than looping through many cells. Furthermore, .AutoFilter treats zero-length strings and empty cells the same.
Something like this:
Sub CopyAndClearFakeBlanks()
Dim WSSource As Worksheet
Dim WSDest As Worksheet
Dim LRow As Long
Dim LPasteRow As Long
Set WSSource = Sheets("Source Data")
Set WSDest = Sheets("Paste Here")
LRow = WSSource.Range("A:A").Find(what:="*", searchdirection:=xlPrevious).Row 'Note that this will ignore blanks, so you may want to use a different method for finding your last row. Depends on how your users would need the data if there are trailing blanks.
On Error Resume Next
LPasteRow = 2 'Always need at least one row before the data for filtering properly; you can delete after if necessary
LPasteRow = WSDest.Range("A:A").Find(what:="*", searchdirection:=xlPrevious).Row + 1
WSDest.AutoFilterMode = False
On Error GoTo 0 'ofc use proper error handling in your actual code
WSDest.Range("A" & LPasteRow & ":A" & LPasteRow + LRow - 1).Value2 = WSSource.Range("A1:A" & LRow).Value2
WSDest.Range("A" & LPasteRow - 1 & ":A" & LPasteRow + LRow).AutoFilter field:=1, Criteria1:="=" 'Show blank or ZLS only
On Error Resume Next
WSDest.Range("A" & LPasteRow & ":A" & LPasteRow + LRow).SpecialCells(xlCellTypeVisible).Clear 'Turn them into true blanks
WSDest.ShowAllData
WSDest.AutoFilterMode = False
On Error GoTo 0
Set WSDest = Nothing
Set WSSource = Nothing
End Sub

Related

finding next empty cell in column - Run-time error 1004 [duplicate]

When I want to find the last used cell value, I use:
Dim LastRow As Long
LastRow = Range("E4:E48").End(xlDown).Row
Debug.Print LastRow
I'm getting the wrong output when I put a single element into a cell. But when I put more than one value into the cell, the output is correct.
What's the reason behind this?
NOTE: I intend to make this a "one stop post" where you can use the Correct way to find the last row. This will also cover the best practices to follow when finding the last row. And hence I will keep on updating it whenever I come across a new scenario/information.
Unreliable ways of finding the last row
Some of the most common ways of finding last row which are highly unreliable and hence should never be used.
UsedRange
xlDown
CountA
UsedRange should NEVER be used to find the last cell which has data. It is highly unreliable. Try this experiment.
Type something in cell A5. Now when you calculate the last row with any of the methods given below, it will give you 5. Now color the cell A10 red. If you now use the any of the below code, you will still get 5. If you use Usedrange.Rows.Count what do you get? It won't be 5.
Here is a scenario to show how UsedRange works.
xlDown is equally unreliable.
Consider this code
lastrow = Range("A1").End(xlDown).Row
What would happen if there was only one cell (A1) which had data? You will end up reaching the last row in the worksheet! It's like selecting cell A1 and then pressing End key and then pressing Down Arrow key. This will also give you unreliable results if there are blank cells in a range.
CountA is also unreliable because it will give you incorrect result if there are blank cells in between.
And hence one should avoid the use of UsedRange, xlDown and CountA to find the last cell.
Find Last Row in a Column
To find the last Row in Col E use this
With Sheets("Sheet1")
LastRow = .Range("E" & .Rows.Count).End(xlUp).Row
End With
If you notice that we have a . before Rows.Count. We often chose to ignore that. See THIS question on the possible error that you may get. I always advise using . before Rows.Count and Columns.Count. That question is a classic scenario where the code will fail because the Rows.Count returns 65536 for Excel 2003 and earlier and 1048576 for Excel 2007 and later. Similarly Columns.Count returns 256 and 16384, respectively.
The above fact that Excel 2007+ has 1048576 rows also emphasizes on the fact that we should always declare the variable which will hold the row value as Long instead of Integer else you will get an Overflow error.
Note that this approach will skip any hidden rows. Looking back at my screenshot above for column A, if row 8 were hidden, this approach would return 5 instead of 8.
Find Last Row in a Sheet
To find the Effective last row in the sheet, use this. Notice the use of Application.WorksheetFunction.CountA(.Cells). This is required because if there are no cells with data in the worksheet then .Find will give you Run Time Error 91: Object Variable or With block variable not set
With Sheets("Sheet1")
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
lastrow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
lastrow = 1
End If
End With
Find Last Row in a Table (ListObject)
The same principles apply, for example to get the last row in the third column of a table:
Sub FindLastRowInExcelTableColAandB()
Dim lastRow As Long
Dim ws As Worksheet, tbl as ListObject
Set ws = Sheets("Sheet1") 'Modify as needed
'Assuming the name of the table is "Table1", modify as needed
Set tbl = ws.ListObjects("Table1")
With tbl.ListColumns(3).Range
lastrow = .Find(What:="*", _
After:=.Cells(1), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
End With
End Sub
Note: this answer was motivated by this comment. The purpose of UsedRange is different from what is mentioned in the answer above.
As to the correct way of finding the last used cell, one has first to decide what is considered used, and then select a suitable method. I conceive at least three meanings:
Used = non-blank, i.e., having data.
Used = "... in use, meaning the section that contains data or formatting."
As per official documentation, this is the criterion used by Excel at the time of saving. See also this official documentation.
If one is not aware of this, the criterion may produce unexpected results, but it may also be intentionally exploited (less often, surely), e.g., to highlight or print specific regions, which may eventually have no data.
And, of course, it is desirable as a criterion for the range to use when saving a workbook, lest losing part of one's work.
Used = "... in use, meaning the section that contains data or formatting" or conditional formatting.
Same as 2., but also including cells that are the target for any Conditional Formatting rule.
How to find the last used cell depends on what you want (your criterion).
For criterion 1, I suggest reading this answer.
Note that UsedRange is cited as unreliable. I think that is misleading (i.e., "unfair" to UsedRange), as UsedRange is simply not meant to report the last cell containing data. So it should not be used in this case, as indicated in that answer. See also this comment.
For criterion 2, UsedRange is the most reliable option, as compared to other options also designed for this use. It even makes it unnecessary to save a workbook to make sure that the last cell is updated.
Ctrl+End will go to a wrong cell prior to saving
(“The last cell is not reset until you save the worksheet”, from
http://msdn.microsoft.com/en-us/library/aa139976%28v=office.10%29.aspx.
It is an old reference, but in this respect valid).
For criterion 3, I do not know any built-in method.
Criterion 2 does not account for Conditional Formatting. One may have formatted cells, based on formulas, which are not detected by UsedRange or Ctrl+End.
In the figure, the last cell is B3, since formatting was applied explicitly to it. Cells B6:D7 have a format derived from a Conditional Formatting rule, and this is not detected even by UsedRange.
Accounting for this would require some VBA programming.
As to your specific question:
What's the reason behind this?
Your code uses the first cell in your range E4:E48 as a trampoline, for jumping down with End(xlDown).
The "erroneous" output will obtain if there are no non-blank cells in your range other than perhaps the first. Then, you are leaping in the dark, i.e., down the worksheet
(you should note the difference between blank and empty string!).
Note that:
If your range contains non-contiguous non-blank cells, then it will also give a wrong result.
If there is only one non-blank cell, but it is not the first one, your code will still give you the correct result.
I created this one-stop function for determining the last row, column and cell, be it for data, formatted (grouped/commented/hidden) cells or conditional formatting.
Sub LastCellMsg()
Dim strResult As String
Dim lngDataRow As Long
Dim lngDataCol As Long
Dim strDataCell As String
Dim strDataFormatRow As String
Dim lngDataFormatCol As Long
Dim strDataFormatCell As String
Dim oFormatCond As FormatCondition
Dim lngTempRow As Long
Dim lngTempCol As Long
Dim lngCFRow As Long
Dim lngCFCol As Long
Dim strCFCell As String
Dim lngOverallRow As Long
Dim lngOverallCol As Long
Dim strOverallCell As String
With ActiveSheet
If .ListObjects.Count > 0 Then
MsgBox "Cannot return reliable results, as there is at least one table in the worksheet."
Exit Sub
End If
strResult = "Workbook name: " & .Parent.Name & vbCrLf
strResult = strResult & "Sheet name: " & .Name & vbCrLf
'DATA:
'last data row
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
lngDataRow = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
lngDataRow = 1
End If
'strResult = strResult & "Last data row: " & lngDataRow & vbCrLf
'last data column
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
lngDataCol = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Column
Else
lngDataCol = 1
End If
'strResult = strResult & "Last data column: " & lngDataCol & vbCrLf
'last data cell
strDataCell = Replace(Cells(lngDataRow, lngDataCol).Address, "$", vbNullString)
strResult = strResult & "Last data cell: " & strDataCell & vbCrLf
'FORMATS:
'last data/formatted/grouped/commented/hidden row
strDataFormatRow = StrReverse(Split(StrReverse(.UsedRange.Address), "$")(0))
'strResult = strResult & "Last data/formatted row: " & strDataFormatRow & vbCrLf
'last data/formatted/grouped/commented/hidden column
lngDataFormatCol = Range(StrReverse(Split(StrReverse(.UsedRange.Address), "$")(1)) & "1").Column
'strResult = strResult & "Last data/formatted column: " & lngDataFormatCol & vbCrLf
'last data/formatted/grouped/commented/hidden cell
strDataFormatCell = Replace(Cells(strDataFormatRow, lngDataFormatCol).Address, "$", vbNullString)
strResult = strResult & "Last data/formatted cell: " & strDataFormatCell & vbCrLf
'CONDITIONAL FORMATS:
For Each oFormatCond In .Cells.FormatConditions
'last conditionally-formatted row
lngTempRow = CLng(StrReverse(Split(StrReverse(oFormatCond.AppliesTo.Address), "$")(0)))
If lngTempRow > lngCFRow Then lngCFRow = lngTempRow
'last conditionally-formatted column
lngTempCol = Range(StrReverse(Split(StrReverse(oFormatCond.AppliesTo.Address), "$")(1)) & "1").Column
If lngTempCol > lngCFCol Then lngCFCol = lngTempCol
Next
'no results are returned for Conditional Format if there is no such
If lngCFRow <> 0 Then
'strResult = strResult & "Last cond-formatted row: " & lngCFRow & vbCrLf
'strResult = strResult & "Last cond-formatted column: " & lngCFCol & vbCrLf
'last conditionally-formatted cell
strCFCell = Replace(Cells(lngCFRow, lngCFCol).Address, "$", vbNullString)
strResult = strResult & "Last cond-formatted cell: " & strCFCell & vbCrLf
End If
'OVERALL:
lngOverallRow = Application.WorksheetFunction.Max(lngDataRow, strDataFormatRow, lngCFRow)
'strResult = strResult & "Last overall row: " & lngOverallRow & vbCrLf
lngOverallCol = Application.WorksheetFunction.Max(lngDataCol, lngDataFormatCol, lngCFCol)
'strResult = strResult & "Last overall column: " & lngOverallCol & vbCrLf
strOverallCell = Replace(.Cells(lngOverallRow, lngOverallCol).Address, "$", vbNullString)
strResult = strResult & "Last overall cell: " & strOverallCell & vbCrLf
MsgBox strResult
Debug.Print strResult
End With
End Sub
Results look like this:
For more detailed results, some lines in the code can be uncommented:
One limitation exists - if there are tables in the sheet, results can become unreliable, so I decided to avoid running the code in this case:
If .ListObjects.Count > 0 Then
MsgBox "Cannot return reliable results, as there is at least one table in the worksheet."
Exit Sub
End If
One important note to keep in mind when using the solution ...
LastRow = ws.Cells.Find(What:="*", After:=ws.range("a1"), SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
... is to ensure that your LastRow variable is of Long type:
Dim LastRow as Long
Otherwise you will end up getting OVERFLOW errors in certain situations in .XLSX workbooks
This is my encapsulated function that I drop in to various code uses.
Private Function FindLastRow(ws As Worksheet) As Long
' --------------------------------------------------------------------------------
' Find the last used Row on a Worksheet
' --------------------------------------------------------------------------------
If WorksheetFunction.CountA(ws.Cells) > 0 Then
' Search for any entry, by searching backwards by Rows.
FindLastRow = ws.Cells.Find(What:="*", After:=ws.range("a1"), SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
End If
End Function
Since the original question is about problems with finding the last cell, in this answer I will list the various ways you can get unexpected results; see my answer to "How can I find last row that contains data in the Excel sheet with a macro?" for my take on solving this.
I'll start by expanding on the answer by sancho.s and the comment by GlennFromIowa, adding even more detail:
[...] one has first to decide what is considered used. I see at least 6 meanings. Cell has:
1) data, i.e., a formula, possibly resulting in a blank value;
2) a value, i.e., a non-blank formula or constant;
3) formatting;
4) conditional formatting;
5) a shape (including Comment) overlapping the cell;
6) involvement in a Table (List Object).
Which combination do you want to test for? Some (such as Tables) may be more difficult to test for, and some may be rare (such as a shape outside of data range), but others may vary based on the situation (e.g., formulas with blank values).
Other things you might want to consider:
A) Can there be hidden rows (e.g. autofilter), blank cells or blank rows?
B) What kind of performance is acceptable?
C) Can the VBA macro affect the workbook or the application settings in any way?
With that in mind, let's see how the common ways of getting the "last cell" can produce unexpected results:
The .End(xlDown) code from the question will break most easily (e.g. with a single non-empty cell or when there are blank cells in between) for the reasons explained in the answer by Siddharth Rout here (search for "xlDown is equally unreliable.") 👎
Any solution based on Counting (CountA or Cells*.Count) or .CurrentRegion will also break in presence of blank cells or rows 👎
A solution involving .End(xlUp) to search backwards from the end of a column will, just as CTRL+UP, look for data (formulas producing a blank value are considered "data") in visible rows (so using it with autofilter enabled might produce incorrect results ⚠️). You have to take care to avoid the standard pitfalls (for details I'll again refer to the answer by Siddharth Rout here, look for the "Find Last Row in a Column" section), such as hard-coding the last row (Range("A65536").End(xlUp)) instead of relying on sht.Rows.Count.
.SpecialCells(xlLastCell) is equivalent to CTRL+END, returning the bottom-most and right-most cell of the "used range", so all caveats that apply to relying on the "used range", apply to this method as well. In addition, the "used range" is only reset when saving the workbook and when accessing worksheet.UsedRange, so xlLastCell might produce stale results⚠️ with unsaved modifications (e.g. after some rows were deleted). See the nearby answer by dotNET.
sht.UsedRange (described in detail in the answer by sancho.s here) considers both data and formatting (though not conditional formatting) and resets the "used range" of the worksheet, which may or may not be what you want.Note that a common mistake ️is to use .UsedRange.Rows.Count⚠️, which returns the number of rows in the used range, not the last row number (they will be different if the first few rows are blank), for details see newguy's answer to How can I find last row that contains data in the Excel sheet with a macro?
.Find allows you to find the last row with any data (including formulas) or a non-blank value in any column. You can choose whether you're interested in formulas or values, but the catch is that it resets the defaults in the Excel's Find dialog ️️⚠️, which can be highly confusing to your users. It also needs to be used carefully, see the answer by Siddharth Rout here (section "Find Last Row in a Sheet")
More explicit solutions that check individual Cells' in a loop are generally slower than re-using an Excel function (although can still be performant), but let you specify exactly what you want to find. See my solution based on UsedRange and VBA arrays to find the last cell with data in the given column -- it handles hidden rows, filters, blanks, does not modify the Find defaults and is quite performant.
Whatever solution you pick, be careful
to use Long instead of Integer to store the row numbers (to avoid getting Overflow with more than 65k rows) and
to always specify the worksheet you're working with (i.e. Dim ws As Worksheet ... ws.Range(...) instead of Range(...))
when using .Value (which is a Variant) avoid implicit casts like .Value <> "" as they will fail if the cell contains an error value.
I would add to the answer given by Siddarth Rout to say that the CountA call can be skipped by having Find return a Range object, instead of a row number, and then test the returned Range object to see if it is Nothing (blank worksheet).
Also, I would have my version of any LastRow procedure return a zero for a blank worksheet, then I can know it is blank.
I wonder that nobody has mentioned this, But the easiest way of getting the last used cell is:
Function GetLastCell(sh as Worksheet) As Range
GetLastCell = sh.Cells(1,1).SpecialCells(xlLastCell)
End Function
This essentially returns the same cell that you get by Ctrl + End after selecting Cell A1.
A word of caution: Excel keeps track of the most bottom-right cell that was ever used in a worksheet. So if for example you enter something in B3 and something else in H8 and then later on delete the contents of H8, pressing Ctrl + End will still take you to H8 cell. The above function will have the same behavior.
Updated at End of 2021
With Excel's new calculation engine and array functionality, and Filter Function, I believe this topic should now be far less contested and that the below options offer the best mix of speed, reliability, and simplicity (which has proven difficult to balance in the past as the numerous posts here illustrate).
Also, I'm defining last used as NOT blank as defined by the isBlank function.
Excel Formula
First, note that the Filter Function makes it much simpler to get a last cell using the below formulas for a specific row or column (in these case Column A or Row 1):
=MAX(FILTER(ROW(A:A),NOT(ISBLANK(A:A))))
=MAX(FILTER(COLUMN(1:1),NOT(ISBLANK(1:1))))
VBA Function For Last Row Specific Range
Using the above function we can convert it into a VBA function, yet make it even faster by limiting the range, while expanding its capabilities by doing multiple columns (thanks to Chris Neilsen for immediate feedback tweeking/suggestions). I also found massive speed improvement by scoping each column to only be a range with a row HIGHER than the previous last row.
Function FindLastRowInRange(someColumns As Range) As Long
Const zFx = "=MAX(FILTER(ROW(????),NOT(ISBLANK(????)),0))"
Dim tRng As Range, i As Long, tRow As Long, pRng As Range
With someColumns.Worksheet
Set tRng = Intersect(someColumns.EntireColumn, .UsedRange)
For i = 1 To tRng.Columns.Count
Set pRng = Intersect(tRng.Columns(i), _
Range(.Rows(FindLastRowInRange + 1), .Rows(.Rows.Count)))
If Not pRng Is Nothing Then
tRow = .Evaluate(Replace(zFx, "????", _
pRng.Address, 1, -1))
If tRow > FindLastRowInRange Then _
FindLastRowInRange = tRow
End If
Next i
End With
End Function
VBA Function For Last Row In Worksheet
To consider the entire worksheet (all columns), I would recommend using a different VBA formula that references the prior one, but is a Volatile Function. This ensures that the formula updates with any changes to a worksheet. Obviously, one could combine these two formulas, but I prefer to limit the usage of volatile functions.
Function FindLastRowInSheet(anywhereInSheet As Range) As Long
Application.Volatile
FindLastRowInSheet = FindLastRowInRange(anywhereInSheet.Worksheet.UsedRange)
End Function
Advantages Compared To Other Options
Allows for some or ALL rows/columns in worksheet without changing approach.
No possibility of missing hidden rows as is a risk with xlup
Ignores formatted/usedrange issues.
Does not interfere with user's Find settings.
Uses worksheet functionality which is faster than VBA calcs.
No counting cells (performance hog).
Hopefully this ends the debate, but if anyone finds weaknesses in this please share.
sub last_filled_cell()
msgbox range("A65536").end(xlup).row
end sub
Here, A65536 is the last cell in the Column A this code was tested on excel 2003.
However this question is seeking to find the last row using VBA, I think it would be good to include an array formula for worksheet function as this gets visited frequently:
{=ADDRESS(MATCH(INDEX(D:D,MAX(IF(D:D<>"",ROW(D:D)-ROW(D1)+1)),1),D:D,0),COLUMN(D:D))}
You need to enter the formula without brackets and then hit Shift + Ctrl + Enter to make it an array formula.
This will give you address of last used cell in the column D.
Thanks to pgsystemtester, this will give you the row number of last used cell:
{=MATCH(INDEX(D:D,MAX(IF(D:D<>"",ROW(D:D)-ROW(D1)+1)),1),D:D,0)}
I was looking for a way to mimic the CTRL+Shift+End, so dotNET solution is great, except with my Excel 2010 I need to add a set if I want to avoid an error:
Function GetLastCell(sh As Worksheet) As Range
Set GetLastCell = sh.Cells(1, 1).SpecialCells(xlLastCell)
End Function
and how to check this for yourself:
Sub test()
Dim ws As Worksheet, r As Range
Set ws = ActiveWorkbook.Sheets("Sheet1")
Set r = GetLastCell(ws)
MsgBox r.Column & "-" & r.Row
End Sub
Sub lastRow()
Dim i As Long
i = Cells(Rows.Count, 1).End(xlUp).Row
MsgBox i
End Sub
sub LastRow()
'Paste & for better understanding of the working use F8 Key to run the code .
dim WS as worksheet
dim i as long
set ws = thisworkbook("SheetName")
ws.activate
ws.range("a1").select
ws.range("a1048576").select
activecell.end(xlup).select
i= activecell.row
msgbox "My Last Row Is " & i
End sub
For the last 3+ years these are the functions that I am using for finding last row and last column per defined column(for row) and row(for column):
Last Column:
Function lastCol(Optional wsName As String, Optional rowToCheck As Long = 1) As Long
Dim ws As Worksheet
If wsName = vbNullString Then
Set ws = ActiveSheet
Else
Set ws = Worksheets(wsName)
End If
lastCol = ws.Cells(rowToCheck, ws.Columns.Count).End(xlToLeft).Column
End Function
Last Row:
Function lastRow(Optional wsName As String, Optional columnToCheck As Long = 1) As Long
Dim ws As Worksheet
If wsName = vbNullString Then
Set ws = ActiveSheet
Else
Set ws = Worksheets(wsName)
End If
lastRow = ws.Cells(ws.Rows.Count, columnToCheck).End(xlUp).Row
End Function
For the case of the OP, this is the way to get the last row in column E:
Debug.Print lastRow(columnToCheck:=Range("E4:E48").Column)
Last Row, counting empty rows with data:
Here we may use the well-known Excel formulas, which give us the last row of a worksheet in Excel, without involving VBA - =IFERROR(LOOKUP(2,1/(NOT(ISBLANK(A:A))),ROW(A:A)),0)
In order to put this in VBA and not to write anything in Excel, using the parameters for the latter functions, something like this could be in mind:
Public Function LastRowWithHidden(Optional wsName As String, Optional columnToCheck As Long = 1) As Long
Dim ws As Worksheet
If wsName = vbNullString Then
Set ws = ActiveSheet
Else
Set ws = Worksheets(wsName)
End If
Dim letters As String
letters = ColLettersGenerator(columnToCheck)
LastRowWithHidden = ws.Evaluate("=IFERROR(LOOKUP(2,1/(NOT(ISBLANK(" & letters & "))),ROW(" & letters & " )),0)")
End Function
Function ColLettersGenerator(col As Long) As String
Dim result As Variant
result = Split(Cells(1, col).Address(True, False), "$")
ColLettersGenerator = result(0) & ":" & result(0)
End Function
Last Row in a regular range or a Table (ListObject)
Finding the last row requires using different methods if the range is a regular range or table (List Object).
Finding the last row in tables requires specifying additional parameters (table name , the column relative position to the first tables column).
I created this universal function for last row, regardless of range type. Just give it any cell reference whatsoever and it will return the last row.
No hassle having to knew range characteristics, especially if your ranges are some times a regular range and sometimes a ListObject.
Using a regular range method on a table might return wrong results.
Sure you can plan ahead of time and use the right method each time, but why bother if you can utilizes a universal function ?
Sub RunMyLastRow()
Dim Result As Long
Result = MyLastRow(Worksheets(1).Range("A1"))
End Sub
Function MyLastRow(RefrenceRange As Range) As Long
Dim WS As Worksheet
Dim TableName As String
Dim ColNumber As Long
Dim LastRow As Long
Dim FirstColumnTable As Long
Dim ColNumberTable As Long
Set WS = RefrenceRange.Worksheet
TableName = GetTableName(RefrenceRange)
ColNumber = RefrenceRange.Column
''If the table (ListObject) does not start in column "A" we need to calculate the
''first Column table and how many Columns from its beginning the Column is located.
If TableName <> vbNullString Then
FirstColumnTable = WS.ListObjects(TableName).ListColumns(1).Range.Column
ColNumberTable = ColNumber - FirstColumnTable + 1
End If
If TableName = vbNullString Then
LastRow = WS.Cells(WS.Rows.Count, ColNumber).End(xlUp).Row
Else
LastRow = WS.ListObjects(TableName).ListColumns(ColNumberTable).Range.Find( _
What:="*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
End If
MyLastRow = LastRow
End Function
''Get Table Name by Cell Range
Function GetTableName(RefrenceRange As Range) As String
If RefrenceRange.ListObject Is Nothing Then
GetTableName = vbNullString
Else
GetTableName = RefrenceRange.ListObject.Name
End If
End Function

VBA code to clear just constants on destination sheet

I have filtered my cumulative sales from SalesMasterSheet to different sheets each named after each particular customer but I got stuck at trying to add excel formulas because my VBA code always clears content of the range of cells on sheetActive.
I have tried the special cell method so as to clear only constants but it doesn't work.
Any help would be appreciated.
below is my code:
Private Sub Worksheet_activate()
Dim i, LastRow
LastRow = Sheets("MasterSheet").Range("A" & Rows.Count).End(xlUp).Row
Sheets("ACCIMA").Range("A1:L500").ClearContents
For i = 2 To LastRow
If Sheets("MasterSheet").Cells(i, "C").Value = "ACCIMA" Then
Sheets("MasterSheet").Cells(i, "C").EntireRow.Copy _
Destination:=Sheets("ACCIMA").Range("A" & Rows.Count).End(xlUp).Offset(1)
End If
Next i
End Sub
So far what it does is copy entries with "ACCIMA" on column C from Mastersheet to sheet("ACCIMA"), but i would like to put a formula in sheet("ACCIMA") but because Sheets("ACCIMA").Range("A1:L500").ClearContents
all formulas clear once i make the sheet active.
Given the low amount of informations you give I sort of understood your problem this way:
If your code always clears content of the range of cells on the active sheet you should try to add the worksheet you are working on in your code, e.g.
cells(1,1).value = "test"
would turn into
worksheets("customer1").cells(1,1).value = "test"
After new informations:
Copy the content of your range "A1:K500", ignore the column "L" which has your formular and then add your formulars in column "L".
worksheets("customer1").cells(1,1).formula = "..."
Keep in mind, that your syntax for the formula changes in comparison with the synatx on the actual excel sheet.
e.g.:
=IF(A1="empty";"empty";"not empty")
Will turn into:
worksheets("customer1").cells(1,1).formula = "=IF(A1=""empty"",""empty"",""not empty"")"

How to add a Formula To Cell using VBA [duplicate]

This question already has answers here:
How do I put double quotes in a string in vba?
(5 answers)
Closed 1 year ago.
I am attempting to write some VBA which will add header text to 3 cells then fill a formula all the way down to the last row. I have written the below, which writes the headers no problems, but when it get's to my first .Formula it throws a
Application Defined or Object Defined error
What needs to be altered so that this macro will execute successfully? (The formulas were pulled directly from the formula in the cell, so I know they are valid formulas at least on the "front-end")
Function Gre()
Range("E2").Select
ActiveCell.FormulaR1C1 = "Under"
Range("F2").Select
ActiveCell.FormulaR1C1 = "Over"
Range("G2").Select
ActiveCell.FormulaR1C1 = "Result"
With Range("E2:E" & Cells(Rows.Count, "C").End(xlUp).Row)
.Formula = "=IF(C2<B2,B2-C2,"")"
End With
With Range("F2:F" & Cells(Rows.Count, "C").End(xlUp).Row)
.Formula = "=IF(C2>B2,C2-B2,0)"
End With
With Range("G2:G" & Cells(Rows.Count, "C").End(xlUp).Row)
.Formula = "=IF(F2>0,'Issue',"")"
End With
End Function
The problem is likely that you are escaping the quotes with the formula.
What you need is:
.Formula = "=IF(C2>B2,B2-C2,"""")"
for the first one, for example. The other quotes need to be doubled as well.
As a side-note, it would also be best to specify the sheet you are working on with something like:
Dim ws as worksheet
Set ws = Sheets("mySheet")
ws.Range("E2").FormulaR1C1 = "Under"
etc.
If you don't do this, you can sometimes have errors happen while running the code.
As suggested by OpiesDad, to minimize ambiguity, avoid ActiveCell and the like.
Using Select will also slow down performance a lot compared to assigning to cells directly.
I'm pretty sure you need to escape quotes in Excel formulas inside of VBA by doubling the quotes, so a normal empty string becomes """". You also have Issue in single quotes in a formula, which I'm pretty sure will error in Excel; that should be in escaped double quotes as well.
I'm having a hard time figuring out what Range("E2:E" & Cells(Rows.Count, "C").End(xlUp).Row) actually does, but it sounds like you want to select E2 to the last used row of the sheet. Avoid Rows.Count or just generally referring to the rows of a sheet, as that will go to row 10^31. Use Worksheet.UsedRange to get the range from the first row and column with content to the last row and column with content. This also includes empty strings and can be a bit tricky sometimes, but is usually better than dealing with thousands of extra rows.
Also,
You don't need to use With if your only enclosing one statement, although it won't cause any problems.
I would not mix use of Range.Formula and Range.FormulaR1C1 unless you have a reason to.
Function Gre()
Dim ws as Worksheet
Set ws = ActiveSheet
Dim used as Range
Set used = ws.UsedRange
Dim lastRow as Integer
lastRow = used.Row + used.Rows.Count - 1
ws.Range("E2").Formula = "Under"
ws.Range("F2").Formula = "Over"
ws.Range("G2").Formula = "Result"
ws.Range("E2:E" & lastRow).Formula = "IF(C2<B2, C2-B2, """")"
ws.Range("F2:F" & lastRow).Formula = "IF(C2<B2, C2-B2, 0)"
ws.Range("G2:G" & lastRow).Formula = "IF(F2>0, ""Issue"", """")"
End Function
The first issue is the selecting of cells. This requires the macro to select the cell, then determine the cell address. If you need to actually select a cell, use Application.ScreenUpdating = False. Then the macro doesn't have to show the cursor selection of a cell. Dropping the select and incorporating the range into the formula assignment code line like below will gain some speed/efficiency.
Range("E2").FormulaR1C1 = "Under"
Range("E2:E" & Cells(Rows.Count, "C").End(xlUp).Row) is the code version of selecting the last cell in a blank column (row 1048576), then using the keystroke of ctrl and the up key to determine the lowest/last used cell. This gets you a row count of 1 every time since the column is blank. Since you're looking for the last row. It may be faster to count down from the top. My favorite method for this is a loop. Increment a variable within a loop, while looking for the last row. Then, the variable can be used instead of your bottom up strategy.
t = 0
Do Until Range("C2").Offset(t, 0).Value = ""
t = t + 1
Loop
With Range("E2:E" & t)
.Formula = "=IF(C2<B2,B2-C2,"""")"
End With`
Just like TSQL, quote characters need their own quote characters.
.Formula = "=IF(C2<B2,B2-C2,"""")"
The Range Fillup VBA function can be utilized in this case to fill all cells from the bottom with a common formula, accounting for Excel Formula Reference Relativity. The code below starts with the range that we got from the loop counter. Next, we set a variable equal to the total rows in Excel minus the row corresponding to the counter row. Then, we resize the original region by the necessary rows and use the FillDown function to copy the first formula down.
Here's the resulting code. This will fill the range starting from the last row in Excel.
Sub Gre()
Range("E2").FormulaR1C1 = "Under"
Range("F2").FormulaR1C1 = "Over"
Range("G2").FormulaR1C1 = "Result"
Do While Range("e2").Offset(t, 0).Value <> ""
t = t + 1
Loop
Range("E2").Offset(t, 0).Formula = "=IF(C2<B2,B2-C2,"""")"
r1 = Range("e2").EntireColumn.Rows.Count
r2 = Range("E2").Offset(t, 0).Row
Range("E2").Offset(t, 0).Resize(r1 - r2, 1).FillDown
Range("F2").Offset(t, 0).Formula = "=IF(C2>B2,C2-B2,0)"
Range("F2").Offset(t, 0).Resize(r1 - r2, 1).FillDown
Range("G2").Offset(t, 0).Formula = "=IF(F2>0,""Issue"","""")"
Range("G2").Offset(t, 0).Resize(r1 - r2, 1).FillDown
End Sub
As well as using double quotes you may need to use 0 in the first two formula otherwise they may evaluate to empty strings. This may give unexpected results for the last formula i.e. incorrectly return "Issue".
If you do not have blank columns between your data and the 3 new columns you can use CurrentRegion to determine the number of rows:
Range("E2:E" & Cells.CurrentRegion.Rows.Count).Formula = "=if(C2'<'B2,B2-C2,0)"
Range("F2:F" & Cells.CurrentRegion.Rows.Count).Formula = "=if(C2>B2,C2-B2,0)"
Range("G2:G" & Cells.CurrentRegion.Rows.Count).Formula = if(F2>0,""Issue"","""")"
Please try the following sample hope it will help you to wright formula in VBA
Sub NewEntry()
Dim last_row As Integer
Dim sht1 As Worksheet
Dim StockName As String
Set sht1 = Worksheets("FNO MW")
last_row = Cells.Find(What:="*", After:=Range("A1"), SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
'MsgBox last_row
StockName = sht1.Cells(last_row, 1).Value
sht1.Cells(last_row, 1).Formula = "=RTD(""pi.rtdserver"", ,"" " & StockName & " "", ""TradingSymbol"")"
End Sub

Broken VBA Loop

I'm sure this is simple I just can't find anything on the Web.
I'm writing a Macro to format XL spreadsheets that i download from a 3rd party application. They come formatted all wacky so i'm trying to make it easier to get the data we need from them.
This is a simple VBA Do Loop that causes the cells in Column BL to update. The data in these cells contain line breaks which don't show up until you double click in the cell. The VBA below causes an update to the cells which achieves the same effect, just with less work. However it is currently crashing excel and I can't figure out why. It works in a single instance, but when I loop -- BOOM!!! -- frozen. Any help would be gently appreciated.
Sub updateCell()
Dim currentValue As String
ActiveSheet.Range("BL1").Select
Do Until ActiveCell.Value = ""
ActiveCell.Offset(1, 0).Select
currentValue = ActiveCell().Value
ActiveCell().Value = currentValue & ""
Loop
End Sub
Try something a bit more direct:
With ActiveSheet
lrow = .Range("BL" & .Rows.Count).End(xlUp).Row '~~> find last row on BL
With .Range("BL1:BL" & lrow) '~~> work on the target range
.Value = .Value '~~> assign its current value to it
End With
End With
Above code is like manually pressing F2 then pressing Enter.
Edit1: Explanation on getting the last row
ActiveSheet.Rows.Count '~~> Returns the number of rows in a sheet which is 1048576
MsgBox ActiveSheet.Rows.Count '~~> run this to confirm
So this line actually concatenates BL to 1048576.
.Range("BL" & .Rows.Count) '~~> Count is a property of the Rows Collection
Same as:
.Range("BL" & 1048576)
And same as:
.Range("BL1048576")
Then to get to the last row, we use Range Object End Method.
.Range("BL" & .Rows.Count).End(xlUp)
So basically, above code go to Cell BL1048576 then like manually pressing Ctrl+Arrow Up.
To return the actual row number of the range, we use the Range Object Row property.
lrow = .Range("BL" & .Rows.Count).End(xlUp).Row
See here more about With Statement.
It has the same effect (with your code) without the loop. HTH
But if what you want is to remove Line Breaks produced by Alt+Enter on a cell, try below:
Dim lrow As Long, c As Range
With ActiveSheet
lrow = .Range("BL" & .Rows.Count).End(xlUp).Row
For Each c In .Range("BL1:BL" & lrow)
c.Value = Replace(c.Value, Chr(10), "")
Next
End With
Where Chr(10) is the equivalent of Line Break replaced with "" using Replace Function.

Select Method of Worksheet Class Failed

I have this sub in Excel 2010 which is supposed to filter through all the cells in a sheet until it finds a match to Proj No, then paste a field from this row into another field.
When I try to run the sub, it gives me an error 1004: Select Method of Worksheet Class Failed. I've marked the line where this occurs. Any assistance would be greatly appreciated.
Option Explicit
Private Sub btnNext_Click()
Dim ProjNo As String
Dim Col As String
Dim Row As String
Dim cell As Range
Unload Dialog
formWait.Show
Sheets("Sheet7").Activate
ProjNo = Worksheets("Sheet1").Range("D6").Value
Col = Cells(Rows.Count, "A").End(xlUp).Row
For Each cell In Range("A2:A" & Col) 
If cell.Value = ProjNo Then
Row = Row & cell.Row
End If
Next cell
Workbooks("Form.xlsm").Sheets("Sheet7").Range("Row, 6").Copy Destination:=Sheets("Sheet1").Range("19, 5") ‘Error
Unload formWait
End Sub
I don't know what GWP is, but I think you want to use ProjNo there. The Range property doesn't accept an argument like that. Unless you have a named range of "Row,6" which you don't because it's not a legal name, you have to supply Range with a valid range reference, like A6 or D2:D12, for example.
Also, you can't concatenate rows and use them in a Range reference to get a larger range. You would have to copy each row inside the loop, union the ranges as you go, or better yet, filter on the value that you want and copy the visible rows.
Try this:
Private Sub btnNext_Click()
With ThisWorkbook.Worksheets("Sheet7")
'filter for the project id
.Range("A1", .Cells(.Rows.Count, 1).End(xlUp)).Resize(, 6).AutoFilter 1, "=" & .Range("D6").Value
'copy the visible rows
.Range("F2", .Cells(.Rows.Count, 6).End(xlUp)).SpecialCells(xlCellTypeVisible).Copy _
ThisWorkbook.Worksheets("Sheet1").Cells(19, 5)
'get rid of the filter
.AutoFilterMode = False
End With
End Sub
There are a few confusing items in your code above, so I wanted to place them long-form here. Let's get started:
Dim Col As String
Dim Row As String
It looks like your design expects these to be of type Long rather than type String. Even if these variables were meant to be strings, I would recommend adjusting their names -- when your fellow developer attempts to review your design, he or she is likely to see names like "Col" or "Row" and think "these are numbers". Easy fix:
Dim Col As Long, Row As Long
The next issue comes up here:
Col = Cells(Rows.Count, "A").End(xlUp).Row
The structure above is a common method for identifying the last ROW, not column. (It also appears that you have switched the "A" and number, which is another easy fix). While it is perfectly acceptable syntactically to name the variable for last row "Col", human users are likely to find this confusing. Identifying the last row (and the last col, which you use in the For Each loop), as explained in fantastic detail here, would be better handled like this:
Dim SheetSeven As Worksheet, SheetOne As Worksheet
Dim LastRow As Long, LastCol As Long
Set SheetSeven = ThisWorkbook.Worksheets("Sheet7")
Set SheetOne = ThisWorkbook.Worksheets("Sheet1")
With SheetSeven
LastRow = .Range("A" & .Rows.Count).End(xlUp).Row
LastCol = .Range("A" & .Columns.Count).End(xlToLeft).Column
End With
This should make your For Each loop look like this:
With SheetSeven
For Each cell in .Range("A2:A" & LastCol)
'... do you comparison and row incrementing here
Next cell
End With
Once you've identified your sheet as a variable, the Range.Copy action should be much easier as well:
With SheetSeven
.Range(.Cells(Row, 6)).Copy _
Destination:=SheetOne.Range(SheetOne.Cells(19, 5))
End With
Also one other thing you may wish to check is the status of Application.ScreenUpdating.
With the release of Office 2013 and later, a SDI (Single Document Interface) was introduced. If Application.ScreenUpdating is False and the workbook is not active, the implied call to Workbook.Activate will fail. Check the status of ScreenUpdating and set it to True if needed. You can set it back to False after the first Activate call for that workbook is made.
See this article:
https://support.microsoft.com/en-us/help/3083825/excel-workbook-is-not-activated-when-you-run-a-macro-that-calls-the-wo
In my case the error came as the sheet was hidden.
so I check if I am not working with the hidden sheet. Or you need to unhide the sheet before you try to select or activate sheet.
For Each sh In ThisWorkbook.Sheets
If Left(sh.Name, 8) <> "Template" Then
sh.Select
sh.Range("A1").Select
End If
Next