Why does my For...Each statement complete in its entirety? - excel

When the following code runs, it doesn't complete in its entirety. I usually have to run it a few more times to ensure all the data in that range has been inspected and the row deleted if it meets my criteria.
Const A% = 1
Const B% = 2
Const C% = 3
Const D% = 4
'Some code
If myCL <> "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore And ws.Cells(r, C) Like myCL Then
Cell.EntireRow.Delete
End If
End If
Next Cell
ElseIf myCL = "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore Then
Cell.EntireRow.Delete
End If
End If
Next Cell
End If
I understand that I should loop through a range in reverse when using something like For i = ## to 1 Step -1, but I don't believe this would apply in this situation.
My issue is that when Cell should meet the criteria, it sometimes skips over it, which I then rerun the code and it will be deleted.

Another method is, instead of adding the rows to an array, or looping backwards and deleting the rows one by one, is to define a DelRng as a Range object.
Everytime, you pass your criteria, than you add that Row to the DelRng object, using the Union function, and at the end, you delete DelRng with one-shot.
Code
Dim DelRng As Range ' new range object, collects all rows that needs to be deleted
If myCL <> "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore And ws.Cells(r, C) Like myCL Then
' add current row to DelRng
If Not DelRng Is Nothing Then
Set DelRng = Application.Union(DelRng, .Rows(r))
Else
Set DelRng = .Rows(r)
End If
End If
End If
Next cell
ElseIf myCL = "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore Then
' add current row to DelRng
If Not DelRng Is Nothing Then
Set DelRng = Application.Union(DelRng, .Rows(r))
Else
Set DelRng = .Rows(r)
End If
End If
End If
Next cell
End If
' now delete the entire rows at once (will save you a lot of run-time)
If Not DelRng Is Nothing Then DelRng.Delete

You're pulling the rug under the For Each statement by deleting the row it is iterating on (For Each scans left to right, from the top row to the bottom row). Excel is nice enough to resume the iteration at "next" cell, which really is located on the row after the one it just deleted, and typically 1 cell to the right. But then, your code missed all of the leftmost cells on the new current row, some of which may fulfill your criteria.
Edit
The issue can be circumvented by somehow taking note of the rows to delete, without deleting them within the For Each loop. My personal favorite way of doing so is to use a Scripting.Dictionary, as demonstrated below:
Sub ForEachWithRowDeleteDemo()
Dim rangeOfInterest As Excel.Range
Dim cell As Excel.Range
Dim dicRowIndexesToDelete As Object 'Scripting.Dictionary
Dim rowIndex As Variant
Set rangeOfInterest = Sheet1.Range("A1:Z10") 'ASSUMPTION: rangeOfInterest is a contiguous range; no checks are made here.
Set dicRowIndexesToDelete = CreateObject("Scripting.Dictionary")
For Each cell In rangeOfInterest.Cells
If cell.Value2 = 123 Then '...your conditions go here.
'Cumulate distinct row indexes.
dicRowIndexesToDelete(cell.Row - rangeOfInterest.Row + 1) = True
End If
Next
If dicRowIndexesToDelete.Count > 0 Then
If rangeOfInterest.Cells.Count = 1 Then
'Exceptional case: rangeOfInterest is a single cell.
rangeOfInterest.EntireRow.Delete
Else
'Mark each of the range's rows.
rangeOfInterest.Clear
For Each rowIndex In dicRowIndexesToDelete.Keys
rangeOfInterest.Cells(rowIndex, 1) = True
Next
'Find the marks and delete the entire rows.
rangeOfInterest.SpecialCells(xlCellTypeConstants).EntireRow.Delete
End If
End If
Set dicRowIndexesToDelete = Nothing
Set cell = Nothing
Set rangeOfInterest = Nothing
End Sub
Notice that all rows are deleted at once, for better performance. This will only work if there are no more than 8'192 separate "islands" of rows to delete; beyond that, the SpecialCells method would fail. The exceptional case of a single cell range of interest must be handled separately, since SpecialCells will consider the whole worksheet as its search zone if applied to a single cell.

This is a proof of concept that should work around the problem you're facing. Instead of deleting the rows in your For Loops, you could assign the Row to a 1-dimensional Array, then build a string and delete the rows all at once outside of your Loop.
Run this on a sheet with the first 7 rows filled out, then hit play and watch.
Sub DeletingRows()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets(1)
Dim myArr(0 To 2) As Long
Dim myStr As String
myArr(0) = 2
myArr(1) = 4
myArr(2) = 6
For Each myRow In myArr
myStr = myStr & myRow & ":" & myRow & ","
Next myRow
myStr = Left(myStr, Len(myStr) - 1)
ws.Range(myStr).EntireRow.Delete
End Sub
How to integrate this into your code
Where x is a long starting at 0.
If myCL <> "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore And ws.Cells(r, C) Like myCL Then
myArr(x) = Cell.Row
x = x + 1
End If
End If
Next Cell
ElseIf myCL = "" Then
For Each Cell In RngB.Cells
If Cell.Value <= myBal Then
r = Cell.Row
If ws.Cells(r, D) <= myScore Then
myArr(x) = Cell.Row
x = x + 1
End If
End If
Next Cell
End If
For Each myRow In myArr
myStr = myStr & myRow & ":" & myRow & ","
Next myRow
myStr = Left(myStr, Len(myStr) - 1)
ws.Range(myStr).Delete

Related

VBA Instr function on 100K+ records

I have 100.000 records/rows with 17 columns. One of these columns needs to be checked to output either a 1 or 0 to the next column. For this I use a loop with the Instr function, but after 10 mins it still isn't outputting anything on my machine and I believe the code is too intensive or slow running it row for row.
Dim rng As Range
Set rng = Range("F:F")
For Each cell In rng
TicketType = cell
If InStr(1, TicketType, "locker", 1) > 0 Then
cell.Offset(0, 1) = 1
Else
cell.Offset(0, 1) = 0
End If
Next
There are only 100 TicketTypes to check however, and based on the names of these TicketTypes it should output a 1 or 0 (match or not). So I was thinking, maybe there is a way to sort the entire table, run through it to see which categories there are, store their vertical ranges, run a check and then output +-10.000 rows at once? I noticed this is instant, so I believe it's really the Instr function that is the bottleneck.
Try this:
Dim rng As Range, f
With ActiveSheet
Set rng = Application.Intersect(.Columns("F"), .UsedRange)
f = "=--NOT(ISERROR(SEARCH(""locker""," & rng(1).Address(False, False) & ")))"
Debug.Print f
rng.Offset(0, 1).Formula = f
rng.Offset(0, 1).Value = rng.Offset(0, 1).Value
End With
Variant array approach
As mentioned by BigBen it's faster than looping through each cell by means of VBA.
Sub VariantArray()
With Sheet1
'~~> Set you relevant range here
Dim lastRow As Long, rng As Range
lastRow = .Range("F" & .Rows.Count).End(xlUp).Row
Set rng = .Range("F1:F" & lastRow)
'~~> create a one based 2-dim datafield array
Dim myArray As Variant
myArray = rng
'~~> check TicketType
Dim i As Long
For i = 1 To UBound(myArray)
myArray(i, 1) = IIf(InStr(1, myArray(i, 1), "locker", 1) > 0, 1, 0)
Next i
'~~> fill target with array values
rng.Offset(0, 1) = myArray
End With
End Sub
you could try filtering:
With Worksheets("actualSheetName") '<-- change "actualSheetName" to your actual sheet name
With .Range("F1", .Cells(.Rows.Count, "F").End(xlUp))
.Offset(, 1).Value = 0
.AutoFilter Field:=1, Criteria1:="*locker*"
.SpecialCells(xlCellTypeVisible).Offset(, 1) = 1
End With
.AutoFilterMode = False
End With
As suggested by BigBen, a far better solution is the usage of a worksheet function, like Find.All() (at least that how I think it's called). If it finds something, it gives a number, else it gives an error. You might turn this into an interesting formula like this:
=IF(IF.ERR(FIND.ALL("locker";A2);0)=0;0;1)

How to write two IF statements for different ranges in a loop, VBA

I am working on an Excel document using VBA. This document contains a database with multiple columns, but for simplicity, let's say I have 2 columns:
Column C corresponds to names
Column F corresponds to numbers.
I'm trying to create a macro that checks all the numbers in column F (with a loop). If the number is above 100, then check the adjacent cell in column C. If the name corresponds to a condition (let's say corresponds to John or Tom), then add the value of the number in another sheet. If none of those apply, check the next cell.
My problem is that I can't find a way to define the cells in column C (Creating a variable/object to call the cells or calling directly the adjacent cell).
My code looks like this:
Sub Test1()
Dim rngnumbers, rngnames, MultipleRange As Range
Set rngnumbers = Sheet2.Range("F2:F999")
Set rngnames = Sheet2.Range("C2:C999")
Set MultipleRange = Union(rngnumbers, rngnames)
For Each numb In rngnumbers
If numb.Value >= 100 Then
If Sheet2.Range("C2") = "John" Or Sheet2.Range("C2") = "Tom" Then '''The problem here is that it only looks at the cell C2 and not the adjacent cell
Sheet1.Range("I999").End(xlUp).Offset(1, 0).Value = numb.Value
Else
End If
End If
Next numb
End Sub
I tried modifying the line:
'If Sheet2.Range("C2") = "John" Or Sheet2.Range("C2") = "Tom" Then'
to something like:
'newname.String = "John" '
But I can't find a way to define newname.
Another idea would be to increment the If statement for the names within the For loop.
Additional note:
I am also not using formulas directly within Excel as I don't want any blank cells or zeros when the if functions are False.
Does this solve your problem - referencing the relevant cell in column C? OFFSET provides a relative reference, in this case look 3 columns to the left of F.
Sub Test1()
Dim rngnumbers As Range, rngnames As Range, MultipleRange As Range, numb As Range
Set rngnumbers = Sheet2.Range("F2:F999")
Set rngnames = Sheet2.Range("C2:C999")
Set MultipleRange = Union(rngnumbers, rngnames)
For Each numb In rngnumbers
If numb.Value >= 100 Then
If numb.Offset(, -3) = "John" Or numb.Offset(, -3) = "Tom" Then
Sheet1.Range("I999").End(xlUp).Offset(1, 0).Value = numb.Value
End If
End If
Next numb
End Sub
Have you considered SUMIFS instead?
You want something like this?
Sub Test1()
Dim lRow As Long, r As Long
lRow = 1000 'last row in your data
Dim ws As Worksheet
Set ws = Worksheets("List with your data")
For i = 2 To lRow
If ws.Range("F" & i) > 100 Then
If ws.Range("C" & i).Value = "John" Or ws.Range("C" & i).Value = "Tom" Then
Worksheets("Another sheet sheet").Range("A" & r) = Range("C" & i).Value ' r - Row, where we want to enter uor text
r = r + 1 'if you want to put next name on the next row
End If
End If
Next
End Sub
Two Ifs in a Loop
Union Version
Option Explicit
Sub Test1()
Const cFirst As Integer = 2
Const cLast As Integer = 999
Const cCol1 As Variant = "F"
Const cCol2 As Variant = "C"
Const cCol3 As Variant = "I"
Dim i As Integer
Dim rngU As Range
With Sheet2
For i = cFirst To cLast
If IsNumeric(.Cells(i, cCol1)) And .Cells(i, cCol1) >= 100 Then
If .Cells(i, cCol2) = "John" _
Or .Cells(i, cCol2) = "Tom" Then
If Not rngU Is Nothing Then
Set rngU = Union(rngU, .Cells(i, cCol1))
Else
Set rngU = .Cells(i, cCol1)
End If
End If
End If
Next
End With
If Not rngU Is Nothing Then
rngU.Copy Sheet1.Cells(cLast, cCol3).End(xlUp).Offset(1, 0)
Set rngU = Nothing
End If
End Sub
I normally work with arrays:
Sub Test1()
Dim rngnumbers As Excel.Range
Dim arrVals As variant
Dim lngRow As long
Arrvals = Sheet2.Range("C2:F999").value
For Lngrow = lbound(arrvals,1) to ubound(arrvals,1)
If arrvals(lngrow,4) >= 100 Then
If arrvals(lngrow,1)= "John" Or arrvals(lngrow,1) = "Tom" Then '''The problem here is that it only looks at the cell C2 and not the adjacent cell
Sheet1.Range("I999").End(xlUp).Offset(1, 0).Value = arrvals(lngrow,4)
Else
End If
End If
Next lngrow
End Sub
Actually I would probably build an output array as well, but my thumb is tired...

put 0 in a next excel column if previous 4 are empty using VBA

Hi all I am trying to make a vb macro that determins are there 4 empty cells in a row if so it should put 0 in a next row otherwais 1 Here is what I 've done so far
Sub QuickCull()
On Error Resume Next
Columns("a").SpecialCells(xlBlanks).EntireRow.Delete
Columns("b").SpecialCells(xlBlanks).EntireRow.Delete
Columns("d").SpecialCells(xlBlanks).EntireRow.Delete
Dim col As Range
Set col = Cells(Rows.Count, "E").End(xlUp)
Dim r As Range
Set r = Range("E2", col).Resize(, 4)
r.Select
Dim cell As Range
For Each cell In r
If cell.Value = "" Then
cell.Value = 0
Else
cell.Value = 1
End If
Next cell
End Sub
This way I put 0 instad of one blank row I thought about making another cell with a sum of those rows, but is where a way to do it more queckly and productivly?
I think you need something like the following, obviously replace "WORKSHEETNAME" with the name of the worksheet:
Dim r as Range, cell as Range, eRow as Long
eRow = Sheets("WORKSHEETNAME").Cells(Rows.Count, 5).End(xlUp).Row
Set r = Sheets("WORKSHEETNAME").Range("E2:E" & eRow)
For each cell in r.cells
If cell.Offset(0,-4).Value = "" _
And cell.Offset(0,-3).Value = "" _
And cell.Offset(0,-2).Value = "" _
And cell.Offset(0,-1).Value = "" Then
cell.Value = 0
Else
cell.Value = 1
End if
Next cell

In VBA, what is the syntax for writing a formula relative to a range?

In Excel, I've written a short script that should remove the first character of each cell containing a "*"symbol at the start of it. What I have so far is
Sub Macro5()
Dim Rng As Range
Dim i As Long
i = 1
While i <= 20000
Set Rng = Range("A" & i)
If InStr(Rng, "*") > 0 Then
Rng.Offset(0, 1).Formula = "=Right(Rng,LEN(Rng)-1)"
i = i + 1
Else: i = i + 1
End If
Wend
End Sub
The line to call the script seems to work, but the formula getting placed into column B is "=Right(Rng,LEN(Rng)-1)", which gives a 'NAME?' error. How do I define the LEN formula to use Rng as a range, rather than as a 'word' on the spreadsheet?
Using R1C1 type formulae makes life much easier in that situation.
Sub RemoveFirstStar()
Dim rng As Range, c As Range
Set rng = Range("A1:A2000")
For Each c In rng.Cells
If Left(c, 1) = "*" Then
c.Offset(0, 1).FormulaR1C1 = "=mid(rc[-1],2,1000)"
End If
Next c
End Sub
For your particular code example, change the line after the IF:
Rng.Offset(0, 1).FormulaR1C1 = "=Right(RC[-1],LEN(RC[-1])-1)"

Using a VBA For Loop to concatenate column in excel

I have a column of data in excel. I want to loop through the data and combine the contents into a single string. I can specify the cell range, but what if the range is unknown. I want to be able to loop until the cell becomes empty. here is what I have so far.
Sub ConcatenationLoop()
Dim rng As Range, i As Integer
Set rng = Range("A1", "A5")
For i = 1 To rng.Rows.Count
With Range("B1")
If .Value = "" Then
.Value = rng.Range("A" & i)
Else
.Value = .Value & ", " & rng.Range("A" & i)
End If
End With
Next
is it possible to combine with something like:
Do Until IsEmpty(ActiveCell)
Much help is appreciated!
End Sub
With Worksheets("YourSheetName")
lastrow = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
Loop it to LastRow.
Get the first empty cell from the top using
lLastRow = sheet.Cells(1, 2).End(xlDown).Row
The use this in your for loop
For i = 1 To lLastRow
You could use the following skeleton:
Sub ALoop()
Dim r As Long
r = 2 '//Start row
While Len(Cells(r, "A")) > 0 '//Or While Not IsEmpty(...)
'// Your code
r = r + 1 '//Don't forget to increment row
Wend
End Sub

Resources