0

I'm trying to write a code that either deletes or keeps rows by a specific word input by the end-user.

I've created two button actions:

Sub Button1_Click()
Dim cell As Range
word1 = InputBox("Enter a word by which you want to keep rows", "Enter")
For Each cell In Selection
    cell.EntireRow.Hidden = (InStr(1, cell, word1, 1) = 0) 'keep by a word input by the user
Next
End Sub

Sub Button2_Click()
Dim cell As Range
word2 = InputBox("Enter a word by which you want to delete rows", "Enter")
For Each cell In Selection
    cell.EntireRow.Hidden = (InStr(1, cell, word2, 1) = 1) 'delete by a word input by the user
Next
End Sub

However, these buttons don't work quite the way I would like them to do.

Problems:

1) I have to specifically select the cells in the column of the text to be searched; if I select the whole block of data,everything will be deleted.

2) Actually, the program would be handier, if it did its magic from the cell J22 onwards (to the right and downwards) until the end of the data is reached, without the need to select anything. What is the best way to do this?

3) If I use these buttons several times sequentially, the rows that I've already deleted keep popping up again. How to make the delete "permanent" each time I use one of these buttons? By changing Hidden to Delete I start to get run-time errors.

Community
  • 1
  • 1
jaggedjava
  • 440
  • 6
  • 14

4 Answers4

3

When you attempt to delete permanently the macro deletes a row, shifts all of the other rows up one to accomodate and this disrupts the flow of your 'For Each...Next'.

There are a couple of ways around this either way it very much changes the shape of your code.
One of them is to add the rows you wish to delete to a union during the loop and then delete the union outside of the loop (example A below). In any case it sounds like you want to specify the range you want this code to work on so I've incorporated that into each example.

Example A

Sub Button1_Click()
Dim endR As Integer, endC As Integer        'depending on size of sheet may need to change to Long
Dim cell As Range, rng As Range, U As Range
Dim ws As Worksheet

Set ws = Sheets(2) ' change accordingly

endR = ws.UsedRange.Rows.Count
endC = ws.UsedRange.Columns.Count

Set rng = Range(ws.Cells(22, 10), ws.Cells(endR, endC))  ' from cell J22 to last used row of the last used column on the right

word1 = InputBox("Enter a word by which you want to keep rows", "Enter")

For Each cell In rng
    If InStr(1, cell, word1, 1) = 0 Then
      If U Is Nothing Then     ' for the first time the code finds a match 
        Set U = cell.EntireRow  ' add row to be deleted to U variable
      Else
        Set U = Union(U, cell.EntireRow) ' for any subsequent matches, add row to be deleted to Union
    End If
End If
Next

U.Delete

End Sub

The other way to do it would be to define the exact ranges you want to work with at the start of your code and then loop backwards through that range using loop control variables instead of for each, that way when you delete a row, the shift up doesn't impact the loop.

Sub Button2_Click()

Dim r As Integer, c As Integer
Dim endR As Integer, endC As Integer
Dim cell As Range, rng As Range
Dim ws As Worksheet

Set ws = Sheets(2) ' change accordingly

endC = ws.UsedRange.Columns.Count

word2 = InputBox("Enter a word by which you want to delete rows", "Enter")

For c = 10 To endC      ' start from J and move to the right
  endR = ws.UsedRange.Rows.Count ' after each column has been dealt with, re-evaluate the total rows in the worksheet
    For r = endR To 22 Step -1    ' start from the last row and work up
        If InStr(1, ws.Cells(r, c), word2, 1) = 1 Then
            ws.Cells(r, c).EntireRow.Delete
        End If
    Next r
Next c

End Sub
Mark W
  • 108
  • 8
  • Thank you for your suggestions. I got example A to work. I could set the range to be crazy big (say 400 000) and it still was fast, so in that way the latter version was not compulsory. However, as I understand, the downside of the code is that it only scans one column for the search word? – jaggedjava Jan 28 '16 at 20:21
  • Example B works on multiple columns. I've looked into example A and if you expand the test range to A1:B9 it would loop through the cells in the following order, A1, B1, A2, B2. I'm confused about something: if you have the word 'Dog' in A1 and 'Cat' in B1. I click button one and want to preserve the row based on the word dog. In cell A1 it finds a match so doesn't add the row to the union for deletion. However in cell B1 it doesn't find a match so the row gets deleted anyway. Is that the kind of operation you want? – Mark W Jan 29 '16 at 13:11
  • also added edit to post to show how to set the range dynamically to what you want in example A. – Mark W Jan 29 '16 at 13:12
2
  1. With your current code, if you select the whole block of data, it checks each cell in that selection individually and acts accordingly. If you have a range selected like A1:J1,000, it will hide every row unless each cell in every row of the selection contains the input word.
  2. Depending on what you exactly want, you could try something Sheets("Sheet1").Cells(Sheets("Sheet1").Rows.count, 10).End(xlUp).Row this returns the rownumber of the last cell in column 10(J), more examples of this in the code below
  3. This is caused by the for loop and the deletion of rows, say For i = 1 To 100 you check cells A1 to A100, if you then delete a row during that loop, the loop will still continue to 100 and not end at 99, the end of the loop is set before the loop starts and does not change during the loop. More information on that and it's solutions here.

General

  • Avoid the .Select/.Activate methods and .Selection property, it is a source of many bugs.
  • Declare all your variables, use Option Explicit to enforce this.

Here is the refactored code with annotations.

Option Explicit

Sub Button1_Click()
'Keep rows based on input


    'Declaration of variables
    Dim i As Long
    Dim strFilterWord As String
    Dim rngCell As Range
    Dim rngToDelete As Range, rngRow As Range
    Dim arrRow() As Variant, arrTmp() As Variant

    'Setting the filter word
    strFilterWord = InputBox("Enter a word by which you want to keep rows", "Enter")

    With ThisWorkbook.Worksheets("Sheet1") 'Replace "Sheet1" with the actual name of your sheet.
        'Setting up for loop, currently range to loop over is J22:J(lastrow with data)
        For Each rngCell In .Range(.Cells(22, 10), .Cells(Rows.Count, 10).End(xlUp))
            'All values of the current row are combined into an array

            'Determining and setting the range of the current row
            Set rngRow = rngCell.Resize(1, 3)
            'Populate a tmp array with the row range values
            arrTmp = rngRow

            'To use the array, it needs to be 1D, currently it is 2D, section below accomplishes this
            'resize the final array
            ReDim arrRow(LBound(arrTmp, 2) To UBound(arrTmp, 2))
            'Copy values to final array
            For i = LBound(arrTmp, 2) To UBound(arrTmp, 2)
                arrRow(i) = arrTmp(1, i)
            Next i

            'the final array is combined to a single string value with " "(spaces) between each array element
            'if the filterword is not found in the string Instr returns a 0
            'If the filterword is found in the string InStr returns a number corresponding to the start position.
            If InStr(1, Join(arrRow, " "), strFilterWord, vbTextCompare) = 0 Then
                'Test to see if the range to delete is empty or not
                If rngToDelete Is Nothing Then
                    'If the range is empty, it is set to the first row to delete.
                    Set rngToDelete = rngCell.EntireRow
                Else
                    'if the range is not empty, the row to delete is added to the range.
                    Set rngToDelete = Union(rngToDelete, rngCell.EntireRow)
                End If
            End If
        Next rngCell

        'After all cells are looped over, the rows to delete are deleted in one go
        If Not rngToDelete Is Nothing Then rngToDelete.Delete
    End With

End Sub

Sub Button2_Click()
'Keep rows based on input


    'Declaration of variables
    Dim i As Long
    Dim strFilterWord As String
    Dim rngCell As Range
    Dim rngToDelete As Range, rngRow As Range
    Dim arrRow() As Variant, arrTmp() As Variant

    'Setting the filter word
    strFilterWord = InputBox("Enter a word by which you want to delete rows", "Enter")

    With ThisWorkbook.Worksheets("Sheet1") 'Replace "Sheet1" with the actual name of your sheet.
        'Setting up for loop, currently range to loop over is J22:J(lastrow with data)
        For Each rngCell In .Range(.Cells(22, 10), .Cells(Rows.Count, 10).End(xlUp))
            'All values of the current row are combined into an array

            'Determining and setting the range of the current row
            Set rngRow = rngCell.Resize(1, 3)
            'Populate a tmp array with the row range values
            arrTmp = rngRow

            'To use the array, it needs to be 1D, currently it is 2D, section below accomplishes this
            'resize the final array
            ReDim arrRow(LBound(arrTmp, 2) To UBound(arrTmp, 2))
            'Copy values to final array
            For i = LBound(arrTmp, 2) To UBound(arrTmp, 2)
                arrRow(i) = arrTmp(1, i)
            Next i

            'the final array is combined to a single string value with " "(spaces) between each array element
            'if the filterword is not found in the string Instr returns a 0
            'If the filterword is found in the string InStr returns a number corresponding to the start position.
            If InStr(1, Join(arrRow, " "), strFilterWord, vbTextCompare) > 0 Then
                'Test to see if the range to delete is empty or not
                If rngToDelete Is Nothing Then
                    'If the range is empty, it is set to the first row to delete.
                    Set rngToDelete = rngCell.EntireRow
                Else
                    'if the range is not empty, the row to delete is added to the range.
                    Set rngToDelete = Union(rngToDelete, rngCell.EntireRow)
                End If
            End If
        Next rngCell

        'After all cells are looped over, the rows to delete are deleted in one go
        If Not rngToDelete Is Nothing Then rngToDelete.Delete
    End With

End Sub
SilentRevolution
  • 1,495
  • 1
  • 16
  • 31
  • Your code is excellent, many thanks indeed for it (and for the helpful comments). However, it seems that I can't make it to scan through all columns for the search text...? – jaggedjava Jan 28 '16 at 15:17
  • @jaggedjava, the way this is set up, didn't take it into account, and truth be told it did take me a while to figure this out. but here it is. It goes through column `J`, and collects every cell value on that row from each column to the right of `J` in an array then joins the array to a single string and the the filterword is checked against that string. – SilentRevolution Jan 28 '16 at 16:27
  • The idea sounds ingenious. The great thing about your code is that it's blazing fast. However, you get a runtime error, if any of the cells in the data block are empty... I'm actually thinking about filling the empty cells with bogus stuff just to get this code work! – jaggedjava Jan 28 '16 at 19:39
  • To be more specific: if you have something in J22, but K22 and L22 are empty, and at the same time J23, K23 and L24 all contain something, the program hangs. – jaggedjava Jan 28 '16 at 19:55
  • The problem occurs because `arrTmp = rngRow` cannot handle a single cell. Because I didn't know which columns you needed to check I made the width variable to whichever column to the right was filled. answer revised to only but always include columns J, K and L – SilentRevolution Jan 28 '16 at 20:10
  • So: 1) the code works now perfectly, 2) it really is blazing fast, you were right about this in the first place. I think I am going to add a question with the InputBox that asks something like "Roughly, how wide is your data (how many columns)?", I think that will nail this one. Can't thank you enough, indeed. – jaggedjava Jan 28 '16 at 20:36
  • Done. I added the lines `Dim howwide As Long`, `howwide = InputBox("Roughly, how wide is your data (how many columns; add a few more to that just to be sure)", "Enter")`, and `Set rngRow = rngCell.Resize(1, howwide)` to the relevant places in code. I think the program is quite perfect now. – jaggedjava Jan 28 '16 at 20:55
1

This should do the trick

 Option Explicit

 Sub DeletingRowContainingSpecificText()

      Dim DataWorkSheet As Worksheet
      'Change "ThisWorkBook" an "Sheet1" as you require
      Set DataWorkSheet = ThisWorkbook.Worksheets("Sheet1")

      Dim LastRow As Long
      Dim LastColumn As Long

      With DataWorkSheet.UsedRange
           LastRow = .Rows(.Rows.Count).Row
           LastColumn = Columns(.Columns.Count).Column
      End With

      Dim word1 As String

      word1 = InputBox("Enter a word by which you want to keep rows", "Enter")

      Dim RowRange As Range
      Dim RowReference As Long
      Dim RowContent As String
      Dim WordFound As Variant

      'When ever you are deleting you need to start at the end and work your way back
      'Otherwise the row after the row you deleted becomes the current row
      For RowReference = LastRow To 22 Step -1
           'Setting the Row Range from Column J to the end for a specific row
           Set RowRange = ThisWorkbook.Worksheets("Sheet1").Range(Cells(RowReference, "J"), Cells(RowReference, LastColumn))

           Set WordFound = RowRange.Find(What:=word1, LookIn:=xlValues)

           If Not WordFound Is Nothing Then
                'Choose if you want to delete or hidden
                RowRange.EntireRow.Hidden = True
                RowRange.EntireRow.Delete
           End If

      Next RowReference

 End Sub

Just paste the Sub Content into your Button1_Click Sub. Otherwise paste this into your WorkBook Module and then test if it is working first.

I did test it and it worked for me.

NB when working with Deleting Rows or Columns always start at the end of the list and work your way to the beginning, this way the reference doesn't get messed up.

Jean-Pierre Oosthuizen
  • 2,653
  • 2
  • 10
  • 34
  • Many very excellent answers were given by forum users. However, the code in this specific answer does exactly what I'm looking for. It looks everywhere, through all the columns (except not the ones that were excluded), and finds if: 1) the text of a cell is the user specified search text; or 2) part of the text of a cell is the user specified search text. Finally, it deletes the rows accordingly. Thank you very much, indeed! – jaggedjava Jan 28 '16 at 14:12
  • Thanks @jaggedjava, remember to upvote the answers you mark as correct as well or if answers are useful. Enjoy StackOverflow! – Jean-Pierre Oosthuizen Jan 28 '16 at 14:14
  • While this is a good solution, I have to point out that deleting rows individually may take a long time because every time a row is deleted the sheet has to be reordered and this can seriously add up if you have a large data set and many rows to delete. – SilentRevolution Jan 28 '16 at 15:07
  • SilentRevolution: You are very correct. However, the good thing is that this code looks through all the columns, not just one, for the search word. I tried to tweak your code to do the same, but being a beginner, I failed miserably... – jaggedjava Jan 28 '16 at 15:15
  • @SilentRevolution I agree. The deleting after using `Union` to join the ranges is probably quicker because of the single `.Delete` operation. I posted on CodeReview about deleting Rows from a sheet. And saw using a `TickCounter` the differences between the deleting – Jean-Pierre Oosthuizen Jan 28 '16 at 15:21
0

the problem resides in using Selection. You should avoid it at all costs!

If the data always is in the same region, this becomes quite simple. Try something like:

Sub Button1_Click()
Dim cell As Range
Dim rData as Range

'Assigns the range for J22 and adjacent rows and columns
Set rData = ActiveSheet.Range("J22").CurrentRegion

word1 = InputBox("Enter a word by which you want to keep rows", "Enter")
For Each cell In rData
    If (InStr(1, cell, word1, 1) = 0) then cell.EntireRow.Delete
Next cell

End Sub

As you are not using Selection anymore, your 3 points get solved

Community
  • 1
  • 1
zfdn.cat
  • 56
  • 5
  • Thank you very much for your suggestions. However, I can't seem to get your code to work... In my Excel (I'm on Office 365) this code arbitrarily deletes rows. What might I be doing wrong? I am trying to delete (or keep, with the other button) all the rows that contain (in any cell, in any column) a certain string. So, if I wanted to keep any row with the text "apple", even rows containing the text "apple tree" should be kept. – jaggedjava Jan 28 '16 at 12:20
  • The `.CurrentRegion` sets the `rData` to all cells which have data and are connected to each other, CurrentRegion is the same as pressing `ctrl-a` inside a table, it selects the entire table. – SilentRevolution Jan 28 '16 at 12:40