Alternatives to ActiveSheet.Paste to reduce memory - excel

I am trying to utilise the VBA/macro function at work. However, I came across a problem which says something like 'error; insufficient memory' and after some browsing on the internet I realise that copy and pasting generally takes up a lot of spaces in the Excel and my Excel in workplace is only 32 bits. Therefore, does anyone know any good alternatives to ActiveSheet.Paste?
My codes are currently as following:
ActiveSheet.Range ("$A$!:$Y$1000").AutoFilter Field:=7, Criterial:=_banker
Range("A1").Select
Range(Selection, Selection.End(x1Down)).Select
Range(Selection, Selection.End(X1ToRight)).Select
Selection.Copy
Sheets.Add After:=ActiveSheet
ActiveSheet.Paste
ActiveSheet.Name= banker
i=i+1
Loop
End Sub

You should copy your code as it is and no writing it. In this way you will avoid spelling mistakes (Range ("$A$!:$Y$1000") instead of "A1:..., X1ToRight instead of XlToRight, "_banker" instead of "banker"). Putting Option Explicit on top of your module, as recommended above will help.
A way to filter and copy the filter range, not involving the clipboard should be the next code. Please, test it and send some feedback:
Sub testFilterCopyRange()
Dim sh As Worksheet, shNew As Worksheet, lastR As Long
Dim banker As String, rng As Range, rngF As Range, arr, i As Long
Dim maxIt As Long 'number of iterations
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row 'last row
maxIt = 1 'set here the maximum number of necessary iterations
sh.AutoFilterMode = False 'eliminate the previous filter
Set rng = sh.Range("A1:H" & lastR) 'set the range to be processed
For i = 1 To maxIt
banker = "7" '"your dinamic criteria"
rng.AutoFilter field:=7, Criteria1:=banker 'filter the range according to above defined criteria
Set rngF = rng.SpecialCells(xlCellTypeVisible) 'set a range to keep the filtered cells in the range
arr = arrayFromDiscRange(rngF, False) 'header inclusive
Set shNew = Sheets.Add(After:=sh): shNew.Name = banker 'add a new sheet and name it
shNew.Range("A1").Resize(UBound(arr), UBound(arr, 2)).value = arr 'drop the array content at once
Next i
End Sub
'function able to transform a filtered (discontinue) range in an array:
Private Function arrayFromDiscRange(rngF As Range, Optional NoHeader As Boolean = False) As Variant
Dim arr, i As Long, j As Long, k As Long, A As Range, R As Range, iRows As Long
'count range rows
For Each A In rngF.Areas
iRows = iRows + A.rows.count
Next A
'Redim the array to keep the range
ReDim arr(1 To iRows - IIf(NoHeader, 1, 0), 1 To rngF.Columns.count): k = 1
For Each A In rngF.Areas 'iterate between the range areas:
For Each R In A.rows 'iterate between the area rows:
If NoHeader And k = 1 Then GoTo Later 'skip the first row, if no header wanted
For j = 1 To R.Columns.count 'iterate between the area row columns:
arr(k, j) = R.cells(1, j).value 'place each row cells value in the array row
Next j
k = k + 1 'intrement the array row to receive values
Later:
Next
Next A
arrayFromDiscRange = arr 'returning the created array
End Function
If something unclear, even if I tried commenting all the code line which could be problematic in understanding, please do not hesitate to ask for clarifications.

Related

If value that is in a column on sheet A but doesn't exist in a column on sheet B add that value to sheet B

I am trying to write a script that will look in a column A on sheet1 and see if it is missing any values from column J on sheet2, and if it is missing have the value added to the bottom of the column on sheet1. I found some example code (see below), however, when I modify it to work across the two sheets I get an error.
Sub Macro1()
Dim rngA As Range, rngB As Range, MySel As Range, LastRow As Long, ws As Worksheet
Set ws = ThisWorkbook.Worksheets(1)
With ws
LastRow = .Range("A" & .Rows.Count).End(xlUp).Row
Set rngA = .Range("A1:A" & LastRow)
Set rngB = .Range("B1", .Range("B" & .Rows.Count).End(xlUp))
End With
For Each cell In rngB
If IsError(Application.Match(cell.Value, rngA, 0)) Then
If MySel Is Nothing Then
Set MySel = cell
Else
Set MySel = Union(MySel, cell)
End If
End If
Next cell
If Not MySel Is Nothing Then MySel.Copy Destination:=ws.Range("A" & LastRow + 1)
End Sub
Any help to modify this to function across sheets would be greatly appreciated. Thanks!
You may try the following code modification, you are getting the error due to the variable cell was not declared and ws.Range("B" & .Rows.Count).End(xlUp) is not a valid range, and you should set Range B by referring to another worksheet if you want to do so:
Sub Macro1()
Dim rngA As Range, rngB As Range, MySel As Range
Dim LastRowA As Long, LastRowB As Long
Dim ws As Worksheet
Dim cell As Range
Set ws = ThisWorkbook.Worksheets(1)
With ws
LastRowA = .Range("A" & .Rows.Count).End(xlUp).Row
LastRowB = .Range("B" & .Rows.Count).End(xlUp).Row
Set rngA = .Range("A1:A" & LastRowA)
Set rngB = .Range("B1:B" & LastRowB)
End With
For Each cell In rngB.Cells
If IsError(Application.Match(cell.Value, rngA, 0)) Then
If MySel Is Nothing Then
Set MySel = cell
Else
Set MySel = Union(MySel, cell)
End If
End If
Next cell
If Not MySel Is Nothing Then MySel.Copy Destination:=ws.Range("A" & LastRowA + 1)
End Sub
Before:
After:
Array approach using one-liner for Match()
Instead of looping through a data range you can execute an array Match()
to compare data values with reference values by a one-liner:
data = Application.Match(ref, data, 0)
Methodical hint
Findings return the position within the reference array, whereas all non-findings (i.e. new and therefore unique values) can be identified easily by a corresponding error entry. This is used to re-write the data array exclusively by the wanted uniques. The resulting data are eventually added in the needed size to the existing data.
Note that commonly the Match() function loops asking for single search values (1st parameter) within a reference array (2nd parameter),
e.g. via Application.Match(SingleSearchValue, reference, 0).
Side note: looping through a range by means of VBA can be time consuming for greater data set, so generally I prefer an array approach.
As OP seems to refer to two sheets with different columns A and J (instead of B),
I demonstrate a solution following this requirement.
Option Explicit
Sub AppendNewItems()
'1) get data & reference arrays via function GetDatafield()
Dim data: data = GetDatafield(Sheet1, "A") ' current data
Dim ref: ref = GetDatafield(Sheet2, "J") ' reference values
Dim NewRow As Long
NewRow = UBound(data) + 1 ' get starting row for new entries
'2) look up all of the data in the reference array and write found positions to data (one-liner)
data = Application.Match(ref, data, 0)
'Edit2: check for no or only 1 reference item ' << 2021-07-23/see comment
On Error Resume Next
Debug.Print data(1, 1)
If Err.Number <> 0 Then
Err.Clear
ReDim tmp(1 To 1, 1 To 1)
tmp(1, 1) = data(1)
data = tmp
End If
'3) take only new (=unique) elements
Dim i As Long, ii As Long
For i = 1 To UBound(data) ' loop through matches
If IsError(data(i, 1)) Then ' identify new (=not found) elements by error
ii = ii + 1 ' increment uniques counter
data(ii, 1) = ref(i, 1) ' replace error element with current reference value
End If
Next
'4) add new data to column A (not more than ii elements)
If ii Then
Sheet1.Range("A" & NewRow).Resize(ii, 1) = data
End If
End Sub
Help function GetDatafield()
Function GetDatafield(sht As Worksheet, Col As String)
Dim LastRow As Long
LastRow = sht.Range(Col & sht.Rows.Count).End(xlUp).Row
'return 1-based 2-dim datafield array
GetDatafield = sht.Range(Col & "1:" & Col & LastRow).Value2
'force single value into array ' << Edit 2021-07-22/see comment
If Not IsArray(GetDatafield) Then ' or: If LastRow = 1 Then
ReDim tmp(1 To 1, 1 To 1)
tmp(1, 1) = sht.Range(Col & "1").Value2
GetDatafield = tmp ' pass 2-dim array
End If
End Function

Array of filtered data to populate ListBox

Okay so I am filtering a sheet ("Data") by a criteria:
Sub Filter_Offene()
Sheets("Data").Range("A:R").AutoFilter Field:=18, Criteria1:="WAHR"
End Sub
Then, I want to put the Filtered Table to populate a Listbox
My problem here is, that the amount of rows can vary, so I thought i could try and list where the filtered table "ends" by doing this cells.find routine:
Dim lRow As Long
Dim lCol As Long
lRow = ThisWorkbook.Sheets("Data").Cells.Find(What:="*", _
After:=Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
lRow = lRow + 1
This unfotunatly also counts "hidden" rows, so in my example it doesnt count 2 but 7..
I've used .Range.SpecialCells(xlCellTypeVisible)before, but It doesn't seem to function with the cells.find above.
Does someone have an Idea on how I can count the visible (=filtered) Table, and then put it in a Listbox?
EDIT: I populate the listbox (unfiltered) like this:
Dim lastrow As Long
With Sheets("Data")
lastrow = .Cells(.Rows.Count, "R").End(xlUp).Row
End With
With Offene_PZ_Form.Offene_PZ
.ColumnCount = 18
.ColumnWidths = "0;80;0;100;100;0;50;50;80;50;0;0;0;0;0;150;150;0"
.List = Sheets("Data").Range("A2:R" & lastrow).Value
End With
But this won't work with filtered Data.
Here is a fun little fact, Excel creates an hidden named range once you start filtering data. If you have continuous data (headers/rows) this would return your range without looking for it. Though since it seem to resemble UsedRange it may still be better to search your last used column and row and create your own Range variable to filter. For this exercise I'll leave it be. Furthermore, as indicated in the comments above, one can loop over Areas of visible cells. I'd recommend a check beforehand just to be safe that there is filtered data other than headers.
Sub Test()
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Data")
Dim Area as Range
ws.Cells(1, 1).AutoFilter 18, "WAHR"
With ws.Range("_FilterDatabase")
If .SpecialCells(12).Count > .Columns.Count Then
For Each Area In .Offset(1).Resize(.Rows.Count - 1, .Columns.Count).SpecialCells(12).Areas
Debug.Print Area.Address 'Do something
Next
End If
End With
End Sub
The above works if no headers are missing obviously.
Here is a VBA code to populate UserForm1.ListBox1.List with filtered rows.
Thanks to #FaneDuru for improvements in the code edited as per his comments.
In Userform1 code
Private Sub UserForm_Initialize()
PopulateListBoxWithVisibleCells
End Sub
In Module
Sub PopulateListBoxWithVisibleCells()
Dim wb As Workbook, ws As Worksheet
Dim filtRng As Range, rw As Range
Dim i As Long, j As Long, x As Long, y As Long, k As Long, filtRngArr
i = 0: j = 0: x = 0: y = 0
Set wb = ThisWorkbook: Set ws = wb.Sheets("Sheet1")
Set filtRng = ws.UsedRange.Cells.SpecialCells(xlCellTypeVisible)
For Each Area In filtRng.Areas
x = x + Area.Rows.Count
Next
y = filtRng.Columns.Count
ReDim filtRngArr(1 To x, 1 To y)
For k = 1 To filtRng.Areas.Count
For Each rw In filtRng.Areas(k).Rows
i = i + 1
arr = rw.Value
For j = 1 To y
filtRngArr(i, j) = Split(Join(Application.Index(arr, 1, 0), "|"), "|")(j - 1)
Next
Next
Next
With UserForm1.ListBox1
.ColumnCount = y
.List = filtRngArr
End With
End Sub
We can also add more fields say row number like Split(rw.Row & "|" & Join(Application.Index(arr, 1, 0), "|"), "|")(j - 1) but for every such intended column increments, we need to increment value of y like y = filtRng.Columns.Count + 1
In order to find x (Number of rows) we don't need the first loop... Simply, x = filtRng.Cells.Count / filtRng.Columns.Count is enough
Try, please the next code, if you want to use a continuous (built) array. It is possible to build it from the discontinuous range address, too:
Sub Filter_Offene()
Dim sh As Worksheet, lastRow As Long, rngFilt As Range, arrFin As Variant
Set sh = Sheets("Data")
lastRow = sh.Range("R" & Rows.count).End(xlUp).Row
rngFilt.AutoFilter field:=18, Criteria1:="WAHR"
Set rngFilt = rngFilt.Offset(1).SpecialCells(xlCellTypeVisible)
arrFin = ContinuousArray(rngFilt, sh, "R:R")
With ComboBox1
.list = arrFin
.ListIndex = 0
End With
End Sub
Private Function ContinuousArray(rngFilt As Range, sh As Worksheet, colLet As String) As Variant
Dim arrFilt As Variant, El As Variant, arFin As Variant
Dim rowsNo As Long, k As Long, i As Long, j As Long, arrInt As Variant
arrFilt = Split(rngFilt.address, ",")' Obtain an array of areas addresses
'real number of rows of the visible cells range:
For Each El In arrFilt
rowsNo = rowsNo + Range(El).Rows.count
Next
'redim the final array at the number of rows
ReDim arFin(1 To rowsNo, 1 To rngFilt.Columns.count)
rowsNo = 1
For Each El In arrFilt 'Iterate between the areas addresses
rowsNo = Range(El).Rows.count 'number of rows of the area
arrInt = ActiveSheet.Range(El).value' put the area range in an array
For i = 1 To UBound(arrInt, 1) 'fill the final array
k = k + 1
For j = 1 To rngFilt.Columns.count
arFin(k, j) = arrInt(i, j)
Next j
Next i
Next
ContinuousArray = arFin
End Function

Get a filtered range into an array

I am trying to get a filtered range into an array, on my test data the array fArr has the proper dim and fLR is the proper count of the filter range
But filRange is always only the header range NOT the filtered range
How to get filRange to be the filtered range?
Or to the point how to get fArr to be an array of the filter data?
Thanks
Sub arrFilterdRng()
Dim fArr As Variant
Dim rRange As Range, filRange As Range, myCell As Range
Dim fLR As Long, rCtr As Long
'Remove any filters
ActiveSheet.AutoFilterMode = False
'~~> Set your range
Set rRange = Sheets("Z").UsedRange
With rRange
'~~> Set your criteria and filter
.AutoFilter Field:=3, Criteria1:="*"
Set filRange = .SpecialCells(xlCellTypeVisible).EntireRow
fLR = .Resize(, 1).SpecialCells(xlCellTypeVisible).Count
Debug.Print fLR
ReDim fArr(1 To fLR, 1 To .Columns.Count)
Debug.Print UBound(fArr, 1), UBound(fArr, 2)
rCtr = 0
For Each myCell In filRange.Columns(1)
rCtr = rCtr + 1
For cCtr = 1 To .Columns.Count
fArr(rCtr, cCtr) = myCell.Offset(0, cCtr - 1).value
Next cCtr
Next myCell
End With
'Remove any filters
ActiveSheet.AutoFilterMode = False
End Sub
My data looks like this (all text)
My feeling is that the wildcard in your criteria is causing the trouble.
"*" only works for strings, so if your data are numbers (including dates) then they would be removed by the filter (ie they wouldn't be visible), so you would indeed only have the header in your range.
If you want numerical values, then one way of doing it would be to define a value, say:
.AutoFilter Field:=3, Criteria1:=">0"
or, if you want limits:
.AutoFilter Field:=3, Criteria1:=">0", Operator:=xlAnd, Criteria2:="<10"
If, on the other hand, you just want anything but blank cells, then the syntax should be:
.AutoFilter Field:=3, Criteria1:="<>"
You should also be aware that if the filtered range contains non-contiguous ranges, then each 'separate' range would be contained within the Areas collection. This means something like filRange.Rows.Count would only return the row count of the first area; and you can get real difficulties when you try to Offset and/or Resize the filtered range. It's also not possible to directly read non-contiguous ranges into an array using the .Value property.
I'm not sure your code is the most efficient way of handling your task, but keeping the same structure it could look like this:
Dim rRange As Range, filRange As Range
Dim myArea As Range, myRow As Range, myCell As Range
Dim fArr() As Variant
Dim r As Long
With ThisWorkbook.Worksheets("Z")
.AutoFilterMode = False
Set rRange = .UsedRange
End With
With rRange
.AutoFilter Field:=3, Criteria1:=">0"
Set filRange = .SpecialCells(xlCellTypeVisible)
End With
With filRange
r = -1 'start at -1 to remove heading row
For Each myArea In filRange.Areas
r = r + myArea.Rows.Count
Next
ReDim fArr(1 To r, 1 To .Columns.Count)
End With
r = 1
For Each myArea In filRange.Areas
For Each myRow In myArea.Rows
If myRow.Row <> 1 Then
For Each myCell In myRow.Cells
fArr(r, myCell.Column) = myCell.Value
Next
r = r + 1
End If
Next
Next
Perhaps your data has more complexity, but you can simply assign the values of a range to an array with:
var = rng.SpecialCells(xlCellTypeVisible).Value
Thus no need to loop over the data.
Here's a working example with this simple grid of data:
This code:
Option Explicit
Sub arrFilterdRng()
Dim ws As Worksheet '<-- your worksheet
Dim rng As Range '<-- your range to filter
Dim var As Variant '<-- will hold array of visible data
Dim lng1 As Long, lng2 As Long
' get sheet; remove filters
Set ws = ThisWorkbook.Worksheets("Sheet2")
ws.AutoFilterMode = False
' get range; apply filter
Set rng = ws.UsedRange
rng.AutoFilter Field:=1, Criteria1:="x"
' assign visible range to array
var = rng.SpecialCells(xlCellTypeVisible).Value
' test array
For lng1 = LBound(var, 1) To UBound(var, 1)
For lng2 = LBound(var, 2) To UBound(var, 2)
Debug.Print var(lng1, lng2)
Next lng2
Next lng1
End Sub
Results in this on the sheet:
And the output to the Immediate window for the content of var is:
a
b
c
x
2
3
x
5
6

VBA- How to copy and paste values to another sheet beginning on next available row

I have a vba code that copies rows on a sheet to another sheet depending if column A = 1 and it works perfectly. I am trying to make it paste to the next available row instead of overwriting the data that is already there in order to make a log. Here is the code I have already but I can't seem to figure out how to make it paste to the next available row. Any help would be greatly appreciated! Thanks in advance!
Sub Log()
Dim rng As Range
Dim lastRow As Long
Dim cell As Variant
Dim count As Long
count = 0
With ActiveSheet
lastRow = .Range("A" & .Rows.count).End(xlUp).Row
Set rng = .Range("A3:A" & lastRow)
For Each cell In rng
If cell.Value = "1" Then
Range(cell.Offset(0, 1), cell.Offset(0, 6)).Copy
Range("'Log'!B3").Offset(count, 0).PasteSpecial xlPasteValues
count = count + 1
End If
Next
End With
End Sub
You just need to loop through the source sheet.
Try using .Cells(row,col) instead of Range..
This example is heavy on the comments to help understand the looping process.
You will need a few additional Functions to make this work using this code.
LastRow Function
Function lastRow(sheet As String) As Long
lastRow = Sheets(sheet).Cells(Rows.Count, "A").End(xlUp).Row 'Using Cells()
End Function
LastCol Function
Function lastCol(sheet As String) As Long
lastCol = Sheets(sheet).Cells(2, Columns.Count).End(xlToLeft).Column
End Function
Code for solution: Assuming you have your target sheet's headers already set up AND the target and source sheet share the same formatting.
Sub Log()
Dim source As String, target As String
Dim sRow As Long, col As Long, tRow As Long
'Declare Sheets
source = "Sheet1"
target = "Sheet2"
'Loop through rows of source sheet
For sRow = 2 To lastRow(source)
'Get current last row of Target Sheet
tRow = lastRow(target) + 1
'Meet criteria for Column A to = 1 on Source
If Sheets(source).Cells(sRow, 1) = "1" Then
'Copy each column of source sheet to target sheet in same order
For col = 1 To lastCol(source)
Sheets(target).Cells(tRow, col) = Sheets(source).Cells(sRow, col)
Next col
End If
Next sRow
End Sub

Faster code for cuting 'good row range' from sh2 to sh1?

Does it exist any way to make this code run faster as it goes one row by one row ?
Sub cut_good_row_range_from_sh2_to_sh1()
Application.ScreenUpdating = False
For i = 2 To Sheets("sheet1").Cells(Rows.Count, "A").End(xlUp).Row
For j = 2 To Sheets("sheet2").Cells(Rows.Count, "A").End(xlUp).Row
'Caution: I want to cut BB:BD, so I select BA:BD !
If Sheets("sheet1").Range("A" & i).Value = Sheets("sheet2").Range("A" & j).Value Then
Sheets("sheet2").Range("BA" & j & ":BS" & j).Cut Sheets("sheet1").Range("BA" & i & ":BS" & i)
End If
Next j
Next i
Application.ScreenUpdating = True
End Sub
Thanks ;)
It has been demonstrated on SO many times that looping over ranges is slow, and looping over variant arrays is much faster.
The 'best' method depends on the specifics of the use case. Making as few assumptions as I can, this demo shows how effective it can be. The assumptions made are
Data only is required, Format is not transfered.
No Formulas exist in the Destination range (If they do, they will be overwritten with their current value)
This is a simplistic example, further optimisations can be made.
Sub Demo()
Dim Found As Boolean
Dim i As Long, j As Long, k As Long
Dim rSrcA As Range, rSrc As Range
Dim vSrcA As Variant, vSrc As Variant
Dim rDstA As Range, rDst As Range
Dim vDstA As Variant, vDst As Variant
Dim rClear As Range
' Get references to Source Data Range and Variant Array
With Worksheets("Sheet2")
Set rSrcA = Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp))
vSrcA = rSrcA.Value
Set rSrc = .Range("BA1:BS1").Resize(UBound(vSrcA, 1))
vSrc = rSrc
End With
' Get references to Destination Data Range and Variant Array
With Worksheets("Sheet1")
Set rDstA = Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp))
vDstA = rDstA.Value
Set rDst = .Range("BA1:BS1").Resize(UBound(vDstA, 1))
vDst = rDst
End With
' Loop Source
For i = 1 To UBound(vSrcA, 1)
' Loop Destination
For j = 1 To UBound(vDstA, 1)
' Compare
If vSrcA(i, 1) = vDstA(j, 1) Then
Found = True
' Update Destination Data Array, to be copied back to sheet later
For k = 1 To UBound(vSrc, 2)
vDst(j, k) = vSrc(i, k)
Next
End If
Next
' If match found, track Source range to clear later
If Found Then
If rClear Is Nothing Then
Set rClear = rSrc.Rows(i)
Else
Set rClear = Union(rClear, rSrc.Rows(i))
End If
Found = False
End If
Next
' Update Destination Range
rDst.Value = vDst
' Clear Source Range
rClear.ClearContents
End Sub
When run on a test data set of 15 source rows and 200 destination rows, this reduced execution time from about 17s to about 10ms

Resources