In Excel via Visual Basic, I am iterating through a CSV file of invoices that is loaded into Excel. The invoices are in a determinable pattern by client.
I am reading them into a dynamic 2D array, then writing them to another worksheet with older invoices. I understand that I have to reverse rows and columns since only the last dimension of an array may be Redimmed, then transpose when I write it to the master worksheet.
Somewhere, I have the syntax wrong. It keeps telling me that I have already Dimensionalized the array. Somehow did I create it as a static array? What do I need to fix in order to let it operate dynamically?
WORKING CODE PER ANSWER GIVEN
Sub InvoicesUpdate()
'
'Application Settings
Application.ScreenUpdating = False
Application.DisplayAlerts = False
Application.Calculation = xlCalculationManual
'Instantiate control variables
Dim allRows As Long, currentOffset As Long, invoiceActive As Boolean, mAllRows As Long
Dim iAllRows As Long, unusedRow As Long, row As Long, mWSExists As Boolean, newmAllRows As Long
'Instantiate invoice variables
Dim accountNum As String, custName As String, vinNum As String, caseNum As String, statusField As String
Dim invDate As String, makeField As String, feeDesc As String, amountField As String, invNum As String
'Instantiate Workbook variables
Dim mWB As Workbook 'master
Dim iWB As Workbook 'import
'Instantiate Worksheet variables
Dim mWS As Worksheet
Dim iWS As Worksheet
'Instantiate Range variables
Dim iData As Range
'Initialize variables
invoiceActive = False
row = 0
'Open import workbook
Workbooks.Open ("path:excel_invoices.csv")
Set iWB = ActiveWorkbook
Set iWS = iWB.Sheets("excel_invoices.csv")
iWS.Activate
Range("A1").Select
iAllRows = iWS.UsedRange.Rows.Count 'Count rows of import data
'Instantiate array, include extra column for client name
Dim invoices()
ReDim invoices(10, 0)
'Loop through rows.
Do
'Check for the start of a client and store client name
If ActiveCell.Value = "Account Number" Then
clientName = ActiveCell.Offset(-1, 6).Value
End If
If ActiveCell.Offset(0, 3).Value <> Empty And ActiveCell.Value <> "Account Number" And ActiveCell.Offset(2, 0) = Empty Then
invoiceActive = True
'Populate account information.
accountNum = ActiveCell.Offset(0, 0).Value
vinNum = ActiveCell.Offset(0, 1).Value
'leave out customer name for FDCPA reasons
caseNum = ActiveCell.Offset(0, 3).Value
statusField = ActiveCell.Offset(0, 4).Value
invDate = ActiveCell.Offset(0, 5).Value
makeField = ActiveCell.Offset(0, 6).Value
End If
If invoiceActive = True And ActiveCell.Value = Empty And ActiveCell.Offset(0, 6).Value = Empty And ActiveCell.Offset(0, 9).Value = Empty Then
'Make sure something other than $0 was invoiced
If ActiveCell.Offset(0, 8).Value <> 0 Then
'Populate individual item values.
feeDesc = ActiveCell.Offset(0, 7).Value
amountField = ActiveCell.Offset(0, 8).Value
invNum = ActiveCell.Offset(0, 10).Value
'Transfer data to array
invoices(0, row) = "=TODAY()"
invoices(1, row) = accountNum
invoices(2, row) = clientName
invoices(3, row) = vinNum
invoices(4, row) = caseNum
invoices(5, row) = statusField
invoices(6, row) = invDate
invoices(7, row) = makeField
invoices(8, row) = feeDesc
invoices(9, row) = amountField
invoices(10, row) = invNum
'Increment row counter for array
row = row + 1
'Resize array for next entry
ReDim Preserve invoices(10,row)
End If
End If
'Find the end of an invoice
If invoiceActive = True And ActiveCell.Offset(0, 9) <> Empty Then
'Set the flag to outside of an invoice
invoiceActive = False
End If
'Increment active cell to next cell down
ActiveCell.Offset(1, 0).Activate
'Define end of the loop at the last used row
Loop Until ActiveCell.row = iAllRows
'Close import data file
iWB.Close
This isn't exactly intuitive, but you cannot Redim(VB6 Ref) an array if you dimmed it with dimensions. Exact quote from linked page is:
The ReDim statement is used to size or resize a dynamic array that has
already been formally declared using a Private, Public, or Dim
statement with empty parentheses (without dimension subscripts).
In other words, instead of dim invoices(10,0)
You should use
Dim invoices()
Redim invoices(10,0)
Then when you ReDim, you'll need to use Redim Preserve (10,row)
Warning: When Redimensioning multi-dimensional arrays, if you want to preserve your values, you can only increase the last dimension. I.E. Redim Preserve (11,row) or even (11,0) would fail.
I stumbled across this question while hitting this road block myself. I ended up writing a piece of code real quick to handle this ReDim Preserve on a new sized array (first or last dimension). Maybe it will help others who face the same issue.
So for the usage, lets say you have your array originally set as MyArray(3,5), and you want to make the dimensions (first too!) larger, lets just say to MyArray(10,20). You would be used to doing something like this right?
ReDim Preserve MyArray(10,20) '<-- Returns Error
But unfortunately that returns an error because you tried to change the size of the first dimension. So with my function, you would just do something like this instead:
MyArray = ReDimPreserve(MyArray,10,20)
Now the array is larger, and the data is preserved. Your ReDim Preserve for a Multi-Dimension array is complete. :)
And last but not least, the miraculous function: ReDimPreserve()
'redim preserve both dimensions for a multidimension array *ONLY
Public Function ReDimPreserve(aArrayToPreserve,nNewFirstUBound,nNewLastUBound)
ReDimPreserve = False
'check if its in array first
If IsArray(aArrayToPreserve) Then
'create new array
ReDim aPreservedArray(nNewFirstUBound,nNewLastUBound)
'get old lBound/uBound
nOldFirstUBound = uBound(aArrayToPreserve,1)
nOldLastUBound = uBound(aArrayToPreserve,2)
'loop through first
For nFirst = lBound(aArrayToPreserve,1) to nNewFirstUBound
For nLast = lBound(aArrayToPreserve,2) to nNewLastUBound
'if its in range, then append to new array the same way
If nOldFirstUBound >= nFirst And nOldLastUBound >= nLast Then
aPreservedArray(nFirst,nLast) = aArrayToPreserve(nFirst,nLast)
End If
Next
Next
'return the array redimmed
If IsArray(aPreservedArray) Then ReDimPreserve = aPreservedArray
End If
End Function
I wrote this in like 20 minutes, so there's no guarantees. But if you would like to use or extend it, feel free. I would've thought that someone would've had some code like this up here already, well apparently not. So here ya go fellow gearheads.
here is updated code of the redim preseve method with variabel declaration, hope #Control Freak is fine with it:)
Option explicit
'redim preserve both dimensions for a multidimension array *ONLY
Public Function ReDimPreserve(aArrayToPreserve As Variant, nNewFirstUBound As Variant, nNewLastUBound As Variant) As Variant
Dim nFirst As Long
Dim nLast As Long
Dim nOldFirstUBound As Long
Dim nOldLastUBound As Long
ReDimPreserve = False
'check if its in array first
If IsArray(aArrayToPreserve) Then
'create new array
ReDim aPreservedArray(nNewFirstUBound, nNewLastUBound)
'get old lBound/uBound
nOldFirstUBound = UBound(aArrayToPreserve, 1)
nOldLastUBound = UBound(aArrayToPreserve, 2)
'loop through first
For nFirst = LBound(aArrayToPreserve, 1) To nNewFirstUBound
For nLast = LBound(aArrayToPreserve, 2) To nNewLastUBound
'if its in range, then append to new array the same way
If nOldFirstUBound >= nFirst And nOldLastUBound >= nLast Then
aPreservedArray(nFirst, nLast) = aArrayToPreserve(nFirst, nLast)
End If
Next
Next
'return the array redimmed
If IsArray(aPreservedArray) Then ReDimPreserve = aPreservedArray
End If
End Function
I know this is a bit old but I think there might be a much simpler solution that requires no additional coding:
Instead of transposing, redimming and transposing again, and if we talk about a two dimensional array, why not just store the values transposed to begin with. In that case redim preserve actually increases the right (second) dimension from the start. Or in other words, to visualise it, why not store in two rows instead of two columns if only the nr of columns can be increased with redim preserve.
the indexes would than be 00-01, 01-11, 02-12, 03-13, 04-14, 05-15 ... 0 25-1 25 etcetera instead of 00-01, 10-11, 20-21, 30-31, 40-41 etcetera.
As only the second (or last) dimension can be preserved while redimming, one could maybe argue that this is how arrays are supposed to be used to begin with.
I have not seen this solution anywhere so maybe I'm overlooking something?
Here ya go.
Public Function ReDimPreserve(ByRef Arr, ByVal idx1 As Integer, ByVal idx2 As Integer)
Dim newArr()
Dim x As Integer
Dim y As Integer
ReDim newArr(idx1, idx2)
For x = 0 To UBound(Arr, 1)
For y = 0 To UBound(Arr, 2)
newArr(x, y) = Arr(x, y)
Next
Next
Arr = newArr
End Function
Here is how I do this.
Dim TAV() As Variant
Dim ArrayToPreserve() as Variant
TAV = ArrayToPreserve
ReDim ArrayToPreserve(nDim1, nDim2)
For i = 0 To UBound(TAV, 1)
For j = 0 To UBound(TAV, 2)
ArrayToPreserve(i, j) = TAV(i, j)
Next j
Next i
A small update to what #control freak and #skatun wrote previously (sorry I don't have enough reputation to just make a comment). I used skatun's code and it worked well for me except that it was creating a larger array than what I needed. Therefore, I changed:
ReDim aPreservedArray(nNewFirstUBound, nNewLastUBound)
to:
ReDim aPreservedArray(LBound(aArrayToPreserve, 1) To nNewFirstUBound, LBound(aArrayToPreserve, 2) To nNewLastUBound)
This will maintain whatever the original array's lower bounds were (either 0, 1, or whatever; the original code assumes 0) for both dimensions.
i solved this in a shorter fashion.
Dim marray() as variant, array2() as variant, YY ,ZZ as integer
YY=1
ZZ=1
Redim marray(1 to 1000, 1 to 10)
Do while ZZ<100 ' this is populating the first array
marray(ZZ,YY)= "something"
ZZ=ZZ+1
YY=YY+1
Loop
'this part is where you store your array in another then resize and restore to original
array2= marray
Redim marray(1 to ZZ-1, 1 to YY)
marray = array2
You could do this array(0)= array(0,1,2,3).
Sub add_new(data_array() As Variant, new_data() As Variant)
Dim ar2() As Variant, fl As Integer
If Not (isEmpty(data_array)) = True Then
fl = 0
Else
fl = UBound(data_array) + 1
End If
ReDim Preserve data_array(fl)
data_array(fl) = new_data
End Sub
Sub demo()
Dim dt() As Variant, nw(0, 1) As Variant
nw(0, 0) = "Hi"
nw(0, 1) = "Bye"
Call add_new(dt, nw)
nw(0, 0) = "Good"
nw(0, 1) = "Bad"
Call add_new(dt, nw)
End Sub
Related
I'm trying to pass data from sheet 3 to sheet 4 based on a criterion (*). With numbers results but with text the program fails.
How to overcome this situation when instead of a number I have text.
Public Sub TestArray3()
'Array to copy data from Sheet3 to Sheet4 Based on criterion "in this case*"
Dim tempVar As Integer, anotherIteration As Boolean, i As Integer
Dim J As Integer, ArraySize As Integer, myArray() As Integer
Dim newArray() As Integer, FinalRow As Integer, linha As Integer
Dim counter As Integer, cel1 As Range
Sheets("Folha3").Select
FinalRow = Cells(Rows.Count, 1).End(xlUp).Row ' Find the last row of data
ArraySize = FinalRow 'Get Array Size
ReDim myArray(ArraySize - 1)
For linha = 1 To FinalRow
Set cel1 = Cells(linha, 1)
If cel1 = "*" Then
myArray(linha - 1) = Val(Cells(linha, "B").Value) 'Populate de Array
End If
Next linha
ReDim newArray(LBound(myArray) To UBound(myArray)) 'Avoid zeros in Array
For i = LBound(myArray) To UBound(myArray)
If myArray(i) <> "0" Then
J = J + 1
newArray(J) = myArray(i)
End If
Next i
ReDim Preserve newArray(LBound(myArray) To J)
ArraySize = J
Sheets("Folha4").Select 'Write data to Sheet 4 column A
Range("A1").Resize(J - LBound(newArray) + 1)=Application.Transpose(newArray)
End Sub
I'm not clear on where you're actually trying to paste from/to, but here's one [of several] ways to move data between worksheets, including both with and without transposing
Hopefully this example should clear up the steps:
Sub copyRangeToOtherSheet()
Dim lastRow As Long, lastCol As Long, rgSrc As Range, rgDest As Range, arr() As Variant
With ThisWorkbook.Sheets("Sheet1") 'set source worksheet
lastRow = .Cells(Rows.Count, "A").End(xlUp).Row 'find last row of Col A
lastCol = .Cells(1, Columns.Count).End(xlToLeft).Column 'find last col of Row 1
Set rgSrc = Range(.Range("A1"), .Cells(lastRow, lastCol)) 'create range (from A1)
End With
arr = rgSrc 'dump range into array
With ThisWorkbook.Sheets("Sheet2") 'set destination sheet
'OPTION #1: Populate destination in "original" orientation
Set rgDest = .Range("A1") 'set destination top-left corner
Set rgDest = rgDest.Resize(UBound(arr, 1), UBound(arr, 2)) 'fit to array rows/col's
rgDest = arr 'dump array to worksheet range
'OPTION #2: Populate destination in "transposed" orientation
Set rgDest = .Range("D1") 'set destination top-left corner
Set rgDest = rgDest.Resize(UBound(arr, 2), UBound(arr, 1)) 'fit to array col's/rows
rgDest = WorksheetFunction.Transpose(arr) 'dump transposed array to worksheet range
End With
End Sub
Note that it's easiest if you don't set the size of the array in advance — Excel will size it automatically as long as the array isn't already dimensioned (which is why it's declared only as arr() As Variant).
On the destination end, we can pick one cell as the top-left of the range, then ReSize the range based on the arrays' upper bounds (UBound).
If we are going to Transpose the cells, we must swap the number of rows/columns in the destination range.
More Information:
One resource I've found very helpful is Chip Pearson's VBA Arrays And Worksheet Ranges.
String vs Integer
It is a little unclear what is happening here, but I have noticed that you have declared all your arrays as integer so you cannot pass strings to them. Try to find out which array you're trying to pass strings to and declare it as variant or implement some 'conditional' code like:
If Not IsNumeric(Cells("A1").Value) then
Variable = 0
End If
Read ashleedawg's guidelines.
You don't have to select a worksheet to do stuff to it (referring to Select). You can write
FinalRow = Sheets("Folha3").Cells(Rows.Count, 1).End(xlUp).Row
or
Sheets("Folha4").Range("A1").Resize(J - LBound(newArray) + 1) _
= Application.Transpose(newArray)
and save a line but more importantly, not jump around in the workboook. Even better is using With:
With Sheets("Folha3")
FinalRow = .Cells(Rows.Count, 1).End(xlUp).Row ' Find the last row of data
ArraySize = FinalRow 'Get Array Size
ReDim myArray(ArraySize - 1)
For linha = 1 To FinalRow
Set cel1 = .Cells(linha, 1)
If cel1 = "*" Then
myArray(linha - 1) = Val(.Cells(linha, "B").Value) 'Populate de Array
End If
Next linha
End With
Notice the '.' in front of each cells (.cells), it is referring to the sheet object.
Try using variables for objects. When you write
Sheets("folha3").
nothing happens you have to remember what it can do. But if you assign it to a variable the intelliSense is activated and you can see the properties and methods of objects e.g.
Dim oWb as Workbook
Dim oWs as Worksheet
Set oWb = Activeworkbook
Set oWs = oWb.Sheets("Folha3")
Now when you write:
oWs.
the IntelliSense shows you the properties and methods of the worksheet object e.g. Activate, Cells, Copy, Delete, Paste etc.
With a few more lines of code you will learn much more.
I use the code hereunder to calculate max values as described in this post (vba max value of group of values). The code works great but once I have more than 65k lines I get a data type mismatch when trying to pase the array:
sht.Range(Cells(1, lColumn), Cells(last.Row, lColumn)).Value = Application.Index(groupsArray, , lColumn)
Could somebody help me to slice the array in chunks. I have tried to get it working myself but without any luck.
Sub FillGroupsMax()
Dim lColumn As Long
Dim sht As Worksheet
Dim groupsArray As Variant 'array with all group infomation
Dim groupsSeen As Variant 'array with group infomation already seen
Application.ScreenUpdating = False 'stop screen updating makes vba perform better
Set sht = ThisWorkbook.Worksheets("import")
Set last = sht.Range("A:A").Find("*", Cells(1, 1), searchdirection:=xlPrevious) 'last cell with value in column A
lColumn = sht.Cells(1, Columns.Count).End(xlToLeft).Column
groupsArray = sht.Range(Cells(1, 1), Cells(last.Row, lColumn))
'collect all the information on the Sheet into an array
'Improves performance by not visiting the sheet
For dRow = 2 To last.Row 'for each of the rows skipping header
'check if group as already been seen
If inArrayValue(Cells(dRow, 1).Value, groupsSeen) > 0 Then
'if it has been seen/calculated attribute value
'Cells(dRow, 4).Value = inArrayValue(Cells(dRow, 1).Value, groupsSeen)
groupsArray(dRow, lColumn) = inArrayValue(Cells(dRow, 1).Value, groupsSeen)
Else
'if it hasn't been seen then find max
'Cells(dRow, 4).Value = getMax(Cells(dRow, 1).Value, groupsArray)
groupsArray(dRow, lColumn) = getMax(Cells(dRow, 1).Value, groupsArray, lColumn)
'array construction from empty
If IsEmpty(groupsSeen) Then
ReDim groupsSeen(0)
'groupsSeen(0) = Array(Cells(dRow, 1).Value, Cells(dRow, 4).Value)
groupsSeen(0) = Array(groupsArray(dRow, 1), groupsArray(dRow, lColumn))
'attribute value to array
Else
ReDim Preserve groupsSeen(0 To UBound(groupsSeen) + 1)
groupsSeen(UBound(groupsSeen)) = Array(groupsArray(dRow, 1), groupsArray(dRow, lColumn))
End If
End If
Next
sht.Range(Cells(1, lColumn), Cells(last.Row, lColumn)).Value = Application.Index(groupsArray, , lColumn)
'reactivate Screen updating
Application.ScreenUpdating = True
End Sub
Function getMax(group As String, groupsArray As Variant, lColumn As Long) As Double
'for each in array
For n = 1 To UBound(groupsArray)
'if its the same group the Max we seen so far the record
If groupsArray(n, 1) = group And groupsArray(n, lColumn - 1) > maxSoFar Then
maxSoFar = groupsArray(n, lColumn - 1)
End If
Next
'set function value
getMax = maxSoFar
End Function
Function inArrayValue(group As String, groupsSeen As Variant) As Double
'set function value
inArrayValue = 0
'if array is empty then exit
If IsEmpty(groupsSeen) Then Exit Function
'for each in array
For n = 0 To UBound(groupsSeen)
'if we find the group
If groupsSeen(n)(0) = group Then
'set function value to the Max value already seen
inArrayValue = groupsSeen(n)(1)
'exit function earlier
Exit Function
End If
Next
End Function
You can write a helper function to use instead of Application.Index
Bonus - it will be much faster than using Index (>5x)
Sub Tester()
Dim arr, arrCol
arr = Range("A2:J80000").Value
arrCol = GetColumn(arr, 5) '<< get the fifth column
Range("L2").Resize(UBound(arrCol, 1), 1).Value = arrCol
End Sub
'extract a single column from a 1-based 2-D array
Function GetColumn(arr, colNumber)
Dim arrRet, i As Long
ReDim arrRet(1 To UBound(arr, 1), 1 To 1)
For i = 1 To UBound(arr, 1)
arrRet(i, 1) = arr(i, colNumber)
Next i
GetColumn = arrRet
End Function
EDIT - since QHarr asked about timing here's a basic example
Sub Tester()
Dim arr, arrCol, t, i as long
arr = Range("A2:J80000").Value
t = Timer
For i = 1 to 100
arrCol = GetColumn(arr, 5) '<< get the fifth column
Next i
Debug.print Timer - t '<<# of seconds for execution
End Sub
Below, whilst not as tidy as could be, is a way to process an array in chunks and Index to access a column and write out to the sheet.
I populated two columns (A:B) with data. Both had 132,000 rows, populated incrementally, with values from 1 to 132,000 in each column for my test run.
You can fiddle with cutOff to get the chunk size just below the point where the fail happens.
The code below is simply to demonstrate the principle of looping in batches, upto the set cutoff in each batch, until all rows have been processed.
Option Explicit
Public Sub WriteArrayToSheet()
Dim wb As Workbook
Dim ws As Worksheet
Set wb = ThisWorkbook
Set sht = wb.Worksheets("Sheet1") 'change as appropriate
Dim myArr() 'dynamic array
myArr = sht.Range("A1").CurrentRegion.Value 'you may want a more robust method
Dim cutOff As Long 'the max value - what ever it is before error occurs
cutOff = 1000
Dim totalRows As Long 'total rows in array read in from sheet
totalRows = UBound(myArr, 1)
Dim totalArraysNeeded As Long
'Determine how many lots of cutOff chunks there are in the total number of array rows
totalArraysNeeded = Application.WorksheetFunction.Ceiling(totalRows / cutOff, 1)
Dim rotations As Long 'number of times to loop original array to handle all rows
Dim rowCountTotal As Long
Dim rowCount As Long
Dim tempArr() 'this will hold the chunk of the original array
Dim rowCounter As Long
Dim lastRow As Long
Dim nextRow As Long
Dim i As Long
Dim j As Long
Dim numRows As Long
rotations = 1
Do While rotations < totalArraysNeeded
If rotations < totalArraysNeeded - 1 Then
ReDim tempArr(1 To cutOff, 1 To UBound(myArr, 2)) 'size chunk array
numRows = cutOff
Else
numRows = totalRows - rowCountTotal
ReDim tempArr(1 To numRows, 1 To UBound(myArr, 2)) 'size chunk array
End If
For i = 1 To numRows
rowCount = 1 'rows in this chunk looped
rowCountTotal = rowCountTotal + 1 'rows in original array looped
For j = LBound(myArr, 2) To UBound(myArr, 2)
tempArr(i, j) = myArr(rowCountTotal, j)
Next j
rowCount = rowCount + 1
Next i
With sht
lastRow = .Cells(.Rows.Count, "E").End(xlUp).Row 'Column where I am writing the sliced column out to
End With
If lastRow = 1 Then
nextRow = 1
Else
nextRow = lastRow + 1
End If
sht.Range("E" & nextRow).Resize(UBound(tempArr, 1), 1) = Application.Index(tempArr, , 1) 'write out to sheet
rotations = rotations + 1
Loop
End Sub
As #Tim suggested, the best way to slice a large array is use a loop to copy the column.
Though in your case, most of the processing time is spent on computing the maximum since your code is using a nested loop.
If you want to reduce significantly the processing time, then use a dictionary:
Sub Usage
GetMaxByGroupTo _
sourceGroups := ThisWorkbook.Range("Sheet1!A2:A100"), _
sourceValues := ThisWorkbook.Range("Sheet1!B2:B100"), _
target := ThisWorkbook.Range("Sheet1!C2")
End Sub
Sub GetMaxByGroupTo(sourceGroups As Range, sourceValues As Range, target As Range)
Dim dict As Object, groups(), values(), r As Long, max
Set dict = CreateObject("Scripting.Dictionary")
groups = sourceGroups.Value2
values = sourceValues.Value2
' store the maximum value of each group in a dictionary for an efficient lookup '
For r = Lbound(groups) to Ubound(groups)
max = dict(groups(r, 1))
If VarType(max) And values(r, 1) <= max Then Else dict(groups(r, 1)) = values(r, 1)
Next
' build and copy the result array to the sheet '
For r = Lbound(groups) to Ubound(groups)
values(r, 1) = dict(groups(r, 1))
Next
target.Resize(Ubound(groups), 1).Value2 = values
End Sub
In Excel, I have an array from A1 to P30 filled with names. Some cells have the same name (duplicate).
Is there a formula possible to list all the content of this array in one single column (on another sheet)? This list must gather only unique name (no duplicate).
Thanks in advance.
Try this:
Function Unique(strRng As String) As Variant()
Dim Arr() As Variant
ReDim Arr(0)
Dim rng As Range
Dim c As Range
Dim Duplicated As Boolean
Dim i As Long
Dim j As Long
j = 0
Set rng = Range(strRng)
For Each c In rng.Cells
Duplicated = False
If c.Value <> vbNullString Then
For i = LBound(Arr) To UBound(Arr)
If c.Value = Arr(i) Then
Duplicated = True
Exit For
End If
Next i
If Not Duplicated Then
ReDim Preserve Arr(j)
Arr(j) = c.Value
j = j + 1
End If
End If
Next c
Unique = Arr
End Function 'Unique
Update
Seems you insist to using a function. Easy. Create a User Defined Function (UDF) as below:
Function Unique(rng As Range) As Variant()
Dim Arr() As Variant
ReDim Arr(0)
Dim c As Range
Dim Duplicated As Boolean
Dim i As Long
Dim j As Long
j = 0
For Each c In rng.Cells
Duplicated = False
If c.Value <> vbNullString Then
For i = LBound(Arr) To UBound(Arr)
If c.Value = Arr(i) Then
Duplicated = True
Exit For
End If
Next i
If Not Duplicated Then
ReDim Preserve Arr(j)
Arr(j) = c.Value
j = j + 1
End If
End If
Next c
Unique = Arr
' OR
'Unique = Application.Transpose(Arr) 'Use this when you want transpose your range from row to column or back.
End Function 'Unique
How to use the function?
Note that this is an array form function.
Write second code in VBA.
select the range you want to return your unique values. (In each sheet and each part of column)
Write =Unique(A1:P30) in formula bar and then press Ctrl + Shift + Enter from keyboard. (Dont press Enter only)
Now, you have a formula that return you unique values of a range as you said.
I prefer to use a Collection or Dictionary to check for duplicates.
In this example I use an ArrayList
Sub ProcessNames()
Dim v As Variant
Dim list As Object
Set list = CreateObject("System.Collections.ArrayList")
With Worksheets("Sheet1")
For Each v In .Range("A1:P30").Value
If Not list.Contains(v) Then list.Add v
End With
'1 Dimensional 0 Based Array which will span 1 Row
v = list.ToArray
'2 Dimensional 1 Based Array that will span 1 Column
v = WorksheetFunction.Transpose(v)
End Sub
I am creating a workbook that will copy and paste data from a source worksheet to multiple other worksheets depending upon values in a column. However, once I start the macro, Excel enters a not responding state. I am operating on anywhere from 4000 to 500,000 rows, but only 4 columns. When I only have ~4000 rows, it works pretty fast (3 seconds). When I have ~30,000 rows, Excel enters a not responding state for ~10 seconds, but then finishes. I didn't wait long enough for the 300,000 row test.
My thought process to do this would be to sort all of the data based upon the strings in column B, put all of column B (which contains the strings I am searching though) into an array, then pull all of the unique strings out into another array. For example, if column B held "Search" in rows 1-200, and "Create" in rows 201-500, the macro will search through the rows and the second array (lets call it Scenario) would end up holding two values, "Search" and "Create".
During the searching, I also created two parallel arrays that correspond with the Scenario array which would hold the beginning and ending rows for that scenario. After that, I would just loop through the values in the parallel arrays and copy/paste from the source worksheet to the other worksheets.
NOTE: The sort works fine
Is there a way to make this faster?
Here is the code:
Allocate Data
Sub AllocateData()
Dim scenarioRange As String 'To hold the composite range
Dim parallelScenarioName() As String 'Holds the unique scenario names
Dim parallelScenarioStart() As Long 'Holds the starting row of the scenario
Dim parallelScenarioEnd() As Long 'Holds the ending row of the scenario
Sheets("raw").Activate 'Raw is the source worksheet
'Populates the parallel scenario arrays
Call GetScenarioList(parallelScenarioName, parallelScenarioStart, parallelScenarioEnd)
'Loops through the scenario parallel array and coes the copy and paste to other worksheets
'Workseets are named the same as the scenarios
For intPosition = LBound(parallelScenarioName) To (UBound(parallelScenarioName) - 1)
scenarioRange = "A" & parallelScenarioStart(intPosition) & ":" & "D" & parallelScenarioEnd(intPosition)
Range(scenarioRange).Select
Selection.Copy
Worksheets(parallelScenarioName(intPosition)).Activate
Range("A1").Select
ActiveSheet.Paste
Sheets("raw").Activate
Next
End Sub
GetScenarioList
Sub GetScenarioList(ByRef parallelScenarioName() As String, ByRef parallelScenarioStart() As Long, ByRef parallelScenarioEnd() As Long)
Dim scenarioName As Variant
Dim TotalRows As Long
Dim arraySize As Long
arraySize = 1
'Prep the parallel array for scenario name with the first value
ReDim parallelScenarioStart(1)
ReDim parallelScenarioName(1)
parallelScenarioStart(0) = 1 'First spot on the scenario start will be row 1
'Prep the first scenario name
'Sometimes a number will be attached on the end of the scenario name delimited by a period. Ignore it.
If (InStr(Cells(1, 2).Text, ".") <> 0) Then
parallelScenarioName(0) = Left(Cells(1, 2).Text, InStr(Cells(1, 2).Text, ".") - 1)
Else
parallelScenarioName(0) = Cells(1, 2).Text
End If
'Get the total amount of rows
TotalRows = Rows(Rows.Count).End(xlUp).row
'Loop through all of the rows
For i = 1 To TotalRows
'Sometimes a number will be attached on the end of the scenario name delimited by a period. Ignore it.
If (InStr(Cells(i, 2).Text, ".") <> 0) Then
scenarioName = Left(Cells(i, 2).Text, InStr(Cells(i, 2).Text, ".") - 1)
Else
scenarioName = Cells(i, 2).Text
End If
'If the scenario name is not contained in the unique array
If IsNotInArray(scenarioName, parallelScenarioName) Then
Call AddScenarioEndRow(i, arraySize, parallelScenarioEnd)
Call AddNewScenarioToParallelArray(scenarioName, arraySize, parallelScenarioName)
Call AddNewScenarioStartRow(i, arraySize, parallelScenarioStart)
End If
Next
'Cleanup. The above code did not cover the ending row of the last scenario
Call AddScenarioEndRow(TotalRows + 1, arraySize, parallelScenarioEnd)
End Sub
IsNotInArray
Function IsNotInArray(stringToBeFound As Variant, ByRef parallelScenarioName() As String) As Boolean
IsNotInArray = Not (UBound(Filter(parallelScenarioName, stringToBeFound)) > -1)
End Function
Parallel Arrays
Sub AddNewScenarioToParallelArray(str As Variant, arraySize As Long, ByRef parallelScenarioName() As String)
arraySize = UBound(parallelScenarioName) + 1
ReDim Preserve parallelScenarioName(arraySize)
parallelScenarioName(arraySize - 1) = str
End Sub
Sub AddScenarioEndRow(row As Variant, ByRef arraySize As Long, ByRef parallelScenarioEnd() As Long)
ReDim Preserve parallelScenarioEnd(arraySize)
parallelScenarioEnd(arraySize - 1) = row - 1
End Sub
Sub AddNewScenarioStartRow(row As Variant, ByRef arraySize As Long, ByRef parallelScenarioStart() As Long)
ReDim Preserve parallelScenarioStart(arraySize)
parallelScenarioStart(arraySize - 1) = row
End Sub
This will work on unsorted data, but will be much faster if you sort first.
Sub AllocateData()
Dim shtRaw As Worksheet, currVal, rng As Range
Dim c As Range, rngCopy As Range, i As Long, tmp
Set shtRaw = Sheets("raw")
On Error GoTo haveError
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Set rng = shtRaw.Range(shtRaw.Range("B1"), _
shtRaw.Cells(Rows.Count, "B").End(xlUp))
currVal = "~~~~~~~~~~~~~~~" 'or any non-value
For Each c In rng.Cells
tmp = c.Value
If tmp <> currVal Then
If Not rngCopy Is Nothing Then
rngCopy.Copy Sheets(currVal).Cells(Rows.Count, _
"A").End(xlUp).Offset(1, 0)
End If
Set rngCopy = c.Offset(0, -1).Resize(1, 4)
currVal = tmp
i = 1
Else
i = i + 1
Set rngCopy = rngCopy.Resize(i, 4)
End If
Next c
If Not rng Is Nothing Then
rngCopy.Copy Sheets(currVal).Cells(Rows.Count, "A").End(xlUp).Offset(1, 0)
End If
haveError:
'must reset calculation, or it will remain on "manual"
Application.Calculation = xlCalculationAutomatic
'ScreenUpdating will auto-reset once the sub exits,
' but I think it's good practise to explicitly reset it
Application.ScreenUpdating = True
End Sub
Copy-paste is, in my expreience, the slowest thing you can do in VBA.
Try simply assigning the values of range 1 to range 2, kinda like this:
range("b1:b4").value=range("a1:a4").value
Make sure the ranges are of the same size.
In your AllocateData sub, you could use something like:
Worksheets(parallelScenarioName(intPosition)).activate
Range(cells(1,1),cells(scenariorange.rows.count,1).value=scenariorange.value
Sheets("raw").Activate
Oh, I have changed scenariorange to be a range variable, lot easier to use in my opinion. Use it like this:
Dim ScenarioRange as Range
Set ScenarioRange = Range("A" & parallelScenarioStart(intPosition) & ":" & "D" & parallelScenarioEnd(intPosition))
Hope this speeds things up. (And I hope you can understand what I'm trying to say here, I'm a bit sleepy... :) )
Also, turning off the screenupdating usually speeds up the program a lot.
application.screenupdating=false
Don't forget to turn it back on at the end of the code!
My requirements ended up changing slightly. The QA lead wanted Metadata in the raw worksheet, so I had the full list of scenarios at my disposal instead of having to look at every row in the raw data. As a result, I could save and sort the scenario list to an array, and then do a .Find(parallelScenarioName(intPosition + 1)).row to get the row of the next scenario.
Because of this change, I did not fully implement and test Tim Williams solution which would iterate through every row in the data. I have to move on for now, but will revisit and test Tim's solution for my own knowledge soon.
The finished code is below.
'This is in a module so that my subs can see it
Option Explicit
Public Const DATASOURCE_WORKSHEET As String = "raw"
'This is the macro is called. Can be considered main.
Sub AllocateImportedData()
Call SortDataSourceWorksheet
Call AllocateData
End Sub
Sub SortDataSourceWorksheet()
Dim entireRangeToSort As String
Dim colToSortUpon As String
Dim lastRow As Long
lastRow = FindLastRowOfRawData
entireRangeToSort = ConstructRangeString("A", 1, "D", lastRow)
colToSortUpon = ConstructRangeString("B", 1, "B", lastRow)
Call SortRangeByColumnAtoZ(entireRangeToSort, colToSortUpon)
End Sub
Sub SortRangeByColumnAtoZ(entireRangeToSort As String, colToSortUpon As String)
ActiveWorkbook.Worksheets(DATASOURCE_WORKSHEET).Sort.SortFields.Clear
ActiveWorkbook.Worksheets(DATASOURCE_WORKSHEET).Sort.SortFields.Add Key:=Range(colToSortUpon), _
SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With ActiveWorkbook.Worksheets(DATASOURCE_WORKSHEET).Sort
.SetRange Range(entireRangeToSort)
.Header = xlGuess
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
End Sub
Sub AllocateData()
Dim scenarioRange As String 'To hold the composite range
Dim parallelScenarioName() As String 'Holds the unique scenario names
Dim parallelScenarioStart() As Long 'Holds the starting row of the scenario
Dim parallelScenarioEnd() As Long 'Holds the ending row of the scenario
Sheets(DATASOURCE_WORKSHEET).Activate
Call PopulateParallelScenarioArrays(parallelScenarioName, parallelScenarioStart, parallelScenarioEnd)
Call PerformAllocation(parallelScenarioName, parallelScenarioStart, parallelScenarioEnd)
Call FinishByActivatingDesiredWorksheet(DATASOURCE_WORKSHEET)
End Sub
Sub PerformAllocation(ByRef parallelScenarioName() As String, ByRef parallelScenarioStart() As Long, ByRef parallelScenarioEnd() As Long)
For intPosition = LBound(parallelScenarioName) To (UBound(parallelScenarioName) - 1)
scenarioRange = ConstructRangeString("A", parallelScenarioStart(intPosition), "D", parallelScenarioEnd(intPosition))
Range(scenarioRange).Select
Selection.Copy
Worksheets(parallelScenarioName(intPosition)).Activate
Range("A1").Select
ActiveSheet.Paste
Sheets(DATASOURCE_WORKSHEET).Activate
Next
End Sub
Sub PopulateParallelScenarioArrays(ByRef parallelScenarioName() As String, ByRef parallelScenarioStart() As Long, ByRef parallelScenarioEnd() As Long)
Dim numberOfScenarios As Long
numberOfScenarios = GetScenarioListFromRaw(parallelScenarioName)
ReDim parallelScenarioStart(numberOfScenarios)
ReDim parallelScenarioEnd(numberOfScenarios)
Call GetStartAndEndRows(parallelScenarioName, parallelScenarioStart, parallelScenarioEnd)
End Sub
Function GetScenarioListFromRaw(ByRef parallelScenarioName() As String) As Long
Dim numberOfScenarios As Long
Dim scenarioRange As String
Const scenarioListStartColumn As String = "F"
Const scenarioListStartRow As Long = "3"
numberOfScenarios = GetNumberOfScenarios(scenarioListStartColumn, scenarioListStartRow)
ReDim parallelScenarioName(numberOfScenarios)
'Populate parallel scenario name
For i = 0 To (numberOfScenarios - 1)
scenarioRange = scenarioListStartColumn & (scenarioListStartRow + i)
parallelScenarioName(i) = Range(scenarioRange).Text
Next
Call AtoZBubbleSort(parallelScenarioName)
GetScenarioListFromRaw = numberOfScenarios
End Function
Function GetNumberOfScenarios(scenarioListStartColumn As String, scenarioListStartRow As Long)
GetNumberOfScenarios = Range(scenarioListStartColumn & scenarioListStartRow, Range(scenarioListStartColumn & scenarioListStartRow).End(xlDown)).Rows.Count
End Function
Sub GetStartAndEndRows(ByRef parallelScenarioName() As String, ByRef parallelScenarioStart() As Long, ByRef parallelScenarioEnd() As Long)
Dim TotalRows As Long
Dim newScenarioRow As Long
'Prep the parallel array for scenario name with the first value
parallelScenarioStart(0) = 1 'First spot on the scenario start will be row 1
'Get the total amount of rows
TotalRows = Rows(Rows.Count).End(xlUp).row
For intPosition = LBound(parallelScenarioName) To (UBound(parallelScenarioName) - 1)
'Find the row of the next scenario
newScenarioRow = Worksheets(DATASOURCE_WORKSHEET).Columns(2).Find(parallelScenarioName(intPosition + 1)).row
'Next scenario row - 1 is going to be the end of the current row
parallelScenarioEnd(intPosition) = newScenarioRow - 1
'Set starting row of next scenario
parallelScenarioStart(intPosition + 1) = newScenarioRow
Next
End Sub
Sub FinishByActivatingDesiredWorksheet(desiredWorksheet As String)
Sheets(desiredWorksheet).Activate
Range("A1").Select
End Sub
Sub AtoZBubbleSort(ByRef parallelScenarioName() As String)
Dim s1 As String, s2 As String
Dim i As Long, j As Long
For i = LBound(parallelScenarioName) To UBound(parallelScenarioName)
For j = i To UBound(parallelScenarioName)
If UCase(parallelScenarioName(j)) < UCase(parallelScenarioName(i)) Then
s1 = parallelScenarioName(j)
s2 = parallelScenarioName(i)
parallelScenarioName(i) = s2
parallelScenarioName(j) = s1
End If
Next
Next
End Sub
Sub ClearWorkbookCells()
Dim anyWS As Worksheet
For Each anyWS In ThisWorkbook.Worksheets
Call ClearWorksheetCells(anyWS)
Next
End Sub
Sub ClearWorksheetCells(ws As Worksheet)
ws.Activate
' Find the last row and create range var
lastRow = FindLastRowOfRawData
ClearRange = "A1:" & "D" & lastRow
'Select the area to clear and perform clear
ActiveSheet.Range(ClearRange).Select
Selection.ClearContents
End Sub
Function FindLastRowOfRawData()
FindLastRowOfRawData = Range("A1").End(xlDown).row
End Function
Function ConstructRangeString(startCol As String, startRow As Long, endCol As String, endRow As Long) As String
ConstructRangeString = startCol & startRow & ":" & endCol & endRow
End Function
I paste an array in a column, the problem is that it leaves some cells in the column empty. How can I erase those cells in the column?
This is what I have:
Private Sub Worksheet_Change(ByVal Target As Range)
Worksheets("Info").Range("A1").Select
Dim i As Integer
Dim iLastRow As Long
iLastRow = ActiveSheet.Cells(ActiveSheet.Rows.Count, "A").End(xlUp).Row
Dim arrmatrix() As String
ReDim arrmatrix(1 To iLastRow, 1 To 1)
For i = 1 To iLastRow
Range("A2").Cells(i, 1).Select
If Selection.Offset(0, 11) = "Pi emitida" Then
arrmatrix(i, 1) = Range("A2").Cells(i, 1).Value
End If
Next i
Worksheets("Inicio").Range("G4:G1000000").ClearContents
Worksheets("Inicio").Range("G4").Resize(UBound(arrmatrix, 1)).Value = arrmatrix()
end sub
There are a few problems with your sample code.
Hopefully, this is on the Info worksheet's code sheet. You should not be attempting to .Activate another worksheet from the Worksheet_Change event macro.
It isn't clear why you need this in a Worksheet_Change event macro. As-is, this will run any time a value anywhere in the worksheet is added/modified/deleted. That sounds like overkill since the only two columns that determine the outcome are columns A and L.
The ReDim statement can be used with Preserve to expand the array but it can only redimension the last rank.
This modification does not rely on selecting or activating the worksheet(s) before processing them. The arrmatrix array is expanded as needed so you do not end up with blank values in the array.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A:A, L:L")) Is Nothing Then
On Error GoTo Fìn
Application.EnableEvents = False
Dim i As Long, n As Long
Dim arrmatrix As Variant
ReDim arrmatrix(1 To 1, 1 To 1)
For i = 2 To Cells(Rows.Count, 1).End(xlUp).Row
If Cells(i, 12).Value = "Pi emitida" Then
n = n + 1
ReDim Preserve arrmatrix(1 To 1, 1 To n)
arrmatrix(1, n) = Cells(i, 1).Value
End If
Next i
With Worksheets("Inicio")
.Range("G4:G" & Rows.Count).ClearContents
.Range("G4").Resize(UBound(arrmatrix, 2), 1) = Application.Transpose(arrmatrix)
End With
End If
Fìn:
Application.EnableEvents = True
End Sub
This will only run when a value in column A or column L is added/changed/deleted.
Since I've been expanding and populating the last rank, I used Application.Transpose to reorient the data before I stuffed it back into the Inicio worksheet.