I am trying to delete entire rows with duplicate values. If the values in column "E" are the same in two rows, I want to delete all row with that value that is duplicated.
The other fields might be or not duplicates of that row and there might be up to ten duplicates and the total number of rows is large ( #rows >4000). This is just one part of a large macro, so I cannot use excel functions. This is what I have so far for deleting rows:
Sub AAAAH()
Application.ScreenUpdating = False
Dim i As Single
Dim j As Single
BottomLineRelease = Sheets("Hours Of Interest").Range("E" & Rows.Count).End(xlUp).Row + 1
rowcount = Sheets("Hours Of Interest").Range("E2:E" & BottomLineRelease).Rows.Count
For i = 2 To Sheets("Hours Of Interest").Cells(Rows.Count, "E").End(xlUp).Row
If Sheets("Hours Of Interest").Range("E" & i) = Sheets("Hours Of Interest").Range("E" & i - 1) Then
j = i - 1
Rows(j).Select
Selection.delete Shift:=xlUp
End If
Next i
Application.ScreenUpdating = True
End Sub
This not only crashes Excel, but the "Selection.delete Shift: =xlup" will not allow the "delete" to stay capitalized. Every time I click away, it goes back to lower case.
Does anyone know a faster or at least functional way to delete these duplicate rows in VBA?
Selection (=Application.Selection) is declared only as Object because it can take various objects (a range, a shape object, a chart etc. etc.). Therefore the intellisense doesn't work as well, it is only determined during execution if .Delete is a valid method.
Try
Sheets("Hours Of Interest").Rows(j).Delete Shift:=xlUp
If you use Sheets(...).Range in your code, you should never get lazy and never use Range or Rows or Cells without that explicit reference, you might be deleting on a different worksheet.
Furthermore, if you delete rows from the top down, every delete changes the row numbers of the following lines.
So you should delete backwars with
for i = [..maximum..] to 0 step -1
You don't need a loop to isolate unique values. You can filter column E for unique values, copy those to a new sheet, and delete the old sheet.
lastRow = Range("A1000000").End(xlUp).Row
Range("A1:H" & lastRow).AdvancedFilter Action:=xlFilterInPlace, _
CriteriaRange:=Range("E1:E" & lastRow), Unique:=True
Cells.Copy
Sheets.Add
ActiveSheet.Paste
Application.DisplayAlerts = False
Sheets("oldSheet").Delete
Application.DisplayAlerts = True
This is way faster than a loop, if memory serves.
Related
I'm very new to VBA and learning through code I find on the internet, and also using macros to see code.
I have an imported xls with three columns of data. I have code that does the following:
Inserts a new column A
Deletes column B
Delete rows with no data
Inserts two columns
So far - okay. What I am then trying to do is insert a number starting at 1 in column A1 and sequentially filling in until all rows with records have a number. I used a macro to see the code, but the range will vary (i.e. there are not always 52 rows in my import).
Is there a way to make this dynamic by only applying a number where there is data in the row (Column B will always have data)?
Thanks in advance - all help greatly appreciated!
Sub DeleteBlankRows()
Dim x As Long
Dim lastRow As Long
Dim A As Long
' INSERT A NEW COLUMN A FOR NUMERICAL SEQUENCE
Columns("A:A").Select
Selection.Insert Shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
'DELETE ALL BLANK ROWS
With ActiveSheet
For x = .Cells.SpecialCells(xlCellTypeLastCell).Row To 1 Step -1
If WorksheetFunction.CountA(.Rows(x)) = 0 Then
ActiveSheet.Rows(x).Delete
End If
Next
End With
'add two new columns for population
ActiveCell.Offset(0, 2).Columns("A:A").EntireColumn.Select
Selection.Delete Shift:=xlToLeft
ActiveCell.Columns("A:B").EntireColumn.Select
Selection.Insert Shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
Selection.NumberFormat = "#"
'code to enter a sequential number starting at 1 for every row that has a record
ActiveSheet.Range("A1").Select
ActiveCell.Select
ActiveCell.FormulaR1C1 = "1"
ActiveCell.Select
Selection.AutoFill Destination:=ActiveCell.Range("A1:A52"), Type:= _
xlFillSeries
ActiveCell.Range("A1:A52").Select
ActiveWindow.SmallScroll Down:=15
End Sub
There are a lot of stuff to improve your my code, but this should get you started
Some things to begin:
Use option explicit at the top of your modules so you don't have unexpected behavior with undefined variables
Always indent your code (see www.rubberduckvba.com a free tool that helps you with that)
Try to separate your logic defining variables and the reusing them
Name your variables to something meaningful and easy to unterstand (avoid x or r)
Write the code steps in plain English first, then develop it in VBA
Check the code's comments, and adapt it to fit your needs
Code
Public Sub PrepareFormat()
' Set a target sheet
Dim targetSheet As Worksheet
Set targetSheet = ActiveSheet ' This could be always the same sheet. If so, replace activesheet with thisworkbook.Sheets("NameOfTheSheet")
' Insert a new column for numerical sequence
targetSheet.Columns("A:A").Insert shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
' Delete all blank rows
Dim counter As Long
With targetSheet
For counter = .Cells.SpecialCells(xlCellTypeLastCell).Row To 1 Step -1
If WorksheetFunction.CountA(.Rows(counter)) = 0 Then
.Rows(counter).Delete
End If
Next counter
End With
' Add two new columns for population (this next lines would make column B empty, so filling sequentally would not work below
'targetSheet.Columns("D:D").Delete shift:=xlToLeft
'targetSheet.Columns("A:B").Insert shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
'targetSheet.Columns("A:B").CurrentRegion.NumberFormat = "#" -> commented this line because cells are going to be empty. What do you want to format as text? maybe this could go after you add the numbers. Also formatting the whole column is a waste of resources
' Insert a number starting at 1 in column A1 (added number 2 to fill down in sequence)
targetSheet.Range("A1").Value = 1
targetSheet.Range("A2").Value = 2
' Sequentially fill in until all rows with records have a number (this doesn't take into account if there are gaps in column b)
Dim referenceRange As Range
Set referenceRange = targetSheet.Range("B1:B" & targetSheet.Range("B" & targetSheet.Rows.Count).End(xlUp).Row)
targetSheet.Range("A1:A2").AutoFill Destination:=referenceRange.Offset(0, -1)
End Sub
Let me know if it works
PS. Check Sidar's answer on how to properly delete empty rows: https://stackoverflow.com/a/9379968/1521579
Could you try this?
'code to enter a sequential number starting at 1 for every row that has a record
'remove your code from here on and substitute with the following
With ActiveSheet
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
ActiveSheet.Range("A1").Select
With ActiveCell
.FormulaR1C1 = "1"
.AutoFill Destination:=ActiveCell.Range("A1:A" & LastRow), Type:=xlFillSeries
End With
I hope someone can help me, as I know that the macro I am trying to create would save about 30 minutes' work if it comes off.
Once a month I download a financial transactions report; the search variables I need to use are in column C. The number of rows for each search variable changes from month to month. I need to search for all rows containing a given variable (e.g. VDEN, VDEM VDEF; these are exact search terms), copy all rows containing the variable and paste them into a one of five workbooks, each with several worksheets in it.
I have some code to do this with one term and one worksheet location, but I don't know how to loop it so that it goes back for a new variable and starts the whole process all over again.
This is what I have already:
Sub Macro2()
Workbooks.Open Filename:="C:\Users\jo\Desktop\Month End.xlsx"
Columns("A:L").Select
Selection.EntireColumn.Hidden = False
Application.DisplayAlerts = False
Cells.Find("Total:").Rows(1).EntireRow.Delete
Application.DisplayAlerts = True
Dim lastrow As Long
lastrow = ActiveSheet.Cells(ActiveSheet.Rows.Count, "A").End(xlUp).Row
With ActiveSheet.Range("a1:l" & lastrow)
.AutoFilter Field:=3, Criteria1:="VDEN"
.Offset(1, 0).Copy
End With
Workbooks.Open Filename:="X:\admin\Finance\2016-17\Transaction Lists\Sample Transactions 2016-17.xlsx"
Windows("Month End.xlsx").Activate
Windows("Sample Transactions 2016-17.xlsx").Activate
Sheets("Pre-Sessional").Select
Columns("A:L").Select
Selection.EntireColumn.Hidden = False
Windows("Sample Transactions 2016-17.xlsx").Activate
Worksheets("Pre-Sessional").Activate
Range("A" & Rows.Count).End(xlUp).Offset(1).Select
ActiveSheet.Paste
Range("D:D,F:F,G:G,H:H,K:K").Select
Selection.EntireColumn.Hidden = True
Windows("Month End.xlsx").Activate
ActiveSheet.ShowAllData
End Sub
Can anybody help?
Many thanks
Look at the OR clause in your filter. I'd suggest recording doing a multiple selection filter, like this And Or Criteria about 2/3 down. Or use a for next loop on an array. Like so Looping Array
My data looks like this.
I would like to delete rows where the value in column named Position from first has value 1. I wrote a macro to do that which is like this
Sub DEL_row()
row_number = 2
Do
DoEvents
row_number = row_number + 1
Position_from_first = Sheet8.Range("E" & row_number)
If InStr(Position_from_first, "1") >= 1 Then
Sheet8.Rows(row_number & ":" & row_number).Delete
End If
Loop Until Position_from_first = ""
MsgBox "completed"
End Sub
But when i run it, I get an error. The msg box says completed but it does not do the required job when I go through the excel sheet.
Can anyone help me what the problem would be.
Thanks a lot for helping...
While your concept is spot on, there are some errors in your syntax and your understanding of VBA. I have listed them below.
No need for Do Events in this code at all.
When deleting rows, you need to start from the last row and step back to the beginning in your looping because as you soon as you delete a row it affects all the row references below that, so your loop count will miss rows if you go forward.
You also need to use .EntireRow.Delete. .Delete will only delete cell content.
I have fixed your code below to reflect these issues. I have also provided a simpler alternative to solve your problem below that which uses the AutoFilter method.
Sub DEL_row()
'get last used row in column E
row_number = Sheet8.Range("E" & Sheet8.Rows.Count).End(xlup).Row
For x = row_number to 2 Step -1
Position_from_first = Sheet8.Range("E" & row_number)
If InStr(Position_from_first, "1") >= 1 Then
'If Sheet8.Range("E" & row_number) = 1 ' this will also work
Sheet8.Rows(row_number & ":" & row_number).EntireRow.Delete
End If
Next
MsgBox "completed"
End Sub
You can avoid the loop altogether (and save time) if you use AutoFilter, like this:
Sub DEL_row()
With Sheet8
'get last used row in column E
row_number = .Range("E" & .Rows.Count).End(xlup).Row
With .Range("E1:E" & row_number)
.AutoFilter 1, "1"
.Offset(1).SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
.AutoFilterMode = False
End With
Msgbox "completed"
End Sub
I gather that you do not want to delete 11, 12, 31, ... etc. so using the InStr function is not an appropriate method of identifying the correct rows to delete. Your sample data image shows column E as having true, right-aligned numbers and the Range.Value2 property can be directly compared against a 1 to determine if the row should be removed.
As mentioned, working from the bottom to the top is important when deleting rows. If you work from the top to the bottom you risk skipping a row when a row has been deleted, everything shifts up and then you increment to the next row.
Option Explicit
Sub DEL_1_rows()
'declare your variables
Dim rw As Long
'this will greatly improve the speed involving row deletion
Application.ScreenUpdating = False
With sheet8 '<~~ this is a worksheet CodeName - see below
For rw = .Cells(Rows.Count, 5).End(xlUp).Row To 2 Step -1
If .Cells(rw, 5).Value2 = 1 Then .Rows(rw).EntireRow.Delete
Next rw
End With
Application.ScreenUpdating = True
MsgBox "completed"
End Sub
An AutoFilter Method can speed up identifying the rows to delete. However, a single large bulk deletion can still take a significant amount of processing time.
Option Explicit
Sub DEL_Filtered_rows()
'this will greatly improve the speed involving row deletion
Application.ScreenUpdating = False
With Sheet8 '<~~ this is a worksheet CodeName - see below
If .AutoFilterMode Then .AutoFilterMode = False
With .Range(.Cells(1, 5), .Cells(Rows.Count, 5).End(xlUp))
.AutoFilter field:=1, Criteria1:=1
With .Resize(.Rows.Count, .Columns.Count).Offset(1, 0)
If CBool(Application.Subtotal(103, .Cells)) Then
.SpecialCells(xlCellTypeVisible).EntireRow.Delete
End If
End With
.AutoFilter
End With
End With
Application.ScreenUpdating = True
MsgBox "completed"
End Sub
I found it a little odd that you were identifying the worksheet with a Worksheet .CodeName property rather than a Worksheet .Name property. I've included a couple of links to make sure you are using the naming conventions correctly. In any event, I've use a With ... End With statement to avoid repeatedly reidentifying the parent worksheet.
I have a large data sheet that I want to search in VBA based on 3 sets of criteria. Each row entry can be assumed to be unique. The format of the sheet/data itself cannot be changed due to requirements. (I've seen several posts on related questions but haven't found a working solution for this yet.)
At first I used the classic VBA find method in a loop:
Set foundItem = itemRange.Find(What:=itemName, Lookin:=xlValues, lookat:=xlWhole, SearchOrder:=xlByRows)
If Not foundItem Is Nothing Then
firstMatchAddr = foundItem.Address
Do
' *Check the other fields in this row for a match and exit if found*
Set foundItem = itemRange.FindNext(foundItem)
Loop While foundItem.Address <> firstMatchAddr And Not foundItem Is Nothing
End If
But because this needs to be called a number of times on large sets of data, the speed of this was no good.
I did some searching and found that I could use the match method with index. So I unsuccessfully tried many variations of that such as:
result = Evaluate("=MATCH(1, (""" & criteria1Name & """=A2:A" & lastRow & ")*(""" & criteria2Name & """=B2:B" & lastRow & ")*(""" & criteria3Name & """=C2:C" & lastRow & "), 0)")
And
result = Application.WorksheetFunction.Index(resultRange, Application.WorksheetFunction.Match((criteria1Name = criteria1Range)*(criteria2Name = criteria2Range)*(criteria3Name = criteria3Range))
And
result = Application.WorksheetFunction.Index(resultRange, Application.WorksheetFunction.Match((criteria1Range=criteria1Name )*(criteria2Range=criteria2Name )*(criteria3Range=criteria3Name ))
Then I tried using AutoFilter to sort:
.Range(.Cells(1,1), .Cells(lastRow, lastCol)).AutoFilter Field:=1, Criteria1:="=" & criteria1Name
.Range(.Cells(1,1), .Cells(lastRow, lastCol)).AutoFilter Field:=2, Criteria1:="=" & criteria2Name
.Range(.Cells(1,1), .Cells(lastRow, lastCol)).AutoFilter Field:=3, Criteria1:="=" & criteria3Name
But because one of the sorting columns contains dates, I had issues getting AutoFilter to work properly.
My question is, how can I search through columns in Excel VBA based on multiple criteria, without looping, returning either the row number or the value in the cell of that row that I am interested in?
You could use an Advanced Filter. Put the column headers in a separate part of the sheet (or a different sheet altogether). Under those column headers, put the criteria you're looking for in each column. Then name that range (including the headers) something like "Criteria". Then the macro becomes:
Sub Macro1()
Sheets("Sheet1").Range("A1").CurrentRegion.AdvancedFilter xlFilterInPlace, Range("Criteria")
End Sub
As a follow up to my comment below, to have the VBA create the criteria range behind the scenes:
Sub Macro1()
'Code up here that defines the criteria
Application.ScreenUpdating = False
Application.DisplayAlerts = False
With Sheets.Add
'Create the advanced filter criteria range
.Range("A1") = "HeaderA"
.Range("B1") = "HeaderB"
.Range("C1") = "HeaderC"
.Range("A2") = criteria1Name
.Range("B2") = criteria2Name
.Range("C2") = criteria3Name
'Alternately, to save space:
'.Range("A1:C1").Value = Array("HeaderA", "HeaderB", "HeaderC")
'.Range("A2:C2").Value = Array(criteria1Name, criteria2Name, criteria3Name)
'Then perform the advanced filter
Sheets("Sheet1").Range("A1").CurrentRegion.AdvancedFilter xlFilterInPlace, .Range("A1:C2")
'Remove behind the scenes sheet now that the filter is completed
.Delete
End With
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
You can use EVALUATE for multiple criteria like so to return the row numbers of mathcing values. This uses the same approach as Is it possible to fill an array with row numbers which match a certain criteria without looping?
It searches 50000 rows to match
the first four letters in column A matches fred
the date in B to be greater than 1/1/2001
apple in column 5
Any rows matching these criteria are retuned as row numbers in x
(rows 1 and 5 in picture below)
code
Sub GetEm2()
x = Filter(Application.Transpose(Application.Evaluate("=IF((LEFT(A1:A10000,4)=""fred"")*(B1:B10000>date(2001,1,1))*(C1:C10000=""apple""),ROW(A1:A10000),""x"")")), "x", False)
End Sub
Application.Transpose is limited to 65536 cells, so a longer range needs to be "chunked" into pieces.
I have the following macro defined which inserts rows into a sheet. After the rows are inserted at specified start addresses, the various ranges are then converted into Tables. My initial thoughts are that the issue lies with the use of xlDown - since this is the place in code where rows are inserted.
At present I have 7 such ranges, however the issue is that first three always have an additional row inserted - this was previously working with no issues, so the fact that its misbehaving is a puzzle to me.
The remaining ranges are correct. The tableStartAdress refers to named ranges whose values correspond to the first cell below the green title, ie A4, A12 etc. rowsToInsert for this example is always 38.
Sub InsertTableRows(tableStartAdress As String, rowsToInsert As Integer)
Dim i As Integer
Dim rowToCopy As String
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.DisplayAlerts = False
Range(tableStartAdress).Offset(1, 0).Rows.Copy
rowToCopy = Range(tableStartAdress).Offset(1, 0).row & ":" & _
Range(tableStartAdress).Offset(1, 0).row
Rows(rowToCopy).Select
Selection.Copy
Range(tableStartAdress).Offset(1, 0).Select
ActiveCell.Resize(rowsToInsert, 1).Select
Selection.Insert Shift:=xlDown
Application.CutCopyMode = False
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
The following pictures depict what I mean.
Before:
.
Once data is populated the first three range/tables have an extra row
, ,
Whilst the remainder are correct
I'd suggest simplifying your code to start. (Might help you track down were things are going wrong.) Since you don't need to select a range before you do something with it....
rowToCopy = Range(tableStartAdress).Offset(1, 0).Row & _
":" & Range(tableStartAdress).Offset(1, 0).Row
Rows(rowToCopy).Select
Selection.Copy
Range(tableStartAdress).Offset(1, 0).Select
ActiveCell.Resize(rowsToInsert, 1).Select
Selection.Insert Shift:=xlDown
is the same as...
Range(tableStartAdress).Offset(1, 0).EntireRow.Copy
Range(tableStartAdress).Offset(1, 0).Resize(rowsToInsert, 1).Insert Shift:=xlDown
which is much easier to look at. A couple thoughts: First, are you sure that tableStartAddress is really always a single cell (and the correct cell)? Are you sure that rowsToInsert is always 38? Beyond that, your code as it's currently written is copying an entire row and inserting it into a range that's theoretically 38 rows by 1 column. I would recommend rewriting this so you first insert however many rows you want, then fill the 38 x 1 range with the data that belongs there.