how to optimize for each loop in vba - excel

I need to classify each row of a range accordingly with another range. The script works just fine. But it takes too much time even if it has no more than 300 rows. E.g. 298 rows take more than 2 minutes.
In order to achieve the classification, the script was built with a for each loop inside another one. All is done in the same worksheet called WSSeg. I tried to use all the good practices that I know of.
Option Explicit
Sub Input_Classification()
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim TBLClassification As ListObject
Dim TBLReference As ListObject
Dim rClassificationCell As Range
Dim rClassification As Range
Dim rReferenceCell As Range
Dim rReference As Range
Set TBLClassification = WSSeg.ListObjects("TBClass")
Set rClassification = TBL.ListColumns(4).DataBodyRange
Set TBLReference = WSSeg.ListObjects("TBResumo")
Set rReference = TBL.ListColumns(4).DataBodyRange
For Each rClassificationCell In rClassification
For Each rReferenceCell In rReference
If rClassificationCell.Offset(0, -1).Value <= rReferenceCell.Value Then
rClassificationCell.Value = rReferenceCell.Value
End If
Next rReferenceCell
Next rClassificationCell
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
I expect the run time code to be shorter. I don't know if I have to use another logic system. Thanks in advance.

Tried to modify the code, it takes only 0.04 Secs with two tables of around 500 rows.
Tried to keep the replacement logic same as the original, But may please check the same, as i am little confused about the same. If find otherwise, please modify them to your need. Also Could not understand the what is TBL in cases with both the tables and assumed the obvious.
Option Explicit
Sub Input_Classification()
Dim WSSeg As Worksheet
Dim TBLClassification As ListObject
Dim TBLReference As ListObject
Dim rClassification As Range
Dim SrcArr As Variant, TrgArr As Variant, SrcCel As Variant
Dim i As Long, Tm As Double
Set WSSeg = ThisWorkbook.Sheets("Sheet1")
Tm = Timer
Set TBLClassification = WSSeg.ListObjects("TBClass")
Set rClassification = TBLClassification.ListColumns(3).DataBodyRange.Resize(TBLClassification.DataBodyRange.Rows.Count, 2)
TrgArr = rClassification.Value
Set TBLReference = WSSeg.ListObjects("TBResumo")
SrcArr = TBLReference.ListColumns(4).DataBodyRange.Value
For i = 1 To UBound(TrgArr, 1)
For Each SrcCel In SrcArr
If TrgArr(i, 1) <= SrcCel Then
TrgArr(i, 2) = SrcCel
End If
Next SrcCel
Next i
rClassification.Value = TrgArr
Debug.Print "Seconds taken " & Timer - Tm
End Sub
Since I personally don't prefer to keep calculations, event processing and screen updating off (in normal cases) i haven't added that standard lines. However you may use these standard techniques, depending on the working file condition.

Related

VBA for each loop with 64K+ ListRows (ouf of memory)

I'm running a VBA for each loop through an Excel table (Listobject) which checks if a file exists based on a given path. My table has expanded though and has 68K Listrows. After launching the code, it quickly gives an error Run-time-error '7': Out of memory
It runs OK with 63K lines (done within 5 minutes) and based on googling there appears to be something called "64K segment boundary". Is this what's affecting my code to run since it really feels like it buffers the row count at first and then bounces back w/o starting to actually run anything. Is there an easy workaround for this without the need to split up my dataset into multiple batches? Frankly, I was quite surprised that 64K limits would still be a thing in Excel in 2021.
Running it on 64bit Excel 2019, but no luck with Office365 either.
Sub CheckFiles()
Dim Headers As ListObject
Dim lstrw As ListRow
Dim strFileName As String
Dim strFileExists As String
Application.ScreenUpdating = False
Set ws = ThisWorkbook.Sheets("Import")
Set Headers = ws.ListObjects("Import")
For Each lstrw In Headers.ListRows
strFileName = lstrw.Range(7)
strFileExists = Dir(strFileName)
If strFileExists = "" Then
lstrw.Range(4) = "not found"
Else
lstrw.Range(4) = "exists"
End If
Next lstrw
Set ws = Nothing
Set Headers = Nothing
Application.ScreenUpdating = True
End Sub
Avoid Accessing the Worksheet
Since you cannot avoid looping, you better do it in the computer's memory, i.e. rather through the elements of an array than through the cells of a range.
The code is still slow, about 10s for 200k rows on my machine, but that's because of Dir.
Note how easy (one line only, when the range contains more than one cell) and how fast (a split second) it is to write (copy) a range to an array (Data = rg.Value) and write (copy) the array back to a range (rg.Value = Data).
Adjust the values in the constants section.
Option Explicit
Sub CheckFiles()
Const wsName As String = "Import" ' Worksheet Name
Const tblName As String = "Import" ' Table Name
Const cCol As Long = 7 ' Criteria Column
Const dCol As Long = 4 ' Destination Column
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
Dim Headers As ListObject: Set Headers = ws.ListObjects(tblName)
Dim Data As Variant ' Data Array
With Headers.ListColumns(cCol).DataBodyRange
If .Rows.Count = 1 Then
ReDim Data(1 To 1, 1 To 1): Data = .Value
Else
Data = .Value
End If
End With
Dim r As Long ' Array Row Counter (DataBodyRange Row Counter)
Dim FileName As String ' File Name Retrieved by Dir
For r = 1 To UBound(Data, 1)
FileName = Dir(CStr(Data(r, 1)))
If Len(FileName) = 0 Then
Data(r, 1) = "not found"
Else
Data(r, 1) = "exists"
End If
Next r
Headers.ListColumns(dCol).DataBodyRange.Value = Data
End Sub
Thank you all! A few takeaways. While obviously trying to write as efficient code as possible, any reasonable performance here is acceptable. With that said, for each loop took approx 5 minutes to run with 63K lines, meawhile it was done in about 15 seconds by the code I accepted as an answer by #VBasic2008 - without capacity problems either.
The only problem I had with this particular code was it being somewhat new approach for me, so possibly building on it in the future needs some dedication in looking deeper into it - but it sure looks efficient. I also put together a regular for ... to loop which also didn't run into problems with 68K lines and would steer between rows and columns with offset function.
Clearly faster than for each as #Pᴇʜ suggested but took approx 2x as long as the array method (30 seconds or so).
Sub CheckFiles_2()
Dim strFileName, strFileExists As String
Dim ws As Worksheet
Dim Headers As ListObject
Dim result As String
Dim counter, RowCount As Long
Application.ScreenUpdating = False
Set ws = ThisWorkbook.Sheets("Import")
Set Headers = ws.ListObjects("Import")
RowCount = Headers.ListRows.Count
For counter = 1 To RowCount
strFileName = Range("anchorCell").Offset(counter, 3)
strFileExists = Dir(strFileName)
If strFileExists = "" Then
result = "not found"
Else
result = "exists"
End If
Range("anchorCell").Offset(counter, 0) = result
Next counter
Set ws = Nothing
Set Headers = Nothing
Application.ScreenUpdating = True
End Sub

VBA code working when I step through it, but not when it's run

I have some basic VBA that is allowing a user to take a field from one table and use it to update another table. It works fine when I step through it, but nothing happens when I run it using F5. I don't get any errors, just nothing happens.
I think it could possibly be that the value hasn't been assigned to one of the variables before the next step occurs, but I've never had that problem before, and I've always assumed VBA wouldn't move to the next step until it had completed the one it's on?
My code is below:
Option Explicit
Sub acceptDateComp()
'set data type
Dim dtType As String
dtType = "opportunity"
'declare sheets
Dim wsComp As Worksheet
Set wsComp = ThisWorkbook.Sheets(dtType & "Comp")
Dim wsBCE As Worksheet
Set wsBCE = ThisWorkbook.Sheets(dtType & "Snapshot")
Dim wsOffline As Worksheet
Set wsOffline = ThisWorkbook.Sheets(dtType & "Database")
'declare tables
Dim bce As ListObject
Set bce = wsBCE.ListObjects(dtType & "Snapshot")
Dim offline As ListObject
Set offline = wsOffline.ListObjects(dtType & "Database")
Dim dateComp As ListObject
Set dateComp = wsComp.ListObjects(dtType & "DateComp")
'declare heights and areas
Dim offlineRange As Range
Set offlineRange = offline.ListColumns(1).DataBodyRange
'check for acceptance, then change values
Dim i As Long
Dim dateID As Long
Dim offlineRow As Long
Dim bceDate As String
For i = 2 To dateComp.ListRows.Count + 1
If dateComp.ListColumns(6).Range(i).Value = "Yes" Then
dateID = dateComp.ListColumns(1).Range(i).Value
offlineRow = Application.WorksheetFunction.Match(dateID, offlineRange, 0)
bceDate = dateComp.ListRows(i - 1).Range(5).Value
offline.ListRows(offlineRow).Range(12).Value = bceDate
End If
Next i
Call opportunityComp
End Sub

Is there a way to reassign a Range variable to a different range?

I am very new to VBA, having started programming it yesterday. I am writing a data processing program which requires keeping track of two cells, one on each spreadsheet. The code which reproduces the errors I am experiencing is below. When I call the sub moveCell() in sub Processor(), nothing happens to DIRow and DIColumn, and the code spits out error 1004 at the line indicated. I have tried using DICell = DICell.Offset(), but it returns the same error.
How can I redefine a Range variable to be a different cell?
'<<Main Processor Code>>'
Sub Processor()
Dim PDRow As Integer
Dim PDColumn As Integer
Dim DIRow As Integer
Dim DIColumn As Integer
PDRow = 1
PDColumn = 1
DIRow = 1
DIColumn = 1
Dim PDCell As Range
Dim DICell As Range
Set PDCell = Worksheets("Processed Data").Cells(PDRow, PDColumn)
Set DICell = Worksheets("Data Input").Cells(DIRow, DIColumn)
Call moveCell(2, 0, "Data Input")
End Sub
'<<Function which moves the cell which defines the range>>'
Sub moveCell(r As Integer, c As Integer, sheet As String)
If sheet = "Processed Data" Then
PDRow = PDRow + r
PDColumn = PDColumn + c
Set PDCell = Worksheets("Data Input").Cells(PDRow, PDColumn)
ElseIf sheet = "Data Input" Then
DIRow = DIRow + r '<<<<<<This line does nothing to DIRow's value
DIColumn = DIColumn + c
Set DICell = Worksheets("Data Input").Cells(DIRow, DIColumn) '<<<<<<This line causes error 1004
End If
End Sub
As far as I can tell, you could instead use a quick Function instead. There doesn't seem to be any difference in your If statement results in the moveCell() function, except which worksheet you're using.
We can make this simpler by referring to the Range you're passing to moveCell.
Option Explicit ' forces you to declare all variables
Sub something()
Dim PDCell As Range
Set PDCell = Worksheets("Processed Data").Cells(1, 1)
Dim DICell As Range
Set DICell = Worksheets("Data Input").Cells(1, 1)
PDCell.Select ' can remove
Set PDCell = moveCell(2, 0, PDCell, PDCell.Worksheet.Name)
PDCell.Select ' can remove
Worksheets(DICell.Worksheet.Name).Activate ' can remove
DICell.Select ' can remove
Set DICell = moveCell(5, 0, DICell, DICell.Worksheet.Name)
DICell.Select ' can remove
End Sub
Function moveCell(rowsToMove As Long, colsToMove As Long, cel As Range, ws As String) As Range
Set moveCell = Worksheets(ws).Cells(cel.Row + rowsToMove, cel.Column + colsToMove)
End Function
I've included some rows you don't need (which I've marked with a comment afterwards), but that will show you how the routine works. You can step through with F8 to help see it step-by-step.
Edit: Although, you don't need a separate function at all. Just use OFFSET().
Set PDCell = ...whatever originally
Set PDCell = PDCell.Offset([rows],[cols])

Hide rows across multiple sheets

I want to hide rows 16 & 17 across the following tabs in my workbook:
Sheet6 (code name)
Sheet7 (code name)
Sheet8 (code name)
There has to be a better and more efficient way to write this code:
Sub Macro1()
Sheet6.Rows("16:17").Hidden = True
Sheet7.Rows("16:17").Hidden = True
Sheet8.Rows("16:17").Hidden = True
End Sub
When this code runs, its take longer than I thought it would.
Any help would be appreciated.
There are several ways; one that comes to mind is adding them to a dictionary and using For Each to loop through it.
I Adore ARRAYS - A Working Solution
Hide
Sub HideRows()
Dim arr As Variant
Dim i As Integer
arr = Array(Sheet6, Sheet7, Sheet8)
For i = LBound(arr) To UBound(arr)
arr(i).Rows("16:17").Hidden = True
Next
End Sub
Show All
Sub ShowRows()
Dim arr As Variant
Dim i As Integer
arr = Array(Sheet6, Sheet7, Sheet8)
For i = LBound(arr) To UBound(arr)
arr(i).Rows.Hidden = False
Next
End Sub
Toggle
Sub ToggleRows()
Dim arr As Variant
Dim i As Integer
arr = Array(Sheet6, Sheet7, Sheet8)
For i = LBound(arr) To UBound(arr)
arr(i).Rows("16:17").Hidden = Not arr(i).Rows("16:17").Hidden
Next
End Sub
Thanks to:
ProfoundlyOblivious for profoundly suggesting and providing the 'Toggle' version.
GMalc for providing the idea of yet another way (not ever seen by me) of using an Array.
Use an array of worksheets...
Dim ws As Worksheet
For Each ws In Worksheets(Array("Sheet6", "Sheet7", "Sheet8"))
ws.Rows("16:17").Hidden = True
Next

How could I have image URLs in column "C" display their corresponding images in column "N" in Excel?

I've an Excel file with a bunch of columns, one of which is "ImageURL", which, of course, displays unique URLs as text.
How could I set it up such that those images are also depicted within another column?
I've used the following macro, but I get a "Invalid outside procedure" compile error.
Dim url_column As Range
Dim image_column As Range
Set url_column = Worksheets(1).UsedRange.Columns("C")
Set image_column = Worksheets(1).UsedRange.Columns("N")
Dim i As Long
For i = 1 To url_column.Cells.Count
With image_column.Worksheet.Pictures.Insert(url_column.Cells(i).Value)
.Left = image_column.Cells(i).Left
.Top = image_column.Cells(i).Top
image_column.Cells(i).EntireRow.RowHeight = .Height
End With
Next
I am, unfortunately, new to VBA, so perhaps, I've not set it up correctly?
Ok, this may sound pretty basic (no pun intended), but based on the limited information you made available, I think that the cause of your problem is that you just pasted those statements in your code module and didn't put them inside a procedure. That will certainly give you an "Invalid outside procedure" compile-time error.
You have to put stuff inside a procedure -- either a Sub or a Function. This case calls for a Sub. Try this:
Sub PlaceImageInCell()
Dim url_column As Range
Dim image_column As Range
Set url_column = Worksheets(1).UsedRange.Columns("A")
Set image_column = Worksheets(1).UsedRange.Columns("B")
Dim i As Long
For i = 1 To url_column.Cells.Count
With image_column.Worksheet.Pictures.Insert(url_column.Cells(i).Value)
.Left = image_column.Cells(i).Left
.Top = image_column.Cells(i).Top
image_column.Cells(i).EntireRow.RowHeight = .Height
End With
Next
End Sub
.Pictures.Insert(stuff) doesn't work in XL 2007 - and I've seen suggestions to use *.Shapes.AddPicture() instead.
Problem is that it requires a String for the filePath and I'm not familiar enough with VBA to make this work.
Sub InsertImage()
Dim urlColumn As Range
Dim imgColumn As Range
Dim fp as String
Set urlColumn = Worksheets(1).UsedRange.Columns("A")
Set imgColumn = Worksheets(1).UsedRange.Columns("B")
Dim i As Long
For i = 2 To urlColumn.Cells.Count
With imgColumn.Worksheet.Shapes.AddPicture(fp, msoTrue, msoTrue, 1, 1, 12, 12)
End With
Next
End Sub
The end result is the following Error:
Compile Error:
Object Required

Resources