2

I am searching through a two-dimensional Variant array to find values that satisfy a condition (in this case if the product is in the Inventory for example), and then if the condition is true, I want to assign the rest of the "row" to a new array.

The array I have as input to the function is a Variant/Variant(1 to 100, 1 to 4) and doesn't have headers *note: the length is variable, so for instance 100 can then become 10 or 1000, but the width 4 remains a constant

My code so far:

Function sortingArray( arr As Variant )
   Dim counter As Integer
   Dim i As Long
   Dim j As Variant
   Dim z As Variant
   Dim inventory As Variant 
   'if I declare it as: Dim inventory(0,1) As Variant it does not compile
   Dim soldItems As Variant
   cntr = 0
   For i = LBound(arr) To UBound(arr)
      If InStr(arr(i,2), "Inventory") Then
         j = arr(i,3)
         z = arr(i,4)
         If counter = 0 Then
            inventory(0,0) = j
            inventory(0,1) = z
         Else
            ReDim Preserve inventory(counter + 1,1)
            inventory(counter + 1,0) = j
            inventory(counter + 1,1) = z
         End If
         counter = counter + 1
      End If
   Next i
   sortingArray = inventory
   End Function

Example Array:

result Inventory_ProdA A1 5.468
result Inventory_ProdA 4/z 10.6704
result Inventory_ProdA b24-0 0.567
result Inventory_ProdA V3 1.2
result Sold_ProdA L2 8.32
result Sold_ProdA A1 13.450
result Sold_ProdA KP/09 8.32
result Sold_ProdA V3 13.450
result Sold_ProdA b24-0 46.08
result Sold_ProdA 4/z 2.8370
result Sold_ProdA 78-ir-8 0.0672
result Inventory_ProdB A1 0.3
result Inventory_ProdB 4/z 0.05801
result Inventory_ProdB b24-0 1.0129
result Inventory_ProdB V3 5.779
result Inventory_ProdB KP/09 18.99
result Sold_ProdB L2 2.355
result Sold_ProdB A1 0.62
result Sold_ProdB 4/z 32.011
result Sold_ProdB KP/09 15.66

Essentially my issue is i cannot resize the new array properly to accommodate the values i assign to it.

As the final goal it is to get a resulting table that as a first column it has non duplicate values of the third column of the initial array arr(i,3) and as a second column it has the sums of arr(i,4) for each of the arr(i,3) if arr(i,2) = inventory, and as a third column it has same but for arr(i,2) = sold

So it would look like this:

group code Inventory Sold
A1 Sum of arr(i,4) if Inventory and A1 Sum of arr(i,4) if Sold and A1
4/z Sum of arr(i,4) if Inventory and 4/z Sum of arr(i,4) if Sold and 4/z
b24-0 Sum of arr(i,4) if Inventory and b24-0 Sum of arr(i,4) if Sold and b24-0
V3 Sum of arr(i,4) if Inventory and V3 Sum of arr(i,4) if Sold and V3
L2 Sum of arr(i,4) if Inventory and L2 Sum of arr(i,4) if Sold and L2
KP/09 Sum of arr(i,4) if Inventory and KP/09 Sum of arr(i,4) if Sold and KP/09
78-ir-8 Sum of arr(i,4) if Inventory and 78-ir-8 Sum of arr(i,4) if Sold and 78-ir-8
7
  • Does this answer your question? Adding an element to the end of an array in VBA Commented Jan 6, 2024 at 2:50
  • It's not the right way to use Redim. Microsoft documentation: > ReDim statement Commented Jan 6, 2024 at 2:57
  • To clarify, as I can no longer edit the post, I have two questions, 1) how to make my code work in terms of assigning the values to the new 2d array of the condition is met, and 2) after that how can I use the sumproduct function to get to the final 3 column array? Commented Jan 6, 2024 at 2:58
  • What is the right way to use Redim in this case, could you please demonstrate? @taller Commented Jan 6, 2024 at 2:59
  • Hi, no it doesn't unfortunately, as it is not just about resizing a one-dimensional array. If that was the case, I tried it and it works, but then it is difficult for me to correlate the two different one dimensional arrays with each other @RonnieRoyston Commented Jan 6, 2024 at 3:04

5 Answers 5

2

The UDF should be used as array formula.

  • Select a range which has 3 columns (eg. F1:H8)
  • Input formula =sortingArray(A1:D20), press Ctrl+Shift+Enter
Option Explicit
Function sortingArray(rngData As Variant) As Variant
    Dim objDicInve As Object, objDicSold As Object
    Dim i As Long, sKey, sType As String
    Dim arrData, arrRes
    If TypeName(rngData) = "Range" Then
        If rngData.Columns.Count <> 4 Then Exit Function
        Set objDicInve = CreateObject("scripting.dictionary")
        Set objDicSold = CreateObject("scripting.dictionary")
        arrData = rngData.Value
        For i = LBound(arrData) To UBound(arrData)
            sKey = arrData(i, 3)
            If Not objDicInve.exists(sKey) Then
                objDicInve(sKey) = 0
                objDicSold(sKey) = 0
            End If
            sType = Split(arrData(i, 2), "_")(0)
            If StrComp(sType, "Inventory", vbTextCompare) = 0 Then
                objDicInve(sKey) = objDicInve(sKey) + arrData(i, 4)
            ElseIf StrComp(sType, "Sold", vbTextCompare) = 0 Then
                objDicSold(sKey) = objDicSold(sKey) + arrData(i, 4)
            End If
        Next i
        ReDim arrRes(objDicInve.Count, 2)
        arrRes(0, 0) = "Group Code"
        arrRes(0, 1) = "Inventory"
        arrRes(0, 2) = "Sold"
        i = 0
        For Each sKey In objDicInve.Keys
            i = i + 1
            arrRes(i, 0) = sKey
            arrRes(i, 1) = objDicInve(sKey)
            arrRes(i, 2) = objDicSold(sKey)
        Next
        sortingArray = arrRes
    End If
End Function

enter image description here

Update: Use array parameter

Function sortingArray2(arrData As Variant) As Variant
    Dim objDicInve As Object, objDicSold As Object
    Dim i As Long, sKey, sType As String, arrRes
    If TypeName(arrData) = "Array" Then
        If UBound(arrData, 2) < 4 Then Exit Function
        Set objDicInve = CreateObject("scripting.dictionary")
        Set objDicSold = CreateObject("scripting.dictionary")
        For i = LBound(arrData) To UBound(arrData)
        ' ...
        ' this part is same as sortingArray()
        ' ...
        sortingArray = arrRes
    End If
End Function

If I were you, I prefer to have a pivot table.

  • Add a helper column (Col5). The formula is =LEFT([@Col2],SEARCH("_",[@Col2])-1).
  • Create a pivot table as below.

enter image description here


  • It is a demo how to use Redim based on OP's code logic. It haven't archieve the final goal.
Function sortingArray(rng As Variant)
    Dim counter As Integer
    Dim i As Long, j As Variant, z As Variant
    Dim inventory() As Variant, arr
    Dim soldItems As Variant
    counter = 0
    arr = rng.Value
    For i = LBound(arr) To UBound(arr)
        If InStr(arr(i, 2), "Inventory") Then
            j = arr(i, 3)
            z = arr(i, 4)
            ReDim Preserve inventory(1, counter)
            inventory(0, counter) = j
            inventory(1, counter) = z
            counter = counter + 1
        End If
    Next i
    sortingArray = Application.Transpose(inventory)
End Function
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for this!! However it doesn't work. My data unfortunately is not data on a sheet but produced by another vba function. So the Range Type check doesn't work, is there any way I can input the Variant/Variant type array I have to the sorting Array function of yours? Thank you so much!! Also thank you for explaining Redim
The issue I get after integrating the array creation in the function is that I can iterate through it, but at the part: objDicInve(sKey) = objDicInve(sKey) + arrData(i, 4) it looks like older values get overwritten when I try to access them at this part: arrRes(i, 1) = objDicInve(sKey)
I managed to get it working, thank you!! If I want to add the remaining (non Inventory/ non Sold) values to an additional column in the resulting array, how should I modify this part below, as there could be a case of new arr(i,3) in the case where arr(i,2) doesn't contain the string "Inventory" or "Sold". If Not objDicInve.exists(sKey) Then objDicInve(sKey) = 0 objDicSold(sKey) = 0 End If
You can use array as parameter of UDF. Then you have to update the validation code. Please try sortingArray2. I don't get the point of your 3rd comment. Please edit your post to share the detail.
1

This you can reach with formulas. Lets assume that the results are in columns H:J

Formula in H1: =UNIQUE(C1:C20,FALSE)

Formula in I1: =SUM((D$1:D$20)*NOT(ISERR(SEARCH("Inventory",B$1:B$20)>0))*(C$1:C$20=H1))

Formula in J1: =SUM((D$1:D$20)*NOT(ISERR(SEARCH("Sold",B$1:B$20)>0))*(C$1:C$20=H1))

Result

enter image description here

1 Comment

Thank you for this, but I am looking to use vba as the input array is in reality a vba produced one.
1

Sum Up Unique By Criteria (Pivot!?)

enter image description here

A Common Scenario (Usage)

Sub Test()

    Dim sData() As Variant, dData() As Variant
    
    With Sheet1
        With .Range("A1").CurrentRegion
            sData = .Resize(.Rows.Count - 1).Offset(1).Value ' exclude headers
        End With
        dData = GetSumsByCriteria(sData, Array("Inventory*", "Sold*"), 3, 2, 4)
        With .Range("F2").Resize(UBound(dData, 1), UBound(dData, 2))
            .Value = dData
        End With
    End With

    ' Or e.g.: 
    PrintData dData

End Sub

The Function

Function GetSumsByCriteria( _
    ByVal SourceData As Variant, _
    ByVal CriteriaArray As Variant, _
    ByVal GroupColumn As Long, _
    ByVal CriteriaColumn As Long, _
    ByVal SumupColumn As Long) _
As Variant

    Dim cLB As Long: cLB = LBound(CriteriaArray)
    Dim cUB As Long: cUB = UBound(CriteriaArray)
    Dim cArr() As Double: ReDim cArr(cLB To cUB)
        
    Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
    dict.CompareMode = vbTextCompare
    
    Dim grVal As Variant, crVal As Variant, srval As Variant
    Dim sr As Long, c As Long
    
    For sr = 1 To UBound(SourceData, 1)
        grVal = SourceData(sr, GroupColumn) ' group
        If Not IsError(grVal) Then
            If Not dict.Exists(grVal) Then dict(grVal) = cArr
            crVal = SourceData(sr, CriteriaColumn) ' criteria
            If Not IsError(crVal) Then
                srval = SourceData(sr, SumupColumn) ' sum-up
                If VarType(srval) = vbDouble Then
                    For c = cLB To cUB
                        If crVal Like CriteriaArray(c) Then
                            SumDictArr dict, grVal, c, srval
                            ' This doesn't work!
                            'dict(grVal)(c) = dict(grVal)(c) + srval
                            Exit For
                        End If
                    Next c
                End If
            End If
        End If
    Next sr
    
    Dim dData() As Variant: ReDim dData(1 To dict.Count, 1 To cUB - cLB + 2)
    
    Dim dr As Long, dc As Long
    
    For Each grVal In dict.Keys
        dr = dr + 1
        dData(dr, 1) = grVal
        dc = 1
        For c = cLB To cUB
            dc = dc + 1
            dData(dr, dc) = dict(grVal)(c)
        Next c
    Next grVal
        
    GetSumsByCriteria = dData

End Function

Function Help

Sub SumDictArr( _
        ByRef dict As Object, _
        ByVal Key As Variant, _
        ByVal Index As Long, _
        ByVal Number As Double)
    Dim Arr() As Double: Arr = dict(Key)
    Arr(Index) = Arr(Index) + Number
    dict(Key) = Arr
End Sub

Print Data Result

  • You can find the PrintData procedure here.
PrintData Result
     A1    5.768  14.07
    4/z 10.72841 34.848
  b24-0   1.5799  46.08
     V3    6.979  13.45
     L2        0 10.675
  KP/09    18.99  23.98
78-ir-8        0 0.0672

3 Comments

thank you so much for this. It does indeed print an array with the first column being the non duplicate values of the 3rd column of my initial array, however the SumDictArray function doesn't work and doesn't also seem to catch the conditions where the strings in the CriteriaArray are true. I tried to implement it by manually adding the summation code in the Function ( where you have commented it doesn't work) but can't make it work. Any further tips ore recommendation would really be appreciated. Thank you
the issue is that the dData array is intact constructed, but dict doesn't have any second dimension (c) associated with it, hence I think ot doesn't populate, I'm trying to see how I can add an extra dimension to it, but it doesn't compile
How exactly are you using this code? If the numbers are text, you could e.g. use If IsNumeric(srval) Then. If it's a case-sensitivity issue, you could e.g. use If LCase(crVal) Like LCase(CriteriaArray(c)) Then. If you're trying to use it as a UDF, that's not gonna happen before introducing a few changes (e.g. SourceData needs to become a range...).
1

My proposal would be to use a dictionary and a simple class for the values

The simple class I named pData looks like that

Option Explicit

Public soldValue As Double
Public inventoryValue As Double

The following function takes the input from a range which looks like your posted data (no headers, but 4 columns though the first column seems to be superfluous) and returns an array with the dimensions (number of rows,4).

Function getExampledata() As Variant

    Dim rg As Range
    Set rg = Range("A1").CurrentRegion
    getExampledata = rg.value

End Function

The array returned from the function above will be the input for the main code in my case but you can replace that with your array as long as this array looks like it should be, see above.

Option Explicit

Const SOLD = "Sold"
Const INVENTORY = "Inventory"
Const DELIMITER = ":"
Sub main()

    Dim vdata As Variant
    'Here you have to use your own code resp. Array
    'Assumption is you have 4 columns like written in your post
    'and the order is like in your post
    vdata = getExampledata
    
    Dim dict As Scripting.Dictionary
    Set dict = New Scripting.Dictionary
    
    Dim groupCode As String ' Name of the 3rd column with entries A1, 4/5,...
    Dim amount As Double    ' Value in the last column
    Dim category As String  ' Inventory or Sold
        
    Dim itemData As pData, myKey As String
    Dim i As Long
    For i = LBound(vdata, 1) To UBound(vdata, 1)
    
        groupCode = vdata(i, 3)
        amount = CDbl(vdata(i, 4))
        category = grepCategory(vdata(i, 2))
        myKey = Trim(groupCode)
                
        If dict.Exists(myKey) Then
            ' In case the groucode already exists do the math
            ' and add to the corresponding category (Sold or Inventory)
            If category = SOLD Then
                dict.Item(myKey).soldValue = dict.Item(myKey).soldValue + amount
            Else
                dict.Item(myKey).inventoryValue = dict.Item(myKey).inventoryValue + amount
            End If
        Else
            ' Create a new item for pData which contains the amounts for inventory and sold
            Set itemData = New pData
            If category = SOLD Then
                itemData.soldValue = amount
            Else
                itemData.inventoryValue = amount
            End If
            dict.Add groupCode, itemData
        End If
        
    Next i
    
    ' output into array
    Dim rData As Variant, key As Variant
    ReDim rData(1 To dict.Count, 1 To 3)
    i = 1
    
    For Each key In dict
        rData(i, 1) = key
        rData(i, 2) = dict.Item(key).inventoryValue
        rData(i, 3) = dict.Item(key).soldValue
        i = i + 1
    Next
            
    ' Write output to a sheet, just to check
    Worksheets.Add
    Range("A1") = "GroupCode": Range("B1") = "Inventory": Range("C1") = "Sold"
    Dim rg As Range
    Set rg = Range("A2").Resize(dict.Count, 3)
    rg.value = rData
            
            
End Sub

Function grepCategory(ByVal prodA As String) As String
    ' returns inventory or sold for inputs like
    ' Inventory_ProdA, Inventory_ProdB, Sold_ProdA, Sold_ProdB
    ' Assumption is that the second column is like you poster
    
    Dim result As String
    
    If UCase(Left(prodA, 4)) = UCase(SOLD) Then
        result = SOLD
    Else
        If UCase(Left(prodA, 9)) = UCase(INVENTORY) Then result = INVENTORY
    End If

    grepCategory = result

End Function

That is the output (comma is the regional decimal separator for me)

enter image description here

Comments

1

Essentially my issue is i cannot resize the new array properly to accommodate the values i assign to it.

This is a classic VBA problem. This will sound flippant, but the best solution (if possible) is to know the size of the array ahead of time. Fortunately, that's something we can learn in this situation. I'll leverage this fact in the code:

Private Function Rewrite(arr As Variant) As Variant

    ' How many unique group codes are there?
    On Error Resume Next
    Dim ii As Long, collectionGroupCodes As New Collection
    For ii = LBound(arr, 1) To UBound(arr, 1)
        collectionGroupCodes.Add Item:=arr(ii, 3), Key:=arr(ii, 3)
    Next ii
    On Error GoTo -1
    
    ' Because collections only accept unique keys, the number of items in the collection is
    ' the number of unique codes
    Dim uniqueGroupCodes As Long
    uniqueGroupCodes = collectionGroupCodes.Count
    
    ' Create an array that's the right size - no need to redim more than once
    Dim sortingArray() As Variant
    ReDim sortingArray(1 To uniqueGroupCodes, 1 To 3)
    
    ' Iterate through the group codes
    Dim groupCode As String, jj As Long
    For jj = 1 To uniqueGroupCodes
    
        groupCode = collectionGroupCodes(jj)
    
        ' Add the group code to the array
        sortingArray(jj, 1) = groupCode
        
        ' Iterate through the arr array
        For ii = LBound(arr, 1) To UBound(arr, 1)
                
            ' Ignore any values that aren't for this groupCode
            If arr(ii, 3) <> groupCode Then GoTo NextItem
            
            If InStr(arr(ii, 2), "Inventory") = 1 Then
            
                ' Add the value to the Inventory
                sortingArray(jj, 2) = sortingArray(jj, 2) + arr(ii, 4)
                
            Else
            
                ' Add the value to the Sold
                sortingArray(jj, 3) = sortingArray(jj, 3) + arr(ii, 4)
                
            End If
NextItem:
        Next ii
    
    Next jj
    
    Rewrite = sortingArray

End Function

The first handful of lines in the function use a Collection to store group codes. A Collection will not accept more than one item with the same key, much like a Scripting.Dictionary that some other users have suggested. If you try to pass the same key to a collection, it throws a "Run-time error '457': This key is already associated with an element of this collection". This is why we have the On Error Resume Next line in there. We are expecting some errors in normal operation, but we ignore them. I do not recommend using On Error Resume Next regularly. General catch-all error ignorers are a bad idea. You'll see that we turn off this error-ignoring after the loop is finished with On Error GoTo -1.

At this point, we have a Collection that contains the unique group codes, so we can create an array that's the proper size and only ReDim once.

Now for the part you've already got working: loop through each of your group codes, find all of its "Inventory" and "Sold" items, and sum these values to store in the output array. For the sake of completeness, I've added that to the function as well.

You've been presented many potential solutions to your problem, but this one:

  • Is a single function which can be used the way your original was meant to be
  • Does not rely on binding extra modules (such as Microsoft Scripting Runtime for Scripting.Dictionary objects)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.