Recursively print next dictonaries in VBA - excel

I'd like to print nested dictionaries in VBA. Basically I have a Dictionary where every key is a String, but each value can either be a String or another Dictionary.
Say my Dictionary has got the value
{ "FOO" => "BAR" , "HELLO" => { "WORLD => ":)", "OTHER" => ":(" } }
I want to display in an Excel spreadsheet:
FOO |BAR |
HELLO|WORLD|:)
HELLO|OTHER|:(
My issue is that I need to find a way to guess what is the type of the value under each key, so when I call dict("HELLO") I can either display the value if it's a string or if it's a dictionary call the same function again.
In order to do so I need to know:
if there is a way to know what is the type of a value stored in a dictionary
if there is a way to cast that value to a target type (string or dictionary)
So here is what I've tried
Function display_dictionary(dict As Scripting.Dictionary, out As Range) As Integer
Dim vkey As Variant
Dim key As String
Dim row_offset As Integer
Dim value As Object
Dim svalue As String
Dim dvalue As Dictionary
Dim each_offset As Integer
row_offset = 0
For Each vkey In dict.Keys()
key = vkey
Set value = dict(key)
if value is String then
svalue = ???
out.offset(row_offset, 0).value = key
out.offset(row_offset, 1).value = svalue
row_offset = row_offset + 1
else if value is Dictionary
dvalue = ???
each_offset = display_dictionary(dvalue, out.offset(row_offset, 1))
For each_row = 0 To each_offset - 1
out.offset(row_offset + each_row) = key
Next
row_offset = row_offset + each_offset
End If
Next
End

I am actually going to propose a bit different way for displaying the results. I think it's more logical but you are welcome to modify it to suit your specific needs. Just as a hint print logical tree of nodes, like below, and then manipulate the results if ever needed.
So the tree would look like this for example (Note I added one more depth level)
and the code to reproduce
Private i As Long
Private depth As Long
Sub Main()
Cells.ClearContents
Dim dict As New Dictionary
Dim subDict As New Dictionary
Dim lvlDict As New Dictionary
lvlDict.Add "LVL KEY", "LVL ITEM"
subDict.Add "HELLO", ":)"
subDict.Add "WORLD", ":("
subDict.Add "OTHER", lvlDict
dict.Add "FOO", "BAR"
dict.Add "BOO", subDict
i = 1
depth = 0
TraverseDictionary dict
Columns.AutoFit
End Sub
Private Sub TraverseDictionary(d As Dictionary)
For Each Key In d.Keys
Range("A" & i).Offset(0, depth) = "KEY: " & Key
If VarType(d(Key)) = 9 Then
depth = depth + 1
TraverseDictionary d(Key)
Else
Range("B" & i).Offset(0, depth) = "ITEM: " & d(Key)
End If
i = i + 1
Next
End Sub
and spreadsheet result:
To get the variable type or its type name you can use this:
Debug.Print TypeName(dict("HELLO")), VarType(dict("HELLO"))

Related

Parse JSON to excel worksheet by JSONConverter

I created a VBA code that enables me to get a string from a website. The string looks like that (json format)
[
{
"CD":151,
"nID":111,
"sNM":"PNAME1",
"GDR":"MA",
"RGN":"MM",
"reID":1,
"status":"RSB",
"NTY":"EG",
"CLNUM":1,
"CLNM":"THIR",
"YER":2022,
"SCHOD":1718,
"STID":2,
"THNM":"BRYYY",
"SCHNO":"HTBAN",
"rCD":6,
"schooL_TYPE":1,
"SGT":1,
"CLCD":3,
"NG1":1,
"NG2":2,
"YCOD":null,
"general":10,
"special":1,
"naT_ID":1,
"GDR_ID":1,
"sGCOD":8,
"sTTY":1,
"obNM":"NTF",
"obID":0,
"STYN":"NTHMY",
"PSNUM":null
},
{
"CD":153,
"nID":222,
"sNM":"ALIIKK",
"GDR":"MA",
"RGN":"MM",
"reID":1,
"status":"RSB",
"NTY":"EG",
"CLNUM":1,
"CLNM":"THIR",
"YER":2022,
"SCHOD":1718,
"STID":2,
"THNM":"SYYYYY",
"SCHNO":"HTBAN",
"rCD":6,
"schooL_TYPE":1,
"SGT":1,
"CLCD":3,
"NG1":1,
"NG2":2,
"YCOD":null,
"general":10,
"special":1,
"naT_ID":1,
"GDR_ID":1,
"sGCOD":8,
"sTTY":1,
"obNM":"NTF",
"obID":0,
"STYN":"NTHMY",
"PSNUM":null
}
]
I am trying to store the data into a collection and arrays
This is my try but I am confused about the line of setting the collection
Dim json As Object, col As Collection
Set json = JSONConverter.ParseJson(sResp)
Set col = json("CD")
Dim a, i As Long
ReDim a(1 To col.Count, 1 To 1)
For i = 1 To col.Count
'a(i, 1) = col.Item(i)("")
Next i
The string should have two persons' data. How can I pares all the data included into two rows?
I got an error Dictionary Key Not Found at this line of JSONConverter module
Option Explicit
Sub demo()
Dim sResp
sResp = "[{'CD':151,'nID':111,'sNM':'PNAME1','GDR':'MA','RGN':'MM','reID':1,'status':'RSB','NTY':'EG','CLNUM':1,'CLNM':'THIR','YER':2022,'SCHOD':1718,'STID':2,'THNM':'BRYYY','SCHNO':'HTBAN','rCD':6,'schooL_TYPE':1,'SGT':1,'CLCD':3,'NG1':1,'NG2':2,'YCOD':null,'general':10,'special':1,'naT_ID':1,'GDR_ID':1,'sGCOD':8,'sTTY':1,'obNM':'NTF','obID':0,'STYN':'NTHMY','PSNUM':null},{'CD':153,'nID':222,'sNM':'ALIIKK','GDR':'MA','RGN':'MM','reID':1,'status':'RSB','NTY':'EG','CLNUM':1,'CLNM':'THIR','YER':2022,'SCHOD':1718,'STID':2,'THNM':'SYYYYY','SCHNO':'HTBAN','rCD':6,'schooL_TYPE':1,'SGT':1,'CLCD':3,'NG1':1,'NG2':2,'YCOD':null,'general':10,'special':1,'naT_ID':1,'GDR_ID':1,'sGCOD':8,'sTTY':1,'obNM':'NTF','obID':0,'STYN':'NTHMY','PSNUM':null}]"
sResp = Replace(sResp, "'", Chr(34))
Dim data As Object, rec As Object, key, fields, arData
Dim n As Long, m As Long, i As Long, j As Long
Set data = JsonConverter.ParseJson(sResp)
fields = data(1).Keys
m = UBound(fields) + 1
n = data.Count ' records
' fill array
ReDim arData(1 To n, 1 To m)
For i = 1 To n
For j = 1 To m
arData(i, j) = data(i)(fields(j - 1))
Next
Next
' dump array
With Sheet1
.Range("A1").Resize(1, m) = fields ' header
.Range("A2").Resize(n, m) = arData
End With
End Sub
I was wrong. The converter also accepts square brackets as the first element, a collection. The suggested additional brackets would not work either, because they create an invalid JSON.
Now CDP1802 has already answered, but I also looked for a correct solution because of my wrong statement (I deleted it so no one else would see it as the truth):
Sub TestJSON()
Dim sResp As String
Dim json As Collection
Dim col As Dictionary
Dim key As Variant
Dim dicts As Long
Dim row As String
sResp = "[{""CD"":151,""nID"":111,""sNM"":""PNAME1"",""GDR"":""MA"",""RGN"":""MM"",""reID"":1,""status"":""RSB"",""NTY"":""EG"",""CLNUM"":1,""CLNM"":""THIR"",""YER"":2022,""SCHOD"":1718,""STID"":2,""THNM"":""BRYYY"",""SCHNO"":""HTBAN"",""rCD"":6,""schooL_TYPE"":1,""SGT"":1,""CLCD"":3,""NG1"":1,""NG2"":2,""YCOD"":null,""general"":10,""special"":1,""naT_ID"":1,""GDR_ID"":1,""sGCOD"":8,""sTTY"":1,""obNM"":""NTF"",""obID"":0,""STYN"":""NTHMY"",""PSNUM"":null},{""CD"":153,""nID"":222,""sNM"":""ALIIKK"",""GDR"":""MA"",""RGN"":""MM"",""reID"":1,""status"":""RSB"",""NTY"":""EG"",""CLNUM"":1,""CLNM"":""THIR"",""YER"":2022,""SCHOD"":1718,""STID"":2,""THNM"":""SYYYYY"",""SCHNO"":""HTBAN"",""rCD"":6,""schooL_TYPE"":1,""SGT"":1,""CLCD"":3,""NG1"":1,""NG2"":2,""YCOD"":null,""general"":10,""special"":1,""naT_ID"":1,""GDR_ID"":1,""sGCOD"":8,""sTTY"":1,""obNM"":""NTF"",""obID"":0,""STYN"":""NTHMY"",""PSNUM"":null}]"
Set json = JsonConverter.ParseJson(sResp)
For dicts = 1 To json.Count
Set col = json(dicts)
For Each key In col.Keys()
'Debug.Print key & ":" & col(key) & ", "
row = row & col(key) & Chr(9)
Next key
Debug.Print row
row = ""
Next dicts
End Sub

Function changes value in sheet

I am creating a function which prototype is:
Function TableToText(ByVal Table As Range) As String
This function is supposed to give me a string and not modify the sheet at all. However the operations that I perform on Table (which is a Range) inside the function also modify my sheet.
I thought ByVal which is the default was supposed to prevent that?
I tried in my function to make another range but to make another Range you use Set so it wouldn't have solved the problem anyway...
Can someone point out what I am missing here? Thank you in advance! :)
The whole function is
Function TableToText(Table As Range) As String
Dim nbColumns As Integer, nbRows As Integer
Dim i As Integer, j As Integer, s As Integer
Dim max As Integer, difference As Integer
nbColumns = Table.Columns.Count
nbRows = Table.Rows.Count
With Table
'' adding the spaces
For j = 1 To nbColumns
max = 0
' Find the longest string in the column
For i = 1 To nbRows
If Len(.Cells(i, j).Value) > max Then
max = Len(.Cells(i, j).Value)
End If
Next i
' Adding the spaces and the |
For i = 1 To nbRows
If Len(.Cells(i, j).Value) < max Then
difference = max - Len(.Cells(i, j).Value)
For s = 1 To difference
.Cells(i, j) = CStr(.Cells(i, j).Value) + " "
Next s
End If
.Cells(i, j) = CStr(.Cells(i, j).Value) + "|"
Next i
Next j
'' Creating the plain text table string
For i = 1 To nbRows
For j = 1 To nbColumns
TableToText = TableToText + .Cells(i, j).Value
Next j
TableToText = TableToText & vbCrLf
Next i
End With
End Function
There's a lot of misleading information and confusion on this page.
I thought ByVal which is the default was supposed to prevent that?
The default is ByRef in VBA. This is unfortunate, because the vast majority of the time, what you mean to do is to pass things ByVal.
Objects are always passed by reference [...] ByVal is ignored.
No. Objects are never "passed", period. What's passed byref/byval is a reference to the object, i.e. a pointer. That does not mean object parameters are always passed ByRef at all.
Let's debunk this claim once and for all.
Public Sub DebunkObjectsAreAlwaysPassedByRefClaim()
Dim thing As Collection 'any object type will do
Set thing = New Collection
DoSomethingByVal thing 'we pass a COPY of the pointer
Debug.Print thing.Count 'no problems here
DoSomethingByRef thing 'we pass a reference to our local pointer; what could possibly go wrong?
Debug.Print thing.Count 'error 91! the object reference is gone!
End Sub
Private Sub DoSomethingByVal(ByVal o As Object)
Set o = Nothing 'affects the local copy only
End Sub
Private Sub DoSomethingByRef(ByRef o As Object)
Set o = Nothing 'affects the same object pointer the caller gave us. this is bad.
End Sub
ByRef vs ByVal makes a major difference: give a procedure your object pointer (ByRef), and they can do anything they like with it - including Set-assigning it to a completely different object reference, or making it Nothing. Give a procedure a copy of your object pointer (ByVal), and whatever they do with it (including Set-assigning it to a completely different object reference or making it Nothing) will only affect that copy.
In both cases, whether you've passed the pointer itself or a copy of it, either way you're giving the procedure access to the same object, so as GSerg explained, any instruction that affects a Range (which you can't create - all Range objects belong to Excel, all you ever get is a pointer to one), regardless of where the pointer comes from, will affect the Range instance state.
So if you don't want to affect any worksheet, don't affect any Range and work with arrays instead.
Objects are always passed by reference even if you specify the byval keyword.
You should use a temporary array to store your values.
For example, something like that :
Function TableToText(Table As Range) As String
Dim nbColumns As Integer, nbRows As Integer
Dim i As Integer, j As Integer, s As Integer
Dim max As Integer, difference As Integer
nbColumns = Table.Columns.Count
nbRows = Table.Rows.Count
Dim tmpValues(nbRows, nbColumns) As String
With Table
'' adding the spaces
For j = 1 To nbColumns
max = 0
' Find the longest string in the column
For i = 1 To nbRows
If Len(.Cells(i, j).Value) > max Then
max = Len(.Cells(i, j).Value)
End If
Next i
' Adding the spaces and the |
For i = 1 To nbRows
If Len(.Cells(i, j).Value) < max Then
difference = max - Len(.Cells(i, j).Value)
For s = 1 To difference
tmpValues(i, j) = CStr(.Cells(i, j).Value) + " "
Next s
End If
tempValues(i, j) = CStr(.Cells(i, j).Value) + "|"
Next i
Next j
'' Creating the plain text table string
For i = 1 To nbRows
For j = 1 To nbColumns
TableToText = TableToText + tmpValues(i, j)
Next j
TableToText = TableToText & vbCrLf
Next i
End With
End Function
Hope it helps.

find max/min value in dictionary values in VBA

I am using VBA to store key and value associated with the key into dictionary.
Sub Dict_Example()
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 to 5
dict.Add i, some number
Next i
I would like to find the highest value and its associated key in dict.
For example, if dict = {1: 5, 2: 10, 3: 6, 4: 11, 5: 3} where 1,2,3,4,5 are keys and 5, 10, 6, 11, 3 are values then it should return 4:11.
How do I do this in VBA?
I would generate an array from the dict.items and use Max/Min function on that. Then loop keys and compare items against that.
Option Explicit
Public Sub Dict_Example()
Dim dict As Object, max As Long, min As Long, arr(), key As Variant, i As Long
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To 5
dict.Add i, i * Application.WorksheetFunction.RandBetween(0, 100)
Next i
max = Application.max(dict.items)
min = Application.min(dict.items)
For Each key In dict
Debug.Print key, dict(key)
If dict(key) = max Then Debug.Print "max= " & dict(key) & vbTab & "key= " & key
Next
Stop
End Sub
You could accomplish this by using a couple arrays to temporarily store your high and low values and associated keys as you iterated through the dictionary like this:
Sub test()
Dim dict As New Dictionary
Dim low(1 To 2)
Dim high(1 To 2)
Dim i As Long
Dim key
For i = 1 To 5
dict.Add i, 'some number
Next i
low(1) = dict.Keys(0)
low(2) = dict(dict.Keys(0))
high(1) = dict.Keys(0)
high(2) = dict(dict.Keys(0))
For i = 0 To dict.Count - 1
If dict(dict.Keys(i)) < low(2) Then
low(1) = dict.Keys(i)
low(2) = dict(dict.Keys(i))
ElseIf dict(dict.Keys(i)) > high(2) Then
high(1) = dict.Keys(i)
high(2) = dict(dict.Keys(i))
End If
Next i
Debug.Print low(1) & ":" & low(2) & vbCrLf & high(1) & ":" & high(2)
End Sub
But sorting like this would only work correctly for numeric values. #Ryan Wildry's comment is the way to go for generally sorting a dictionary, then you would grab your values using dict(dict.Keys(0)) and dict(dict.Keys(dict.Count - 1)) respectively where dict references your sorted dictionary.
EDIT:
You'll need to add a library reference to the Microsoft Scripting Runtime for this to work.

How can I tell the differences between two strings in Excel?

I created an assessment that applicants fill out in Excel. I have a key where I copy their answers in and it tells me if their answers match my key. I'd like to add a formula that will also show me the differences between the applicant's cell (B2) and the key's cell (A2), to make it easier to see what they got wrong.
I tried using =SUBSTITUTE(B2,A2,"") but this only gives me differences at the beginning or end of the string. Usually, the difference is in the middle.
For example, my key (cell A2) might say: Cold War | Bay of Pigs | Fidel Castro
And the applicant (cell B2) might say: Cold War | Cuban Missile Crisis | Fidel Castro
I want this formula to return: "Cuban Missile Crisis"
You may try something like this...
Function CompareStrings(keyRng As Range, ansRng As Range) As String
Dim arr() As String
Dim i As Long
arr() = Split(ansRng.Value, "|")
For i = 0 To UBound(arr)
If InStr(keyRng.Value, arr(i)) = 0 Then
CompareStrings = arr(i)
Exit Function
End If
Next i
End Function
Then you can use this UDF like below...
=CompareStrings(A2,B2)
If you want to compare them in the reverse order also and return the not matched string part from any of them, try this...
Function CompareStrings(ByVal keyRng As Range, ByVal ansRng As Range) As String
Dim arr() As String
Dim i As Long
Dim found As Boolean
arr() = Split(ansRng.Value, "|")
For i = 0 To UBound(arr)
If InStr(keyRng.Value, Trim(arr(i))) = 0 Then
found = True
CompareStrings = arr(i)
Exit Function
End If
Next i
If Not found Then
arr() = Split(keyRng.Value, "|")
For i = 0 To UBound(arr)
If InStr(ansRng.Value, Trim(arr(i))) = 0 Then
CompareStrings = arr(i)
Exit Function
End If
Next i
End If
End Function
Use this as before like below...
=CompareStrings(A2,B2)
So the function will first compare all the string parts of B2 with A2 and if it finds any mismatch, it will return that part of string and if it doesn't find any mismatch, it will then compare all the parts of string in A2 with B2 and will return any mismatch part of string. So it will compare both ways.
The function above displays only the first difference. Here is an update that displays all differences between two strings.enter image description here
Function CompareStrings(ByVal keyRng As Range, ByVal ansRng As Range) As String
Dim arr() As String
Dim i As Long
arr() = Split(ansRng.Value, " ")
CompareStrings = "+["
For i = 0 To UBound(arr)
If InStr(keyRng.Value, Trim(arr(i))) = 0 Then
CompareStrings = CompareStrings & " " & arr(i)
End If
Next i
CompareStrings = CompareStrings & " ] -["
arr() = Split(keyRng.Value, " ")
For i = 0 To UBound(arr)
If InStr(ansRng.Value, Trim(arr(i))) = 0 Then
CompareStrings = CompareStrings & " " & arr(i)
End If
Next i
CompareStrings = CompareStrings & " ]"
End Function

Excel VBA: Unable to reassign value to array

I have code that loops through a dictionary. the value for each key in the dictionary is a 2-item array (dictionary looks like name: [string, integer]).
when I reference the dictionary later on, I can see and print the string and integer in the array belonging to the dictionary entry, but I can't change the integer through a normal assignment like dictionary(name)(2) = 5; after doing so in the code, I print the array value to a debug file, and the array value is the original value, not its changed value. I have no idea why this doesn't work, and how I can get it to work. everything I've read on arrays says you just assign array(0) = something.
here is part of the code below:
defining the original dictionary:
dim Dict as Object
Set Dict = CreateObject("Scripting.Dictionary")
for i = 1 to 10
strnum = "str"&i
Dict.Add strnum, array("str"&i+1,0)
next
'printing each item in dict returns "Str1: [str2,0]", etc. as it should
working with the dictionary:
For each Cell in Range("a1:a11")
If Cell.Value <> "" And Dict.exists(Cell.Value) Then
name = Cell.Value
range = Dict(name)(0)
If Dict(name)(1) = 1 Then
'we have already located the name here
Else
Dict(name)(1) = 1
s = "Setting the found flag to " & Dict(name)(1)
debug.print s
'Dict(name)(1) returns 0 when it should return 1
end if
end if
next cell
Range a1:a11 is Str1,Str2,Str3,Str4,Str5...Str11.
What can I do to fix this?
You are creating some weird aliasing with
for i = 1 to 10
Str = "str"&i
arr(1) = "str"&i+1
arr(2) = 0
Dict.Add Str, arr
next
since you only created a single array when you dimensioned arr.
You could create a dictionary of dictionaries to do what you wanted:
Sub test()
Dim Dict As Object, Str, i, arr
Set Dict = CreateObject("Scripting.Dictionary")
For i = 1 To 10
Set arr = CreateObject("Scripting.Dictionary")
arr.Add 1, "str" & i + 1
arr.Add 2, 0
Str = "str" & i
Dict.Add Str, arr
Next
For i = 1 To 10
Debug.Print (Dict("str" & i)(1))
Next i
Dict("str1")(1) = "Bob"
Debug.Print Dict("str1")(1) 'prints Bob
End Sub

Resources