Macro Performance - Deleting Columns - excel

I have a worksheet with ~4,000 rows and 300 columns.
For my report, I have to remove a bunch columns and only keep about 50 of them, based on the header (in row 1).
I have the following code (obviously only listing 4 of the 50 columns) but this takes about 40 minutes to run. Is there a way to increase the performance of this?
Sub delete_columns()
Mylist = Array("ID","Status","First_Name","Last_Name")
LC = Cells(1, Columns.Count).End(xlToLeft).Column
For mycol = LC To 1 Step -1
x = ""
On Error Resume Next
x = WorksheetFunction.Match(Cells(1, mycol), Mylist, 0)
If Not IsNumeric(x) Then Columns(mycol).EntireColumn.Delete
Next mycol
End sub

Collect the columns you want to delete in a variable ColumnsToDelete first and delete all of them at once after the loop. Advantage of that is you have only one delete action (each action takes time) so this is less time consuming. Also you don't need to deactivate screen updating or calculation with this because this is already optimized to run only one update/calculation.
Option Explicit
Public Sub delete_columns()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1") ' adjust your sheet name here!
Dim ColumnNames As Variant
ColumnNames = Array("ID", "Status", "First_Name", "Last_Name")
Dim LastColumn As Long
LastColumn = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Dim ColumnsToDelete As Range
Dim iCol As Long
For iCol = 1 To LastColumn ' no need for backwards looping if we delete after loop.
Dim MatchedAt As Double
MatchedAt = 0
On Error Resume Next ' deactivate error reporting
MatchedAt = WorksheetFunction.Match(ws.Cells(1, iCol), ColumnNames, 0)
On Error Goto 0 'NEVER forget to re-activate error reporting!
If MatchedAt > 0 Then
If ColumnsToDelete Is Nothing Then ' add first found column
Set ColumnsToDelete = ws.Columns(iCol).EntireColumn
Else ' add all other found columns with union
Set ColumnsToDelete = Union(ColumnsToDelete, ws.Columns(iCol).EntireColumn)
End If
End If
Next mycol
' if columns were found delete them otherwise report
If Not ColumnsToDelete Is Nothing Then
ColumnsToDelete.Delete
Else
MsgBox "Nothing found to delete."
End If
End Sub

The first step would be to preface your Subroutine with
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
and end it with
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
This will mean that Excel doesn't try to recalculate the sheet every time you delete a column, it does it in one fell swoop at the end.
Unfortunately, we are working with Columns here, not Rows — otherwise, I'd suggest using a Filter to drop the Loop. Match can sometimes be a bit slow, so you may want to consider swapping the Array for a Dictionary, or having a Fuction to quickly loop through the Array and search for the value.
Not strictly a speed thing, but using Application.Match instead of WorksheetFunction.Match allows you to streamline your code inside the loop slightly:
If IsError(Application.Match(Cells(1, mycol).Value, Mylist, 0)) Then Columns(mycol).Delete

Keep only columns occurring in titles array
"For my report, I have to remove a bunch columns and only keep about 50 of them, based on the header (in row 1)."
The slightly shortened code in OP only lists 4 of the 50 headers in array MyList ; thus following MCV E rules
In the following example code I demonstrate a way to approve performance, explained in several steps;
in my tests it performed in 0.09 seconds over 3.000 rows (against nearly the same time of 0.10 seconds for #PEH 's methodically fine approach
, but which imho should be changed to If MatchedAt = 0 Then instead of > 0 to include the listed columns, not to delete them!)
[1] Don't focus on deletion (~250 columns), but get an array of column numbers to be maintained (~4..50 columns); see details at help function getNeededColNums()
showing an undocumented use of Application.Match()
[2] Hide the found columns to preserve them from eventual deletion
[3] Delete all columns left visible in one go using the SpecialCells method
[4] Redisplay the hidden columns left untouched
A main reason for the mentioned poor performance in the original post (OP) is that repeated deletion of columns shifts the entire worksheet up to 250 times (i.e. ~75% of titled columns).
A further note to the original post: always use Option Explicit to force variable declarations and fully qualify all range references,
e.g. like x = Application.Match(Sheet1.Cells(1, mycol), myList, 0).
Sub ExampleCall()
Dim t#: t = Timer
'[1]Get array of column numbers to be maintained
Dim ws As Worksheet: Set ws = Sheet1 ' << reference wanted sheet e.g. by Code(Name)
Dim cols: cols = getNeededColNums(ws) '1-based 1-dim array
Debug.Print Join(cols, ",")
'[2]Hide found columns to preserve them from eventual deletion
Dim i As Long
For i = 1 To UBound(cols)
ws.Columns(cols(i)).Hidden = True
Next
'[3]Delete columns left visible
Application.DisplayAlerts = False
ws.Range("A1", ws.Cells(1, LastCol(ws))).SpecialCells(xlCellTypeVisible).EntireColumn.Delete
Application.DisplayAlerts = True
'[4]Redisplay untouched hidden columns
ws.Range("A1", ws.Cells(1, UBound(cols))).EntireColumn.Hidden = False
Debug.Print "**" & Format(Timer - t, "0.00 secs") ' 0.09 seconds!
End Sub
'Help function getNeededColNums()
Note that Application.Match() doesn't compare only a single argument against a complete list of column titles, but is capable to pass even an array as first argument:
Application.Match(titles, allTitles, 0)
Assuming existing titles, this results in a 1-based array with the same dimension boundaries as the first argument and which returns the found column numbers. So you get valid list without need of further checks (IsNumeric or Not IsError in the late-bound Application form) or even error handling in the WorksheetFunction.
Function getNeededColNums(ws As Worksheet)
'Note: returns 1-based 1-dim array (assuming existant titles)
Dim titles As Variant
titles = Array("ID", "Status", "First_Name", "Last_Name")
'get all existing titles
Dim allTitles As Variant
allTitles = ws.Range("1:1").Resize(1, LastCol(ws)).Value2
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'get column numbers to be maintained
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
getNeededColNums = Application.Match(titles, allTitles, 0)
End Function
Help function LastCol()
Function LastCol(ws As Worksheet, Optional rowNum As Long = 1) As Long
'Purp.: return the last column number of a title row in a given worksheet
LastCol = ws.Cells(rowNum, ws.Columns.Count).End(xlToLeft).Column
End Function

Related

I would like to go to EOF in Excel with faster performance [duplicate]

I am trying to find the last row the same way I found the last column:
Sheets("Sheet2").Cells(1,Sheets("Sheet2").Columns.Count).End(xlToLeft).Column
I know this way but it is not as helpful as the prior would be:
u = Sheets("Sheet1").Range("A65536").End(xlUp).Row
I tried:
Sheets("Sheet2").Cells(Sheets("Sheet2",1).Rowa.Count).End(xlToUP).Column
Synopsis: I would like the below way for last row.
Sheets("Sheet2").Cells(1,Sheets("Sheet2").Columns.Count).End(xlToLeft).Column
You should use a with statement to qualify both your Rows and Columns counts. This will prevent any errors while working with older pre 2007 and newer 2007 Excel Workbooks.
Last Column
With Sheets("Sheet2")
.Cells(1, .Columns.Count).End(xlToLeft).Column
End With
Last Row
With Sheets("Sheet2")
.Range("A" & .Rows.Count).End(xlUp).Row
End With
Or
With Sheets("Sheet2")
.Cells(.Rows.Count, 1).End(xlUp).Row
End With
How is this?
dim rownum as integer
dim colnum as integer
dim lstrow as integer
dim lstcol as integer
dim r as range
'finds the last row
lastrow = ActiveSheet.UsedRange.Rows.Count
'finds the last column
lastcol = ActiveSheet.UsedRange.Columns.Count
'sets the range
set r = range(cells(rownum,colnum), cells(lstrow,lstcol))
This function should do the trick if you want to specify a particular sheet. I took the solution from user6432984 and modified it to not throw any errors. I am using Excel 2016 so it may not work for older versions:
Function findLastRow(ByVal inputSheet As Worksheet) As Integer
findLastRow = inputSheet.cellS(inputSheet.Rows.Count, 1).End(xlUp).Row
End Function
This is the code to run if you are already working in the sheet you want to find the last row of:
Dim lastRow as Integer
lastRow = cellS(Rows.Count, 1).End(xlUp).Row
I use this routine to find the count of data rows. There is a minimum of overhead required, but by counting using a decreasing scale, even a very large result requires few iterations. For example, a result of 28,395 would only require 2 + 8 + 3 + 9 + 5, or 27 times through the loop, instead of a time-expensive 28,395 times.
Even were we to multiply that by 10 (283,950), the iteration count is the same 27 times.
Dim lWorksheetRecordCountScaler as Long
Dim lWorksheetRecordCount as Long
Const sDataColumn = "A" '<----Set to column that has data in all rows (Code, ID, etc.)
'Count the data records
lWorksheetRecordCountScaler = 100000 'Begin by counting in 100,000-record bites
lWorksheetRecordCount = lWorksheetRecordCountScaler
While lWorksheetRecordCountScaler >= 1
While Sheets("Sheet2").Range(sDataColumn & lWorksheetRecordCount + 2).Formula > " "
lWorksheetRecordCount = lWorksheetRecordCount + lWorksheetRecordCountScaler
Wend
'To the beginning of the previous bite, count 1/10th of the scale from there
lWorksheetRecordCount = lWorksheetRecordCount - lWorksheetRecordCountScaler
lWorksheetRecordCountScaler = lWorksheetRecordCountScaler / 10
Wend
lWorksheetRecordCount = lWorksheetRecordCount + 1 'Final answer
This gives you the last used row in a specified column.
Optionally you can specify the worksheet, otherwise it will take the active sheet.
Function getLastRow(col As Integer, Optional ws As Worksheet) As Long
If ws Is Nothing Then Set ws = ActiveSheet
If ws.Cells(ws.Rows.Count, col).Value <> "" Then
getLastRow = ws.Cells(ws.Rows.Count, col).Row
Exit Function
End If
getLastRow = ws.Cells(Rows.Count, col).End(xlUp).Row
If shtRowCount = 1 Then
If ws.Cells(1, col) = "" Then
getLastRow = 0
Else
getLastRow = 1
End If
End If
End Function
Sub test()
Dim lgLastRow As Long
lgLastRow = getLastRow(2) 'Column B
End Sub
This is the best way I've seen to find the last cell.
MsgBox ActiveSheet.UsedRage.SpecialCells(xlCellTypeLastCell).Row
One of the disadvantages to using this is that it's not always accurate. If you use it then delete the last few rows and use it again, it does not always update. Saving your workbook before using this seems to force it to update though.
Using the next bit of code after updating the table (or refreshing the query that feeds the table) forces everything to update before finding the last row. But, it's been reported that it makes excel crash. Either way, calling this before trying to find the last row will ensure the table has finished updating first.
Application.CalculateUntilAsyncQueriesDone
Another way to get the last row for any given column, if you don't mind the overhead.
Function GetLastRow(col, row)
' col and row are where we will start.
' We will find the last row for the given column.
Do Until ActiveSheet.Cells(row, col) = ""
row = row + 1
Loop
GetLastRow = row
End Function
Problems with normal methods
Account for Blank Rows / Columns -
If you have blank rows or columns at the beginning of your data then methods like UsedRange.Rows.Count and UsedRange.Columns.Count will skip over these blank rows (although they do account for any blank rows / columns that might break up the data), so if you refer to ThisWorkbook.Sheets(1).UsedRange.Rows.Count you will skip lines in cases where there are blank rows at the top of your sheet, for example on this sheet:
This will skip the top row from the count and return 11:
ThisWorkbook.Sheets(1).UsedRange.Rows.Count
This code will include the blank row and return 12 instead:
ThisWorkbook.Sheets(1).UsedRange.Cells(ThisWorkbook.Sheets(1).UsedRange.Rows.Count, 1).Row
The same issue applies to columns.
Full Sheets -
Identifying the last row or column can be difficult if your sheet is full (this only matters if either your data contains over a million lines or might have values in the final rows or columns of your data). For example, if you use xlEndUp or similar and the cell you're referring to is populated then the code will skip over data, in extreme cases your entire data set can be skipped if for example the data continues from the last row of the sheet (where you start your xlEndUp) solidly up to the first row (in this case the result would be 1).
'This code works, but...
'Will not function as intended if there is data in the cell you start with (Cell A:1048576).
Dim Sht1 as Range: Set Sht1 = ThisWorkbook.Sheets(1)
Sht1.Cells(Sht1.Rows.Count, 1).End(xlUp).Row
Columns with blank rows -
The above code also assumes that your data extends the entire way down column 1, if you have blank entries in column 1 you may lose rows as the code will find the first filled row from the bottom only for column 1.
Unnecessary Looping -
Self explanatory, best to avoid looping where possible as if you're dealing with a lot of data and repeating the looping process often it can slow down your code.
Solution
Note that this is targeted at finding the last "Used" Row or Column on an entire sheet, this doesn't work if you just want the last cell in a specific range.
I've setup some Functions here
Private Function GetLastRow(Sheet As Worksheet)
'Gets last used row # on sheet.
GetLastRow = Sheet.UsedRange.Cells(Sheet.UsedRange.Rows.Count, 1).Row
End Function
Private Function GetLastCol(Sheet As Worksheet)
'Gets last used column # on sheet.
GetLastCol = Sheet.UsedRange.Cells(1, Sheet.UsedRange.Columns.Count).Column
End Function
Examples of calling these Functions:
Sub CallFunctions()
'Define the Target Worksheet we're interested in:
Dim Sht1 As Worksheet: Set Sht1 = ThisWorkbook.Sheets(1)
'Print the last row and column numbers:
Debug.Print "Last Row = "; GetLastRow(Sht1)
Debug.Print "Last Col = "; GetLastCol(Sht1)
End Sub
I preferred search last blank cell:
Il you want last empty cell of column you can do that
Dim sh as Worksheet, r as range
set sh = ActiveWorksheet 'if you want an other it's possible
'find a value
'Columns("A:D") 'to check on multiple columns
Set r = sh.Columns("A").Find(What:="*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
'no value return first row
If r Is Nothing Then Set r = sh.Cells(1, "A") Else Set r = sh1.Cells(r.Row + 1, "A")
If this is to insert new row, find on multiple columns is a good choice because first column can contains less rows than next columns
I use the following function extensively. As pointed out above, using other methods can sometimes give inaccurate results due to used range updates, gaps in the data, or different columns having different row counts.
Example of use:
lastRow=FindRange("Sheet1","A1:A1000")
would return the last occupied row number of the entire range. You can specify any range you want from single columns to random rows, eg FindRange("Sheet1","A100:A150")
Public Function FindRange(inSheet As String, inRange As String) As Long
Set fr = ThisWorkbook.Sheets(inSheet).Range(inRange).find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
If Not fr Is Nothing Then FindRange = fr.row Else FindRange = 0
End Function

Is it possible to use VBA code on a already filtered sheet?

I have a sheet with about 6000 rows. In my code I first filter out some rows.
Sheets("privata").Rows("2:" & Rows.count).AutoFilter Field:=26, Criteria1:=">=2020-01-30 09:00:00", Operator:=xlAnd, Criteria2:="<=2020-01-30 09:30:00"
Sheets("privata").Rows("2:" & Rows.count).AutoFilter Field:=24, Criteria1:="<>OK"
Sheets("privata").Rows("2:" & Rows.count).AutoFilter Field:=25, Criteria1:="<>SUPPLY_CONTROL,"
Its now down to about 350 rows. After I've filtered it I copy and paste the data to another sheet
Sheets("privata").UsedRange.Copy
Sheets("toptre").Range("A1").PasteSpecial xlPasteAll
After I've copied the data I work on it in various ways in the new sheet.
The entire code takes a while to run. After stepping through the code I discovered that the filtering out process is super quick. What takes time is the pasting of the data in to the other sheet.
Is there a possibility to work with the original filtered sheet? When I try to, it uses all 6000 rows, not just the filtered out ones.
Example of what I want to do:
For i = 2 To RowCount + 1
employee = Sheets("privata").Cells(i, 25)
onList = False
For j = 1 To UBound(employeeList)
If employee = employeeList(j) Then
onList = True
Exit For
End If
Next j
If onList = False Then
countEmployees = countEmployees + 1
employeeList(countEmployees) = employee
End If
If onList = True Then
onList = False
End If
Next i
When referring to Cells(2, 25) I want to refer to the second row in the filtered sheet. Which might be row 3568 in the sheet. Is that possible?
/Jens
After the filtering has been applied, you can make the copy/paste process very fast if you don't use a loop, but use Selection. For example:
Sub TryThis()
Dim r As Range
Sheets("privata").Select
Set r = ActiveSheet.AutoFilter.Range
r.Select
Selection.Copy Sheets("toptre").Range("A1")
End Sub
Usually you want to avoid Selection in VBA. However, you will end up with:
a block of data in sheet "toptre"
the block will include the header row and all visible rows
the block will be just a block (un-filtered)
I am not sure if this will make your process any faster, but it attempts to accomplish what you ask about in your question:
You could use the expression suggested by #GSerg 's comment to create a range object with only the visible rows in the data sheet, e.g.
Dim filteredRange As Range
Set filteredRange = Sheets("privata").UsedRange.Rows.SpecialCells(xlCellTypeVisible)
Assuming there is at least 1 visible row in the sheet (meaning that the above statement will not throw an error), you could then use the following function to access that range as if it were a single, contiguous range:
Function RelativeCell(rng As Range, ByVal row As Long, ByVal col As Long) As Range
Dim areaNum As Long: areaNum = 0
Dim maxRow As Long: maxRow = 0
Dim areaCount As Long: areaCount = rng.Areas.Count
Do While maxRow < row
areaNum = areaNum + 1
If areaNum > areaCount Then
Set RelativeCell = Nothing
Exit Function
End If
maxRow = maxRow + rng.Areas(areaNum).Rows.Count
Loop
Dim lastArea As Range: Set lastArea = rng.Areas(areaNum)
Set RelativeCell = lastArea.Cells(row - (maxRow - lastArea.Rows.Count), col)
End Function
To print all the filtered values in column B, for example, you could use the above method on the filteredRange object (set earlier) this way:
Dim r As Long: r = 1
Do
Dim cell As Range: Set cell = RelativeCell(filteredRange, r, 2)
If cell Is Nothing Then Exit Do
Debug.Print cell.Value
r = r + 1
Loop
To simplify the above code, you could also use a function to know the last relative row number in the filtered range using the following function:
Function RelativeCellLastRow(rng As Range) As Long
Dim r As Long: r = 0
Dim i As Long
For i = 1 To rng.Areas.Count
r = r + rng.Areas(i).Rows.Count
Next
RelativeCellLastRow = r
End Function
Then, the code to print all the filtered values in column B would be reduced to this:
Dim r As Long
For r = 1 To RelativeCellLastRow(filteredRange)
Debug.Print RelativeCell(testRng, r, 2).Value
Next
If you use RelativeCellLastRow, it would be good to ensure that it is only executed once, to avoid unnecessary recalculations. In the For loop above, it is only executed once, since VBA only executes the limits of a For loop before the first iteration. If you need the value several times, you can store it in a variable and use the variable instead.
The idea behind the RelativeCell function is that the range returned by the call to SpecialCells is a multi-area range, i.e. a range made up of several non-contiguous ranges. What relativeCell does is to skip through the non-contiguous areas until it finds the row number it is looking for. If the row number is beyond the total number of rows in the range, the function returns Nothing, so the calling code must be aware of this to avoid calling a method or property on Nothing.
It is also worth nothing that RelativeCell works on a range with hidden rows, not hidden columns. With hidden columns, the code becomes a little more complex, but the complexity can be encapsulated in the RelativeCell function without affecting the code that uses the function.
Again, I am not sure whether this will make your code faster. When I did some tests to emulate your scenario using a sheet with 6000+ rows and 30 columns of random strings, the copy/paste after the filtering ran very quickly, but it could be because of the machine I am using, the version of Excel that I am using (2016), or the data I used. Having said that, I hope the above code is of some help.

Macro in VBA for excel that processes large quantities of data

I need some help with a particular macro I am working on.
The macro processes columns of data that have been imported from a pdf file. The import process produces multiple sheets of consistent data, all variables stay in the same columns across multiple sheets. This macro needs to read the three columns of numbers, subtract all cells in two columns one from another, place solved value in an empty column at the end of each row. Then repeat with another combination of two columns. After that, it needs to compare the solved values against a margin value, and generate a new sheet that pulls the whole row of data that the failed margin value is in to a new sheet at the front of the workbook.
This is what I have so far.
I can preform the function on one sheet so far, but don't know how to automate this to the other sheets. Numbers populate columns B, C, and D, Answers should be placed in G, H and any other columns after H are empty.
Private Sub FindAndCreateSheet3dBm()
' Declare variables
Dim eWs As Worksheet
Dim rMargin As Range
Dim myUnion As Range
'Column G: subrtact max and measured values
Worksheets("page 6").Range("G1:G21").Formula = "=(C1-D1)"
'*need to fix sheet reference, make all sheets, add flexible range to
'end of G range
'Column H: subrtact measured and min values
Worksheets("page 6").Range("H1:H21").Formula = "=(D1-B1)"
'*need to fix sheet reference, make all sheets, add flexible range to
'end of H range
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Create the report sheet at first position then name it "Less than 3dBm"
Dim wsReport As Worksheet
Dim rCellwsReport As Range
Set wsReport = ThisWorkbook.Sheets.Add(Before:=ThisWorkbook.Sheets(1))
wsReport.Name = "Less than 3dBm"
Set rCellwsReport = wsReport.Cells(1, 1)
'Create union of columns to search G and H?
Set myUnion = Union(Columns("G"), Columns("H"))
'Check whole Workbook, union G and H for values less than rMargin
NextSheet:
Next
End Sub
Thank you
This should work for your needs. Before I get into my code, I just want to note that usually the response you'll get from the community when asking a 'how do I do this' question is that SO is not a code for me site. We are happy to help fix broken code, but these kinds of problems can generally be solved with Google.
That being said, I wanted a break from the project I was working on, so I threw this together. My hope here is that you can use it as a learning opportunity of how to write better code (and maybe get some kudos from your boss in the process).
Here's the code:
Private Sub FindAndCreateSheet3dBm()
' Ideally, you wouldnt even use something like this. For your purposes
' it will get you going. I highly recommend finding a dynamic way of
' determining the positions of the data. It may be consistent now, but
' in the world of programming, everything changes, especially when
' you think it wont.
Const FIRST_INPUT_COL As Long = 3 ' Column C
Const SECOND_INPUT_COL As Long = 4 ' D
Const THIRD_INPUT_COL As Long = 2 ' B
Const FIRST_OUTPUT_COL As Long = 7 ' G
Const SECOND_OUTPUT_COL As Long = 8 ' H
Dim marginReport As Worksheet
Set marginReport = ThisWorkbook.Sheets.Add(Before:=ThisWorkbook.Sheets(1))
marginReport.Name = "Less than 3dBm"
Dim targetWorksheet As Worksheet
For Each targetWorksheet In ThisWorkbook.Worksheets
If Not targetWorksheet Is marginReport Then
Dim inputData As Variant
inputData = targetWorksheet.UsedRange.value
Dim outputData As Variant
' I resize the array to be the exact same as the first, but to add two additional columns
ReDim outputData(LBound(inputData, 1) To UBound(inputData, 1), LBound(inputData, 2) To UBound(inputData, 2) + 2)
Dim i As Long
Dim j As Long
' Loop through rows
For i = LBound(inputData, 1) To UBound(inputData, 1)
' Loop through columns
For j = LBound(inputData, 2) To UBound(inputData, 2)
' Essentially, just copy the data
outputData(i, j) = inputData(i, j)
Next
Next
Dim offSetValue As Long
If LBound(outputData, 2) = 1 Then offSetValue = -1
' For your purposes I will use hardcoded indices here, but it is far more ideal to manage this in a more flexible manner
For i = LBound(outputData, 1) To UBound(outputData, 1)
outputData(i, FIRST_OUTPUT_COL) = outputData(i, FIRST_INPUT_COL) - outputData(i, SECOND_INPUT_COL)
outputData(i, SECOND_OUTPUT_COL) = outputData(i, FIRST_OUTPUT_COL) - outputData(i, THIRD_INPUT_COL)
If LessThanMargin(outputData(i, SECOND_OUTPUT_COL)) Then
For j = LBound(outputData, 2) To UBound(outputData, 2)
' I start with the output worksheet, and use the 'End(xlUp) to find the first
' non-blank row. I then iterate columnwise and add values to the row beneath it.
' The offSetValue variable ensures I am not skipping any cells if the array
' is 1-Based versus the default 0-Base.
marginReport.Range("A1048576").End(xlUp).Offset(1, j + offSetValue).value = outputData(i, j)
Next
End If
Next
OutputArray outputData, targetWorksheet, "UpdatedData_" & UCase(Replace(targetWorksheet.Name, " ", "_"))
End If
Next
End Sub
' I am just checking for a negative number here, but change this to use the logic you need
Public Function LessThanMargin(ByVal InputValue As Double)
LessThanMargin = InputValue < 0
End Function
Public Sub OutputArray(ByVal InputArray As Variant, ByVal InputWorksheet As Worksheet, ByVal TableName As String)
Dim AddLengthH As Long
Dim AddLengthW As Long
If NumberOfArrayDimensions(InputArray) = 2 Then
If LBound(InputArray, 1) = 0 Then AddLengthH = 1
If LBound(InputArray, 2) = 0 Then AddLengthW = 1
Dim r As Range
If Not InputWorksheet Is Nothing Then
With InputWorksheet
.Cells.Clear
Set r = .Range("A1").Resize(UBound(InputArray, 1) + AddLengthH, UBound(InputArray, 2) + AddLengthW)
r.value = InputArray
.ListObjects.Add(xlSrcRange, r, , xlYes).Name = TableName
With .ListObjects(1).Sort
.Header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End With
End If
End If
End Sub
I use arrays to solve the problem since they are far more efficient when processing data versus using excel-formulas. While this is unlikely to make a performance boost on a ~200 row project, it makes tremendous differences when you're dealing with a few thousand rows or even more.
I also used constants for the column positions to make it easier for you to adjust these in the future. This comes with a caution though, even constants (for this purpose) are terrible habit so dont get used to them. Learn how to calculate where the data is.
Finally, please (for the love of all that is programmatic) don't just copy and paste this code and never look back. I put this up here for you (and others) to learn from it. Not for it to be some sort of quick fix. I hope you can use it to grow.

Better way to find last used row

I am trying to find the last row the same way I found the last column:
Sheets("Sheet2").Cells(1,Sheets("Sheet2").Columns.Count).End(xlToLeft).Column
I know this way but it is not as helpful as the prior would be:
u = Sheets("Sheet1").Range("A65536").End(xlUp).Row
I tried:
Sheets("Sheet2").Cells(Sheets("Sheet2",1).Rowa.Count).End(xlToUP).Column
Synopsis: I would like the below way for last row.
Sheets("Sheet2").Cells(1,Sheets("Sheet2").Columns.Count).End(xlToLeft).Column
You should use a with statement to qualify both your Rows and Columns counts. This will prevent any errors while working with older pre 2007 and newer 2007 Excel Workbooks.
Last Column
With Sheets("Sheet2")
.Cells(1, .Columns.Count).End(xlToLeft).Column
End With
Last Row
With Sheets("Sheet2")
.Range("A" & .Rows.Count).End(xlUp).Row
End With
Or
With Sheets("Sheet2")
.Cells(.Rows.Count, 1).End(xlUp).Row
End With
How is this?
dim rownum as integer
dim colnum as integer
dim lstrow as integer
dim lstcol as integer
dim r as range
'finds the last row
lastrow = ActiveSheet.UsedRange.Rows.Count
'finds the last column
lastcol = ActiveSheet.UsedRange.Columns.Count
'sets the range
set r = range(cells(rownum,colnum), cells(lstrow,lstcol))
This function should do the trick if you want to specify a particular sheet. I took the solution from user6432984 and modified it to not throw any errors. I am using Excel 2016 so it may not work for older versions:
Function findLastRow(ByVal inputSheet As Worksheet) As Integer
findLastRow = inputSheet.cellS(inputSheet.Rows.Count, 1).End(xlUp).Row
End Function
This is the code to run if you are already working in the sheet you want to find the last row of:
Dim lastRow as Integer
lastRow = cellS(Rows.Count, 1).End(xlUp).Row
I use this routine to find the count of data rows. There is a minimum of overhead required, but by counting using a decreasing scale, even a very large result requires few iterations. For example, a result of 28,395 would only require 2 + 8 + 3 + 9 + 5, or 27 times through the loop, instead of a time-expensive 28,395 times.
Even were we to multiply that by 10 (283,950), the iteration count is the same 27 times.
Dim lWorksheetRecordCountScaler as Long
Dim lWorksheetRecordCount as Long
Const sDataColumn = "A" '<----Set to column that has data in all rows (Code, ID, etc.)
'Count the data records
lWorksheetRecordCountScaler = 100000 'Begin by counting in 100,000-record bites
lWorksheetRecordCount = lWorksheetRecordCountScaler
While lWorksheetRecordCountScaler >= 1
While Sheets("Sheet2").Range(sDataColumn & lWorksheetRecordCount + 2).Formula > " "
lWorksheetRecordCount = lWorksheetRecordCount + lWorksheetRecordCountScaler
Wend
'To the beginning of the previous bite, count 1/10th of the scale from there
lWorksheetRecordCount = lWorksheetRecordCount - lWorksheetRecordCountScaler
lWorksheetRecordCountScaler = lWorksheetRecordCountScaler / 10
Wend
lWorksheetRecordCount = lWorksheetRecordCount + 1 'Final answer
This gives you the last used row in a specified column.
Optionally you can specify the worksheet, otherwise it will take the active sheet.
Function getLastRow(col As Integer, Optional ws As Worksheet) As Long
If ws Is Nothing Then Set ws = ActiveSheet
If ws.Cells(ws.Rows.Count, col).Value <> "" Then
getLastRow = ws.Cells(ws.Rows.Count, col).Row
Exit Function
End If
getLastRow = ws.Cells(Rows.Count, col).End(xlUp).Row
If shtRowCount = 1 Then
If ws.Cells(1, col) = "" Then
getLastRow = 0
Else
getLastRow = 1
End If
End If
End Function
Sub test()
Dim lgLastRow As Long
lgLastRow = getLastRow(2) 'Column B
End Sub
This is the best way I've seen to find the last cell.
MsgBox ActiveSheet.UsedRage.SpecialCells(xlCellTypeLastCell).Row
One of the disadvantages to using this is that it's not always accurate. If you use it then delete the last few rows and use it again, it does not always update. Saving your workbook before using this seems to force it to update though.
Using the next bit of code after updating the table (or refreshing the query that feeds the table) forces everything to update before finding the last row. But, it's been reported that it makes excel crash. Either way, calling this before trying to find the last row will ensure the table has finished updating first.
Application.CalculateUntilAsyncQueriesDone
Another way to get the last row for any given column, if you don't mind the overhead.
Function GetLastRow(col, row)
' col and row are where we will start.
' We will find the last row for the given column.
Do Until ActiveSheet.Cells(row, col) = ""
row = row + 1
Loop
GetLastRow = row
End Function
Problems with normal methods
Account for Blank Rows / Columns -
If you have blank rows or columns at the beginning of your data then methods like UsedRange.Rows.Count and UsedRange.Columns.Count will skip over these blank rows (although they do account for any blank rows / columns that might break up the data), so if you refer to ThisWorkbook.Sheets(1).UsedRange.Rows.Count you will skip lines in cases where there are blank rows at the top of your sheet, for example on this sheet:
This will skip the top row from the count and return 11:
ThisWorkbook.Sheets(1).UsedRange.Rows.Count
This code will include the blank row and return 12 instead:
ThisWorkbook.Sheets(1).UsedRange.Cells(ThisWorkbook.Sheets(1).UsedRange.Rows.Count, 1).Row
The same issue applies to columns.
Full Sheets -
Identifying the last row or column can be difficult if your sheet is full (this only matters if either your data contains over a million lines or might have values in the final rows or columns of your data). For example, if you use xlEndUp or similar and the cell you're referring to is populated then the code will skip over data, in extreme cases your entire data set can be skipped if for example the data continues from the last row of the sheet (where you start your xlEndUp) solidly up to the first row (in this case the result would be 1).
'This code works, but...
'Will not function as intended if there is data in the cell you start with (Cell A:1048576).
Dim Sht1 as Range: Set Sht1 = ThisWorkbook.Sheets(1)
Sht1.Cells(Sht1.Rows.Count, 1).End(xlUp).Row
Columns with blank rows -
The above code also assumes that your data extends the entire way down column 1, if you have blank entries in column 1 you may lose rows as the code will find the first filled row from the bottom only for column 1.
Unnecessary Looping -
Self explanatory, best to avoid looping where possible as if you're dealing with a lot of data and repeating the looping process often it can slow down your code.
Solution
Note that this is targeted at finding the last "Used" Row or Column on an entire sheet, this doesn't work if you just want the last cell in a specific range.
I've setup some Functions here
Private Function GetLastRow(Sheet As Worksheet)
'Gets last used row # on sheet.
GetLastRow = Sheet.UsedRange.Cells(Sheet.UsedRange.Rows.Count, 1).Row
End Function
Private Function GetLastCol(Sheet As Worksheet)
'Gets last used column # on sheet.
GetLastCol = Sheet.UsedRange.Cells(1, Sheet.UsedRange.Columns.Count).Column
End Function
Examples of calling these Functions:
Sub CallFunctions()
'Define the Target Worksheet we're interested in:
Dim Sht1 As Worksheet: Set Sht1 = ThisWorkbook.Sheets(1)
'Print the last row and column numbers:
Debug.Print "Last Row = "; GetLastRow(Sht1)
Debug.Print "Last Col = "; GetLastCol(Sht1)
End Sub
I preferred search last blank cell:
Il you want last empty cell of column you can do that
Dim sh as Worksheet, r as range
set sh = ActiveWorksheet 'if you want an other it's possible
'find a value
'Columns("A:D") 'to check on multiple columns
Set r = sh.Columns("A").Find(What:="*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
'no value return first row
If r Is Nothing Then Set r = sh.Cells(1, "A") Else Set r = sh1.Cells(r.Row + 1, "A")
If this is to insert new row, find on multiple columns is a good choice because first column can contains less rows than next columns
I use the following function extensively. As pointed out above, using other methods can sometimes give inaccurate results due to used range updates, gaps in the data, or different columns having different row counts.
Example of use:
lastRow=FindRange("Sheet1","A1:A1000")
would return the last occupied row number of the entire range. You can specify any range you want from single columns to random rows, eg FindRange("Sheet1","A100:A150")
Public Function FindRange(inSheet As String, inRange As String) As Long
Set fr = ThisWorkbook.Sheets(inSheet).Range(inRange).find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
If Not fr Is Nothing Then FindRange = fr.row Else FindRange = 0
End Function

excel vba how to copy the value of multiple non-contiguous ranges into an array

I am trying to copy the value of multiple non-contiguous ranges into an array. I wrote code like this:
summaryTempArray = .range("A2:D9,A11:D12,A14:D15").Value
But it copies only the first part (A2:D9). Then, I tried the following and I get the error - "Method Union of Object _Global Failed" - is there any mistake in the way that I am using union?
summaryTempArray = Union(.range("A2:D9"), .range("A11:D12"), .range("A14:D15")).Value
Don't know what was wrong with your union, but it would have created the same range, which you stated in your first attempt.
The problem is, you have now multiple areas. Which you can, and as far as I know, has to address now.
Here is an example, which will resolve in an array of all areas, without adding each cell individually, but adding each area individually to the summary array:
Public Sub demo()
Dim summaryTempArray() As Variant
Dim i As Long
With Tabelle1
ReDim summaryTempArray(1 To .Range("A2:D9,A11:D12,A14:D15").Areas.Count)
For i = 1 To .Range("A2:D9,A11:D12,A14:D15").Areas.Count
summaryTempArray(i) = .Range("A2:D9,A11:D12,A14:D15").Areas(i)
Next i
End With
End Sub
Hope this helps.
I believe Jook's solution is as good as you are going to get if it is important to get the source ranges into an array. However, I think the solution should include instructions on extracting values from a ragged array. This is not difficult but the syntax is obscure.
I cannot get your Union statement to fail either. I assume there is something about the context that causes the failure which I cannot duplicate.
The code below shows that the two ranges are the same and that only the first sub-range is loaded to an array as you reported. It finishes with an alternative approach that might be satisfactory.
Option Explicit
Sub Test()
Dim CellValue() As Variant
Dim rng As Range
With Worksheets("Sheet1")
Set rng = .Range("A2:D9,A11:D12,A14:D15")
Debug.Print rng.Address
Set rng = Union(.Range("A2:D9"), .Range("A11:D12"), .Range("A14:D15"))
Debug.Print rng.Address
' The above debug statements show the two ranges are the same.
Debug.Print "Row count " & rng.Rows.Count
Debug.Print "Col count " & rng.Columns.Count
' These debug statements show that only the first sub-range is included the
' range counts.
CellValue = rng.Value
Debug.Print "Rows " & LBound(CellValue, 1) & " to " & UBound(CellValue, 1)
Debug.Print "Cols " & LBound(CellValue, 2) & " to " & UBound(CellValue, 2)
' As you reported only the first range is copied to the array.
rng.Copy Destination:=Worksheets("Sheet2").Range("A1")
' This shows you can copy the selected sub-ranges. If you can copy the
' required data straight to the desired destination, this might be a
' solution.
End With
End Sub
I had the same problem & tried a few methods without success until I hit on this:-
dim i as integer
Dim rng1 as range
Dim str as string
dim cels() as string
Set rng1 = sheet1.Range("A2:D9,A11:D12,A14:D15")
str = rng1.address(0,0)
cels() = split(str, ",") '<--- seems to work OK
for i = 0 to 2
Debug.Print cels(i)
Next i
I would be interested if this is an "incorrect" conversion method.
It is possible to create a multi dimensional array from non concurrent cell ranges. What I did was use a bit of the code above for the range copy mechanic I learned 2 things; that with that method you can refer to the actual cells and not just the data and you can also move and preserve order with it. In my personal project we have to use some excel files to fill out calibration data. It runs the calculations and produces a report of calibration record for our files to refer to later. These stock files are boring! I wanted to spruce it up a bit and color most of the documents empty cells depending on if the calibration passed or not. The files separate the individual check steps so the ranges I wanted to look through were not always adjacent. What I came up with is to use the copy function below to create a new sheet and paste all the non-concurrent ranges into one nice new set of concurrent ones and then have my array look at the new sheet to draw my table. I have it run the lookup I needed and then get rid of the now useless sheet.
Public Sub ColorMeCrazy()
' First Declare your variables that you will need line notes will be added to all the ones for just the array problem
Dim chkarray As Variant
Dim i As Integer ' for the array lookup loop
Dim j As Integer ' also for the array lookup loop
Dim chk1 As Boolean
Dim chk2 As Boolean
Dim cpyrange As Range ' the non-concurrent range collector haha.
Dim cz As Range
chk2 = True
Set cz = Worksheets("AN_PRM-36").Range("A1:I1,C2:I2,I3:I35,A30:H32,D33:H35,C34:C35,A36:E36,A22:H23,D24:H24,A16:H16,A8:H9,D10:H10")
' the next item below sets the ranges i wish to use. see that they arent all just right next to eachother.
Set cpyrange = Worksheets("AN_PRM-36").Range("G7:H7,G15:H15,G21:H21,G28:H29")
' this is the new sheet i made to create the array with
Sheets.Add.Name = "AN_PRM-36tmp"
' the data gets coppied to the new sheet but now its all together
cpyrange.Copy Destination:=Worksheets("AN_PRM-36tmp").Range("A1")
' now i tell the array i want it to use the data on the new sheet
chkarray = Worksheets("AN_PRM-36tmp").Range("A1:B5")
'this was my look up for the nonsense that i wanted to do later
For i = LBound(chkarray, 1) To UBound(chkarray, 1)
For j = LBound(chkarray, 2) To UBound(chkarray, 2)
Debug.Print chkarray(i, j)
If chkarray(i, j) = "Pass" Then
chk1 = True
Else
chk2 = False
End If
Next
Next
If chk1 = True And chk2 = True Then
cz.Interior.ColorIndex = 4
Else
cz.Interior.ColorIndex = 3
End If
' this last bit will get rid of the new sheet and not ask you are you sure you want it gone.
Application.DisplayAlerts = False
Sheets("AN_PRM-36tmp").Delete
Application.DisplayAlerts = True
End Sub

Resources