I have a dict made up of multiple items (dates) per key, looks like this
Key Items dates()
23359720 03/12/2020 , 04/12/2020, 05/12/2020
23293711 26/01/2021
How can I transfer each key & item into an array (including repeating keys for multiple items)? so that array:
23359720 03/12/2020
23359720 04/12/2020
... ....
This is the code I have:
Sub Test_Dates()
'
Dim TESTWB As Workbook
Dim TESTWS As Worksheet
Set TESTWB = ThisWorkbook
Set TESTWS = TESTWB.Worksheets("TEST")
Dim Dict As New Scripting.Dictionary
For i = 2 To TESTWS.Cells(1, 1).End(xlDown).Row
Dict.Add TESTWS.Cells(i, 1).Value, getDates(TESTWS.Cells(i, 2), TESTWS.Cells(i, 3))
Next i
For i = 0 To Dict.Count - 1
Dim DateItem As Variant
For Each DateItem In Dict.Items(i)
Debug.Print Dict.Keys(i), DateItem
Next DateItem
Next i
'Dict to Array
Dim PricingDatesArr As Variant
PricingDatesArr = Dict.Keys
PricingDatesArr = Dict.Items
End Sub
This is to get dates between 2 sets of dates (i,2) (i,3) in the code
Function getDates(ByVal StartDate As Date, ByVal EndDate As Date) As Variant
Dim varDates() As Date
Dim lngDateCounter As Long
ReDim varDates(0 To CLng(EndDate) - CLng(StartDate))
For lngDateCounter = LBound(varDates) To UBound(varDates)
varDates(lngDateCounter) = CDate(StartDate)
StartDate = CDate(CDbl(StartDate) + 1)
Next lngDateCounter
getDates = varDates
End Function
This is the third question you have posted in as many days regarding Dictionaries. You might want to take a little time out a read up on Dictionary data structures a bit more.
The code below will return a 1 to n,by 1 to 2 array from a dictionary with arrays of dates.
Option Explicit
Public Function UnpackDictionaryOfArrays(ByRef ipDict As Scripting.DIctionary) As Variant
Dim myKeys As Collection
Set myKeys = New Collection
Dim myItems As Collection
Set myItems = New Collection
Dim myKey As Variant
Dim myItem As Variant
For Each myKey In ipDict
myItem = ipDict(myKey)
If IsArray(myItem) Then
Dim myElement As Variant
For Each myElement In myItems
myKeys.Add myKey
myItems.Add myElement
Next
Else
myKeys.Add myKey
myItems.Add myItem
End If
Next
' Now compile into a single array
Dim myKeyItem As Variant
ReDim myKeyItem(1 To myKeys.Count, 1 To 2)
Dim myIndex As Long
For myIndex = 1 To myKeys.Count
myKeyItem(myIndex, 1) = myKeys(myIndex)
myKeyItem(myIndex, 2) = myItems(myIndex)
Next
UnpackDictionaryOfArrays = myKeyIndex
End Function
Dictionary With Arrays to 2D One-Based Array
' Dict to Array
' Count the number of dates.
Dim Arr As Variant
Dim pCount As Long
For Each Arr In Dict.Items
pCount = pCount + UBound(Arr) - LBound(Arr) + 1
Next Arr
' Resize the array to the number of dates.
Dim PricingDatesArr As Variant: ReDim PricingDatesArr(1 To pCount, 1 To 2)
' Write the data to the array.
Dim Key As Variant
Dim n As Long, r As Long
For Each Key In Dict.Keys
For n = LBound(Dict(Key)) To UBound(Dict(Key))
r = r + 1
PricingDatesArr(r, 1) = Key
PricingDatesArr(r, 2) = Dict(Key)(n)
Next n
Next Key
' Print the elements of the array.
For r = 1 To pCount
Debug.Print PricingDatesArr(r, 1), PricingDatesArr(r, 2)
Next r
' Copy to a range.
'SheetX.Range("A1").Resize(pCount, 2).Value = PricingDatesArr
EDIT: The Function Edition
Usage in Your Code
' Dict to Array
Dim PricingDatesArr As Variant
PricingDatesArr = UnpivotDictionaryArrays(dict)
If IsEmpty(PricingDatesArr) Then Exit Sub
' Print the elements of the array.
For r = 1 To pCount
Debug.Print PricingDatesArr(r, 1), PricingDatesArr(r, 2)
Next r
' Copy to a range.
'SheetX.Range("A1").Resize(pCount, 2).Value = PricingDatesArr
The Function
Function UnpivotDictionaryArrays( _
ByVal dict As Object) _
As Variant
Const ProcName As String = "UnpivotDictionaryArrays"
On Error GoTo ClearError
' Count the items of the source (dictionary) arrays.
Dim Item As Variant
Dim n As Long
For Each Item In dict.Items
n = n + UBound(Item) - LBound(Item) + 1
Next Item
' Write the values from the dictionary to the destination array.
Dim Data As Variant: ReDim Data(1 To n, 1 To 2)
Dim r As Long
For Each Item In dict.Keys
For n = LBound(dict(Item)) To UBound(dict(Item))
r = r + 1: Data(r, 1) = Item: Data(r, 2) = dict(Item)(n)
Next n
Next Item
UnpivotDictionaryArrays = Data
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
Related
I am trying to find a way to average an array column value with a condition on items from another column in that array - I am aware that a class or dictionary might be the best solution but I would like to stick to an array as in my real scenario I have to use an array.
In this case the data is as follows
Risk ID Data set 1 Data set 2
23359720 1154 587
23359720 1254 658
23359720 854 756
23293773 965 1456
20053692 1458 458
I would like to find the average of Data sets 1 and 2 per Risk ID, here is what I've tried but does not work - I have seen that this it's not possible to use for each and point it to a specific column, but not sure what else to do in the case of an array?
Edit: expected result data:
ArrayResultAverage()
Risk ID Avg Data set 1 Avg Data set 2
23359720 1087.33 667
23293773 965 1456
20053692 1458 458
Sub Test_Arr_Avg()
'
Dim TESTWB As Workbook
Dim TESTWS As Worksheet
Set TESTWB = ThisWorkbook
Set TESTWS = TESTWB.Worksheets("TEST")
'Array set up
Dim RngTest As Range
Dim ArrTestAvg As Variant
NbRowsTest = TESTWS.Range("A1").End(xlDown).Row
Set RngTest = TESTWS.Range(TESTWS.Cells(1, 1), TESTWS.Cells(NbRowsTest, 3))
ArrTestAvg = RangeToArray2D(RngTest)
'Find the average of Data Range 1 for each item in Risk ID
For k = 1 To UBound(ArrTestAvg, 1)
Dim Sum As Variant
Sum = 0
For Each Item In ArrTestAvg(k, 1)
Sum = Sum + ArrTestAvg(k, 2)
Dim AverageDataSet1 As Variant
AverageDataSet1 = Sum / UBound(ArrTestAvg(Item)) + 1
Debug.Print AverageDataSet1
Next Item
Next k
End Sub
Public Function RangeToArray2D(inputRange As Range) As Variant()
Dim size As Integer
Dim inputValue As Variant, outputArray() As Variant
inputValue = inputRange
On Error Resume Next
size = UBound(inputValue)
If Err.Number = 0 Then
RangeToArray2D = inputValue
Else
On Error GoTo 0
ReDim outputArray(1 To 1, 1 To 1)
outputArray(1, 1) = inputValue
RangeToArray2D = outputArray
End If
On Error GoTo 0
End Function
Get Averages of Unique Data
Adjust the values in the constants section, especially the destination worksheet name (it's the same as the source worksheet name) and its first cell address.
The dictionary's keys hold the unique risk ids, while its items (values) hold the associated destination rows.
The result is written to the same array (which is too big) but with dr the destination row size is tracked and only three columns will be copied.
Before the calculation of the averages, column 1 holds the unique risk ids (the same order as in the dictionary), columns 2 and 3 hold the sums while columns 4 and 5 hold the counts of the first and second data set respectively.
Option Explicit
Sub Test_Arr_Avg()
' Source
Const sName As String = "Sheet1"
' Destination
Const dName As String = "Sheet1"
Const dFirstCellAddress As String = "E1"
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Read from source.
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim slRow As Long: slRow = sws.Cells(sws.Rows.Count, "A").End(xlUp).Row
Dim srg As Range: Set srg = sws.Range("A1", sws.Cells(slRow, "C"))
Dim srCount As Long: srCount = srg.Rows.Count
' Write source range values to array.
Dim Data As Variant: Data = GetRange(srg)
' Add two helper columns for the count.
ReDim Preserve Data(1 To srCount, 1 To 5)
' Sum up and count uniques.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim dr As Long: dr = 1 ' first row are headers
Dim sr As Long
Dim cr As Long
Dim c As Long
For sr = 2 To srCount
' Sum up.
If dict.Exists(Data(sr, 1)) Then
cr = dict(Data(sr, 1))
For c = 2 To 3
Data(cr, c) = Data(cr, c) + Data(sr, c)
Next c
Else
dr = dr + 1
cr = dr
dict(Data(sr, 1)) = cr
For c = 1 To 3
Data(cr, c) = Data(sr, c)
Next c
End If
' Count.
For c = 4 To 5
Data(cr, c) = Data(cr, c) + 1
Next c
Next sr
' Calculate averages.
For cr = 2 To dr
For c = 2 To 3
Data(cr, c) = Data(cr, c) / Data(cr, c + 2)
' You might want to round the results instead:
'Data(cr, c) = Round(Data(cr, c) / Data(cr, c + 2), 2)
Next c
Next cr
Application.ScreenUpdating = False
' Write to destination.
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
With dws.Range(dFirstCellAddress).Resize(, 3)
.Resize(dr).Value = Data
' Clear below.
.Resize(dws.Rows.Count - .Row - dr + 1).Offset(dr).Clear
' Apply various formatting.
.Font.Bold = True ' headers
.Resize(dr - 1, 2).Offset(1, 1).NumberFormat = "#0.00" ' averages
.EntireColumn.AutoFit ' columns
End With
'wb.Save
' Inform.
Application.ScreenUpdating = True
MsgBox "Risk ids averaged.", vbInformation
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a range ('rg') in a 2D one-based array.
' Remarks: If ˙rg` refers to a multi-range, only its first area
' is considered.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetRange( _
ByVal rg As Range) _
As Variant
Const ProcName As String = "GetRange"
On Error GoTo ClearError
If rg.Rows.Count + rg.Columns.Count = 2 Then ' one cell
Dim Data As Variant: ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
GetRange = Data
Else ' multiple cells
GetRange = rg.Value
End If
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
It would be complicated to use a single Dictionary. Here I add a Sub-Dictionary for each Risk ID to the main Dictionary. The Sub-Dictionary is used to hold all the values for each ID. The final step is to create an array of averages for all the main Dictionary items.
Sub Test_Arr_Avg()
Dim Data As Variant
With TestWS.Range("A1").CurrentRegion
Data = .Offset(1).Resize(.Rows.Count - 1, 3)
End With
Dim Results As Variant
Results = KeyedAverages(Data, 1, 2)
Stop
End Sub
Function KeyedAverages(Data As Variant, IDColumn As Long, ValueColumn As Long)
Dim Map As Object
Set Map = CreateObject("Scripting.Dictionary")
Dim Key As Variant
Dim r As Long
For r = 1 To UBound(Data)
Key = CStr(Data(r, IDColumn))
If Len(Key) > 0 Then
If Not Map.Exists(Key) Then Map.Add Key, CreateObject("Scripting.Dictionary")
With Map(Key)
.Add CStr(.Count), Data(r, ValueColumn)
End With
End If
Next
Dim Results As Variant
Dim Values As Variant
ReDim Results(1 To Map.Count, 1 To 2)
Dim n As Long
For Each Key In Map.Keys
n = n + 1
Values = Map(Key).Items
Results(n, 1) = Key
Results(n, 2) = WorksheetFunction.Average(Values)
Next
KeyedAverages = Results
End Function
Public Function TestWB() As Workbook
Set TestWB = ThisWorkbook
End Function
Public Function TestWS() As Worksheet
Set TestWS = TestWB.Worksheets("Test")
End Function
Note: Code updated to exclude empty items.
I want a cod that will find duplicates and return it in separate array.
So I found a code that would be perfect for me, but the thing is that this code is removing duplicates. I thought that it will be a simple job to change it, but somehow I cannot manage to do it....
I was thinking that it will be in this part of code If Err.Number <> 0 Then coll.Remove txt but have no idea how to change it. I have tried changing <> with = but it seems not to work.
Can someone tell me where and how should I change the code to get duplicates from 2 arrays.
Sub test()
Dim arr1 As Variant
Dim arr2 As Variant
Dim arr3 As Variant
Dim coll As Collection
Dim I As Long, j As Long, ii As Long, txt As String, x
With Worksheets("Sheet1")
LastRowColumnA = .Cells(.Rows.Count, 1).End(xlUp).Row
arr1 = .Range("A2:C" & LastRowColumnA).Value
End With
With Worksheets("Sheet2")
LastRowColumnA = .Cells(.Rows.Count, 1).End(xlUp).Row
arr2 = .Range("A2:C" & LastRowColumnA).Value
End With
Set coll = New Collection
On Error Resume Next
For I = LBound(arr1, 1) To UBound(arr1, 1)
txt = Join(Array(arr1(I, 1), arr1(I, 2), arr1(I, 3)), Chr(2))
coll.Add txt, txt
Next I
For I = LBound(arr2, 1) To UBound(arr2, 1)
txt = Join(Array(arr2(I, 1), arr2(I, 2), arr2(I, 3)), Chr(2))
Err.Clear
coll.Add txt, txt
If Err.Number <> 0 Then coll.Remove txt
Next I
ReDim arr3(1 To coll.Count, 1 To 3)
For I = 1 To coll.Count
x = Split(coll(I), Chr(2))
For ii = 0 To 2
arr3(I, ii + 1) = x(ii)
Next
Next I
Worksheets("test").Range("A2").Resize(UBound(arr3, 1), 3).Value = arr3
Columns("A:C").EntireColumn.AutoFit
End Sub
Regards,
Timonek
Extract Duplicates
If you set CountSameWorksheetDuplicates to True, it will return the duplicates of each worksheet even if they are not found in the other worksheet.
Option Explicit
Sub ExtractDuplicates()
Const sName1 As String = "Sheet1"
Const sCols1 As String = "A:C"
Const sfRow1 As Long = 2
Const sName2 As String = "Sheet2"
Const sCols2 As String = "A:C"
Const sfRow2 As Long = 2
Const dName As String = "Test"
Const dfCellAddress As String = "A2"
Const CountSameWorksheetDuplicates As Boolean = False
Dim Delimiter As String: Delimiter = Chr(2)
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim sData As Variant
sData = RefColumns(wb.Worksheets(sName1).Rows(sfRow1).Columns(sCols1))
Dim cCount As Long: cCount = UBound(sData, 2)
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim r As Long
Dim sKey As Variant
For r = 1 To UBound(sData, 1)
sKey = StrJoinedDataRow(sData, r, Delimiter)
If CountSameWorksheetDuplicates Then
DictAddCount dict, sKey
Else
DictAdd dict, sKey, 1
End If
Next r
sData = RefColumns(wb.Worksheets(sName2).Rows(sfRow2).Columns(sCols2))
If CountSameWorksheetDuplicates Then
For r = 1 To UBound(sData, 1)
sKey = StrJoinedDataRow(sData, r, Delimiter)
DictAddCount dict, sKey
Next r
Else
Dim dict2 As Object: Set dict2 = CreateObject("Scripting.Dictionary")
dict2.CompareMode = vbTextCompare
For r = 1 To UBound(sData, 1)
sKey = StrJoinedDataRow(sData, r, Delimiter)
DictAdd dict2, sKey
Next r
For Each sKey In dict2.Keys
DictAddCount dict, sKey
Next sKey
Set dict2 = Nothing
End If
Erase sData
For Each sKey In dict.Keys
If dict(sKey) = 1 Then dict.Remove sKey
Next sKey
Dim drCount As Long: drCount = dict.Count
If drCount = 0 Then Exit Sub
Dim dData As Variant: ReDim dData(1 To drCount, 1 To cCount)
r = 0
Dim c As Long
For Each sKey In dict.Keys
sData = Split(sKey, Delimiter)
r = r + 1
For c = 1 To cCount
dData(r, c) = sData(c - 1)
Next c
Next sKey
Dim drg As Range
Set drg = wb.Worksheets(dName).Range(dfCellAddress).Resize(drCount, cCount)
drg.Value = dData
drg.Resize(drg.Worksheet.Rows.Count - drg.Row - drCount + 1) _
.Offset(drCount).Clear ' clear below
drg.EntireColumn.AutoFit
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Creates a reference to the range from the first row of a range
' ('FirstRowRange') to the row range containing
' the bottom-most non-empty cell in the row's columns.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function RefColumns( _
ByVal FirstRowRange As Range) _
As Range
If FirstRowRange Is Nothing Then Exit Function
With FirstRowRange.Rows(1)
Dim lCell As Range
Set lCell = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , xlByRows, xlPrevious)
If lCell Is Nothing Then Exit Function ' empty range
Set RefColumns = .Resize(lCell.Row - .Row + 1)
End With
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a row of a 2D array in a delimited string.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function StrJoinedDataRow( _
ByVal Data As Variant, _
ByVal RowIndex As Long, _
Optional ByVal Delimiter As String = " ") _
As String
Const ProcName As String = "StrJoinedDataRow"
On Error GoTo ClearError
Dim c As Long
Dim cString As String
For c = LBound(Data, 2) To UBound(Data, 2)
cString = cString & CStr(Data(RowIndex, c)) & Delimiter
Next c
StrJoinedDataRow = Left(cString, Len(cString) - Len(Delimiter))
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Adds a value ('Key') to a key of an existing ('ByRef')
' dictionary ('dict') adding another value ('Item')
' to the key's associated item.
' Remarks: Error and blank values are excluded.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub DictAdd( _
ByRef dict As Object, _
ByVal Key As Variant, _
Optional ByVal Item As Variant = Empty)
If Not IsError(Key) Then
If Len(Key) > 0 Then
dict(Key) = Item
End If
End If
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Adds a value ('Key') to a key of an existing ('ByRef')
' dictionary ('dict') increasing its count being held
' in the key's associated item.
' Remarks: Error and blank values are excluded.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub DictAddCount( _
ByRef dict As Object, _
ByVal Key As Variant)
If Not IsError(Key) Then
If Len(Key) > 0 Then
dict(Key) = dict(Key) + 1
End If
End If
End Sub
Dim Dict as Object
Dict = CreateObject("Scripting.Dictionary")
Dim Line As Object
For Each line in MyArray
On Error Resume Next
Dict.Add(Line, "")
On Error Goto 0
Next
Dictionaries don't allow duplicate keys. We are only setting keys and ignoring the value by not setting it. The dictionary raises an error if the key exists.
As shown in the image, I want to first filter by column A for "Yes".
The above image shows after the filter and I want to save each unique "ID" in columns B and put them into an array called myArr. Ideally, myArr = [101, 5137, 97] and I would be able to call each value in the array using myArr(1), myArr(2), myArr(3)
Below is the code I had, but there are 2 problems:
my arr doesn't seem to be an actual array
it doesn't print the correct answers 101, 5137, 97. Instead, it only prints out 101, 5137
With [a1].CurrentRegion
.AutoFilter 1, "Yes"
'first create arr which include duplicated data
arr = .Offset(1, 1).Resize(.Rows.Count - 1, 1).SpecialCells(xlVisible)
'myArr is an array with unique values
myArr = Application.Unique(arr)
'print out each value of myArr to check if myArr is correct
For Each num In myArr
Debug.Print num
Next num
.AutoFilter
End With
Please give me some ideas on what's wrong with my code above.
Your code is failing because once you apply the filter, the range is no longer contiguous. Your method will only capture a contiguous range.
Because you are setting the Autofilter value from within your routine, lets just check the values inside of an array, and then add the correct values to a dictionary, which will only accept unique values anyways.
Public Sub testUniqueArray()
Dim arrTemp As Variant, key As Variant
Dim dict As Object
Dim i As Long
arrTemp = [a1].CurrentRegion.Value
Set dict = CreateObject("Scripting.Dictionary")
For i = LBound(arrTemp) To UBound(arrTemp)
If arrTemp(i, 1) = "Yes" Then
dict(arrTemp(i, 2)) = 1
End If
Next i
For Each key In dict.Keys
Debug.Print key
Next key
End Sub
Unique Values from Filtered Column to Array
Option Explicit
Sub PrintUniqueValues()
Const CriteriaColumn As Long = 1
Const ValueColumn As Long = 2
Const CriteriaString As String = "Yes"
Dim ws As Worksheet: Set ws = ActiveSheet
' You better improve e.g. by using the worksheet (tab) name...
'Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
'Dim ws As Worksheet: Set ws = wb.Worksheets("Sheet1")
' ... or by using the code name:
'Dim ws As Worksheet: Set ws = Sheet1
Application.ScreenUpdating = False
If ws.AutoFilterMode Then
ws.AutoFilterMode = False
End If
Dim rg As Range: Set rg = ws.Range("A1").CurrentRegion
rg.AutoFilter CriteriaColumn, CriteriaString
Dim Arr As Variant: Arr = ArrUniqueFromFilteredColumn(rg, ValueColumn)
ws.AutoFilterMode = False
Application.ScreenUpdating = True
If IsEmpty(Arr) Then Exit Sub
' Either (preferred when dealing with arrays)...
Dim n As Long
For n = LBound(Arr) To UBound(Arr)
Debug.Print Arr(n)
Next n
' ... or:
' Dim Item As Variant
' For Each Item In Arr
' Debug.Print Item
' Next Item
End Sub
Function ArrUniqueFromFilteredColumn( _
ByVal rg As Range, _
ByVal ValueColumn As Long) _
As Variant
If rg Is Nothing Then Exit Function
If ValueColumn < 1 Then Exit Function
If ValueColumn > rg.Columns.Count Then Exit Function
Dim crg As Range
Set crg = rg.Columns(ValueColumn).Resize(rg.Rows.Count - 1).Offset(1)
Dim CellsCount As Long
CellsCount = WorksheetFunction.Subtotal(103, crg) ' 103 - CountA
If CellsCount = 0 Then Exit Function ' no match or only empty cells
'Debug.Print "CellsCount = " & CellsCount
Dim scrg As Range: Set scrg = crg.SpecialCells(xlCellTypeVisible)
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare ' ignore case
Dim cCell As Range
Dim Key As Variant
For Each cCell In scrg.Cells
Key = cCell.Value
If Not IsError(Key) Then
If Len(Key) > 0 Then
dict(Key) = Empty
' The previous line is a short version of:
'If Not dict.Exists(Key) Then dict.Add Key, Empty
End If
End If
Next cCell
If dict.Count = 0 Then Exit Function ' only errors and blanks
'Debug.Print "dict.Count = " & dict.Count
ArrUniqueFromFilteredColumn = dict.Keys
End Function
I'm trying to get a grasp of the VBA Dictionary.
I believe I have succeeded into storing the text of multiple cells into an array into the dictionary. But I can't find any way to get it out of there.
End goal is the ability to add multiple sources into 1 dictionary and then extract them by key or item or all at once into a new tab.
Here is the code I'm working on:
Sub tutorial_dictionary_Select()
'Must add reference to Tools > References > Microsoft Scripting Runtime
Dim dict As New Dictionary
dict.CompareMode = CompareMethod.TextCompare
Dim source As Worksheet
Set source = ActiveSheet
Dim last_row As Long
last_row = Cells(Rows.Count, 1).End(xlUp).Row
Dim last_col As Long
last_col = ActiveSheet.UsedRange.Columns.Count
Dim i As Long, n As Long
Dim hasHeader As Boolean
Dim arr(0 To 99) As Variant
If IsNumeric(Range("B1").Value) Or IsNumeric(Range("C1").Value) Then
i = 0
Else
hasHeader = True
i = 1
n = 1
For Each rngCell In Range(Cells(1, 2), Cells(1, last_col))
arr(n) = rngCell.Text
'MsgBox arr(n)
Range("I" & n) = arr(n)
Range("J" & n) = n
n = n + 1
Next rngCell
End If
Dim strVal As String
Dim Item(0 To 99) As Variant
Dim header As Range
Dim rng As Range
Dim rngTemp As Range
For Each rngCell In Range(Cells(1 + i, 1), Cells(last_row, 1))
i = i + 1
strVal = rngCell.Text
If Not dict.Exists(strVal) Then
n = 0
For Each headerCell In Range(Cells(i, 2), Cells(i, last_col))
n = n + 1
Item(n) = headerCell.Text
MsgBox headerCell.Text
Next headerCell
dict(strVal) = Item
Else
MsgBox "already exists"
End If
Next rngCell
sFruit = InputBox("Check value of key")
MsgBox "The value of " & sFruit & " is " & dict(sFruit)
The message box gives a type mismatch and I feel like I've tried everything to get those values either put in a cell or debugged and I'm clueless.
As you can see, the code it totally flexible checking for height and width (and a header). And it generates the correct items and their position in the array on the right:
https://i.imgur.com/5Ba6jaN.png
Yes!! That did the trick! I've been stuck on this for days, thank you so much.
Could you maybe elaborate what this code does? The script will run without the first line, but not without the second.
ar = Array()
ReDim ar(lastCol)
In case a key already exists, I want to add the amounts of the 2nd entry to what was already stored in the dictionary. I was able to figure this out myself:
Dim ws As Worksheet
Set ws = ActiveSheet
Dim dict As New Dictionary
Set dict = New Dictionary
dict.CompareMode = TextCompare
Dim lastRow As Long
lastRow = ws.Cells(Rows.Count, 1).End(xlUp).Row
Dim lastCol As Long
lastCol = ws.UsedRange.Columns.Count
Dim i As Integer
Dim startRow As Long
startRow = 2
Dim ar As Variant
Dim sKey As String
For iRow = startRow To lastRow
sKey = ws.Cells(iRow, 1).Text
If Not dict.Exists(sKey) Then
ar = Array()
ReDim ar(lastCol)
For i = 2 To lastCol
ar(i) = ws.Cells(iRow, i)
Next
dict(sKey) = ar
Else
ar = dict(sKey)
For i = 2 To lastCol
ar(i) = ar(i) + ws.Cells(iRow, i)
Next
End If
Next
Is this the right way to do it? I expected to do something like dict(sKey)(3) = dict(sKey)(3) + ws.Cells(iRow, 3) but that didn't work.
You can't assign the same array to every key in the dictionary, you have to create a new one each time.
Sub tutorial_dictionary_Select()
'Must add reference to Tools > References > Microsoft Scripting Runtime
Dim dict As New Dictionary
dict.CompareMode = CompareMethod.TextCompare
Dim wb As Workbook, ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.ActiveSheet
Dim last_row As Long, last_col As Long, start_row As Long
Dim hasHeader As Boolean, i As Integer, iRow As Long
last_row = ws.Cells(Rows.Count, 1).End(xlUp).Row
last_col = ws.Range("A1").CurrentRegion.Columns.Count
Debug.Print "last_col", last_col
' check for header
hasHeader = True
start_row = 2
For i = 2 To last_col
If IsNumeric(ws.Cells(1, i)) Then
hasHeader = False
start_row = 1
End If
Next
Dim ar As Variant, sKey As String
For iRow = start_row To last_row
sKey = ws.Cells(iRow, 1).Text
If Not dict.Exists(sKey) Then
ar = Array()
ReDim ar(last_col)
For i = 2 To last_col
ar(i) = ws.Cells(iRow, i)
Next
dict(sKey) = ar
End If
Next
Dim sFruit As String
sFruit = InputBox("Check value of key")
MsgBox "The value of " & sFruit & " is " & dict(sFruit)(3)
End Sub
Okay I've been trying to store my item + properties into an array and then multiple items in an array in a dictionary if that makes sense.
Dim ws As Worksheet
Set ws = ActiveSheet
Dim dict As New Dictionary
Set dict = New Dictionary
dict.CompareMode = TextCompare
Dim lastRow As Long
lastRow = ws.Cells(Rows.Count, 1).End(xlUp).Row
Dim lastCol As Long
lastCol = ws.UsedRange.Columns.Count
Dim i As Long, u As Long
Dim startRow As Long
startRow = 2
Dim ar As Variant, items As Variant, item As Variant
Dim sKey As String
Dim itemAdded As Boolean
For iRow = startRow To lastRow
sKey = ws.Cells(iRow, 1).Text
item = ws.Cells(iRow, 2).Text
If Not dict.Exists(sKey) Then
MsgBox "adding new customer"
items = Array()
ReDim items(99)
ar = Array()
ReDim ar(lastCol)
'--------------------
For i = 2 To lastCol
ar(i) = ws.Cells(iRow, i)
Next
items(0) = ar
dict(sKey) = items
Else
MsgBox "customer already exists"
items = dict(sKey)
For u = LBound(items) To UBound(items)
If item = items(u)(2) Then
MsgBox "item already exists"
ar = items(u)
For i = 3 To lastCol
ar(i) = ar(i) + ws.Cells(iRow, i)
Next
items(u) = ar
dict(sKey) = items
itemAdded = True
End If
Next
If Not itemAdded = True Then
MsgBox "adding new item"
For i = 2 To lastCol
ar(i) = ws.Cells(iRow, i)
Next
items(u) = ar
dict(sKey) = items
End If
End If
Next
What is working is that I can recall Items(u)(2) and see the name of an item. But the If item = items(u)(2) Then statement is giving me a type mismatch even though it passes as true on the first loop, why? I tried declaring as String and as Variant but neither combination is working.
https://imgur.com/4eKak8q
I've got a sub representing a commandbutton of my userform, this userform has the perpose of listing (in a listbox) all unique items found in a column of a two-dimensional array. At frst I would like to implant an extra variable to hold and thus represent the number of times the unique item appears in the array. Secondly I would like the (Unique) items listed as:
Unique item 1 (number of appearances).
Example 1 (23)
Example 2 (39)
Example 3 (101)
Example 4 (9)
...
Example n (#)
Here is the code, can some body help me out?
Private Sub CommandButton5_Click()
Dim ws As Worksheet
Dim dictUnq As Object
Dim UnqList() As String
Dim aData As Variant
Dim vData As Variant
Dim pData As Variant
Dim i As Variant
Dim PrintString1() As String
i = 1
Set ws = ActiveWorkbook.Sheets("Sheet3")
Set dictUnq = CreateObject("Scripting.Dictionary")
Application.ScreenUpdating = False
Application.EnableEvents = False
With ws.Range("G2", ws.Cells(ws.Rows.Count, "G").End(xlUp))
If .Row < 2 Then Exit Sub 'No data
If .Cells.Count = 1 Then
ReDim aData(1 To 1, 1 To 1)
aData(1, 1) = .Value
Else
aData = .Value
End If
End With
SBI_Omschrijving.ListBox1.Clear
For Each vData In aData
If Len(vData) > 0 Then
If Not dictUnq.exists(vData) Then dictUnq.Add vData, vData
End If
Next vData
Debug.Print dictUnq(vData)
SBI_Omschrijving.ListBox1.List = dictUnq.keys
MsgBox "Unique findings: " & dictUnq.Count
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Use a dictionary to store the count? This demonstrates the principle. Note in your example I think you may only be adding one column G so I don't know of you intended more?
Sub test()
Dim myArray()
myArray = ActiveSheet.Range("A1").CurrentRegion.Value
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim i As Long
For i = LBound(myArray, 1) To UBound(myArray, 1) 'Depending on column of interest. Loop that
If Not dict.Exists(myArray(i, 1)) Then
dict.Add myArray(i, 1), 1
Else
dict(myArray(i, 1)) = dict(myArray(i, 1)) + 1
End If
Next i
Dim key As Variant
For Each key In dict.keys
Debug.Print key & "(" & dict(key) & ")"
Next key
End Sub
Your example might be something like (can't test dictionary on a mac I'm afraid so coding in my head)
Sub test()
Dim aData()
Dim ws As Worksheet
Dim targetRange As Range
Dim lastRow As Long
Set ws = ActiveSheet
lastRow = ws.Cells(ws.Rows.Count, "G").End(xlUp).Row
If lastRow = 1 Then Exit Sub
Set targetRange = ws.Range("G2:G" & lastRow)
If targetRange.Cells.Count = 1 Then
ReDim aData(1 To 1, 1 To 1)
aData(1, 1) = targetRange.Value
Else
aData = targetRange.Value2
End If
Dim dictUnq As Object
Set dictUnq = CreateObject("Scripting.Dictionary")
Dim i As Long
For i = LBound(aData, 1) To UBound(aData, 1) 'Depending on column of interest. Loop that
If Not dictUnq.Exists(aData(i, 1)) Then
dictUnq.Add aData(i, 1), 1
Else
dictUnq(aData(i, 1)) = dictUnq(aData(i, 1)) + 1
End If
Next i
Dim key As Variant
For Each key In dictUnq.keys
Debug.Print key & "(" & dictUnq(key) & ")"
Next key
End Sub
another possibility
Option Explicit
Private Sub CommandButton5_Click()
Dim dictUnq As Object
Set dictUnq = CreateObject("Scripting.Dictionary")
Dim cell As Range
With ActiveWorkbook.Sheets("Sheet3")
For Each cell In .Range("G2", .Cells(.Rows.Count, "G").End(xlUp))
dictUnq(cell.Value) = dictUnq(cell.Value) + 1
Next
End With
If dictUnq.Count = 0 Then Exit Sub
Dim key As Variant
With SBI_Omschrijving.ListBox1
.Clear
.ColumnCount = 2
For Each key In dictUnq.keys
.AddItem key
.List(.ListCount - 1, 1) = dictUnq(key)
Next
End With
MsgBox "Unique findings: " & dictUnq.Count
End Sub