More efficient way of deleting rows - excel

I have some code that deletes rows that are not in a specified list of row numbers that are to be kept. It functions as intended.
For lRow = numRowsInBBS To 1 Step -1
lMatch = 0
On Error Resume Next
lMatch = Application.Match(lRow, ws.Range("AE4:AE" & numRows).Value, 0&)
On Error GoTo 0
If Not CBool(lMatch) Then
wsImport.Cells(lRow, 1).EntireRow.Delete
End If
Next
End Sub
However, this takes a monumental amount of time. To do this on 150 rows takes a couple of minutes of processing. I have documents that could be 1000s of rows long.
Essentially I want to delete all rows on a specified sheet EXCEPT for the row numbers specified in AE4:AE?? (This is calculated by numRows) on a different sheet.
The data range is not contiguous, AE4:AE?? could list numbers 3,4,5,33,66,101,110 as rows to keep. All other rows are to be deleted.
Is there a better way of achieving my goal here?
I hear autofilter is much faster, but don't see how I can apply it here as I am not matching a string or any content in the cells, simply the row numbers.
EDIT:
As per suggestion, I have tried the autofilter way:
Dim rowsToKeep() As Variant: rowsToKeep = ws.Range("AE4:AE" & numRows)
Dim allRows As Range: Set allRows = Range("ZZ1:ZZ" & numRowsInBBS)
With wsImport
.Range(allRows.Address).Formula = "=row()"
.Range(allRows.Address).AutoFilter Field:=1, Criteria1:=rowsToKeep, Operator:=xlFilterValues
.Range(allRows.Address).SpecialCells(xlCellTypeVisible).EntireRow.Delete
.Range(allRows.Address).AutoFilter Field:=1
End With
I am trying to:
Set the data in the range AE4:AE?? as the data for an array -
Then use ZZ as a helper column containing row numbers -
Then filter out the rows I want to keep -
Then delete all visible rows -
Then show the rows that were filtered
However, the filter is hiding everything, which suggests to me there is something wrong with rowsToKeep, and yes AE4:AE?? on the other sheet does contain values.

Try this (Untested)
Deleting rows in a loop will always be slower. What the below code does is that it stores the rows that needs to be deleted in a range object and then deletes them at the end of the loop in One go.
Dim delRng As Range
For lRow = 1 To numRowsInBBS
On Error Resume Next
lMatch = Application.Match(lRow, ws.Range("AE4:AE" & numRows).Value, 0&)
On Error GoTo 0
If Not CBool(lMatch) Then
If delRng Is Nothing Then
Set delRng = wsImport.Rows(lRow)
Else
Set delRng = Union(delRng, wsImport.Rows(lRow))
End If
End If
Next
If Not delRng Is Nothing Then delRng.Delete
Using CountIf (Untested)
Dim delRng As Range
For lrow = 1 To numRowsInBBS
If Application.WorksheetFunction.CountIf(ws.Range("AE4:AE" & numRows), lrow) > 0 Then
If delRng Is Nothing Then
Set delRng = wsImport.Rows(lrow)
Else
Set delRng = Union(delRng, wsImport.Rows(lrow))
End If
End If
Next
If Not delRng Is Nothing Then delRng.Delete

Related

Excel VBA Fill Series

I need to fill a range from 10,000 all the way until the end of the column. I have the lRow variable that is finding the last row, and then I have the IF loop that is filling the range just like I need to. The clear contents part is to remove any previous values and fill new ones. The Range("E2").Value = 1 is there to start the series.
The problem is I can't get it to fill the Range("F2:F" & lRow) in a step value of 10,000. The macro just fills out in a step value of 1. Any ideas?
I've tried recording macros but it never works quite right. It needs to be sort of dynamic as the list will grow over time.
This is what it should look like:
Sub MLOS_PriorityTable_StepValues()
Dim ws As Worksheet
Dim lRow As Long
Dim featOrder As Range
Dim Style As Range
Dim dataRange As Range
Dim currentArea As Range
Set ws = ActiveSheet
Set featOrder = ws.Range("A1:ZZ1").Find("FeatureOrder")
Set Style = ws.Range("A1:ZZ1").Find("Style")
Range("E2:E1000").ClearContents
Range("F2:F1000").ClearContents
Range("E2").Value = 1
Range("F2").Value = 10000
lRow = Cells(Rows.Count, Style.Column).End(xlUp).Row
On Error Resume Next
Set dataRange = Range("E2:E" & lRow).SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not dataRange Is Nothing Then
For Each currentArea In dataRange.Areas
With currentArea
With .Offset(-1, 0).Resize(.Rows.Count + 1)
.Cells(1).AutoFill Destination:=.Cells, Type:=xlFillSeries
End With
End With
Next currentArea
End If
On Error Resume Next
Set dataRange = Range("F2:F" & lRow).SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not dataRange Is Nothing Then
For Each currentArea In dataRange.Areas
With currentArea
With .Offset(-1, 0).Resize(.Rows.Count + 1)
.Cells(1).AutoFill Destination:=.Cells, Type:=xlFillSeries
End With
End With
Next currentArea
End If
End Sub
Is the result of F just E*10000? It doesn't matter too much, there's a few ways to solve.
Find your last row number (which you did as lRow)
Iterate for i = 2 to lRow which will loop from the top row with data to the last
Do something in here like Range("F"&i) = Range("E"&i)*10000 assuming that is the relationship or Range("F"&i) = Range("F"&i-1)+10000
next i
it will just iterate cell by cell, make the calculation, and move on.
You may need to make it more robust if you have a script that hops around multiple sheets or workbooks so the range or cell or whatever references you use are correct.
Don't know how fast this would be vs the fill Series function but it doesn't look at face value like it would be too slow.
let me know how you get on!
Rob S

Select only content that's needed

I've created an Excel document with a sheet, named "Plakken". This sheet contains a button which paste a table, that the user copied from an intranet page, on to the sheet "Template (2)".
In the example below, you can see the table on "template (2)"
What i like to do now, is copy some data from the pasted table to sheet "opslaan", the data i want to copy is:
Artikelnummer (article number)
Artikelnaam (article name)
datum (date)
These are the 3 left columns, for example:
But the data in the table is seperated with rows containing the word "vulgebied".
Example:
I'm searching for a way to only copy all the data listed above, and paste them on the sheet "opslaan".
The table is different every time, sometimes there are more or few lines between the "vulgebied" row, but the style of the table is always the same.
Edit:
I think something that might work is:
Option Explicit
Sub DeleteRow()
Dim i As Long
Dim rng As Range
With ActiveWorkbook.Sheets(1)
For i = 100000 To 1 Step -1
With .Cells(i, "C")
If .Value = "Vulgebied" Then
If rng Is Nothing Then
Set rng = .Cells
Else
Set rng = Application.Union(rng, .Cells)
End If
End If
End With
Next i
If Not rng Is Nothing Then rng.EntireRow.Delete
End With
End Sub
But because the word "vulgebied" contains a different number after the word, i don't know how to solve that..
Example: https://files.fm/u/n4k4z6yz
In order to find cells that might contain the word "Vulgebied", use the function Instr.
Modified Code
Option Explicit
Sub DeleteRow()
Dim i As Long
Dim DelRng As Range
Application.ScreenUpdating = False
With ThisWorkbook.Sheets("Template (2)")
For i = 100000 To 1 Step -1
If InStr(.Range("C" & i).Value, "Vulgebied") > 0 Then
If DelRng Is Nothing Then
Set DelRng = .Range("C" & i)
Else
Set DelRng = Application.Union(DelRng, .Range("C" & i))
End If
End If
Next i
If Not DelRng Is Nothing Then DelRng.EntireRow.Delete
End With
Application.ScreenUpdating = False
End Sub
Note: a safer way to make sure "Vulgebied" is found within a cell is by using the LCase function (converts the all characters of the word to lower case), see below:
If InStr(LCase(.Range("C" & i).Value), "vulgebied") > 0 Then

Hide table rows *unless* any of 3 columns (in that row) are not blank

I've built this code, and it's working fine. However I expect there must be a more elegant way to embed the range 'c' into the Evaluate function rather than how I've used 'r' to determine the row number, and build that into the reference.
(I'm learning). Copy of (very stripped down) xlsm available here: https://www.dropbox.com/s/e6pcugqs4zizfgn/2018-11-28%20-%20Hide%20table%20rows.xlsm?dl=0
Sub HideTableRows()
Application.ScreenUpdating = False
Dim c As Range
Dim r As Integer
For Each c In Range("ForecastTable[[Group]:[Item]]").Rows
r = c.Row
If Application.Evaluate("=COUNTA(B" & r & ":D" & r & ") = 0") = True Then
c.EntireRow.Hidden = True
Else: c.EntireRow.Hidden = False
End If
Next c
Application.ScreenUpdating = True
End Sub
There's no specific question/problem, but here's my suggested code improvements.
Most notably, I wouldn't execute the Hidden procedure until you have all the rows. That way you don't have repeatedly do something that only need be completed once. This will always be the best practice when looping and manipulating data. Make changes to the sheet AFTER you have identified the range.
With the above change, you don't need to turn off ScreenUpdating.
The Evaluate function is fine, but isEmpty is probably the best option. There are probably slightly faster methods, perhaps checking multiple if-statements, but that's getting into fractions of a second over thousands of rows (probably not worth researching).
Technically you don't really need to loop by rows. You can get by with a single cell in a row, then checking the next two over, see utilization of Offset to generate that range. This also creates a more dynamic than using hard-coded columns ("A"/"B"...etc")
Long is recommended over Integer but this is pretty small, and I'm only mentioning it because I posted about it here.. Technically you don't even need it with the above changes.
Here's the code:
Sub HideTableRows()
Dim c As Range, hIdeRNG As Range, WS As Worksheet
'based on OP xlsm file.
Set WS = Sheet4
'used range outside of used range to avoid an if-statement on every row
Set hIdeRNG = WS.Cells(Rows.Count, 1)
'loops through range of single cells for faster speed
For Each c In Range("ForecastTable[Group]").Cells
If IsEmpty(Range(c, c.Offset(0, 2))) = 0 Then
'only need a single member in the row.
Set hIdeRNG = Union(hIdeRNG, c)
End If
Next c
'Hides rows only if found more than 1 cell in loop
If hIdeRNG.Cells.Count > 1 Then
Intersect(WS.UsedRange, hIdeRNG).EntireRow.Hidden = True
End If
End Sub
Final Thought: There's some major enhancements coming out to Excel supposedly in early 2019 that might be useful for this type of situation if you were looking for a non-VBA solution. Click here for more info from MS.
Flipping the logic a bit, why not just filter those three columns for blanks, then hide all the visible filtered blank rows in one go?
Something like this:
Sub DoTheHide()
Dim myTable As ListObject
Set myTable = Sheet4.ListObjects("ForecastTable")
With myTable.Range
.AutoFilter Field:=1, Criteria1:="="
.AutoFilter Field:=2, Criteria1:="="
.AutoFilter Field:=3, Criteria1:="="
End With
Dim rowsToHide As Range
On Error Resume Next
Set rowsToHide = myTable.DataBodyRange.SpecialCells(xlCellTypeVisible)
On Error GoTo 0
myTable.AutoFilter.ShowAllData
If Not rowsToHide Is Nothing Then
rowsToHide.EntireRow.Hidden = True
End If
End Sub
Since c is used to iterate over the rows and each row contains the 3 cells in question ("=COUNTA(B" & r & ":D" & r & ") = 0") is equivalent to ("=COUNTA(" & c.Address & ") = 0"). But using the WorksheetFunction directly is a better appraoch.
It should be noted that Range("[Table]") will return the proper result as long as the table is in the ActiveWorkbook. It would be better to useThisWorkbook.Worksheets("Sheet1").Range("[Table]")`.
Sub HideTableRows()
Application.ScreenUpdating = False
Dim row As Range, target As Range
With Range("ForecastTable[[Group]:[Item]]")
.EntireRow.Hidden = False
For Each row In .rows
If Application.WorksheetFunction.CountA(row) = 0 Then
If target Is Nothing Then
Set target = row
Else
Set target = Union(target, row)
End If
End If
Next
End With
If Not target Is Nothing Then target.EntireRow.Hidden = True
Application.ScreenUpdating = True
End Sub

Finds cells on multiple conditions and cut paste the same to another sheet (Simplified)

Any alternative or suggestions to Fasten the below stated code that finds cells on multiple conditions and cut paste the same to another sheet.
Sub test()
'For Move Entire Row to New Worksheet if Cell Contains Specific Text's
'Using autofilter to Copy rows that contain certain text to a sheet called commodity
Dim LR As Long
Range("A2").EntireRow.Insert Shift:=xlDown
LR = Sheets("Data").Cells(Rows.Count, "E").End(xlUp).Row
LR1 = Sheets("Commodity").Cells(Rows.Count, "A").End(xlUp).Row + 1
With Sheets("Data").Range("e:e")
.AutoFilter Field:=1, Criteria1:=("*SILVER*")
.SpecialCells(xlCellTypeVisible).EntireRow.Copy
Destination:=Sheets("Commodity").Range("A" & LR1)
.SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
With Sheets("Data").Range("e:e")
.AutoFilter Field:=1, Criteria1:=("*GOLD*")
.SpecialCells(xlCellTypeVisible).EntireRow.Copy
Destination:=Sheets("Commodity").Range("A" & LR1)
.SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
With Sheets("Data").Range("e:e")
.AutoFilter Field:=1, Criteria1:=("*MCX*")
.SpecialCells(xlCellTypeVisible).EntireRow.Copy
Destination:=Sheets("Commodity").Range("A" & LR1)
.SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
End Sub
As well as the good suggestions #ShaiRado is making, what's slowing this down is your repeated interactions with the Excel functions and interface. Ideally, you'd read the data into variables within VBA, and then, all within VBA, check for matches and prepare an output array. That way you'd only have one interaction between VBA and Excel, namely to write the output array to your target sheet. It's also costly in time to delete one row at a time, so you might be better to create just one 'delete range' and hit it all in one go.
Skeleton code to achieve this is given below (but note, it will need a 'column offset' calculation if your used range doesn't start at "A", and you might choose a more reliable function than UsedRange). You call the routine like so:
TransferData "Silver", "Gold", "MCX"
And the routine itself might like something like this:
Private Sub TransferData(ParamArray searchItems() As Variant)
Dim srcData As Variant
Dim txData() As Variant
Dim item As Variant
Dim r As Long, c As Long
Dim txIndexes As Collection
Dim delRng As Range
'Read source data into an array
'Note: I've used UsedRange as I don't know your sheet layout
srcData = ThisWorkbook.Worksheets("Data").UsedRange.Value2
'Check for matches and record index number
Set txIndexes = New Collection
For r = 1 To UBound(srcData, 1)
For Each item In searchItems
If srcData(r, 5) = item Then
txIndexes.Add r
Exit For
End If
Next
Next
'Trasfer data to output array
ReDim txData(1 To txIndexes.Count, 1 To UBound(srcData, 2))
r = 1
For Each item In txIndexes
For c = 1 To UBound(srcData, 2)
txData(r, c) = srcData(item, c)
Next
r = r + 1
Next
'Write the transfer data to target sheet
With ThisWorkbook.Worksheets("Commodity")
.Cells(.Rows.Count, "A").End(xlUp).Resize(UBound(txData, 1), UBound(txData, 2)) = txData
End With
'Delete the transfered rows
For Each item In txIndexes
With ThisWorkbook.Worksheets("Data")
If delRng Is Nothing Then
Set delRng = .Cells(item, "A")
Else
Set delRng = Union(delRng, .Cells(item, "A"))
End If
End With
Next
If Not delRng Is Nothing Then delRng.EntireRow.Delete
End Sub

Excel VBA Range Finding and Deletion

I have a couple of questions regarding VBA which I hope you folks can help me with. I'm a very new coder to VBA, so any help you can provide is very much appreciated.
Objective - Remove all rows from "cellRange" if a similar value is found in "valueRange"
Code so far
Sub DeleteRows()
Set valueRange = Worksheets("Delete Rows").Range("A4:A65000")
Set cellRange = Worksheets("Load File").Columns(Worksheets("Delete Rows").Range("F1").Value)
For Each Cel In cellRange.Cells
For Each Value In valueRange.Cells
If Cel.Value = Value.Value Then
Cel.EntireRow.Delete
End If
Next Value
Next Cel
End Sub
Problem 1: valueRange doesn't always have all 65000 rows populated. How can I only make it so that the range only grabs those from A4:(until it hits an empty column)
Problem 2: Similar to problem 1, but the cellRange
Problem 3: Whenever a row is deleted, it seems to affect how the range is set. Meaning that if it deletes row #10 in, then the loop goes to row#11 without checking row #10 again. How can I tell the look to do a second pass or to go through the file again.
P1: Two options here
a) if the Cel.Value is Empty, Exit For
b) proper range selection, refer to this guy here: Excel: Selecting all rows until empty cell
P2: Same as above
P3: As For-Each can't go "backwards" the best you can do is
a) Don't delete the row but store it's number instead e.g. in a Long array, then add a For-Next and delete the "marked" rows like:
For x = UBound(myLongArray)-1 To 0 Step -1
cel(x).EntireRow.Delete
Next x
b) instead of For-Each, store the number of rows (via the ROWS function) in a variable and go through the rows with a 'Step -1' loop
As others mention, you have to step backwards when deleting.
Also, I modified to avoid unnecessary iteration over each cell in ValueRange, instead use the Match() function to check if Cel.Value exists in ValueRange.
Sub DeleteRows()
Dim r as Long
Dim valueRange as Range, cellRange as Range
Dim Cel as Range
Set valueRange = Worksheets("Delete Rows").Range("A4:A65000").End(xlUp) '<~~ Get the last unused row
Set cellRange = Worksheets("Load File").Columns(Worksheets("Delete Rows").Range("F1").Value)
For r = cellRange.Cells.Count to 1 Step -1 '<~~ When deleting rows you must step backwards through the range to avoid the error you are encountering.'
Set Cel = cellRange.Cells(r)
'Check to see if Cel.Value exists in the ValueRange using the "Match" function'
If Not IsError(Application.Match(Cel.Value,ValueRange,False) Then
Cel.EntireRow.Delete
End If
Next r
End Sub
Here you go.
' Declare your variables to get intellisense
Dim rngDelete As Range
Dim cellRange As Range
Dim valueRange As Range
' Get only the rows with data
Set valueRange = Worksheets("Delete Rows").Range("A4")
If valueRange.Offset(1, 0) <> "" Then
Set valueRange = Worksheets("Delete Rows").Range(valueRange, valueRange.End(xlDown))
End If
' Get only the rows with data
Set cellRange = Worksheets("Load File").Cells(Worksheets("Delete Rows").Range("F1").value,1)
If cellRange.Offset(1, 0) <> "" Then
Set cellRange = Worksheets("Load File").Range(cellRange, cellRange.End(xlDown))
End If
Dim cel As Range
Dim value As Range
' make cel your outer since it has more rows
For Each cel In cellRange.Cells
For Each value In valueRange.Cells
If value.value = cel.value Then
' Don't delete it yet but store it in a list
If rngDelete Is Nothing Then
Set rngDelete = cel.EntireRow
Else
Set rngDelete = Union(rngDelete, cel.EntireRow)
End If
' no need to look further
Exit For
End If
Next
Next
' Wipe them out all at once
rngDelete.Delete

Resources