4

Often times it's required that you go through a range of cells, and based on some criteria, delete an entire row.

In practice, it's best to start at the end of the range, and work up.

Dim i as Long
For i = lastRow to 1 Step -1
    If Cells(i, 2).Value = "del" then Rows(i).EntireRow.Delete
End if

However, most of the time I'm working with a Range object.

Is there a way to work backwards, with a range object, that doesn't require the use of a For i type loop?

Dim rng as Range, cel as Range
Set rng = Range("A1:A100")

For each cel in rng step -1
   if cel.value = "del" then cel.EntireRow.Delete
next cel

This errors Expected: End of Statement on the Step -1 portion, which I expected (no pun intended).

The idea is that I don't have to basically re-arrange my data into Cells() when trying to work backwards on a Range variable. I find it a little kludgy to use range variables a bunch, but when wanting to remove rows from that range, have to switch to using Cells([long],[long]) if that makes sense.

Edit: Just came up with this, but it still feels kludgy:

Dim k As Long, cel as Range
Set cel = rng.cells(rng.cells.count)
For k = cel.Row To rng.Cells(1).Row Step -1
    If rng.Cells(k).Value = "del" Then rng.Cells(k).EntireRow.Delete
Next k
BruceWayne
  • 22,923
  • 15
  • 65
  • 110
  • 2
    `For Each`'s order is implementation specific - it just calls `_NewEnum` to get the next item in the iteration, and that means you have no control over the ordering. It's entirely up to the enumerable interface you're using. – Comintern Nov 01 '18 at 18:42
  • 1
    `For i = rng.row + rng.rows.count - 1 to rng.row Step -1` | if `rng.Cells(i-rng.row+1,1).Value = "del" then rows(i).Delete` – Scott Craner Nov 01 '18 at 19:08
  • @ScottCraner - Yeah, I was thinking something like that. Still feels a little kludgy, as we can't use "clean" variables, but I get it... – BruceWayne Nov 01 '18 at 19:47
  • Added an approach using the filtering possibilities of the `Application.Index` function. – T.M. Nov 02 '18 at 10:13

3 Answers3

6

Yes, you can do it without a For i = statement. Just create a special range that you will delete once you finish your loop.

Dim cel As Range, rng As Range
Dim delRng As Range

For Each cel In rng
    If cel.Value = "del" Then
        If delRng Is Nothing Then
            Set delRng = cel
        Else
            Set delRng = Union(delRng, cel)
        End If
    End If
Next cel

If Not delRng Is Nothing Then delRng.EntireRow.Delete

And you don't even have to step backwards.

K.Dᴀᴠɪs
  • 9,945
  • 11
  • 33
  • 43
  • 1
    Nice & straight forward +1 :-) – T.M. Nov 02 '18 at 10:40
  • 1
    Ooooh I like this a lot. While it does require the use of another variable, it lets me keep the method of using a range object without switching "styles". This is great! (And I honestly expected to need some other variable or helper, so that's totally fine!). Also, very concise and logical :D This is the best answer so far (I'll leave a day or so more just in case someone else pops in, before marking as The Answer) – BruceWayne Nov 02 '18 at 15:24
3

The other way round

"Is there a way to work backwards, with a range object, that doesn't require the use of a For i type loop?"

In addition to @K-Davis ' valid solution, I demonstrate how to use the advanced filtering possibilties of the Application.Index method. It only expects three arguments:

  • a 2-dim datafield array v based on the entire data set
  • a 1-dim array of the row numbers to be maintained, called via a helper function getAr(v, 1), where argument 1 defines the 1st column
  • (a 1-dim array of all columns, created automatically via Evaluate)

Instead of deleting rows, this approach used the whole data set (e.g. A2:C10 omitting an assumed title row) and writes back the filtered datafield array assigning it to rng again.

Main procedure

Sub DelRows()
  Dim rng As Range,  v
  Set rng = ThisWorkbook.Worksheets("Sheet1").Range("A2:C10") ' << change to entire data range
' [1a] create 2-dim data field array (1-based)
  v = rng.Value2
' [1b] filter out rows to be deleted
  v = Application.Transpose(Application.Index(v, getAr(v, 1), Evaluate("row(1:" & rng.Columns.Count & ")")))
' [2] write data field back to resized range
  rng = ""                                  ' clear lines
  rng.Resize(UBound(v), UBound(v, 2)) = v
End Sub

Helper function getAr()

Function getAr(v, ByVal colNo&) As Variant()
' Purpose: collect row numbers not to be deleted (criteria no "DEL" in 1st column)
' Note:    called by above procedure DelRows
  Dim ar, i&, n&
  ReDim ar(0 To UBound(v) - 1)
  For i = 1 To UBound(v)
      If UCase$(v(i, colNo)) <> "DEL" Then
         ar(n) = i: n = n + 1
      End If
  Next i
  ReDim Preserve ar(0 To n - 1): getAr = ar
End Function

Related SO link

Cf. Insert new first column in datafield array without loops or API calls

T.M.
  • 9,436
  • 3
  • 33
  • 57
  • 1
    Interesting approach – K.Dᴀᴠɪs Nov 02 '18 at 13:54
  • 1
    Woah, thanks for this. Looks really interesting, and I'll be sure to study it. Cheers! – BruceWayne Nov 02 '18 at 15:25
  • Additional hint: the applied *array* method is faster than looping through a range in VBA. *Not representative, but for 32,000 rows with more than 20% "Del" rows it took me only 0.34 seconds on my computer, for 1,000 rows just 0.25 seconds. Test it by yourself :-)* – T.M. Nov 03 '18 at 18:47
  • Further hint: the demonstrated `Index` method seems to be restricted to 65,536 rows that can be restructured at once. – T.M. Nov 03 '18 at 18:51
2

I know you said you don't like For i, but IMHO this is the cleanest way to go

For i = rng.Rows.Count To 1 Step -1
    With rng.Cells(i, 2)
        If .Value = "del" then
            .Entirerow.Delete
        End If
    End With
Next

Note that the rng.Cells construct is relative to rng

Eg if rng is A100:A1000 then rng.Cells(rng.Rows.Count, 1) refers to A1000

chris neilsen
  • 52,446
  • 10
  • 84
  • 123
  • This is pretty good, except if `rng` is `A100:A1000`, it'll start at row 900, no? I think @ScottCraner's comment would be the way to use this type of loop. – BruceWayne Nov 01 '18 at 20:23
  • @bruce No, the `rng.Cells` construct is relative to `rng`. It'll start at the last row in `rng` – chris neilsen Nov 01 '18 at 20:57
  • Hm, I must be doing something wrong. If I do `Set rng = Range("A10:A100") // for i = rng.rows.count to 1 step -1 // cells(i,1).select`, it starts at `A91`. – BruceWayne Nov 02 '18 at 15:23
  • 1
    Change `Cells(I, 1)` to `rng.Cells(i, 1)` . Cells on it's own is relative to the active sheet – chris neilsen Nov 02 '18 at 19:21
  • 1
    Aha! Thanks for clarifying, I didn't catch that. – BruceWayne Nov 02 '18 at 19:27
  • 1
    @Bruce your Q as asked is about iterating a range backwards. This answer addresses that directly, and makes no comments on if it's the "best" method to delete rows from a range. If your real interest is in other methods to delete some rows from a range, that's been asked many times before, and there are lots of good answers demonstrating quite a few different methods. Which one is best for you will depend on your situation - how big is your range, what proportion is to be deleted, are there formulas in the range or refer to the range, can you sort, etc – chris neilsen Nov 02 '18 at 19:33