Need more efficiency than For Each Loop vba - vba

I am a newcomer to vba/excel macros and need a more efficient way to run the below code. I am using a for each loop to return a value from a row based on a column's value (same row). The code works, but takes far too much processing power and time to get through the loops (often freezing the computer or program). I would appreciate any suggestions...
'The following is searching each cell in a range to determine if a cell is not empty. If the cell is not empty, the macro will copy the value of the cell and paste it in to another worksheet (same row)
Set rng = Worksheets("Demographic").Range("AU2:AU" & lastRow)
i = "2"
For Each cell In rng
If Not IsEmpty(cell.Value) Then
Sheets("Demographic").Range("AU" & i).Copy
Sheets("Employee import").Range("F" & i).PasteSpecial xlPasteValues
End If
i = i + 1
Next
'The following is searching each cell in a range to determine if a cell contains a "T". If the cell contains a "T", the macro will copy the value of a different column (same row) and paste it in to another worksheet (same row)
Set rng = Worksheets("Demographic").Range("AM2:AM" & lastRow)
i = "2"
For Each cell In rng
If cell.Value = "T" Then
Sheets("Demographic").Range("AO" & i).Copy
Sheets("Employee import").Range("G" & i).PasteSpecial xlPasteValues
End If
i = i + 1
Next

A formula array should be your best hope. This supposes that the cells that do not match will lead to empty values in the destination range:
chk = "Demographic!AU2:AU" & lastRow
src = "Demographic!AU2:AU" & lastRow
With Sheets("Employee import").Range("F2:F" & lastRow)
.FormulaArray = "=IF(" & chk & "<> """"," & src & ", """")"
.Value = .Value '<-- if you want to remove the formulas and keep only the copied values
End With
chk = "Demographic!AM2:AM" & lastRow
src = "Demographic!AO2:AO" & lastRow
With Sheets("Employee import").Range("G2:G" & lastRow)
.FormulaArray = "=IF(" & chk & "= ""T""," & src & ", """")"
.Value = .Value '<-- if you want to remove the formulas and keep only the copied values
End With
Not sure that it will be faster with your dataset though, you can only verify by trying it.

If you just want a straight data transfer (ie no formulas or formats), and your data set is large, then you could consider writing the data in one batch by way of an array.
Your own code shouldn't be horrendously slow though, so it suggests you have some calculations running or maybe you're handling Worksheet_Change events. If this is possible, then you might want to disable those during the data transfer:
With Application
.EnableEvents = False
.ScreenUpdating = False
.Calculation = xlCalculationManual
End With
Just remember to reset them at the end of your routine:
With Application
.EnableEvents = True
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
End With
If you went the array route, skeleton code would be like so:
Dim inData As Variant
Dim outData() As Variant
Dim r As Long
'Read the demographic data
With Worksheets("Demographic")
inData = .Range(.Cells(2, "AU"), .Cells(.Rows.Count, "AU").End(xlUp)).Value2
End With
'Use this if your column F is to be entirely overwritten
ReDim outData(1 To UBound(inData, 1), 1 To UBound(inData, 2))
'Use this if you have exisiting data in column F
'With Worksheets("Employee import")
' outData = .Cells(2, "F").Resize(UBound(inData, 1)).Value2
'End With
'Pass the values across
For r = 1 To UBound(inData, 1)
If Not IsEmpty(inData(r, 1)) Then
outData(r, 1) = inData(r, 1)
End If
Next
'Write the new values
Worksheets("Employee import").Cells(2, "F").Resize(UBound(outData, 1)).Value = outData

as for your first copy/paste values, it actually doesn't need any check, since blank values would be pasted as blank ones...
so you could go:
With Worksheets("Demographic")
With .Range("AU2", .Cells(.Rows.count, "AU").End(xlUp))
Worksheets("Employee import").Range("F2").Resize(.Rows.count).Value = .Value
End With
End With
as for your 2nd copy/paste values, you could paste all values and then filter not wanted ones and clear them in target sheet
like follows:
With Worksheets("Demographic")
With .Range("AM2", .Cells(.Rows.count, "AM").End(xlUp))
Worksheets("Employee import").Range("G2").Resize(.Rows.count).Value = .Offset(, 2).Value
End With
End With
With Worksheets("Employee import")
With .Range("G1", .Cells(.Rows.count, "G").End(xlUp))
.AutoFilter field:=1, Criteria1:="<>T"
.Resize(.Rows.count).Offset(1).SpecialCells(xlCellTypeVisible).ClearContents
End With
.AutoFilterMode = False
End With
that said, if your workbook has many formulas and/or event handlers then you would also greatly benefit from disabling them (Application.EnableEvents = False, Application.Calculation = xlCalculationManual) before running your code and enabling them back (Application.EnableEvents = True, Application.Calculation = xlCalculationAutomatic) after you code completes

Related

Trying to Highlight Used Range of a Column

I'm running into trouble highlighting a column's used range. The following code creates copies of two worksheets, removes some values and then is supposed to highlight certain columns.
Sub CreateAnalysisSheets()
Dim cell, HlghtRng As Range
Dim i As Integer
Dim ref, findLast, findThis As String
Dim lastRow As Long
findLast = "2016"
findThis = "2017"
Application.ScreenUpdating = False
Sheets(1).Copy After:=Sheets(2)
ActiveSheet.Name = Left(Sheets(1).Name, InStr(1, Sheets(1).Name, " ")) & "Analysis"
Sheets(2).Copy After:=Sheets(3)
ActiveSheet.Name = Left(Sheets(2).Name, InStr(1, Sheets(2).Name, " ")) & "Analysis"
Sheets("RM Analysis").Select
For Each cell In ActiveSheet.UsedRange
If cell.Value = "NULL" Then
cell.ClearContents
End If
Next cell
For Each cell In Range("1:1")
ref = cell.Value
lastRow = Range("R" & Rows.Count & "C" & cell.Column).End(xlUp).Row
Set HlghtRng = Range(Cells(1, cell.Column) & Cells(lastRow, cell.Column))
If InStr(1, ref, findLast) > 0 And InStr(1, ref, "YTD") = 0 Then
HlghtRng.Interior.ColorIndex = 8
End If
Next cell
For Each cell In Sheets(4).UsedRange
If cell.Value = "NULL" Then
cell.ClearContents
End If
Next cell
Sheets("RM Analysis").Select
Application.ScreenUpdating = True
End Sub
The problem comes at lastRow = Range("R" & Rows.Count & "C" & cell.Column).End(xlUp).Row where I get an Method 'Range' of Object '_Global' Failed. I've tried searching for ways to fix this issue, but everything I've tried (ActiveSheet.Range and Sheets("RM Analysis").Range) has yet to work.
Anyone see where I'm going wrong here?
The xlR1C1 syntax is fouling up your request for the last non-blank cell.
lastRow = Cells(Rows.Count, cell.Column).End(xlUp).Row
I would highly recommend that you avoid relying on the ActiveSheet and use explicit parent worksheet references. This can be made quite simple using With ... End With and preceding all Range and Cells with a . like .Range(...) or .Cells(...).
Once you within a With ... End With statement, all of the references need to be prefaced with a .. Additionally, the following is not a string concatenation (e.g. &) but as .Range(starting cell comma ending cell) operation.
with worksheets("RM Analysis")
...
Set HlghtRng = .Range(.Cells(1, cell.Column), .Cells(lastRow, cell.Column))
...
end with
this should do
Columns(1).Interior.ColorIndex = 3
change the number of column as to the column you wanna highlit

First VBA code... looking for feedback to make it faster

I wrote a small VBA macro to compare two worksheets and put unique values onto a new 3rd worksheet.
The code works, but every time I use if excel goes "not responding" and after 30-45sec comes back and everything worked as it should.
Can I make this faster and get rid of the "not responding" issue? is it just my computer not being fast enough?
I start with about 2500-2700 rows in each sheet I'm comparing.
Sub FilterNew()
Dim LastRow, x As Long
Sheets.Add(After:=Sheets(Sheets.Count)).Name = "New" 'Adds a new Sheet to store unique values
Sheets(1).Rows("1:1").Copy Sheets("New").Rows("1:1") 'Copies the header row to the new sheet
Sheets(1).Select
LastRow = Range("B1").End(xlDown).Row
Application.ScreenUpdating = False
For Each Cell In Range("B2:B" & LastRow)
x = 2 'This is for looking through rows of sheet2
Dim unique As Boolean: unique = True
Do
If Cell.Value = Sheets(2).Cells(x, "B").Value Then 'Test if cell matches any cell on Sheet2
unique = False 'If the cells match, then its not unique
Exit Do 'And no need to continue testing
End If
x = x + 1
Loop Until IsEmpty(Sheets(2).Cells(x, "B"))
If unique = True Then
Cell.EntireRow.Copy Sheets("New").Range("A" & Rows.Count).End(xlUp).Offset(1, 0)
End If
Next
Application.ScreenUpdating = True
End Sub
This belongs in Code Review, but here is a link
http://www.excelitems.com/2010/12/optimize-vba-code-for-faster-macros.html
With your code your main issues are:
Selecting/Activating Sheets
Copy & pasting.
Fix those things and youll be set straight my friend :)
instead of a do...loop to find out duplicate, I would use range.find method:
set r = SHeets(2).range("b:b").find cell.value
if r is nothing then unique = true else unique = false
(quickly written and untested)
What about this (it's should help):
Sub FilterNew()
Dim Cel, Rng As Range
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Sheets.Add(After:=Sheets(Sheets.Count)).Name = "New" 'Adds a new Sheet to store unique values
Sheets(1).Rows("1:1").Copy Sheets("New").Rows("1:1") 'Copies the header row to the new sheet
Set Rng = Sheet(1).Range("B2:B" & Sheet(1).Range("B1").End(xlDown).Row)
For Each Cel In Rng
If Cel.Value <> Sheet(2).Cells(Cel.Row, 2).Value Then Cel.EntireRow.Copy Sheets("New").Range("A" & Rows.Count).End(xlUp).Offset(1, 0) ' The only issue I have with this is that this doesn't actually tell you if the value is unique, it just tells you ins not on the same rows of the first and second sheet - Is this alright with you?
Next
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub

VBA Look through List

I've got the following code which gets the word dividend from a column and then takes the whole row and copy pastes it to a new sheet.
Sub SortActions()
Dim i&, k&, s$, v, r As Range, ws As Worksheet
Set r = [index(a:a,match("###start",a:a,),):index(a:a,match("###end",a:a,),)].Offset(, 6)
k = r.Row - 1
v = r
For i = 1 To UBound(v)
If LCase$(v(i, 1)) = "dividend" Then
s = s & ", " & i + k & ":" & i + k
End If
Next
s = Mid$(s, 3)
If Len(s) Then
Set ws = ActiveSheet
With Sheets.Add(, ws)
ws.Range(s).Copy .[a1]
Rows("1:1").Select
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Sheets("20140701_corporate_action_servi").Select
Rows("2:2").Select
Selection.Copy2
Range("C32").Select
Sheets("Sheet11").Select
ActiveSheet.Paste
End With
End If
End Sub
Is there a way to make this dynamic. So if I want to search for more than word. For example if I have several rows with dividends and special dividends -> it would take all rows of dividends and all rows of special dividends and put them in separate sheets. I have tried ti with recording a macro it doesn't work as the words can differ. Maybe getting the content into a list would work. Please assist . Thanks
As suggested by #Macro Man , I am submitting images of an example sheet and sheet after filter with a simple macro for filtering one field. Please all credit to #Macro Man, it is for illustration in a simple way.
Simple code as follows.
Sub Filter1Field()
With Sheet1
.AutoFilterMode = False
With .Range("A1:H13")
.AutoFilter
.AutoFilter Field:=5, Criteria1:="Dividend"
End With
End With
End Sub
*****UPDATE*******
If your other criteria such as "Sp. Dividend" is other field but on the same row as shown in the image appended and you wish to copy to other sheet you can use the code given below. Another image shows results obtained on sheet2. You can adopt the code to your requrements.
You can use this code:
Sub Test2()
Dim LastRow As Long
Sheets("Sheet2").UsedRange.Offset(0).ClearContents
With Worksheets("Sheet1")
.Range("A1:H13").AutoFilter
.Range("A1:H13").AutoFilter field:=5, Criteria1:="Dividend"
.Range("A1:H13").AutoFilter field:=6, Criteria1:="=Sp. Dividend"
LastRow = .Range("A" & .Rows.Count).End(xlUp).Row
.Range("A1:A" & LastRow).SpecialCells(xlCellTypeVisible).EntireRow.Copy _
Destination:=Sheets("Sheet2").Range("A1")
End With
End Sub

Find first empty row in Excel and select

I tried adapting this post: Error in finding last used cell in VBA to my needs but couldn't quite get it to work.
I'm pasting data into a new worksheet and then want to select the first empty row after data. Currently what's happening is the data is pasted and then the very first row in the sheet is selected. See code below. Any thoughts?
'runs when user enters data
If Target.Cells.Count = 1 And _
Not Application.Intersect(Target, [I3:I10000]) Is Nothing Then
Application.EnableEvents = False
'User inputs type of event
Archive = InputBox("Was this event a Win, Loss, or Close? (Please input Win/Loss/Close)")
With Target
If Archive = "Win" Then
'all data to transfer is selected and cut
.EntireRow.Select
Selection.Cut
'the receiving sheet is selected and data is pasted to the selected cell
Sheets("Win").Select
ActiveSheet.Paste
'the selection on the sheet the data was cut from is deleted
Sheets("Begin").Select
Selection.Delete
'this is the issue I'm having - I want to select the row below the row I just copied into.
Sheets("Win").Select
lastRow = Range("C" & .Rows.Count).End(xlUp).Row
ActiveSheet.Range("C" & lastRow & ":C" & lastRow).EntireRow.Select
Sheets("Begin").Select
Try replacing this:
'this is the issue I'm having - I want to select the row below the row I just copied into.
Sheets("Win").Select
lastRow = Range("C" & .Rows.Count).End(xlUp).Row
ActiveSheet.Range("C" & lastRow & ":C" & lastRow).EntireRow.Select
with this:
With Sheets("Win")
lastRow = .Range("C" & .Rows.Count).End(xlUp).Row
.Cells(lastRow + 1, 1).EntireRow.Select
End With
Just to add to the existing answer. You can avoid doing so much selection by using a construction more like this:
On Error GoTo problem
Dim Archive As String
If (Target.Cells.Count = 1) And _
Not (Excel.Application.Intersect(Target, [I3:I10000]) Is Nothing) Then
Excel.Application.EnableEvents = False
'User inputs type of event
Archive = InputBox("Was this event a Win, Loss, or Close? (Please input Win/Loss/Close)")
With Target
'>>>> good idea to defend against users entering "win" instead of "Win"
If (LCase(Archive) = "win") Then
'>>>> find the last row in Win sheet at the beginning
With Sheets("Win")
lr = .Range("C" & .Rows.Count).End(Excel.xlUp).Row
End With
'>>>> as you are cutting there should be no need to do any subsequent deletion or clearcontents
.EntireRow.Cut Sheets("Win").Rows(lr + 1)
End If
End With
End If
problem:
Excel.Application.EnableEvents = True

Excel VBA - Compare two Columns in two different sheets then copy/paste - speed - It takes over an hour

Here an absolute beginner at any form of coding, this is the first time ever I try to use VBA.
I have managed after a week and a half of searching and testing and learning to reach the below posted code and I have hit a WALL (and I'm not even done yet!)
What I am trying to achieve:
Compare the data in sheet1 with the data in sheet2 found in Columns K respectively A (there are ca. 55.000 rows in K and 2500 in A) the data might repeat itself as these are product codes and it's ok as at the end of this I want to be able to see which ones have expired.
so .. If K = A then it has to copy adjacent values found in Sheet2 - columns O, P & Q and Paste them in Sheet2 - Columns O, P & Q and if no match is found then right not found. In the Example below I have only tried to copy Q, it would probably take forever if I tried adding O & P.
(Note: I have found this code in one of the forms here and used it after trying different other ways with select/ Copy/ Paste etc. but none have worked)
Later I would like to try adding another column in Sheet1 and based on the Date which will be copied to Sheet1 and into column P populate it with Expired or Soon to be expired depending on the case, but this is an entire different story and I haven't even begun thinking how to do it.
The problem is that my current code takes over an hour and it's still not finished yet while I am writing this!!! And I do not understand where have I gone wrong ....
Dim lastRow1 As Long
Dim lastRow2 As Long
Dim tempVal As String
lastRow1 = Sheets("Sheet1").Range("K" & Rows.Count).End(xlUp).Row
lastRow2 = Sheets("Sheet2").Range("A" & Rows.Count).Row
For sRow = 2 To lastRow1
tempVal = Sheets("MatCode").Cells(sRow, "A").Text
For tRow = 2 To lastRow2
If Sheets("Sheet1").Cells(tRow, "K") = tempVal Then
Sheets("Sheet1").Cells(tRow, "Q") = Sheets("Sheet2").Cells(sRow, "Q")
End If
Next tRow
Next sRow
Dim match As Boolean
'now if no match was found, then put NO MATCH in cell
For lRow = 2 To lastRow2
match = False
tempVal = Sheets("Sheet1").Cells(lRow, "K").Text
For sRow = 2 To lastRow1
If Sheets("Sheet2").Cells(sRow, "A") = tempVal Then
match = True
End If
Next sRow
If match = False Then
Sheets("Sheet1").Cells(lRow, "Q") = "NO MATCH"
End If
Next lRow
End Sub
I have also used:
With Application
.AskToUpdateLinks = False
.ScreenUpdating = False
.DisplayAlerts = False
.EnableEvents = False
End With
To make sure nothing stands in the way.
Please Help!
This will loop through rows to match column A on Sheet1 with column K on sheet2. On a non-match "No Match" will be put in Sheet1 column Q.
On a match Sheet2 columns O,P and Q will be copied to Sheet1 columns O,P and Q.
This took about 10 seconds to run for over 12k in column A and over 2500 in column K.
Sub match_columns()
Dim I, total, fRow As Integer
Dim found As Range
total = Sheets(1).Range("A" & Rows.Count).End(xlUp).Row
For I = 1 To total
answer1 = Worksheets(1).Range("A" & I).Value
Set found = Sheets(2).Columns("K:K").Find(what:=answer1) 'finds a match
If found Is Nothing Then
Worksheets(1).Range("Q" & I).Value = "NO MATCH"
Else
fRow = Sheets(2).Columns("K:K").Find(what:=answer1).Row
Worksheets(1).Range("O" & I).Value = Worksheets(2).Range("O" & fRow).Value
Worksheets(1).Range("P" & I).Value = Worksheets(2).Range("P" & fRow).Value
Worksheets(1).Range("Q" & I).Value = Worksheets(2).Range("Q" & fRow).Value
End If
Next I
End Sub
Thank you again #Mooseman for providing the solution!
I only had to change Range A with K, at first even so I was not able to make it work as it copied only the first line. I already had some code which opened the Worksheets and copied them to a new Worksheet/added new columns ..etc., to be SavedAs for later use, and it seems that because of this your code was not able to loop properly (not sure how to explain this) in any case at the end of the open / save workbooks ..etc I have introduced a Call Sub Procedure which worked like a charm!
Also, introduced two extra lines to properly format columns O and P as Date.
I am sure it could have looked better than this, but so far it works!
And thank you to everyone who provided me with suggestions, there is still a lot to learn and I am planning to test other ways just for the sake of learning, but I needed this to work now.
Sub Button1_Click()
With Application
.AskToUpdateLinks = False
.ScreenUpdating = False
.DisplayAlerts = False
.EnableEvents = False
.Calculation = xlCalculationManual
End With
'Code to Open / Save / introduce new columns into Sheet(1)
Call match_columns
End Sub
Sub match_columns()
Dim I, total, frow As Integer
Dim found As Range
total = Sheets(1).Range("K" & Rows.Count).End(xlUp).Row
'MsgBox (total) --> used to test if it can count/see the total number of rows
For I = 2 To total
answer1 = Worksheets(1).Range("K" & I).Value
Set found = Sheets(2).Columns("A:A").Find(what:=answer1) 'finds a match
If found Is Nothing Then
Worksheets(1).Range("Q" & I).Value = "NO MATCH"
Else
frow = Sheets(2).Columns("A:A").Find(what:=answer1).Row
Worksheets(1).Range("O" & I).Value = Worksheets(2).Range("O" & frow).Value
Worksheets(1).Range("P" & I).Value = Worksheets(2).Range("P" & frow).Value
Worksheets(1).Range("Q" & I).Value = Worksheets(2).Range("Q" & frow).Value
End If
Next I
Worksheets(1).Range("P2", "P" & total).NumberFormat = "dd.mm.yyyy"
Worksheets(1).Range("O2", "O" & total).NumberFormat = "dd.mm.yyyy"
With Application
.ScreenUpdating = True
.DisplayAlerts = True
.EnableEvents = True
.AskToUpdateLinks = True
.Calculation = xlCalculationAutomatic
End With
End Sub
This is slow because your macro is iterating through 55,000 * 2,500 rows of data, twice. That's 275,000,000 cycles.
I think the solution is to scrap the macro and use VLOOKUP or Index Match.
You could add this formula to cell Q2 of sheet1:
=IFERROR(INDEX(Sheet2!$Q:$Q,MATCH(Sheet1!$K2,Sheet2!$A:$A,0)),"NO MATCH")
That is how I would do this. If you need it to be a macro, you can write a macro that just sets Sheet1 K2 to have this formula and drag the formula down.