Nested dictionary object not found - vba

I am attempting to take some table, a list of fields to group by, and a number field as input, to produce an output table that contains the group-by fields and medians for the number field.
I have this working for a single field to group by, but not multiple fields.
I am using nested dictionaries to eventually aggregate my grouped-field values and their medians.
When I try to access the inner dictionary I get a
'424' Run-time error: Object required
Once I put the inner dictionary in the outer dictionary I cannot access it like one would suspect.
Public Sub MedianByGroups(ByVal inputTable As String, ByVal strField As String, _
ByVal strGroup As String, ByVal outputTable As String, _
Optional ByVal strCriteria As String)
DoCmd.SetWarnings False
Dim db As DAO.Database: Set db = CurrentDb()
Dim rstDomain As DAO.Recordset
Dim strSQL As String
Dim varMedian As Long: varMedian = 0
Dim intRecords As Long
Dim splitgroup As Variant
splitgroup = Split(strGroup, ", ")
strSQL = "SELECT DISTINCT " & strGroup & " INTO TMP_GROUPINGTABLE FROM " & _
inputTable & ";"
DoCmd.RunSQL (strSQL)
Set rstDomain = db.OpenRecordset("TMP_GROUPINGTABLE")
Dim i As Integer: i = 0
Dim group_dict As New Dictionary
Dim tmp_dict As Dictionary
Set tmp_dict = New Dictionary
If Not (rstDomain.EOF And rstDomain.BOF) Then
rstDomain.MoveFirst
Do Until rstDomain.EOF = True
For Each Label In splitgroup
tmp_dict.Add Label, rstDomain.Fields(Label).Value
Next Label
group_dict.Add i, tmp_dct
Set tmp_dict = New Dictionary
i = i + 1
rstDomain.MoveNext
Loop
End If
rstDomain.Close
For Each key In group_dict.Keys
For Each addlKey In group_dict(key).Keys
Debug.Print addlKey, group_dict(key)(addlKey)
Next addlKey
Next key
DoCmd.SetWarnings True
end sub
If one had a table, called "MYTABLE", that looked like this:
GROUP1 GROUP2 VAL1
A C 400
B D 500
One would call this sub like:
call MedianByGroups("MYTABLE", "VAL1', "GROUP1, GROUP2", "OUTPUTTABLE")
This doesn't do the Median or any other parts yet, as I am hitting this stumbling point with the dictionaries.
Where it fails is the inner for loop on that nested for loop near the end.
For Each addlKey In group_dict(key).Keys
Edit: As ComputerVersteher pointed out, the issue was a spelling mistake I had made. The solution to avoid this as Andre mentioned is to add the option explicit to the module.

Not a complete answer -- I set up an example without the MS Access-specific parts and the logic in your code seems to run fine. As near as I can tell, there's no real difference in the logic below and your code above. Does this example run for you?
Option Explicit
Sub TestingNestedDictionaries()
Dim group_dict As New Dictionary
Dim tmp_dict As Dictionary
Set tmp_dict = New Dictionary
Dim labels As Variant
labels = Array("red", "orange", "yellow", "green", "blue", "indigo", "violet")
Dim i As Long
For i = 1 To 5
Dim label As Variant
For Each label In labels
tmp_dict.Add label, label & "-" & i
Next label
group_dict.Add i, tmp_dict
Set tmp_dict = New Dictionary
Next i
'--- pull out one color
' Const THIS_COLOR As String = "green"
' Dim group As Variant
' For Each group In group_dict
' Dim subDict As Dictionary
' Set subDict = group_dict(group)
' Debug.Print subDict(THIS_COLOR)
' Next group
Dim key As Variant
Dim addlKey As Variant
For Each key In group_dict.Keys
For Each addlKey In group_dict(key).Keys
Debug.Print addlKey, group_dict(key)(addlKey)
Next addlKey
Next key
End Sub

Related

How to join substrings to define integer variable?

I am trying to join two substrings into one that defines an integer variable.
Example:
Dim ACTGPA as Integer
Dim ACTGPB as Integer
Dim ACPrio as Integer
ACTGPA = 5
ACTGPB = 10
Following a lot of code I have a loop that selects the value from a recordset containing a number column called either "TGP_A" or TGP_B".
Set rs4 = db.OpenRecordset("SELECT * FROM tbl_Syllabus WHERE Mis_Name = '" & strMisName & "'")
Dim item As Variant
For Each item In rs4.Fields
If item.Name = "TGP_A" Or item.Name = "TGP_B" Then
If Nz(rs4(item.Name).Value, "") > 0 Then
If strACCon1 = "" Then
strACCon1 = item.Name
End If
End If
End If
Next item
I want to set the ACPrio value to either the value of ACTGPA or ACTGPB, depending on which item name is chosen in the loop.
Using replace I remove the _ from the item name in order to define ACTGPA (or B).
I get
"Error 13, type mismatch"
on this next line. Probably because it thinks I am trying to set ACPrio (integer) to something that is a string - but I am trying to write the integer ACTGPA (or ACTGPB).
ACPrio = "AC" & Replace(strACCon1, "_", "")
Should be the same as this:
ACPrio = ACTGPA 'eg. settings the ACPrio value to that of ACTGPA (or ACTGPB), which is 5 (or 10).
You can't use a string to reference a variable (or its value) by name.
But you can add the necessary values to a collection with their name and then access them later on by name, like in this sample:
Public Sub Sample()
Dim ACTGPA As Integer
Dim ACTGPB As Integer
Dim ACPrio As Integer
ACTGPA = 5
ACTGPB = 10
' Prepare the collection by adding the values with their name:
Dim col As Collection
Set col = New Collection
col.Add 5, "ACTGPA"
col.Add 10, "ACTGPB"
Dim strACCon1 As String
' Set for testing to "TGP_A":
strACCon1 = "TGP_A"
ACPrio = col("AC" & Replace(strACCon1, "_", ""))
Debug.Print ACPrio
' Set for testing to "TGP_B":
strACCon1 = "TGP_B"
ACPrio = col("AC" & Replace(strACCon1, "_", ""))
Debug.Print ACPrio
End Sub
The output is
5
10
And maybe you can omit the variables ACTGPA and ACTGPB at all now, if it fits to your concept and overall code.

Excel VBA: MoviesByGenre Function

I am trying to write an Excel VBA function that will do two things. First, it will determine the number of movies in each genre and print it to the immediate window using the printMovieData function I have written. Secondly, it will return the genre that has the most number of movies using the FindMax function I have written. I have provided my codes for printMovieData, FindMax, and what I have so far for MoviesByGenre, however, I am not sure what I am doing wrong and am looking for help to get the function working. Currently, Excel is returning the #VALUE! error.
printMovieData:
Function printMovieData(title As String, arrayTopic, arrayOther)
printMovieData = ""
For i = 1 To UBound(arrayTopic)
Debug.Print arrayTopic(i) & " : " & arrayOther(i)
Next i
End Function
FindMax:
Function FindMax(valueArray, nameArray) As String
Dim i As Integer
Dim maxValue As Variant
maxValue = 0
For i = 1 To UBound(valueArray)
If valueArray(i) >= maxValue Then
maxValue = valueArray(i)
FindMax = nameArray(i)
End If
Next i
End Function
MoviesByGenre:
Function MoviesByGenre(genreRng As Range) As String
Dim i As Integer
Dim genreArray(1 To 4) As String
Dim countArray
genreArray(1) = Action
genreArray(2) = Comedy
genreArray(3) = Drama
genreArray(4) = Musical
For i = 1 To UBound(genreArray)
For j = 1 To genreRng.Count
If genreRng.Cells(j) = genreArray(i) Then
countArray(i) = countArray(i) + 1
End If
Next j
Next i
MoviesByGenre = printMovieData("Movies by Genre", genreArray, countArray)
MoviesByGenre = FindMax(countArray, genreArray)
End Function
TBH there are quite a lot of reasons why I wouldn't expect your code to work.
There are some assumptions made such as how you use genreRng.Count. This is assuming data to count is either one row or one column.
The following assumes that genreRng.Cells.Count, Ubound(genreArray) and UBound(countArray) are all the same. You don't ensure this.
There are missing variable declarations, no use of Option Explicit and a number of other things.
Overarching though, is that I think you want a different object to handle your count. This is where Collection and Scripting Dictionaries are very useful.
You can have the key as the genre and the count is held in the associated value. If the key already exists, i.e. a repeat genre, just add one to the existing count.
With that in mind, a starting point, might be something like (sorry, no error handling added):
Option Explicit
Public Sub test()
Dim genreCount As Object
Set genreCount = CreateObject("Scripting.Dictionary")
Set genreCount = MoviesByGenre(ActiveSheet.Range("A1:A3"), genreCount)
printMovieData "Movies by genre", genreCount
FindMax genreCount
End Sub
Public Function MoviesByGenre(ByRef genreRng As Range, ByVal genreCount As Object) As Object
Dim j As Long
For j = 1 To genreRng.Count 'assumes 1 column/row
Dim currentGenre As String
currentGenre = genreRng.Cells(j, 1)
If Not genreCount.Exists(currentGenre) Then
genreCount.Add currentGenre, 1
Else
genreCount(currentGenre) = genreCount(currentGenre) + 1
End If
Next j
Set MoviesByGenre = genreCount
End Function
Public Function printMovieData(ByVal title As String, ByVal genreCount As Object)
Dim key As Variant
Debug.Print title & vbCrLf 'put to next line
For Each key In genreCount.keys
Debug.Print key & " : " & genreCount(key)
Next key
End Function
Public Function FindMax(ByVal genreCount As Object) As String
Dim maxValue As Long
Dim maxGenre As String
Dim key As Variant
For Each key In genreCount.keys
If genreCount(key) > maxValue Then
maxValue = genreCount(key)
maxGenre = key
End If
Next key
Debug.Print vbNewLine & "Max genre is " & maxGenre & " with " & maxValue
End Function
Input and output:
Input:
Output:

VBA change Case to look for LIKE string

How can I change this code used to select page items in an XL pivotTable
Dim pvtSM1 As PivotTable
Dim pviSM1 As PivotItem
Dim pvfSM1 As PivotField
Set pvtSM1 = ActiveSheet.PivotTables("SM1")
' set Pivot field variable to "RESULT"
Set pvfSM1 = pvtSM1.PivotFields("RESULT")
' loop through all Pivot Items in "RESULT" Pivot Field
For Each pviSM1 In pvfSM1.PivotItems
Select Case pviSM1.Name
Case "4K2..00", "4K21.00", "4K22.00", "4K23.00", "4K41.00", "4K42.00", "4K43.00", "4KA1.00", "4KA2.00"
pviSM1.Visible = True
Case Else
pviSM1.Visible = False
End Select
Next pviSM1
End With
...into a LIKE "4K2*", "4K4*", "4KA*"
To save me adding all the exact codes
Dim pvtSM1 As PivotTable
Dim pviSM1 As PivotItem
Dim pvfSM1 As PivotField
I swear, I read them 5 times (okay, 3) before I figured out the [single-character] difference between them. And I've no idea whatsoever what SM1 might stand for. Suggestion:
Dim pvtTable As PivotTable
Dim pvtItem As PivotItem
Dim pvtField As PivotField
Use meaningful names that you can read out loud without sounding like an Ewok.
Better suggestion - declare variables closer to where you're using them, instead of in a wall of declarations at the top of your procedure; and then use Comintern's suggestion to get rid of the Select Case block altogether:
Dim pvtTable As PivotTable
Set pvtTable = MyPivotTableSheet.PivotTables("SM1") ' don't assume what the ActiveSheet is
Dim pvtField As PivotField
Set pvtField = pvtTable.PivotFields("RESULT")
Dim pvtItem As PivotItem
For Each pvtItem In pvtField.PivotItems
pvtItem.Visible = pvtItem.Name Like "4K[24A]*"
Next
And heck, naming is hard - don't name things per their type, name things per their purpose.
The code throws a runtime error if PivotTables("SM1") doesn't exist, or if PivotFields("RESULT") doesn't refer to anything. The best way to avoid this is to separate the concerns into small, specialized functions, that do one thing and that do it well:
Private Function FindPivotTable(ByVal sheet As Worksheet, ByVal name As String) As PivotTable
If sheet Is Nothing Then Err.Raise 5, "FindPivotTable", "'sheet' argument cannot be Nothing"
On Error Resume Next
Dim result As PivotTable
Set result = sheet.PivotTables(name)
On Error GoTo 0
Err.Clear
If result Is Nothing Then
Err.Raise 9, "FindPivotTable", "Could not locate pivot table '" & name & "' on worksheet '" & sheet.Name & "'."
Exit Function
End If
Set FindPivotTable = result
End Function
Private Function FindPivotField(ByVal pivot As PivotTable, ByVal name As String) As PivotField
If pivot Is Nothing Then Err.Raise 5, "FindPivotField", "'pivot' argument cannot be Nothing"
On Error Resume Next
Dim result As PivotField
Set result = pivot.PivotFields(name)
On Error GoTo 0
Err.Clear
If result Is Nothing Then
Err.Raise 9, "FindPivotField", "Could not locate pivot field '" & name & "' in pivot table '" & pivot.Name & "'."
Exit Function
End If
Set FindPivotField = result
End Function
Now your procedure can focus on its task, and you can reuse these specialized functions instead of either writing frail error-prone code, or copy-pasting the same fail-safe code over and over again:
On Error GoTo ErrHandler
Dim sourcePivot As PivotTable
Set sourcePivot = FindPivotTable(MyPivotTableSheet, "SM1")
If sourcePivot Is Nothing Then Exit Sub
Dim resultField As PivotField
Set resultField = FindPivotField(sourcePivot, "RESULT")
If resultField Is Nothing Then Exit Sub
Dim item As PivotItem
For Each item In resultField.PivotItems
item.Visible = item.Name Like "4K[24A]*"
Next
Exit Sub
ErrHandler:
MsgBox "Error in '" & Err.Source & "': " & Err.Description
...yet it still feels bloated, so I'd take that and parameterize it, so that it works off targetField - and since that's supposed to be "RESULT", I'll call the parameter resultField:
Private Sub SetItemVisibilityByPattern(ByVal resultField As PivotField, ByVal likePattern As String)
If resultField Is Nothing Then Exit Sub
Dim item As PivotItem
For Each item In resultField.PivotItems
item.Visible = item.Name Like likePattern
Next
End Sub
And now it's the caller's responsibility to figure out how resultField gets there, and you're left with a very, very simple procedure that does one single thing.
Like works, for basic pattern searches. When you start having need for more complicated patterns (e.g. match "4K2*" but also "685*"), consider using a regular expression pattern (here referencing the Microsoft VBScript Regular Expressions 5.5 library):
Private Sub SetItemVisibilityByPattern(ByVal resultField As PivotField, ByVal regexPattern As String)
If resultField Is Nothing Then Exit Sub
With New RegExp
.Pattern = regexPattern
Dim item As PivotItem
For Each item In resultField.PivotItems
item.Visible = .Execute(item.Name).Count > 0
Next
End With
End Sub
With a single regular expression pattern you can match anything you like, with as many possible alternatives as you need:
SetItemVisibilityByPattern(resultField, "(4K[24A]|685|923).*")
If you really want select case with likes here's bulky example:
Sub test()
Dim str As String
str = InputBox("feed me a string")
Select Case str Like "4K[24A]*"
Case True
Call MsgBox("Da!")
'pviSM1.Visible = True
Case False
Call MsgBox("Net!")
'pviSM1.Visible = false
End Select
End Sub

String values left out of PropertyInfo.GetValue

I am not very well-versed in Reflection, but I have been working on this bit of code for a few days trying to obtain the values of class properties. I am using an API to find the values inside of cron jobs managed by the program VisualCron.
I'll explain the structure a bit. Each cron job has several tasks inside of it which have their own settings. The settings are stored in properties inside the TaskClass class that are declared like so:
Public Property <propertyname> As <classname>
Each property is tied to its own class, so for instance there is an Execute property inside TaskClass which is declared like this:
Public Property Execute As TaskExecuteClass
Inside TaskExecuteClass are the properties that hold the values I need. With the below block of code I have been able to retrieve the property values of all types EXCEPT strings. Coincidentally, the string values are the only values I need to get.
I know there must be something wrong with what I've written causing this because I can't find anyone with a similar issue after lots and lots of searching. Can anyone help me please?
Dim strAdd As String = ""
For Each t As VisualCronAPI.Server In vcClient.Servers.GetAll()
For Each f As VisualCron.JobClass In t.Jobs.GetAll
For Each s As VisualCron.TaskClass In f.Tasks
Dim propVal As Object
Dim propInfo As PropertyInfo() = s.GetType().GetProperties()
For i As Integer = 0 To propInfo.Length - 1
With propInfo(i)
If s.TaskType.ToString = propInfo(i).Name.ToString Then
Dim asm As Assembly = Assembly.Load("VisualCron")
Dim typeName As String = String.Format("VisualCron.{0}", propInfo(i).PropertyType.Name)
Dim tp As Type = asm.GetType(typeName)
Dim construct As ConstructorInfo = tp.GetConstructor(Type.EmptyTypes)
Dim classInst As Object = construct.Invoke(Nothing)
Dim classProps As PropertyInfo() = classInst.GetType().GetProperties()
For h As Integer = 0 To classProps.Length - 1
With classProps(h)
If .GetIndexParameters().Length = 0 Then
propVal = .GetValue(classInst, Nothing)
If Not propVal Is Nothing Then
strAdd = f.Name & " - " & s.Name & " - " & .Name & " - " & propVal.ToString
End If
End If
If strAdd <> "" Then
ListBox1.Items.Add(strAdd)
End If
End With
Next
End If
End With
Next
Next s
Next f
Next t
I solved my own problem, albeit in a crappy way. This is probably incredibly inefficient, but speed isn't a necessity in my particular case. Here is the code that works:
Dim strAdd As String = ""
For Each t As VisualCronAPI.Server In vcClient.Servers.GetAll()
For Each f As VisualCron.JobClass In t.Jobs.GetAll
For Each s As VisualCron.TaskClass In f.Tasks
Dim propVal As Object
Dim propInfo As PropertyInfo() = s.GetType().GetProperties()
For i As Integer = 0 To propInfo.Length - 1
With propInfo(i)
If s.TaskType.ToString = propInfo(i).Name.ToString Then
Dim asm As Assembly = Assembly.Load("VisualCron")
Dim typeName As String = String.Format("VisualCron.{0}", propInfo(i).PropertyType.Name)
Dim tp As Type = asm.GetType(typeName)
Dim construct As ConstructorInfo = tp.GetConstructor(Type.EmptyTypes)
Dim classInst As Object = construct.Invoke(Nothing)
Dim classProps As PropertyInfo() = classInst.GetType().GetProperties()
For h As Integer = 0 To classProps.Length - 1
With classProps(h)
If .GetIndexParameters().Length = 0 Then
propVal = .GetValue(CallByName(s, propInfo(i).Name.ToString, [Get]), Nothing)
If Not propVal Is Nothing Then
If propVal.ToString.Contains("\\server\") Or propVal.ToString.Contains("\\SERVER\") Then
strAdd = f.Name & " - " & s.Name & " - " & .Name & " - " & propVal.ToString
ListBox1.Items.Add(strAdd)
End If
End If
End If
End With
Next
End If
End With
Next
Next s
Next f
Next t
The piece of code that made the difference was the
classProps(h).GetValue(CallByName(s, propInfo(i).Name.ToString, [Get]), Nothing)
line.
If there are any suggestions for improving this code - I'm assuming that I've still got a lot of mistakes in here - then please comment for the future viewers of this answer and so I can adjust my code and learn more about how all of this works.

How to get the last record id of a form?

I currently have a form in access.
What I want to do is get the value of the last record added.
For example, if i have 10 records, I want to get the value "10", because this is the id of the added last record. I am trying to run a query with the function last id inserted() but it is not working.
This the code I am using :
Dim lastID As Integer
Query = "select last_insert_id()"
lastID = Query
MsgBox (lastID)
What am I missing?
There is a function DMax that will grab the highest number.
Dim lastID As Integer
lastID = DMax("IDField","YourTable")
' or = DMax("IDField","YourTable","WhenField=Value")
MsgBox lastID
The other Domain functions are:
DAvg
DCount
DFirst
DLast
DLookup
DMin
DStDev
DStDevP
DSum
DVar
DVarP
Check with your friendly F1 key for more info
Following on from the last comments, here's a piece of code I used recently to turn the last ID value of a record set into variable for use in VBA. It's not great, however, because I still can't work out how to turn the record's ID field value directly into a variable. Instead I used the inelegant solution of copying the record set into an excel workbook, and then setting the variable value to the value of the cell I just copied into.
EDIT: Worked out how to turn the ID into a simple variable: new code at end
This is all run from a single client workbook:
Option Explicit
Public AftUpD As Long
Public BfrUpD As Long
Sub AssignLstRowAftUpD2()
Dim dbPP As DAO.Database
Dim ResTemp As DAO.Recordset
Dim z As Long
Dim SelectLast As String
SelectLast = "SELECT Max(Table1.ID) AS MaxOfID FROM Table1"
'Debug.Print SelectLast
Set dbPP = OpenDatabase("C:\filepath\Database11.mdb")
Set ResTemp = dbPP.OpenRecordset(SelectLast)
If ResTemp.EOF Then
GoTo EndLoop
End If
Worksheets("Diagnostics").Visible = True
Worksheets("Diagnostics").Range("C4").CopyFromRecordset ResTemp
z = Sheets("Diagnostics").Range("C4").Value
Sheets("Diagnostics").Visible = False
AftUpD = z
'Debug.Print AftUpD
EndLoop:
ResTemp.Close
dbPP.Close
Set dbPP = Nothing
Set ResTemp = Nothing
'Set SelectionLast = Nothing
'z = Nothing
End Sub
Then I used this value as a variable to make a new SQL query:
Sub Query()
'This query uses the highest ID value in a companion spreadsheet (the public
'variable BfrUpD), which is set in a sub I haven't posted here, to find out
'how many records have been added to the database since the last time the
'spreadsheet was updated, and then copies the new records into the workbook
'Be warned: If you run this query when BfrUpD is equal to or greater than AftUpD it
'will cause a crash. In the end user version of this, I use several If tests,
'comparing BfrUpD with other public variables, to make sure that this doesn't
'happen.
Dim WBout As Excel.Workbook, WSout As Excel.Worksheet
Dim dbPP1 As DAO.Database
Dim qryPP1 As DAO.Recordset
Dim ResTemp1 As DAO.Recordset
Dim TestValue As String
Dim strSQL2 As String
TestValue = BfrUpD
'Debug.Print TestValue
strSQL2 = "SELECT * FROM Table1 WHERE (((Table1.ID)>" & TestValue & "))"
'Debug.Print strSQL2
Set dbPP1 = OpenDatabase("C:\filepath\Database11.mdb")
Set qryPP1 = dbPP1.OpenRecordset(strSQL2)
Set WBout = Workbooks.Open("C:\filepath\h.xlsm")
Set WSout = WBout.Sheets("sheet1")
WSout.Range("A1").End(xlDown).Offset(1, 0).CopyFromRecordset qryPP1
qryPP1.Close
dbPP1.Close
WBout.Save
WBout.Close
MsgBox "Data copied. Thank you."
Set WBout = Nothing
Set WSout = Nothing
Set dbPP1 = Nothing
Set qryPP1 = Nothing
Set ResTemp1 = Nothing
End Sub
EDIT: Code for getting field value directly into variable
Dim dbPP As DAO.Database
Dim ResTemp As DAO.Recordset
Dim z As Long
Dim SelectLast As String
SelectLast = "SELECT Max(Table1.ID) AS MaxOfID FROM Table1"
'Debug.Print SelectLast
Set dbPP = OpenDatabase("C:\filepath\Database11.mdb")
Set ResTemp = dbPP.OpenRecordset(SelectLast)
z = ResTemp(0) 'specifying it's array location (I think) - there is only one
'item in this result, so it will always be (0)
AftUpD = z
'Debug.Print AftUpD
ResTemp.Close
dbPP.Close
Set dbPP = Nothing
Set ResTemp = Nothing
'Set SelectionLast = Nothing
'z = Nothing
End Sub
What you would do is set up and save a query that gets the value for you first. Call it MaxID
e.g
SELECT Max(ID) as result FROM Your_Table_Name
Then, in your VBA code, set your variable to that
eg.
Dim IDresult As Integer
IDresult = DLookup("[result]", "MaxID")
MsgBox(IDresult)