PasteSpecial twice crashes Excel VBA - excel

I've been looking for a solution to using .PasteSpecial more than once in a an excel vba macro.
I have reports that can sometimes have thousands of rows. In these reports are two columns, I need to merge these columns so that if one column has blanks, I want the value from the second column; otherwise, just keep the value in the first column. I need to apply this twice in two different places. This is all tucked into a larger code.
My solution is to utilize .PasteSpecial with "Skip Blanks". It is quick for excel to process, much faster than looping row by row. The problem is that the code keeps crashing excel.
After debugging, here is what I've learned so far:
*The first .PasteSpecial always works, but when it gets to the second .PasteSpecial it always fails.
*I've tried STOP after the first .PasteSpecial then step through the code, and after I step through the second.PasteSpecial the code works just fine.
*If I step through the second .PasteSpecial it works like nothing is wrong - but if I just run the code like normal it crashes.
*I switched the order of the two .PasteSpecials within the code. When I do this, it no longer crashes on the problematic .PasteSpecial, but it does crash on the originally working .PasteSpecial.
Based on this, I know the problem is Excel doesn't like .PasteSpecial twice in a code. Still cannot find a work around. I've tried emptying the clip board, and I don't know enough how to set up an array let alone if that is efficient for this much data. Anybody know of a solution or work around?
Here is my .PasteSpecial code:
MainSheet.Range("N:N").Copy
MainSheet.Range("P:P").PasteSpecial Paste:=xlPasteAll, Operation:=xlNone, SkipBlanks:=True, Transpose:=False
MainSheet.Range("R:R").Copy
MainSheet.Range("Q:Q").PasteSpecial Paste:=xlPasteAll, Operation:=xlNone, SkipBlanks:=True, Transpose:=False

Your issue is that you are committing CPU to do OS level tasks when you don't actually need to paste anything.
The cells have values... so make them Equal and they... will... be...
Range("C1").Value = Range("A1").Value
Alternatively you can use a power query to just do this where Table 2 Col2 is null and ID = ID

Not needing format, please test the next code. It uses an array, not uses clipboard and is faster. If no need to exist a correspondence between the rows in the two filled columns, you can use the next fast way:
Sub copyColumnsArray()
Dim lastR As Long, arrCopy, arrFin, i As Long, k As Long
lastR = MainSheet.Range("N" & rows.count).End(xlUp).row
arr = MainSheet.Range("N1:N" & lastR).value
'fill another array only with non empty values:__________________
ReDim arrFin(UBound(arr) To 1): k = 1
For i = 1 To UBound(arrCopy)
If arrCopy(i, 1) <> "" Then arrFin(k, 1) = arrCopy(i, 1): k = k + 1
Next i
ReDim Preserve arrFin(k - 1 To 1)
'______________________________________________________
MainSheet.Range("P1").Resize(UBound(arrFin), 1).value = arrFin
lastR = MainSheet.Range("R" & rows.count).End(xlUp).row
arr = MainSheet.Range("R1:R" & lastR).value
'fill another array only with non empty values:__________________
ReDim arrFin(UBound(arr) To 1): k = 1
For i = 1 To UBound(arrCopy)
If arrCopy(i, 1) <> "" Then arrFin(k, 1) = arrCopy(i, 1): k = k + 1
Next i
ReDim Preserve arrFin(k - 1 To 1)
'______________________________________________________
MainSheet.Range("Q1").Resize(UBound(arrFin), 1).value = arrFin
End Sub

EDIT:
A better answer that worked more reliably than my original found solution is below. This is an adaptation of some advice from FaneDuru's answer. This solution is more taxing on resources; however, for now - it performs the task reliably (without crashing). I would love for there to be a better answers than row looping; however, this does answer my OP. Thank you for all the help!
Sub copyColumnsArray()
Dim lastR As Long, arrCopy
lastR = MainSheet.Range("N" & rows.count).End(xlUp).row
arrCopy = MainSheet.Range("N1:N" & lastR).value
Dim ArrayIndex as Variant
Dim RowCount as String
RowCount = 1
For Each ArrayIndex in arrCopy
If ArrayIndex = "" then
RowCount = RowCount +1
'Skip Blank
else
MainSheet.Range("P"+RowCount).value = ArrayIndex
RowCount = RowCount + 1
end if
Next
lastR = MainSheet.Range("R" & rows.count).End(xlUp).row
arrCopy = MainSheet.Range("R1:R" & lastR).value
RowCount = 1
For Each ArrayIndex in arrCopy
If ArrayIndex = "" then
RowCount = RowCount +1
'Skip Blank
else
MainSheet.Range("Q"+RowCount).value = ArrayIndex
RowCount = RowCount + 1
end if
Next
End Sub

Related

Can we change complex excel formula into VBA

Can anyone please write this formula in VBA code, this is making the sheet heavy and whenever i add or edit raw data (Data sheet) it starts "Calculating 4 processors" and takes much time.
In raw Data Sheet there are almsot 18000 entries and in other sheet where i am extracting the status contains 8000 entries, however it would be much helpful if it sees till the last raw.
=IF(SUMPRODUCT((Data!$A$2:$A$17989=A7076)(Data!$B$2:$B$17989=B7076)(Data!$C$2:$C$17989="Combine")),"Available",IF(COUNTIFS(Data!$A$2:$A$17989,A7076,Data!$B$2:$B$17989,B7076,Data!$C$2:$C$17989,"Feed *")=2,"Available","Not Available"))
I have read many articles on web and tried them but not helpful and i am wondering if VBA is one of the best solution for this complex formula.
what i have recorded is below:
Sub Macro1()
'
ActiveCell.FormulaR1C1 = _
"=IF(SUMPRODUCT((Data!R2C1:R17989C1=RC[-5])*(Data!R2C2:R17989C2=RC[-4])*(Data!R2C3:R17989C3=""Combine"")),""Available"",IF(COUNTIFS(Data!R2C1:R17989C1,RC[-5],Data!R2C2:R17989C2,RC[-4],Data!R2C3:R17989C3,""Feed *"")=2,""Available"",""Not Available""))"
Range("F2").Select
Selection.Copy
Range("F3:F7076").Select
ActiveSheet.Paste
Application.CutCopyMode = False
Selection.End(xlUp).Select
Range("F3").Select
ActiveWorkbook.Save
End Sub
thank you
Please, test the next solution. Activate the sheet to be processed and run SetAvailability:
Sub SetAvailability()
Dim sh As Worksheet, shD As Worksheet, lastRA As Long, lastRD As Long, arrD, arr, arrF, i As Long
Set sh = ActiveSheet
Set shD = Worksheets("Data")
lastRA = sh.Range("A" & sh.rows.count).End(xlUp).Row
lastRD = shD.Range("A" & shD.rows.count).End(xlUp).Row
arrF = sh.Range("F2:F" & lastRA).Value2
arr = sh.Range("A2:B" & lastRA).Value2
arrD = shD.Range("A2:C" & lastRD).Value2
For i = 1 To UBound(arr)
arrF(i, 1) = getAvailability(arrD, arr(i, 1), arr(i, 2), "Combine", "Feed *")
Next i
'drop the processed aray content at once:
sh.Range("F2").Resize(UBound(arrF), 1).Value2 = arrF
MsgBox "Ready..."
End Sub
Function getAvailability(arrD, strAA, strBB, strAv As String, strFeed As String) As String
Dim countFeed As Long, i As Long
For i = 1 To UBound(arrD)
If arrD(i, 1) = strAA And UCase(arrD(i, 2)) = UCase(strBB) Then
If UCase(arrD(i, 3)) = UCase(strAv) Then getAvailability = "Available": Exit Function
If arrD(i, 3) Like strFeed Then countFeed = countFeed + 1
If countFeed = 2 Then getAvailability = "Available": Exit Function
End If
Next i
getAvailability = "Not Available"
End Function
It should be fast enough, working only in memory and dropping the processed array content at once, at the end of the code. And, no any workbook charge because of complicated formula...
The above solution assumes that strings as "Feed *" will be at least two, for matchings in A:A and B:B, meaning that more (than two) such matches are allowed to consider the return as "Available".
It will take some time, anyhow, but I am curious how match for your specific data ranges.
It will be faster if the availability is bigger (the iteration on "Data" sheet array stops after finding the match...).

What is the correct Dim-type and why is my macro getting slower?

I have an Excel-macro that basically works just fine for most of the cases, but there are three issues that bug me.
The code is a bit longer so I've reduced it to address the issues: (The issues are also marked in my code.)
Nr.1: When uniqueArray consists of more than one entry, the Dim for item and uniqueArray are fine. But when I've tested the unlikely case that uniqueArray consists of only one entry, I got the error, that the types don't match. I'm usally not programming stuff in Excel, so I'm not really familiar with the different types in vba. Do I need arrays here or can I just change the Dim?
Nr.2: The code gets slower and slower, the more sheets are added to the workbook by the macro. Is that normal behaviour, or can I speed up my code a bit?
Nr.3: A few years ago I had an issue with a slow macro. Then I found the hint with a forced pause. I've tried it with this macro again, and it improved the speed by a huuuge amount of time. How come a pause speeds up a macro?
Sub Three_Issues()
Dim ColumnLetter As String
Dim cell As Range
Dim sheetCount, TotalRow, TotalCol As Integer
'Dim item, uniqueArray As Variant
Dim item, uniqueArray() As Variant
Dim lastRow As Long
Application.ScreenUpdating = False
'Get unique brands:
With Sheets("Brand")
.Columns(1).EntireColumn.Delete
Sheets("Sales").Columns("R:R").AdvancedFilter Action:=xlFilterCopy, CopyToRange:=.Range("A1"), Unique:=True
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
'uniqueArray = .Range("A3:A" & lastRow)
'Update:
If .Range("A3:A" & lastRow).Cells.Count = 1 Then
ReDim uniqueArray(1, 1)
uniqueArray(1, 1) = .Range("A3")
Else
uniqueArray = .Range("A3:A" & lastRow).Value
End With
TotalRow = Sheets("Sales").UsedRange.Rows.Count
TotalCol = Sheets("Sales").UsedRange.Columns.Count
ColumnLetter = Split(Cells(1, TotalCol).Address, "$")(1) 'Num2Char
sheetCount = 0 'Counter for statusbar
For Each item In uniqueArray 'item=Brand
'->Issue 1: Runtimer error 13 Types don't match: This happens if the uniqueArray consists of only one brand.
'Then item is Variant/Empty and uniqueArray is Variant/String
'If uniqueArray consists of more than one brand - which is usually the case - it works fine.
'item=Variant/Empty uniqueArray=e.g. Variant/Variant(1 to 2, 1 to 1)
'Can I change the Dim statement to solve this special case, or do I need arrays maybe?
'Filter sales for each brand:
With Sheets("Sales")
.Range(.Cells(2, 1), .Cells(TotalRow, TotalCol)).AutoFilter Field:=18, Criteria1:=item
End With
With Sheets("Agents")
'Delete old...
.Range(.Cells(2, 1), .Cells(2, 1).End(xlDown)).Clear
'...and get new
Sheets("Sales").Range(Sheets("Sales").Cells(3, 2), Sheets("Sales").Cells(2, 2).End(xlDown)).SpecialCells(xlCellTypeVisible).Copy
.Range("A2").PasteSpecial Paste:=xlPasteValues
Application.CutCopyMode = False
End With
'List with all agents
For Each cell In Worksheets("Agents").Range("A2", Worksheets("Agents").Range("A1").End(xlDown))
With Sheets("Report")
.Range("I4") = cell 'Copy agent and update the formulas within the report
'->Issue 2: It takes around 10 seconds to fill 10 sheets with the reports of 10 agents.
'When I reach 70-80 sheets, it slows down to 30 seconds for 10 sheets.
'Is this just because of the number of sheets, or can I speed it up again?
.Range(.PageSetup.PrintArea).Copy
Sheets.Add After:=Sheets("Report")
Selection.PasteSpecial Paste:=xlPasteAllUsingSourceTheme, Operation:=xlNone _
, SkipBlanks:=False, Transpose:=False
Selection.PasteSpecial Paste:=xlPasteColumnWidths, Operation:=xlNone, _
SkipBlanks:=False, Transpose:=False
ActiveSheet.UsedRange.Value = ActiveSheet.UsedRange.Value 'Replace all formulas with values
Application.CutCopyMode = False
ActiveSheet.Name = cell
sheetCount = sheetCount + 1
If sheetAnz Mod 10 = 0 Then Application.StatusBar = sheetAnz 'Get statusupdate every 10 sheets
End With
Next
'->Issue 3: I create up to 400 sheets and when I want to continue and do some sorting of the sheets for example it takes a very long time.
'But if I add this break for a second, it works reasonably fine again. Why is that? Does vba needs the break to catch up with itself?
'Since the issue is not the sorting and the other stuff after the pause.
Application.Wait (Now + TimeValue("0:00:01")) 'Code becomes faster after that...
'Continue with other stuff.... sorting sheets and so on
Next
Application.ScreenUpdating = True
End Sub
Any ideas on one of the issues?
You can output a array with 1 value or multiple values using the below UDF. This would benefit from also passing along a worksheet variable so objects can be properly qualified
Call the function from your current macro like so
uniqueArray = MyArr(lastrow)
Public Function MyArr(lastrow As Long) As Variant
If Range("A3:A" & lastrow).Cells.Count = 1 Then
ReDim MyArr(1, 1)
MyArr(1, 1) = Range("A3")
Else
MyArr = Range("A3:A" & lastrow).Value
End If
End Function

delete rows if there are 2 consecutive empty rows

what i want to do is to delete rows if there are 2 consecutive empty rows and also to have the empty rows between the header and the first set of data row to be deleted as well.This is my original
input and what i want to have is this. i have tried to find some codes here and there and come up with this code.
Sub Testing()
Dim i As Long , lRow As Long
Dim ws As Worksheet
Set ws = Activesheet
With ws
With .Range("C:C")
fr = .Find(what:="*", after:=.Cells(1, 1), LookIn:=xlValues).row
If fr > 2 Then
.Rows("2:" & fr - 1).EntireRow.Delete
End If
End With
i = 1
For i = 1 To lRow
If IsEmpty(Cells(i, 3)) And IsEmpty(Cells(i + 1, 3)) Then
.Rows(i).EntireRow.Delete
End If
Next i
End With
End Sub
However, there are still some consecutive empty rows in the middle of the data set. I know that is because i am increasing i which will look at the next cell but i am not sure how to solve it. I am new to vba and even newer to SO posting so let me know if there is anything i am doing wrong and thank you for your help.
The only thing you need to do is looping backwards. Instead of
For i = 1 To lRow
do
For i = lRow To 1 Step -1
This is because looping from the bottom doesn't have any influence on the row counting of the not yet processed rows, but looping top to bottom does.
Also you can skip i = 1 right before For it doesn't have any influence since For starts with whatever i is specified as lower bound.
I think your code is just an example but just in case note that lRow is never set to a value in your code and therefore is 0.
Note that in this line
If IsEmpty(Cells(i, 3)) And IsEmpty(Cells(i + 1, 3)) Then
your Cells objects are not referenced to the sheet of the With statement because you forgot the . in the beginning. It should be
If IsEmpty(.Cells(i, 3)) And IsEmpty(.Cells(i + 1, 3)) Then
Furthermore I highly recommend that if you use the Range.Find method
fr = .Find(what:="*", after:=.Cells(1, 1), LookIn:=xlValues).row
that you always specify the LookAt parameter as xlWhole or xlPart (see XlLookAt). Because the the LookAt parameter has no default value (sadly) and if you don't specify it, VBA will use either xlWhole or xlPart whatever was used last by either the user interface or VBA. So you cannot know which one was used before and it will become pretty random (or your code might sometimes work and sometimes not).
Alternative (much faster) approach …
… is to keep the forward loop and collect all rows to delete in a variable RowsToDelete to delete them in the end at once. It is so much faster because every delete action takes time and in this approach you only have one delete action … versus one delete action per row in the other approach.
Dim RowsToDelete As Range
For i = 1 To lRow 'forward loop is no issue here because we just collect
If IsEmpty(.Cells(i, 3)) And IsEmpty(.Cells(i + 1, 3)) Then
If RowsToDelete Is Nothing Then 'first row
Set RowsToDelete = .Rows(i).EntireRow
Else 'append more rows with union
Set RowsToDelete = Application.Union(RowsToDelete, .Rows(i).EntireRow)
End If
End If
Next i
'delete all collected rows (after the loop, so delete doesn't affect row counting of the loop)
If Not RowsToDelete Is Nothing Then
RowsToDelete.Delete
End If
I think you need to decrease i after deleting a row.
For i = 1 To lRow
If IsEmpty(Cells(i, 3)) And IsEmpty(Cells(i + 1, 3)) Then
.Rows(i).EntireRow.Delete
i = i - 1
lRow = lRow - 1
End If
If i > lRow Then Exit For
Next i
Dim blankCtr As Integer
blankCtr = 0
With ActiveSheet
For i = .Range("A" & Rows.Count).End(xlUp).Row To 2 Step -1
If Application.WorksheetFunction.CountA(Rows(i)) = 0 Then
blankCtr = blankCtr + 1
If .Rows(i).Cells(1).End(xlUp).Row = 1 Then
.Rows(i & ":" & .Rows(i).Cells(1).End(xlUp).Offset(1).Row).Delete
Exit Sub
End If
If blankCtr > 1 Then
.Rows(i).Delete
blankCtr = blankCtr - 1
End If
Else
blankCtr = 0
GoTo here
End If
here:
Next i
End With

More efficient method to sort read-in text file

I've read in a text file to excel from a database and I've done it in such a way that it filters out unnecessary columns. My approach to filter rows was to use two subroutines and call the 2nd from within the first. It takes ~8 seconds for the sheet to be filtered and there is only 400 or so rows. The fact that it takes that long (even though it works) is that my code is inefficient. If anyone has a better method I would greatly appreciate the knowledge! To delimit rows I've used the following VBA:
Sub FilterAndDelete()
Dim LR As Long, i As Long
LR = Range("A" & Rows.Count).End(xlUp).Row
For i = LR To 1 Step -1
Select Case Left(Range("A" & i).Value, 3)
Case "CHA", "HAM", "BKN"
Call FilterAndDeleteB
Case Else
Rows(i).Delete
Call FilterAndDeleteB
End Select
Next i
End Sub
Sub FilterAndDeleteB()
Dim Br As Long, i As Long
Br = Range("B" & Rows.Count).End(xlUp).Row
For i = Br To 1 Step -1
Select Case Left(Range("B" & i).Value, 1)
Case "-"
Rows(i).Delete
Case Else
'do nothing
End Select
Next i

How to speed up Copy Paste Values when using Offset

I want make this basic function of "copy&paste-values-on-a-new-row-each-time" run as fast as possible since the macro repeats the calculations hundreds of thousands of times. I just can't find the exact answer after searching this forum for ages.
Currently, I'm copying output numbers from a fixed range and, elsewhere on the worksheet, pasting the values on a new row for each new set of results.
Here's the portion of the code doing this:
Row = Row +1
Range("g15:ax15").copy
Range("ea18").select
ActiveCell.Offset(Row,0).select
Selection.PasteSpecial Paste:=xlPasteValues
Now from what I have found on this forum, I can replace the Copy/Paste functions completely with Range(destination).value = Range(results).value to speed things up. However, I can't figure out how to do this if the destination rows need to be offset by 1 each time. Also, I've read that one could even do away with "select" to speed things up further! How?
There are a number of options:
//This uses the `Destination` key word
Sub CopyAndPaste()
Dim i as long
For i = 1 to 10
Range("g15:ax15").Copy Destination:=Range("ea18").Offset(i, 0)
next i
End Sub
//If you need `PasteSpecial` then you cannot use `Destination` hence this version
Sub CopyAndPaste()
Dim i as long
For i = 1 to 10
Range("g15:ax15").Copy
Range("ea18").Offset(i, 0).PasteSpecial Paste:=xlPasteValues
next i
End Sub
Sometimes reading values into an array first and then writing back to the spreadsheet is quicker. Here is an example:
Sub CopyAndPaste()
Dim i As Long, numbers As Variant, rw As Long
numbers = Range("g15:ax15")
rw = 18
For i = 1 To 10
rw = rw + 1
Range(Cells(rw, 131), Cells(rw, 131 + UBound(numbers, 2) - 1)) = numbers
Next i
End Sub
You can do it without copying as yo mention (using a variant array as you are copying values only, not formats)
X = Range("g15:ax15").Value2
[ea18].Offset(1, 0).Resize(UBound(X, 1), UBound(X, 2)).Value2 = X
or with your variable offset
Dim lngCnt As Long
lngCnt = lngCnt + 1
X = Range("g15:ax15").Value2
[ea18].Offset(lngCnt, 0).Resize(UBound(X, 1), UBound(X, 2)).Value2 = X
Row = Row +1
Range("g15:ax15").copy
Range("ea18").Offset(Row,0).PasteSpecial Paste:=xlPasteValues
Select is a more-or-less useless method inherited from recordings.

Resources