+ Reply to Thread
Results 1 to 3 of 3

Thread: VBA: Working with "Areas" within 2d Arrays

  1. #1
    Forum Moderator DonkeyOte's Avatar
    Join Date
    10-22-2008
    Location
    Suffolk, UK
    MS-Off Ver
    2002, 2007 & 2010
    Posts
    21,423

    VBA: Working with "Areas" within 2d Arrays

    Of late I have become more and more convinced that Index is the best "value for money" Worksheet Function in Excel.

    In worksheet function form the INDEX function can for example do the following (all context aside):

    a) return single Values

    =INDEX($A:$A,4)
    note: here the above acts like INDIRECT in so far as if row 4 is deleted the above would still return contents of the "new" A4 and not error
    (we are for sake of simplicity assuming Col A is not deleted in above example)


    b) return Array of Values for use in conjunction with other functions

    =SUMIF($A$2:$A$10,"Apple",INDEX($B$2:$Z$10,0,MATCH("values",$B$1:$Z$1,0)))
    c) be used to create Ranges, eg for use as Dynamic Named Ranges

    Name: _PivotData
    RefersTo: =$A$1:INDEX($A:$IV,COUNTA($A:$A),COUNTA($1:$1))
    which all in all makes it incredibly flexible and it does all of this whilst being non-volatile (post XL97) unlike equivalent function types (eg OFFSET, INDIRECT etc...)

    However, further to it's "native" flexibility it's also very useful in VBA specifically when working with Arrays.

    Let's assume we have a numeric 2d Array which for sake of "alternatives" is not sourced from a Range object.

    Using a very basic (and admittedly unrealistic) set up along the lines of:

    Public Sub Example()
        Dim lngY As Long, lngX As Long
        Dim MyArray(1 To 50000, 1 To 4) As Double
        For lngY = LBound(MyArray, 1) To UBound(MyArray, 1) Step 1
            For lngX = LBound(MyArray, 2) To UBound(MyArray, 2) Step 1
                MyArray(lngY, lngX) = Rnd
            Next lngX
        Next lngY
    End Sub
    Let's now assume we want to Sum the contents of the 2nd "Column" of our 2d Array without having to iterate each "item", how can we do this ?

    INDEX offers us a very simple method:

    Debug.Print Application.Sum(Application.Index(MyArray,0,2))
    The same can be applied for a given "row" - say the 10th row of our Array

    Debug.Print Application.Sum(Application.Index(MyArray,10,0))
    This is very handy but what if we wanted to sum an area within our Array - say from "row" 50 to "row" 15000 (inclusive) whilst omitting "columns" 1 & 4 ?

    In terms of the native function and a range this is relatively straightfoward, if we assume our 50000 x 4 matrix is A1:D50000 then:

    =SUM(INDEX(A:D,50,2):INDEX(A:D,15000,3))
    
    or
    
    =SUM(INDEX(B:B,50):INDEX(C:C,15000))
    but how can we reflect the same in VBA using INDEX ?

    Previously I thought this was not possible... then along came our very own lecxe with this very clever trick:

    Debug.Print Application.Sum(Application.Index(MyArray, Evaluate("ROW(50:15000)"), Array(2, 3)))
    note: you could use shorthand for the Evaluate if preferred - ie [row(50:15000)]

    That's pretty darn clever !!!


    Better yet this technique can handle non contiguous parts of the source array - say we wanted the same rows but to sum columns 1 & 4 rather than 2 & 3:

    Debug.Print Application.Sum(Application.Index(MyArray, Evaluate("ROW(50:15000)"), Array(1, 4)))
    that's super clever !!! especially when you consider the "native" INDEX equivalent:

    =SUM(INDEX(A:A,50):INDEX(A:A,15000),INDEX(D:D,50):INDEX(D:D,15000))

    Better still (is this even possible?!) pending actions we could even revert the ordering of the "columns"

    So let's now assume that we have a 5th Column in our Array which is populated with strings which may or may not repeat ("apple", "banana" etc)

    We now want to match "apple" in between "rows" 50 and 15000 in our Array and return the value associated with that match from "Column" 3

    Using the above technique we could create a right to left VLOOKUP using this sub array technique

    Debug.Print Application.VLookup("apple", Application.Index(MyArray, Evaluate("ROW(50:15000)"), Array(5, 3)), 2, 0)

    If you feel you must reply to the above (not expected) please note the following:

    a) the above is not a question

    b) none of the techniques outlined above are being advocated as "best practice"

    c) the set ups above are entirely hypothetical
    Following on from the above:

    though there are countless alternative methods for all of the above they it is not the intention to discuss them here - this post is merely to demonstrate lecxe's Evaluate w/INDEX trick.


    The post in which this trick was first brought to my attention resides a) on another Excel Message Board and b) within a restricted forum of that Message Board so at this stage I can not provide a direct link unfortunately.
    Last edited by DonkeyOte; 01-01-2011 at 04:08 PM. Reason: typo in c) formula

  2. #2
    Valued Forum Contributor
    Join Date
    10-15-2007
    Location
    Home
    MS-Off Ver
    Office 2010, W7
    Posts
    215

    Re: VBA: Working with "Areas" within 2d Arrays

    Hi

    This is how I got to the vba statement that DO posted to sum some values in an array:

    Debug.Print Application.Sum(Application.Index(MyArray, Evaluate("ROW(5:14)"), Array(1, 4)))
    (I used just 10 rows to make it easier to test in the worksheet)

    It sums elements in rows 5 to 14 in columns 1 and 4 of the array.

    I was inspired by how the Index() function works in the worksheet with ranges.

    In this case, to add the values in the cells in rows 5 to 14 in columns A and D one could use an efficient formula like the one DO posted:

    =SUM(INDEX(A:A,5):INDEX(A:A,14),INDEX(D:D,5):INDEX(D:D,14))

    It's not possible, however, to reproduce it directly in vba.

    I thought then of another way, using Index() as an array function that returns an array, to extract the values into a contiguous rectangular range and then to sum the result range.

    For ex., for this vba statement:

    - select I1:H9
    - in the formula bar write: =INDEX(A:D,ROW($5:$14),{1,4})
    - confirm with CSE

    Now in I1:I9 you have A5:A14 and in H1:H9 you have D5:D14.

    The values in this rectangular range is what you get in vba with

    Application.Index(MyArray, Evaluate("ROW(5:14)"), Array(1, 4))
    ,you get a 2D array with these values.

    Now in another cell you'd use "=SUM(I1:H9)" to get the sum of these values. That's what you get with the vba

    Application.Sum(2D array calculated before)
    Although this way of calculating the sum of the values in the cells in the worksheet does not seem direct or efficient it has the advantage of allowing for a direct reproduction in vba in this simple and concise way:

    Debug.Print Application.Sum(Application.Index(MyArray, Evaluate("ROW(5:14)"), Array(1, 4)))
    This statement replicates exactly the same behaviour of Index() in the worksheet but in vba it can also be used with arrays.


    Happy new year!

    lecxe

  3. #3
    Forum Moderator DonkeyOte's Avatar
    Join Date
    10-22-2008
    Location
    Suffolk, UK
    MS-Off Ver
    2002, 2007 & 2010
    Posts
    21,423

    Re: VBA: Working with "Areas" within 2d Arrays

    @lecxe, thanks for this great post - I trust others who find it interesting will "tip your scales" accordingly.

    One final use for Index in VBA (which lecxe kindly reminded me of) is something I used earlier today, namely:

    circumventing the need for Transpose when creating 1d Arrays from Horizontal Range Vectors:

    Dim vArray As Variant
    vArray = Application.Index(Range("A1:J1").Value,1,0)
    Last edited by DonkeyOte; 01-02-2011 at 05:04 PM. Reason: removed assertion re: Vertical Vectors as plainly not true (thanks lecxe ;)!)

+ Reply to Thread

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts

Search Engine Friendly URLs by vBSEO 3.2.0