0

I would like to copy data from one sheet to another.

I put the range that I want to copy into an array (LookupSource) because it's faster to work on arrays than looping through cells.

After filling my two dimensional array (LookupSource), I would like to keep only some records based on critieria (Column A = 10000), so I am trying to copy from LookupSource, the rows that fetch this criteria to the two dimensional array (DataToCopy) which will be copied to the destination sheet.

My problem is that I am not able to do that because as it seems I am not able to make a dynamic resize for the first dimension (rows) of the second array (DataToCopy).

Any Idea how to fill DataToCopy from LookupSource based on my condition ?

The error "index out of range" that I am getting is at the Line : ReDim Preserve DataToCopy(1 to j, 1 to 6)

not at first time, but on second time that I enter the For loop after the Next I I suppose it's because the J is variable and I am not allowed to change the first dimension of the array.

How to deal with that ?

Any better Idea from what I am doing ?

to give you an example here is a small part of the sheet that I want to copy (I took only 8 rows, but in real there thousands). I want to copy only the rows that have 10000 in column A.

enter image description here

Here is my code

Dim LookupSource as Variant      
Dim DataToCopy() As Variant        
Dim i As Long
Dim j As Long


With MySheet
'MyRange is a defined name that reprensent column A, B, C, D, E, F
LookupSource = .Range(.Range("MyRange")(1, 1), .Range("MyRange")(8, 6)).Value2

j = 1

For i = LBound(LookupSource) To UBound(LookupSource)

If LookupSource(i, 1) = 10073 Then
ReDim Preserve DataToCopy(1 to j, 1 to 6)
DataToCopy(j, 1) = LookupSource(i, 1)
DataToCopy(j, 2) = LookupSource(i, 2)
DataToCopy(j, 3) = LookupSource(i, 3)
DataToCopy(j, 4) = LookupSource(i, 4)
DataToCopy(j, 5) = LookupSource(i, 5)
DataToCopy(j, 6) = LookupSource(i, 6)
j = j + 1
End If

Next i

end with
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
JustGreat
  • 551
  • 1
  • 11
  • 26
  • You can only resize the last dimension of an array. – Scott Craner Apr 20 '20 at 22:53
  • Use `Application.WorksheetFunction.CountIf()` to set the size of the array before the loop then you will not need `ReDim Preserve` – Scott Craner Apr 20 '20 at 22:54
  • 1
    Better yet, filter the data, copy the visible cells and then paste that on the new sheet. – Scott Craner Apr 20 '20 at 22:57
  • what will be the faster way to do ? CountIf to know how many have the number 1000 in column A, or filter the data ? and how to copy only the visible part please ? – JustGreat Apr 21 '20 at 00:30
  • @ScottCraner I thought about taking the same size of the source array, of course I will get at the end of the destination array blank items, but when I copy from array to range it won't be visible because it will copy empty values in the cells...what do you think about that idea ? so between this idea and the 2 ideas that you proposed (CountIf/Filter) which one is more performant ? – JustGreat Apr 21 '20 at 01:33
  • Posted a simple solution profiting from the advanced possibilities of the `Application.Index()` function thus overcoming the restrictions of `ReDim Preserve` in a multidimensional array :) – T.M. Apr 21 '20 at 20:16

2 Answers2

1

How to overcome the restrictions of ReDim Preserve in multidimensional arrays

As mentioned by @ScottCraner, a ReDim Preserve can change only the last dimension of a given (datafield) array. Therefore trying to resize a 2-dimensional array's first dimension (="rows") will fail.

However you can overcome this inconvenience applying the relatively unknown filtering capability of Application.Index() (c.f. section [2]) and profit from the additional bonus of less loops.

Further reading: see Some pecularities of the Application.Index() function

Sub GetRowsEqual10000()
    With Sheet1
        Dim lastRow As Long:  lastRow = .Range("A" & .Rows.Count).End(xlUp).Row
        Dim rng     As Range: Set rng = .Range("A2:F" & lastRow)
    End With

    '[1] get data
    Dim data: data = rng

    '[2] rearrange data via Application.Index() instead ReDim Preserve plus loops
    data = Application.Index(data, ValidRows(data, Condition:=10000), Array(1, 2, 3, 4, 5, 6))
End Sub

Help function ValidRows()

Function ValidRows(arr, Condition) As Variant
'Purpose: a) check condition (e.g. values equalling 10000) and b) get valid row numbers c) in a 2-dim vertical array
ReDim tmp(1 To UBound(arr))     ' provide for 1-based 2-dim array
Dim i As Long, ii As Long
For i = 1 To UBound(arr)                ' loop through 1st "column"
    If arr(i, 1) = Condition Then       '   a) check condition 
        ii = ii + 1: tmp(ii) = i        '   b) collect valid row numbers
    End If
Next i
ReDim Preserve tmp(1 To ii)             '   resize tmp array (here the 1st dimension is also the last one:) 
ValidRows = Application.Transpose(tmp)  ' c) return transposed result as 2-dim array
End Function

Edit due to comment (2020-04-22)

Short hints to the most frequent use of Application.Index():

Frequently the Application.Index() function is used to get an entire row or column array out of a 2-dim array without need to loop.
Accessing your 1-based 2-dimensional datafield array like that requires to indicate a single row or column number and to set the neighbour argument column or row number to 0 (zero), respectively which might result in e.g.

        Dim horizontal, vertical, RowNumber As Long, ColumnNumber As Long
    RowNumber = 17: ColumnNumber = 4
    horizontal = Application.Index(data, RowNumber, 0)
    vertical   = Application.Index(data, 0, ColumnNumber)

(Addressing a single array element will be done directly, however via data(i,j) instead of a theoretical Application.Index(data, i, j))

How to use Application.Index() for restructuring/filtering purposes:

In order to profit from the advanced possibilities of Application.Index() you need to pass not only the array name (e.g. data), but the row|column arguments as Arrays, e.g.

    data = Application.Index(data, Application.Transpose(Array(15,8,10)), Array(1, 2, 3, 4, 5, 6))

Note that the rows parameter becomes a "vertical" 2-dim array by transposition, where Array(15,8,10) would even change the existing row order (in the example code above this is done in the last code line within the ValidRows() function). The columns argument Array(1,2,3,4,5,6) on the other hand remains "flat" or "horizontal" and allows to get all existing column values as they are.

So you eventually you are receiving any data elements within the given element indices (think them as coordinates in a graphic).

T.M.
  • 9,436
  • 3
  • 33
  • 57
  • 1
    it's really very very interesting, thanks a lot for these information, but I am not understanding well the Application.Index. can you please clarifiy me this ? what i understood is that it takes as arguments, Data, Rows and columns, and it will give you the value inside the data. of the item ithat you provided it's row and column .. here instead of giving 1 row and 1 coloum,you are giving an array of colum and array of rows so it give you back an array of values, right ? – JustGreat Apr 22 '20 at 13:52
  • @JustGreat - edited further explanations to my answer; hopefully they are helpful. Try the recommended SO site with further example links :-) – T.M. Apr 22 '20 at 18:30
  • @JustGreat - did my edit answer your questions? - Feel free to accept one of both answers by ticking the green checkmark &/or upvote if helpful :-) – T.M. Apr 25 '20 at 13:01
  • thanks a lot, I used the solution proposed by VBASIC because he was faster, but if I want to do that again I will use yours because it seems very smart and I liked it, I just didn't use it because by the time you clarify it, I have already finished my code, so I didn't want to start over again, but next time I will definetly use it. To be fair, I will tag his answer as solved because he was faster in the answer, and I will add +1 to your solution because it's really very good and I liked it more than first solution, thanks again – JustGreat May 15 '20 at 12:42
0

Range Lookup Function

The Code

Option Explicit

'START ****************************************************************** START'
' Purpose:      Filters a range by a value in a column and returns the result  '
'               in an array ready to be copied to a worksheet.                 '
'******************************************************************************'
Function RangeLookup(LookUpValue As Variant, LookupRange As Range, _
  Optional LookupColumn As Long = 1) As Variant

    Dim LookUpArray As Variant    ' LookUp Array
    Dim DataToCopy As Variant     ' DataToCopy (RangeLookup) Array
    Dim countMatch As Long        ' DataToCopy (RangeLookUp) Rows Counter
    Dim r As Long, c As Long      ' Row and Column Counters

    ' Check the arguments.
    Select Case VarType(LookUpValue)
         Case 2 To 8, 11, 17
         Case Else: Exit Function
    End Select
    If LookupRange Is Nothing Then Exit Function
    If LookupColumn < 1 Or LookupColumn > LookupRange.Columns.Count _
      Then Exit Function

    ' Copy values of Lookup Range to Lookup Array.
    LookUpArray = LookupRange

    ' Task: Count the number of values containing LookUp Value
    '       in LookUp Column of LookUp Array which will be
    '       the number of rows in DataToCopy Array.
    '       The number of columns in both arrays will be the same.

    ' Either:
    ' Count the number of values containing LookUp Value.
    countMatch = Application.WorksheetFunction _
      .CountIf(LookupRange.Columns(LookupColumn), LookUpValue)

    ' Although the previous looks more efficient, it should be tested.

'    ' Or:
'    ' Loop through rows of LookUpArray.
'    For r = 1 To UBound(LookUpArray)
'        ' Check if the value in current row in LookUp Column
'        ' is equal to LookUp Value.
'        If LookUpArray(r, LookupColumn) = LookUpValue Then
'            ' Increase DataCopy Rows Counter.
'            countMatch = countMatch + 1
'        End If
'    Next r

    ' Check if no match was found.
    If countMatch = 0 Then Exit Function

    ' Task: Write the matching rows in LookUp Array to DataToCopy Array.

    ' Resize DataToCopy Array to DataToCopy Rows counted in the previous
    ' For Next loop and the number of columns in Lookup Array.
    ReDim DataToCopy(1 To countMatch, 1 To UBound(LookUpArray, 2))
    ' Reset DataToCopy Rows Counter.
    countMatch = 0
    ' Loop through rows of LookUp Array.
    For r = 1 To UBound(LookUpArray)
        ' Check if the value in current row in LookUp Column
        ' is equal to LookUp Value.
        If LookUpArray(r, LookupColumn) = LookUpValue Then
            ' Increase DataCopy Rows Counter.
            countMatch = countMatch + 1
            ' Loop through columns of LookUp (DataToCopy) Array.
            For c = 1 To UBound(LookUpArray, 2)
                ' Write the current value of LookUp Array to DataToCopy Array.
                DataToCopy(countMatch, c) = LookUpArray(r, c)
            Next c
        End If
    Next r

    ' Write values from DataToCopy Array to RangeLookup Array.
    RangeLookup = DataToCopy

End Function
'END ********************************************************************** END'

You should use it e.g. like this:

Sub TryRangeLookup()

    Dim LookupRange As Range
    Dim DataToCopy As Variant

    With MySheet
    'MyRange is a defined name that reprensent column A, B, C, D, E, F
        Set LookupRange = .Range(.Range("MyRange")(1, 1), _
          .Range("MyRange")(8, 6)).Value2
    End With
    RangeLookUp 10073, DataCopy   
    If Not IsArray(DataToCopy) Then 
        MsgBox "No data found.": Exit Sub ' or whatever...
    Endif
    ' Continue with code...

End Sub
VBasic2008
  • 44,888
  • 5
  • 17
  • 28
  • Thanks a lot for your suggestion and efforts, is it more performant to loop that way or to use CountIf to get the number of countMatch ? in your code, shouldn't we have `countMatch = 1` instead of `countMatch = 0` , when you Reset DataToCopy Rows Counter ? Shoudln't we replace `LookUpArray(r, 1)` by `LookUpArray(r, LookupColumn)` ? – JustGreat Apr 21 '20 at 12:44
  • 1
    @JustGreat: You got me on two accounts, but the countMatch stays 0, because I'm counting before writing. Found the LookupColumn error while trying to find a value in another column. Thx. Gotta try this CountIf, now. – VBasic2008 Apr 21 '20 at 12:51
  • Thanks again, will wait your answer about the CountIf, but tell me please why you added the check of datatype of the variable ? – JustGreat Apr 21 '20 at 13:01
  • @JustGreat: You can safely delete it, but I was trying to make it bullet proof so that you can continue e.g. with: If 'DataToCopy = Empty then Exit Sub'. It's kind of a study. Sorry if that's bothering you.Of course on the other hand if 'DataToCopy' is Empty you may not know why. Ah, the pros and the cons. – VBasic2008 Apr 21 '20 at 13:09
  • Nope, it's not bothering at all, I was just trying to understand the why, nothing negative in my question at all. By the way, what are the datatype represented by 2 To 8, 11, 17 ? – JustGreat Apr 21 '20 at 13:11
  • The idea behind is to make sure that it's not an object and that it's a type like string, integer etc..to avoid any failure at the line : LookUpArray(r, LookupColumn), Am I right ? – JustGreat Apr 21 '20 at 13:17
  • 1
    @JustGreat: I've edited my asnwer. Here's a link to [VarType](https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/vartype-function). I'm in a hurry, now. Be back in a couple of hours. Good Luck. – VBasic2008 Apr 21 '20 at 13:43
  • Thanks again, but there are errors in the TryRangeLookup Sub. You should call `RangeLookUp 10073, LookupRange ` instead of `RangeLookUp 10073, DataCopy ` .... and maybe later to do `DataToCopy = RangeLookUp 10073, LookupRange ` before the `If Not IsArray(DataToCopy) Then ` – JustGreat Apr 21 '20 at 14:42