dictionary.exists(key) ADDS the key - vba

I am going crazy with VBA dictionaries, as the Exists() method makes no sense.
I thought you can use the dict.Exists(key) method to check if a key is in the dictionary without further actions. The problem is that when checking it, the key is automatically added into the dictionary. It really makes no sense!
Here's my code. Am I doing something wrong?
Function getContracts(wb As Workbook) As Dictionary
Dim cData As Variant, fromTo(1 To 2) As Variant
Dim contracts As New Dictionary, ctrDates As New Collection
Dim positions As New Dictionary, p As Long, r As Long
Dim dataSh As String, i As Long
dataSh = "Export"
cData = wb.Worksheets(dataSh).UsedRange
For i = LBound(cData) To UBound(cData)
fromTo(1) = cData(i, 1)
fromTo(2) = cData(i, 2)
Set ctrDates = Nothing
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not contracts.Exists(cData(i, 3)) Then ' Here it detects correctly that the key doesn't exist
ctrDates.Add fromTo
contracts.Add cData(i, 3), ctrDates ' And here it fails because the key just got added by .Exists()
Else
Set ctrDates = contracts(cData(i, 3))
ctrDates.Add fromTo
contracts(cData(i, 3)) = ctrDates
End If
Else
Debug.Print "Not a valid date in line " & i
End If
Next i
End Function

You can shorten your code to
For i = LBound(cData) To UBound(cData)
fromTo(1) = cData(i, 1)
fromTo(2) = cData(i, 2)
Set ctrDates = Nothing
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not IsEmpty(contracts(cData(i, 3))) Then Set ctrDates = contracts(cData(i, 3))
ctrDates.Add fromTo
Set contracts(cData(i, 3)) = ctrDates
Else
Debug.Print "Not a valid date in line " & i
End If
Next i
If one changes a value at a key it will automatically add the key if it does not exist.
Further reading on dictionaries
PS: This might also circumvent the strange behaviour described in the comments as you do not use the exist method. But on the other hand I have never experienced such a strange behaviour when using dictionaries

Collections of Date Pairs in a Dictionary
A reference to the Microsoft Scripting Runtime library is necessary for this to work.
Option Explicit
Sub GetContractsTEST()
Const dName As String = "Export"
Dim wb As Workbook: Set wb = ThisWorkbook
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
Dim Contracts As Scripting.Dictionary: Set Contracts = GetContracts(dws)
If Contracts Is Nothing Then Exit Sub
Dim Key As Variant, Item As Variant
For Each Key In Contracts.Keys
Debug.Print Key
For Each Item In Contracts(Key)
Debug.Print Item(1), Item(2)
Next Item
Next Key
End Sub
Function GetContracts(ByVal ws As Worksheet) As Scripting.Dictionary
Const ProcName As String = "GetContracts"
On Error GoTo ClearError
Dim cData As Variant: cData = ws.UsedRange.Value
Dim fromTo(1 To 2) As Variant
Dim Contracts As New Scripting.Dictionary
Contracts.CompareMode = TextCompare
Dim r As Long
For r = LBound(cData) To UBound(cData)
fromTo(1) = cData(r, 1)
fromTo(2) = cData(r, 2)
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not Contracts.Exists(cData(r, 3)) Then
Set Contracts(cData(r, 3)) = New Collection
End If
Contracts(cData(r, 3)).Add fromTo
Else
Debug.Print "Not a valid date in line " & r
End If
Next r
Set GetContracts = Contracts
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function

Possible Solution:
I had the same issue, this tends to happen when the compare more has not been set. I have not dug any deeper into this as the issue cannot always be replicated and the documentation around .Exists() and .CompareMode isn't that thorough source.
(as everyone has said you should enable the Microsoft Scripting Runtime reference for early binding)
When creating a new dictionary set its .CompareMode to vbBinaryCompare this will set a more strict compare mode and also in my case fixes the .Exists() bug. Do note that you can only set .CompareMode on an empty dictionary
Dim NewDictionary As New Scripting.Dictionary
NewDictionary.CompareMode = vbBinaryCompare
If NewDictionary.Exists(key) Then
'do things
End If

Related

Object or With Variable Not Set

Option Explicit
Public Sub consolidateList()
DeleteTableRows (ThisWorkbook.Worksheets("Master").ListObjects("MasterSheet"))
FillTableRows
End Sub
Private Sub FillTableRows()
'set up worksheet objects
Dim wkSheet As Worksheet
Dim wkBook As Workbook
Dim wkBookPath As String
Set wkBook = ThisWorkbook
wkBookPath = wkBook.Path
Set wkSheet = wkBook.Worksheets("Master")
'set up file system objects
Dim oFile As Object
Dim oFSO As Object
Dim oFolder As Object
Dim oFiles As Object
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFolder = oFSO.GetFolder(wkBookPath)
Set oFiles = oFolder.Files
'set up loop
Dim checkBook As Excel.Workbook
Dim reportDict As Dictionary
Application.ScreenUpdating = False
'initial coordinates
Dim startRow As Long
Dim startColumn As Long
startColumn = 3
Dim i As Long 'tracks within the row of the sheet where information is being pulled from
Dim k As Long 'tracks the row where data is output on
Dim j As Long 'tracks within the row of the sheet where the data is output on
Dim Key As Variant
j = 1
k = wkSheet.Range("a65536").End(xlUp).Row + 1
Dim l As Long
'look t Set checkBook = Workbooks.Open(oFile.Path)hrough folder and then save it to temp memory
On Error GoTo debuger
For Each oFile In oFiles
startRow = 8
'is it not the master sheet? check for duplicate entries
'oFile.name is the name of the file being scanned
'is it an excel file?
If Mid(oFile.Name, Len(oFile.Name) - 3, 4) = ".xls" Or Mid(oFile.Name, Len(oFile.Name) - 3, 4) = ".xlsx" Then
Set checkBook = Workbooks.Open(oFile.Path)
For l = startRow To 600
If Not (IsEmpty(Cells(startRow, startColumn))) Then
'if it is, time do some calculations
Set reportDict = New Dictionary
'add items of the payment
For i = 0 To 33
If Not IsEmpty(Cells(startRow, startColumn + i)) Then
reportDict.Add Cells(4, startColumn + i), Cells(startRow, startColumn + i)
End If
Next i
For i = startRow To 0 Step -1
If Not IsEmpty(Cells(i, startColumn - 1)) Then
reportDict.Add "Consumer Name", Cells(i, startColumn - 1)
Exit For
End If
Next i
'key is added
For Each Key In reportDict
'wkSheet.Cells(k, j) = reportDict.Item(Key)
Dim myInsert As Variant
Set myInsert = reportDict.Item(Key)
MsgBox (myInsert)
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
j = j + 1
Next Key
wkSheet.Cells(k, j) = wkSheet.Cells(k, 9) / 4
wkSheet.Cells(k, j + 1) = oFile.Name
'
k = k + 1
' Set reportDict = Nothing
j = 1
Else
l = l + 1
End If
startRow = startRow + 1
Next l
checkBook.Close
End If
' Exit For
Next oFile
Exit Sub
debuger:
MsgBox ("Error on: " & Err.Source & " in file " & oFile.Name & ", error is " & Err.Description)
End Sub
Sub DeleteTableRows(ByRef Table As ListObject)
On Error Resume Next
'~~> Clear Header Row `IF` it exists
Table.DataBodyRange.ClearContents
'~~> Delete all the other rows `IF `they exist
Table.DataBodyRange.Offset(1, 0).Resize(Table.DataBodyRange.Rows.count - 1, _
Table.DataBodyRange.Columns.count).Rows.Delete
On Error GoTo 0
End Sub
Greetings. The above code consolidates a folder of data that's held on excel spreadsheets into one master excel spreadsheet. The goal is to run a macro on Excel Spreadsheet named master on the worksheet named master which opens up other excel workbooks in the folder, takes the information, and puts it into a table in the worksheet "master". After which point, it becomes easy to see the information; so instead of it being held on hundreds of worksheets, the records are held on one worksheet.
The code uses a dictionary (reportDict) to temporarily store the information that is needed from the individual workbooks. The goal then is to take that information and place it in the master table at the bottom row, and then obviously add a new row either after a successful placement or before an attempted placement of data.
The code fails at the following line:
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
The failure description is "object or with variable not set" and so the issue is with the reportDict.Item(Key). My guess is that somehow VBA is not recognizing the dictionary item as stable, but I don't know how to correct this. Eventually the goal is to have code which does:
for each key in reportDict
- place the item which is mapped to the key at a unique row,column in the master table
- expand the table to accomodate necessary data
next key
Implicit default member calls are plaguing your code all over.
reportDict.Add Cells(4, startColumn + i), Cells(startRow, startColumn + i)
That's implicitly accessing Range.[_Default] off whatever worksheet is currently the ActiveSheet (did you mean that to be wkSheet.Cells?), to get the Key - since the Key parameter is a String, Range.[_Default] is implicitly coerced into one, and you have a string key. The actual dictionary item at that key though, isn't as lucky.
Here's a MCVE:
Public Sub Test()
Dim d As Dictionary
Set d = New Dictionary
d.Add "A1", Cells(1, 1)
Debug.Print IsObject(d("A1"))
End Sub
This procedure prints True to the debug pane (Ctrl+G): what you're storing in your dictionary isn't a bunch of string values, but a bunch of Range object references.
So when you do this:
Dim myInsert As Variant
Set myInsert = reportDict.Item(Key)
You might as well have declared myInsert As Range, for it is one.
This is where things get interesting:
MsgBox (myInsert)
Nevermind the superfluous parentheses that force-evaluate the object's default member and pass it ByVal to the MsgBox function - here you're implicitly converting Range.[_Default] into a String. That probably works.
So why is this failing then?
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
Normally, it wouldn't. VBA would happily do this:
wkSheet.ListObjects(1).DataBodyRange.Cells(2, 1).[_Default] = reportDict.Item(Key).[_Default]
And write the value in the DataBodyRange of the ListObject at the specified location.
I think that's all just red herring. Write explicit code: if you mean to store the Value of a cell, store the Value of a cell. If you mean to assign the Value of a cell, assign the Value of a cell.
I can't replicate error 91 with this setup.
This, however:
DeleteTableRows (ThisWorkbook.Worksheets("Master").ListObjects("MasterSheet"))
...is also force-evaluating a ListObject's default member - so DeleteTableRows isn't receiving a ListObject, it's getting a String that contains the name of the object you've just dereferenced... but DeleteTableRows takes a ListObject parameter, so there's no way that code can even get to run FillTableRows - it has to blow up with a type mismatch before DeleteTableRows even gets to enter. In fact, it's a compile-time error.
So this is a rather long answer that doesn't get to the reason for error 91 on that specific line (I can't reproduce it), but highlights a metric ton of serious problems with your code that very likely are related to this error you're getting. Hope it helps.
You need to iterate through the dictionary's Keys collection.
dim k as variant, myInsert As Variant
for each k in reportDict.keys
debug.print reportDict.Item(k)
next k

Split Word document into multiple parts and keep the text format

My Task
Split a Word document into multiple parts based on a delimiter while preserving the text format.
Where I am?
I tried a basic example with one document but without an array and it worked.
Option Explicit
Public Sub CopyWithFormat()
Dim docDestination As Word.Document
Dim docSource As Word.Document
Set docDestination = ActiveDocument
Set docSource = Documents.Add
docSource.Range.FormattedText = docDestination.Range.FormattedText
docSource.SaveAs "C:\Temp\" & "test.docx"
docSource.Close True
End Sub
Where do I stuck?
I put the whole document into an array and loop through it. Right not I get an error 424 - Object necessary on this line:
docDestination.Range.FormattedText = arrNotes(I).
I also tried these four variants without luck:
docDestination.Range.FormattedText = arrNotes(I).Range.FormattedText
docDestination.Range.FormattedText = arrNotes(I).FormattedText
docDestination.Range.FormattedText = arrNotes.Range.FormattedText(I)
docDestination.Range.FormattedText = arrNotes.FormattedText(I)
Could you please help and point me into the right direction on how to access the array properly?
My Code
Option Explicit
Sub SplitDocument(delim As String, strFilename As String)
Dim docSource As Word.Document
Dim docDestination As Word.Document
Dim I As Long
Dim X As Long
Dim Response As Integer
Dim arrNotes
Set docSource = ActiveDocument
arrNotes = Split(docSource.Range, delim)
For I = LBound(arrNotes) To UBound(arrNotes)
If Trim(arrNotes(I)) <> "" Then
X = X + 1
Set docDestination = Documents.Add
docDestination.Range.FormattedText = arrNotes(I) 'throws error 424
docDestination.SaveAs ThisDocument.Path & "\" & strFilename & Format(X, "0000")
docDestination.Close True
End If
Next I
End Sub
Sub test()
'delimiter & filename
SplitDocument "###", "Articles "
End Sub
Range.FormattedText returns a range object. The Split function, on the other hand, returns an array of strings which don't include formatting. Therefore your code should find the portion of the document you wish to copy and assign that part's FormattedText to a variable declared as Range. That variable could then be inserted into another document.
Private Sub CopyRange()
Dim Src As Range, Dest As Range
Dim Arr As Range
Set Src = Selection.Range
Set Arr = Src.FormattedText
Set Dest = ActiveDocument.Range(1, 1)
Dest.FormattedText = Arr
End Sub
The above code actually works. All you would need to do is to find a way to replace the Split function in your concept with a method that identifies ranges in the source document instead of strings.

Mass Export outlook letters with ConversationID

I need to extract all emails with standard outlook fields (from/to/subject/date, including category and most importantly, ConversationID) into Excel/csv.
I'm using MS Office 2016, no idea about version of Exchange server.
I tried several ways to do so on my mailbox:
1) exported data through standard outlook interface
2) exported data into MS access via standard export master
3) extracted data to MS PowerBI from MS Exchange directly
In all 3 cases I wasn't able to get ConversationID (PowerBI extract had some ID but it was not ConversationID)
Now I understand that it should be extracted through MAPI somehow, but I'm totally illiterate on this topic. Some searches advised to use special software for that, like Transcend, but it's obviously too expensive for one user :)
I also found VBA code to get data into Excel directly but it is not working for me:
http://www.tek-tips.com/viewthread.cfm?qid=1739523
Also found this nice explanation what is ConversationID - might be helpful for others intrested in topic:
https://www.meridiandiscovery.com/how-to/e-mail-conversation-index-metadata-computer-forensics/
Here is some sample code to get you started, I already had something similar to your ask. The code is commented, but feel free to ask questions :)
Option Explicit
Public Sub getEmails()
On Error GoTo errhand:
'Create outlook objects and select a folder
Dim outlook As Object: Set outlook = CreateObject("Outlook.Application")
Dim ns As Object: Set ns = outlook.GetNameSpace("MAPI")
'This option open a new window for you to select which folder you want to work with
Dim olFolder As Object: Set olFolder = ns.pickFolder
Dim emailCount As Long: emailCount = olFolder.Items.Count
Dim i As Long
Dim myArray As Variant
Dim item As Object
ReDim myArray(4, (emailCount - 1))
For i = 1 To emailCount
Set item = olFolder.Items(i)
'43 is olMailItem, only consider this type of email message
'I'm assuming you only want items with a conversationID
'Change the logic here to suite your specific needs
If item.Class = 43 And item.ConversationID <> vbNullString Then
'Using an array to write to excel in one go
myArray(0, i - 1) = item.Subject
myArray(1, i - 1) = item.SenderName
myArray(2, i - 1) = item.To
myArray(3, i - 1) = item.CreationTime
myArray(4, i - 1) = item.ConversationID
End If
Next
'Adding headers, then writing the data to excel
With ActiveSheet
.Range("A1") = "Subject"
.Range("B1") = "From"
.Range("C1") = "To"
.Range("D1") = "Created"
.Range("E1") = "ConversationID"
.Range("A2:E" & (emailCount + 1)).Value = TransposeArray(myArray)
End With
Exit Sub
errhand:
Debug.Print Err.Number, Err.Description
End Sub
'This function is used to bypass the limitation of -
'application.worksheetfunction.transpose
'If you have too much data added to an array you'll get a type mismatch
'Found here - http://bettersolutions.com/vba/arrays/transposing.htm
Public Function TransposeArray(myArray As Variant) As Variant
Dim X As Long
Dim Y As Long
Dim Xupper As Long: Xupper = UBound(myArray, 2)
Dim Yupper As Long: Yupper = UBound(myArray, 1)
Dim tempArray As Variant
ReDim tempArray(Xupper, Yupper)
For X = 0 To Xupper
For Y = 0 To Yupper
tempArray(X, Y) = myArray(Y, X)
Next
Next
TransposeArray = tempArray
End Function

Excel - User defined function getting is being called even when not active

I have a user defined function in excel. The function contains Application.Volatile at the top and it works great.
The problem I am experiencing now is that when I have the workbook open (lets call it workbook 1) together with another workbook (call it workbook 2), every time I make a change to workbook 2, all cells in workbook 1 that call this UDF gets a #VALUE! error.
Why is this happening?
I hope I provided enough info. If not please let me know.
Thanks
David
Hi guys, thanks for the help.
Sorry about that... here is the code:
Function getTotalReceived(valCell As Range) As Variant
Application.Volatile
If ActiveWorkbook.Name <> "SALES.xlsm" Then Return
Dim receivedWs As Worksheet, reportWs As Worksheet
Dim items As Range
Set reportWs = Worksheets("Report")
Set receivedWs = Worksheets("Received")
Dim myItem As String, index As Long
myItem = valCell.Value
Set items = receivedWs.Range("A:A")
index = Application.Match(myItem, items, 0)
If IsError(index) Then
Debug.Print ("Error: " & myItem)
Debug.Print (Err.Description)
GoTo QuitIt
End If
Dim lCol As Long, Qty As Double, mySumRange As Range
Set mySumRange = receivedWs.Range(index & ":" & index)
Qty = WorksheetFunction.Sum(mySumRange)
QuitIt:
getTotalReceived = Qty
End Function
Your problem is with the use of ActiveWorkbook,ActiveWorksheet or ActiveCell or other Active_____ objects in your UDF. Notice that Application.Volitile is an application-level property. Anytime you switch sheets, books, cells, charts, etc. the corresponding "active" object changes.
As an example of proper UDF coding practice I put together this short example:
Function appCallerTest() As String
Dim callerWorkbook As Workbook
Dim callerWorksheet As Worksheet
Dim callerRange As Range
Application.Volatile True
Set callerRange = Application.Caller
Set callerWorksheet = callerRange.Worksheet
Set callerWorkbook = callerWorksheet.Parent
appCallerTest = "This formula is in cell: " & callerRange.Address(False, False) & _
" in the sheet: " & callerWorksheet.Name & _
" in the workbook: " & callerWorkbook.Name
End Function
Function getTotalReceived(valCell As Range) As Variant
Application.Volatile
Dim index, v, Qty
v = valCell.Value
'do you really need this here?
If ActiveWorkbook.Name <> ThisWorkbook.Name Then Exit Function
If Len(v) > 0 Then
index = Application.Match(v, _
ThisWorkbook.Sheets("Report").Range("A:A"), 0)
If Not IsError(index) Then
Qty = Application.Sum(ThisWorkbook.Sheets("Received").Rows(index))
Else
Qty = "no match"
End If
Else
Qty = ""
End If
getTotalReceived = Qty
End Function
You actually have 2 errors in your function. The first was partially addressed by Mr. Mascaro - you need to use the Range reference that was passed to the function to resolve the Workbook that it is from. You can do this by drilling down through the Parent properties.
The second issue is that you are testing to see if Application.Match returned a valid index with the IsError function. This isn't doing what you think it's doing - IsError checks to see if another cell's function returned an error, not the previous line. In fact, if Application.Match raises an error, it is in your function so you have to handle it. I believe the error you need to trap is a type mismatch (error 13).
This should resolve both issues:
Function getTotalReceived(valCell As Range) As Variant
Application.Volatile
Dim book As Workbook
Set book = valCell.Parent.Parent
If book.Name <> "SALES.xlsm" Then Exit Function
Dim receivedWs As Worksheet, reportWs As Worksheet
Dim items As Range
Set reportWs = book.Worksheets("Report")
Set receivedWs = book.Worksheets("Received")
Dim myItem As String, index As Long
myItem = valCell.Value
Set items = receivedWs.Range("A:A")
On Error Resume Next
index = Application.Match(myItem, items, 0)
If Err.Number = 13 Then GoTo QuitIt
On Error GoTo 0
Dim lCol As Long, Qty As Double, mySumRange As Range
Set mySumRange = receivedWs.Range(index & ":" & index)
Qty = WorksheetFunction.Sum(mySumRange)
QuitIt:
getTotalReceived = Qty
End Function

Find cell based on a property

I need to find a cell into a worksheet but I'd like to avoid looking for a string.
The problem I have is that the worksheet will be edited by my client. If ever he decides to write the string I'm looking for before the good one, the app will crash.
Sub FindSpecificCell()
Dim ws As Worksheet
Set ws = ActiveWorkbook.Sheets("TP 1")
Dim myRange As Range
Dim rangeFinal As Range
Set monRange = ws.Range("A:AJ")
Set rangeFinal = myRange.Find("Description du test")
Debug.Print " "
Debug.Print "Looking for ""Description du test"" in TP 1 "
Debug.Print "column : " & rangeFinal.Column
Debug.Print "row : " & rangeFinal.Row
End Sub
Is there a way to insert a kind of property inside the cell in order to be sure that I'm working on the good one?
You can't associated properties with a specific cell directly, but you can use properties with the worksheet to store this information. I've used a couple methods like this before:
'Set the provided value of the custom property with the provided name in the provided sheet.
Private Sub SetCustomPropertyValue(InSheet As Worksheet, WithPropertyName As String, WithValue As Variant)
Dim objCP As CustomProperty
Dim bolFound As Boolean
bolFound = False 'preset.
For Each objCP In InSheet.CustomProperties
'if this property's name is the one whose value is sought...
If (StrComp(objCP.Name, WithPropertyName, vbTextCompare) = 0) Then
objCP.Value = WithValue
bolFound = True
Exit For
End If
Next
'if the property didn't already exist on the sheet, add it.
If (Not bolFound) Then Call InSheet.CustomProperties.Add(WithPropertyName, WithValue)
End Sub
'Return the value of the custom property with the provided name in the provided sheet.
Private Function GetCustomPropertyValue(InSheet As Worksheet, WithPropertyName As String) As Variant
Dim objCP As CustomProperty
GetCustomPropertyValue = Empty
For Each objCP In InSheet.CustomProperties
'if this property's name is the one whose value is sought...
If (StrComp(objCP.Name, WithPropertyName, vbTextCompare) = 0) Then
GetCustomPropertyValue = objCP.Value
Exit For
End If
Next
End Function
Then you can do something like this to write and read back values:
Sub test()
Dim strPropName As String
strPropName = "MyRange_" & Selection.Address
Dim strWhatIWantToStore As String
strWhatIWantToStore = "Here's what I want to store for this range"
Call SetCustomPropertyValue(ActiveSheet, strPropName, strWhatIWantToStore)
MsgBox GetCustomPropertyValue(ActiveSheet, strPropName)
End Sub