Getting unique values using dictionary - would like to understand more - vba

I have this code I made from studying multiple posts.
https://www.youtube.com/watch?v=j2RfI75Yfg8
https://www.mrexcel.com/board/threads/storing-unique-values-from-advanced-filter-to-an-array.1048617/
Option Explicit
Sub GetTheUniqueValues()
Dim dict As New Scripting.Dictionary
Dim rng_col_a As Range
Dim col_a_last_row As Long
Dim source_array As Variant
Dim i As Long
Dim j As Long
Dim new_array As Variant
dict.CompareMode = TextCompare
col_a_last_row = ActiveSheet.Range("A1048576").End(xlUp).row
Set rng_col_a = Range(ActiveSheet.Range("A2"), ActiveSheet.Range("A" & col_a_last_row))
source_array = rng_col_a
For i = LBound(source_array) To UBound(source_array)
If source_array(i, 1) <> "" Then dict(source_array(i, 1)) = Empty
Next i
new_array = dict.Keys
For j = LBound(new_array) To UBound(new_array)
ActiveSheet.Range("H" & j + 2).Value = new_array(j)
Next j
End Sub
I would like to understand more about
If source_array(i, 1) <> "" Then dict(source_array(i, 1)) = Empty
I am new to VBA and programming in general so may I now what the "dict(source_array(i, 1)) = Empty" does and why this particular line of code is effective of only putting unique values in the dictionary.
What does it do if the condition is true?
What does it do if the condition is false?
Thank you.

Using dict("a") = Empty tells the dict dictionary that its element with key a has no value (is Empty).
If the dict dictionary doesn't have a key "a", then the code dict("a") = Empty will create it.
That means when cycling through all the values in the source_array, it won't (can't) create duplicates. If key "a" already exists it'll just assign Empty to it again, if it doesn't exist, it'll create it.
This is better than trying to add keys e.g. using
dict.Add "a", Empty
Will only work if key "a" doesn't already exist, but e.g.
dict("a") = Empty
Will either assign Empty to key "a" or create it. It can't error like the first method.
The If source_array(i, 1) <> "" is simply checking that there is a value to create in the first place.
If it's true then it'll try to create the key, if it's false it'll just move to the next i value.

If source_array(i, 1) <> "" Then dict(source_array(i, 1)) = Empty only create a new key and do not allocate any value. If instead of Empty will be 1, the final result will be the same, due to the fact that only the dictionary (unique) keys count...
dict.Keys is an array (independent of the key item values) and only it is used by this code. A shorter version of dropping the dictionary keys would be (without iteration between the array elements):
ActiveSheet.Range("H2").Resize(UBound(new_array) + 1, 1).Value = WorksheetFunction.Transpose(new_array)
A shorter (but interesting, I think) version, for the whole code, would be the next one (it does not need a reference to 'Microsoft Scripting Runtime'):
Sub GetTheUniqueValues()
Dim source_array As Variant, sh As Worksheet, j As Long, col_a_last_row As Long
Set sh = ActiveSheet
col_a_last_row = sh.Range("A" & Rows.count).End(xlUp).row
source_array = sh.Range(sh.Range("A2"), sh.Range("A" & col_a_last_row)).Value
With CreateObject("scripting.dictionary")
For j = 1 To UBound(source_array)
.Item(source_array(j, 1)) = Application.Index(source_array, j, 0)
Next
sh.Range("H2").Resize(.count, UBound(source_array, 2)) = Application.Index(.Items, 0, 0)
End With
End Sub
It would be a bigger challenge to understand it. But still not something very complicated...

Related

how to pull elements from a list in vba

I have extract something like this from a database:
[{"identifier":{"strategyType":"element1"},"elnSchedules":[{"paymentDate":["element2","element2"]},{"paymentDate":["element2","element2"]}],"composition":{"components":[{"instrument":{"exerciseType":["element3","element3"]}},{"instrument":{"exerciseType":["element3","element3"]}}]},"links":[]}]
I want to build a vba and create a table in excel that has header: strategyType, paymentDate, exerciseType
and elements: element1, element2, element3 under the corresponding header(while each element can only appear once).
so far I have:
For i = 1 To jsonO.Count
'set headings
If i = 1 Then
j = 1
For Each StrKey In jsonO(i).Keys()
activeWS.Cells(i + offset, j) = StrKey
j = j + 1
Next
End If
j = 1
For Each StrKey In jsonO(i).Keys()
If (StrKey <> "links") Then
activeWS.Cells(i + offset + 1, j) = jsonO(i)(StrKey)
j = j + 1
End If
Next
But this only extracts identifier, eLnschedules, and composition and not able to get into specific element.
Is there any way to do it?
Thanks.
-------------------NEW QUESTION----------------------
I used what's posted on the answer and was trying to build a new function under the function posted in the answer(while both of them are called by the main function):
Public Sub GetEndDate()
Dim activeWS As Worksheet
Set activeWS = ThisWorkbook.Worksheets("Data")
Dim jsonStr As String, Json As Object, headers()
'headers = Array("strategyType", "paymentDate", "exerciseType")
jsonStr = [{"optionFeatures":{"Strike Setting":[{"endDate":["2018-10-16"]}]},"links":[]}] '<== read from cell
Set Json = JsonConverter.ParseJson(jsonStr)(1)
activeWS.Cells(1, 13) = Json("optionFeatures")("Strike Setting")("endDate")
End Sub
However it was not able to read from the string, or do I need to reset the lib again?
Thanks.
Using JSONConverter.bas to parse the JSON string read in from a cell as shown below. This assumes you only want one instance of each value.
Note:
After adding in JSONConverter.bas you need to go VBE > Tools > References > Add a reference to Microsoft Scripting Runtime.
Your JSON structure is as follows:
[] indicates a collection, items accessed by index starting from 1. {} indicates a dictionary with items accessed by key.
I traverse the tree using the appropriate syntax to retrieve the first occurrence of each element.
Option Explicit
Public Sub GetInfoFromSheet()
Dim jsonStr As String, Json As Object, headers()
headers = Array("strategyType", "paymentDate", "exerciseType")
jsonStr = [A1] '<== read from cell
Set Json = JsonConverter.ParseJson(jsonStr)(1)
With ThisWorkbook.Worksheets("Sheet1")
.Cells(1, 1).Resize(1, UBound(headers) + 1) = headers
.Cells(2, 1) = Json("identifier")("strategyType")
.Cells(2, 2) = Json("elnSchedules")(1)("paymentDate")(1)
.Cells(2, 3) = Json("composition")("components")(1)("instrument")("exerciseType")(1)
End With
End Sub

Excel VBA - Formula Counting Unique Value error

I am trying to calculate the count of Unique values based on a condition.
For example,
For a value in column B, I am trying to count the Unique values in Column C through VBA.
I know how to do it using Excel formula -
=SUMPRODUCT((B2:B12<>"")*(A2:A12=32)/COUNTIF(B2:B12,B2:B12))
that value for 32 is dynamic - Programmatically I am calling them inside my vba code as Name
This is my code :
Application.WorksheetFunction.SumProduct((rng <> "") * (rng2 = Name) / CountIfs(rng, rng))
This is the sample data with the requirement
Alternatively, I Concatenated both the columns for keeping it simple and hoping to identify the Unique values which starts with name* method.
I don't know where I am going wrong. Kindly share your thoughts.
You may try something like this...
Function GetUniqueCount(Rng1 As Range, Lookup As String) As Long
Dim x, dict
Dim i As Long, cnt As Long
Set dict = CreateObject("Scripting.Dictionary")
x = Rng1.Value
For i = 1 To UBound(x, 1)
If x(i, 1) = Lookup Then
dict.Item(x(i, 1) & x(i, 2)) = ""
End If
Next i
GetUniqueCount = dict.Count
End Function
Then you can use it like below...
=GetUniqueCount($A$2:$B$10,C2)
Where A2:B10 is the data range and C2 is the name criteria.
I'd put the values into an array, create a temporary 2nd array and only add values to this array if they are not already present, and then replace the original array. Then it's just a simple matter to sum the unique values:
Sub Unique
dim arr(10) as variant, x as variant
dim arr2() as variant
for x = 1 to 10 ' or whatever
arr(x) = cells(x, 1) ' or whatever
next x
arr2 = UniqueValuesArray(arr)
' now write some code to count the unique values, you get the idea
End Sub
Function UniqueValuesArray(arr As Variant) As Variant()
Dim currentRow, arrpos As Long
Dim uniqueArray() As Variant
Dim x As Long
arrpos = 0
ReDim uniqueArray(arrpos)
For x = 0 To UBound(arr)
If UBound(Filter(uniqueArray, arr(x))) = -1 Then
ReDim Preserve uniqueArray(arrpos)
uniqueArray(arrpos) = arr(x)
arrpos = arrpos + 1
End If
Next x
UniqueValuesArray = uniqueArray
End Function

How to create dynamic variable names VBA

I am trying to create a dynamic number of variables in VBA based on the value in a cell.
Essentially what I'd like to end up with is something like Team1, Team2... to TeamX.
Any help is greatly appreciated
Dim i, x As Integer
Set x = Range("J4").Value
Dim Team(1 To x) As String
Dim Manager(1 To x) As String
Range("A3").Select
For i = 1 To x
Dim Team(i) As Integer
A dictionary would probably help in this case, it's designed for scripting, and while it won't let you create "dynamic" variables, the dictionary's items are dynamic, and can serve similar purpose as "variables".
Dim Teams as Object
Set Teams = CreateObject("Scripting.Dictionary")
For i = 1 To x
Teams(i) = "some value"
Next
Later, to query the values, just call on the item like:
MsgBox Teams(i)
Dictionaries contain key/value pairs, and the keys must be unique. Assigning to an existing key will overwrite its value, e.g.:
Teams(3) = "Detroit"
Teams(3) = "Chicago"
Debug.Print Teams(3) '## This will print "Chicago"
You can check for existence using the .Exist method if you need to worry about overwriting or not.
If Not Teams.Exist(3) Then
Teams(3) = "blah"
Else:
'Teams(3) already exists, so maybe we do something different here
End If
You can get the number of items in the dictionary with the .Count method.
MsgBox "There are " & Teams.Count & " Teams.", vbInfo
A dictionary's keys must be integer or string, but the values can be any data type (including arrays, and even Object data types, like Collection, Worksheet, Application, nested Dictionaries, etc., using the Set keyword), so for instance you could dict the worksheets in a workbook:
Dim ws as Worksheet, dict as Object
Set dict = CreateObject("Scripting.Dictionary")
For each ws in ActiveWorkbook.Worksheets
Set dict(ws.Name) = ws
Next
This will get you started. But before you start I recommend watching these WiseOwlTutorials tutorial on Youtube:
Selecting Cells (Range, Cells, Activecell, End, Offset)
Worksheets, Charts and Sheets
Variables
Arrays
Dim i, x As Integer
x = Range("J4").Value
Dim Team() As Integer
Dim Manager() As String
ReDim Team(1 To x) As Integer
ReDim Manager(1 To x) As String
Range("A3").Select
For i = 1 To x
Team(i) = i
Next

How do I use a string as a variable in vba?

This is what my cells look like:
This is my code, I'll explain it below.
Sub Macro1()
Dim product as String
Dim group as Long
Dim recordno as Long
dim pol_number as Long
dim plan_name as Long
product = "corp"
group = 1
recordno = 1
pol_number = 1
plan_name = "TTT"
Range("A2").Select
For i = 1 to 5
ActiveCell.Value = Selection.End(xlUp).Value
ActiveCell.Offset(0,1).Select
Next i
End Sub
I want to fill in all of the cells with the variable values. I understand that variables are not case sensitive, and I understand that the code I have will just fill the cell with the text in the upmost cell of the column, but I don't know if there is a function that would take the text of the top cell and convert it to a variable. Is that possible?
Try this to go from variables to cells
Dim values as Variant
'Array 0 to 4
values = Array(product,group,recordno,pol_number,plan_name)
Range("A2").Resize(1,5).Value2 = values
The reverse is
Dim values as Variant
'Array 1 to 5
values = Range("A2").Resize(1,5).Value2
product = values(1,1)
group = values(1,2)
recordno = values(1,3)
pol_number = values(1,4)
plan_name = values(1,5)
If you do something like
someCell.Value = someOtherCell.Value
and someOtherCell.Value is "product" then someCell won't be filled with what you have saved in the variable product but with "product" (I included the quotation marks to emphasize that's it's a string). That's a good thing because otherwise it would mess your code up if you accidentally put in the name of some random variable in your code.
If your requirements are like this:
You have values for PRODUCT etc that you write to write in the row below PRODUCT etc.
The headers are not always in the same order.
You might want to add new variables later on without too much fuss.
Them some kind of keyed list might be what your looking for. That means that rather than referencing the variable by a numerical index, you can reference them using names.
If the order is fixed, you might be better of just using an array where item 1 is the product name, item 2 is the group number etc, like ja72 and Sgdva suggested.
However, if you still want to reference the variables by name, you could use a collection:
Dim coll As New Collection
With coll
.Add "corp", "product"
.Add 1, "group"
.Add 1, "recordno"
'...
End With
Then instead of selecting cells and referencing ActiveCell you should reference the cells directly (using selections and ActiveCell can be avoided most of the times and slows down the macro and can even cause unnecessary errors)
For i = 1 To 5
Cells(2, i).value = coll(Cells(1, i).value)
Next i
An alternative to a collection is a dictionary which offers an easy way to check if a key exists (with a collection you have to catch the error)
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
With dict
.Add "product", "corp"
.Add "group", 1
.Add "recordno", 1
'...
End With
Now you can check if the entry exists first so it won't throw an error:
For i = 1 To 5
If dict.Exists(LCase(Cells(1, i).value)) Then 'note that the dictionary keys are case sensitive
Cells(2, i).value = dict(LCase(Cells(1, i).value))
Else
MsgBox "Entry for " & LCase(Cells(1, i).value) & " not found!"
End If
Next i
Note that when you use dict("somekey") and the entry "somekey" doesn't exist, it won't throw an error but add an empty entry.
Why not an array and then loop through the elements as needed?
Dim ArrayTitles() As Variant 'since strings and numbers are mixed
ReDim Preserve ArrayTitles(5)
ArrayTitles(1) = "corp"
ArrayTitles(2) = 1
ArrayTitles(3) = 1
ArrayTitles(4) = 1
ArrayTitles(5) = "TTT"
Range("A2").Select
For i = 1 To 5
MsgBox (ArrayTitles(i))
I'm thinking what you are trying to accomplish can be solved in this way
for j = 1 to 6 'Or whatever your last column happens to be
if UCase(cells(1, j)) = "PRODUCT" then
if ActiveCell.Column = j then
ActiveCell.Value = "corp"
end if
end if
next j
Something like that?

Function will not return array when range contains only one value

I have a function meant to return an array which is created out of a single-column list of data. I have been using this function's return value essentially as a pseudo-global variable (LINENAMES_ARRAY) which I pass to many functions. Those functions than do checks on it such as If Len(Join(LINENAMES_ARRAY)) = 0 Then or go through items with For Each statements. Here is the code:
Function LINENAMES_ARRAY() As Variant
'returns an array of all items in the main sheet linenames column
LINENAMES_ARRAY = Application.Transpose(MAIN.Range( _
MAIN.Cells(MAIN_HEAD_COUNT + 1, MAIN_LINENAMES_COLUMN), _
MAIN.Cells(LINENAMES_COUNT + 1, MAIN_LINENAMES_COLUMN)))
End Function
I recently stumbled on one of those you-don't-see-it-till-you-see-it problems while using this workbook for a new project, where if the array happens to be only 1 element, everything fails. Apparently in that case, this returns a single value so Join() will fail For Each __ in LINENAMES_ARRAY will too. Why won't it treat this as a 1x1 array rather than a free value? I have started to mitigate the problem by rewriting functions where this is called, to check whether it is an array, then do some other procedure. Things like:
For j = 1 To LINENAMES_COUNT
LINES_BOX.AddItem lineNames(j)
Next j
is changed to:
If Not IsArray(LINENAMES_ARRAY) Then
myListBox.AddItem CStr(LINENAMES_ARRAY)
Else
For j = 1 To LINENAMES_COUNT
LINES_BOX.AddItem LINENAMES_ARRAY(j)
Next j
End If
However this becomes messy and is adding many extra checks to my code that I would prefer to handle in the LINENAMES_ARRAY function. Is there a way to return a 1x1 array? Or any other workaround?
An array can have a single element if you create it as a single element array and populate it in an array manner.
Option Explicit
Dim MAIN_HEAD_COUNT As Long
Dim LINENAMES_COUNT As Long
Dim MAIN_LINENAMES_COLUMN As Long
Dim MAIN As Worksheet
Sub stuff()
Dim arr As Variant
Set MAIN = Worksheets("Sheet1")
MAIN_LINENAMES_COLUMN = 2
MAIN_HEAD_COUNT = 2
LINENAMES_COUNT = 2
arr = LINENAMES_ARRAY()
Debug.Print IsArray(arr)
Debug.Print LBound(arr) & ":" & UBound(arr)
End Sub
Function LINENAMES_ARRAY() As Variant
Dim a As Long, tmp() As Variant
ReDim tmp(0 To LINENAMES_COUNT - MAIN_HEAD_COUNT)
For a = 0 To LINENAMES_COUNT - MAIN_HEAD_COUNT
tmp(a) = MAIN.Range(MAIN.Cells(MAIN_HEAD_COUNT + 1, MAIN_LINENAMES_COLUMN), _
MAIN.Cells(LINENAMES_COUNT + 1, MAIN_LINENAMES_COLUMN)).Cells(a).Value2
Next a
'returns an array of all items in the main sheet linenames column
LINENAMES_ARRAY = tmp
End Function
Results from the VBE's Immediate window:
True
0:0