A. B. Duran
A. B. Duran

Reputation: 61

Returns dictionary (associated array) of sub-folders and the amount of email contained within each subfolder within main folder

I provided a solution to click on a folder and return how many items were contained within that folder.

Now, they've asked if that return can be kept, and broken down by sub-folders within the main folder clicked on.

Example:

INBOX has 3 sub-folders: Folder1, Folder2, Folder3

INBOX contains 3 emails of which one email comes from each sub-folder. Thus: INBOX Total: 3
Folder1 Total: 1
Folder2 Total: 1
Folder3 Total: 1

I created a loop that gets all subfolders contained within a main folder into an array.

My next thought was to convert that to a dictionary where I pre-set the items contained to 0. Then upon forming the dictionary using the loop I'm currently using to check if something is within the date range to also see what "folder" it belongs to and add one to the value I've pre-set to zero in the dictionary (associated array) as many times as there is a "match"

Below is what I've attempted:

Sub Countemailsperday()
Dim objOutlook As Object, objnSpace As Object, objFolder As MAPIFolder
Dim EmailCount As Integer
Dim ODate As String
Dim ODate2 As String
Dim dict As Dictionary
Set dict = New Dictionary
Dim coll As New Collection
Dim oDict As Object

Set oDict = CreateObject("Scripting.Dictionary")
' Dim Dict As Scripting.Dictionary

ODate = InputBox("Start Date? (format YYYY-MM-DD")
ODate2 = InputBox("End Date? (format YYYY-MM-DD")
Set objOutlook = CreateObject("Outlook.Application")
Set objnSpace = objOutlook.GetNamespace("MAPI")
On Error Resume Next
Set objFolder = Application.ActiveExplorer.CurrentFolder
If Err.Number <> 0 Then
    Err.Clear
    MsgBox "No such folder."
    Exit Sub
End If
EmailCount = objFolder.Items.Count
MsgBox "Number of emails in the folder: " & EmailCount, , "email count"
Dim ssitem As MailItem
Dim dateStr As String
Dim numholder As Integer
Dim myItems As Outlook.Items
'Dim dict As Object
Dim msg As String
Dim oParentFolder As MAPIFolder
Dim i As Integer
Dim iElement As Integer
Dim sArray() As String
Dim ArrayLen As Integer
Dim Subtractor As Integer
Dim str As String
ReDim sArray(0) As String
Set oParentFolder = objFolder
Set myItems = objFolder.Items
'Set Dict = New Scripting.Dictionary
If oParentFolder.Folders.Count Then
    For i = 1 To oParentFolder.Folders.Count
        If Trim(oParentFolder.Folders(i).Name) <> "" Then
            iElement = IIf(sArray(0) = "", 0, UBound(sArray) + 1)
            ReDim Preserve sArray(iElement) As String
            sArray(iElement) = oParentFolder.Folders(i).Name
        End If
    Next i
Else
    sArray(0) = oParentFolder.Name

End If

ArrayLen = UBound(sArray) - LBound(sArray) + 1
'MsgBox "thingy thing"
'MsgBox "thing" & sArray(1) ' This is how to iterate through the Dictionary
myItems.SetColumns ("ReceivedTime")
' Determine date of each message:
 ' MsgBox DateValue(ODate)
For Subtractor = 0 To (ArrayLen - 1)
    If oDict.Exists(sArray(Subtractor)) Then
        oDict(sArray(Subtractor)).Add

With dict
    For Subtractor = 0 To (ArrayLen - 1)
        If ArrayLen = 1 Then
            .Add Key = objFolder.Name, Item = 0
        Else
            If Subtractor = 0 Then
                .Add Key = CStr(sArray(Subtractor)), Item = 0
            Else

            End If
            str = CStr(sArray(Subtractor))
        End If
    Next Subtractor
End With
MsgBox str
If dict.Exists(str) Then
    Debug.Print (dict(str))
Else
    Debug.Print ("Not Found")
End If
MsgBox dict(str)
numholder = 0
'For Each
For Each myItem In myItems
    dateStr = GetDate(myItem.ReceivedTime)
     ' MsgBox DateValue(dateStr)
    If DateValue(dateStr) >= DateValue(ODate) And DateValue(dateStr) <= DateValue(ODate2) Then
        If Not dict.Exists(dateStr) Then
            dict(dateStr) = 0
            numholder = numholder
        End If
        dict(dateStr) = CLng(dict(dateStr)) + 1
        numholder = numholder + 1
    End If
Next myItem
' Output counts per day:
msg = ""
For Each o In dict.Keys
    msg = msg & o & ": " & dict(o) & " items" & vbCrLf
Next
If msg = "" Then
    MsgBox "There are no emails during this time range"
End If
If msg <> "" Then
    MsgBox "Number of emails during date range: " & numholder
    MsgBox msg
End If
Set objFolder = Nothing
Set objnSpace = Nothing
Set objOutlook = Nothing
End Sub

Function GetDate(dt As Date) As Date
GetDate = Year(dt) & "-" & Month(dt) & "-" & Day(dt)
End Function

I want to accomplish the following:

INBOX Total: 3
Folder1 Total: 1
Folder2 Total: 1
Folder3 Total: 1

as well as to handle the case where the folder that's clicked on contains no subfolders.

Upvotes: 0

Views: 147

Answers (1)

Tony Dallimore
Tony Dallimore

Reputation: 12413

I do not understand your code. You do things but do not explain how they contribute to your objective. There is date processing code which does not appear relevant. If one could write code and never need to look at it again, the lack of comments would be OK. But normally, after six, twelve or twenty months, a routine needs some attention. Perhaps there is an edge condition that is not handled correctly or perhaps the requirement has changed. Maintaining poorly documented code is a nightmare.

The code at the bottom of this answer is a simplified version of a routine I wrote some years ago. It does not do exactly what you appear to request and it does not use the technique you request. Perhaps my code will be acceptable. If not, I believe I have included enough explanations to allow you to amend my code to your requirements.

First an introduction to the techniques I have used. As peakpeak suggests, my code uses recursion. I have also used a collection instead of a dictionary. These techniques are not documented in the code because they are standard features of VBA and I do not document VBA within my code.

I do not use dictionaries. Collections provide all the functionality I have ever required. Dictionaries, as I understand it, have much in common with collections and have some functionality that collections lack. But more importantly for me, they lack some of the functionality of collections that I find essential.

You specify a collection so:

   Dim Coll As New Collection
or
   Dim Coll As Collection
   Set Coll = New Collection

Coll.Add X will create a new entry at the end of Coll containing X. You can add new entries in the middle of existing entries and you can remove existing entries but I do not use this functionality in the code below.

In Coll.Add X, X can be almost anything. It can be a simple value such as a string, a long or a Boolean. It can be an array or an instance of a class. It cannot be an instance of a user type. You cannot amend an entry within a collection. Should you need to amend an entry, you must remove the existing entry and add the amended version in the same position.

Since an entry within a collection can be anything, you need to be careful. If variable I is a Long:

I = I + Coll(5)

will give a runtime error if Coll(5) is a string or anything else that cannot be added to a Long.

If you add an array to a Collection, the syntax for reading it is, perhaps, not immediately obvious. Consider:

Coll.Add VBA.Array(Fldr.Name, Level, NumEmails)

Suppose the above Add has created the third entry in Coll; that is Coll(3). Then:

  • Coll(3)(0) is FldrName
  • Coll(3)(1) is Level
  • Coll(3)(2) is NumEmails

Note that I use VBA.Array instead of Array because Array is affected by the Option Base statement. By using VBA.Array I know the lower bound will always be zero.

On reflection, perhaps this syntax is not so strange. If I declare Dim Arr(0 To 5) As Long, I write Arr(0) to access element 0 of Arr. My Coll(3) is an array so I write Coll(3)(0) to access element 0 of Coll(3).

Recursion is where a routine calls itself. This technique is ideal for processing tree-like structures. There are techniques that are faster and not such heavy users of memory but none of these other techniques are so simple to use.

Suppose the folder hierarchy to be processed is:

FolderA
  FolderB
    FolderC
    FolderD
      FolderE
  FolderF 
  FolderG

My routine is NumEmailsByFolder and has parameters:

  1. Reference to top level folder
  2. Long Level
  3. Reference to collection FldrDtls

Level is not mentioned in your requirement but without it you cannot tell that FolderF is within FolderA. I tend to think of the top level as level 0 but you can use any value you find convenient.

The external routine creates an empty collection, which my routine call FldrDtls, and then calls:

NumEmailsByFolder([FolderA], 0, [FldrDtls]) 

Where [X] indicates a reference to object X.

NumEmailsByFolder counts the number of emails in FolderA, adds an entry to FldrDtls with the name “FolderA”, level 0, and the email count. It then calls itself for FolderB, FolderF and FolderG with level 1. This makes for pretty simple code. The secret of recursion is the sequence in which the interpreter actions all the different calls:

Calls in sequence executed                      Entry added to FldrDtls
NumEmailsByFolder([FolderA], 0, [FldrDtls])     FolderA     0  Count
NumEmailsByFolder([FolderB], 1, [FldrDtls])     FolderB     1  Count
NumEmailsByFolder([FolderC], 2, [FldrDtls])     FolderC     2  Count
NumEmailsByFolder([FolderD], 2, [FldrDtls])     FolderD     2  Count
NumEmailsByFolder([FolderE], 2, [FldrDtls])     FolderE     3  Count
NumEmailsByFolder([FolderF], 1, [FldrDtls])     FolderF     1  Count
NumEmailsByFolder([FolderG], 1, [FldrDtls])     FolderG     1  Count

The entries in FldrDtls are in the sequence wanted with subfolders following their parent folders. I have only four levels in my example hierarchy but the same code will handle 10 or 100 levels with all the difficult stuff handled by the interpreter.

Most people seem to find recursion difficult to understand at first; certainly I did when I was taught it at university many years ago. Then suddenly you see the light and you no longer understand why you found it difficult. I compare it with learning to drive a car. At the end of the first lesson you know you will never be able to turn the wheel, press one or more pedals, move the gearstick, look in the mirror and use the indicator while trying to avoid other road users all at the same time. But a few lessons later, you can do all that and more.

My routine is:

Sub NumEmailsByFolder(ByRef FldrPrnt As Folder, ByVal Level As Long, _
                      ByRef FldrDtls As Collection)

  ' Adds an entry to FldrDtls for FldrPrnt.
  ' Calls itself for each immediate subfolder of FldrPrnt.

  ' Each entry in FldrDtls is an zero-based array containing:
  '  * (0) Folder name
  '  * (1) Level of folder within hierarchy.  The level of the first (top)
  '        folder is as specified in the call.  Each level down is one more.
  '  * (2) Number of emails in folder. Note: this value does not include
  '        any emails in any subfolders

  ' The external routine that calls this routine will set the parameters:
  '  * FldrPrnt can be a Store or a MAPIFolder at any level with the
  '    folder hierarchy.
  '  * Level might typically be set to zero or one but the initial value
  '    is unimportant to this routine.
  '  * FldrDtls would normally be an empty collection.  This is not checked
  '    so FldrDtls may contain existing entries if this is convenient for
  '    the calling routine.

  ' On return to the external routine, the entries in FldrDtls might be:
  '    Inbox        0     10
  '    SubFldr1     1      5
  '    SubSubFldr1  2      3
  '    SubSubFldr2  2      4
  '    SubFldr2     1      9

  Dim ErrNum As Long
  Dim InxI As Long
  Dim InxS As Long
  Dim ItemsCrnt As Items
  Dim SubFldrsCrnt As Folders
  Dim NumMailItems As Long

  With FldrPrnt

    'Count MailItems, if any
    Err.Clear
    NumMailItems = 0

    ' In the past, I have had code crash when I attempted to access the
    ' Items of a folder but I have had no such error recently. This could
    ' be because I am now retired and my employer's Outlook installation
    ' had folders without items.  Alternatively, it could be because
    ' Outlook 2016 is more robust than Outlook 2003. I use On Error to
    ' ensure any such error does not crash my routine.

    On Error Resume Next
    Set ItemsCrnt = FldrPrnt.Items
    ErrNum = Err.Number
    On Error GoTo 0
    If ErrNum = 0 Then
      ' Only attempt to count MailItems within FldrPrnt if attempting to
      ' access its Items does not give an error.
      For InxI = 1 To ItemsCrnt.Count
        If ItemsCrnt(InxI).Class = olMail Then
          NumMailItems = NumMailItems + 1
        End If
      Next
    End If

    FldrDtls.Add VBA.Array(FldrPrnt.Name, Level, NumMailItems)

    Set SubFldrsCrnt = FldrPrnt.Folders

    ' See above for explanation of On Error

    ErrNum = Err.Number
    On Error GoTo 0
    If ErrNum = 0 Then
      ' Only attempt to count MailItems within FldrPrnt if attempting to
      ' access its Folders does not give an error.
      For InxS = 1 To SubFldrsCrnt.Count
        Call NumEmailsByFolder(SubFldrsCrnt(InxS), Level + 1, FldrDtls)
      Next
    End If

  End With

End Sub

I hope you agree that this code is simple considering what it can achieve. If I thought it was safe to remove the error handling code, the routine would be even smaller.

To demonstrate how to call this routine, add the following code:

Option Explicit
Sub TestNumEmailsByFolder()

  Dim FldrDtls As Collection
  Dim Fldr1 As Folder
  Dim Fldr2 As Folder
  Dim Fldr3 As Folder
  Dim FldrCrnt As Folder
  Dim FldrInx As Variant
  Dim InxF As Long

  Set Fldr1 = Session.Folders("[email protected]").Folders("Inbox").Folders("Test")
  Set Fldr2 = Session.Folders("[email protected]").Folders("Inbox")
  Set Fldr3 = Session.Folders("[email protected]")

  For Each FldrInx In Array(Fldr1, Fldr2, Fldr3)
    Set FldrCrnt = FldrInx
    Set FldrDtls = New Collection
    Call NumEmailsByFolder(FldrCrnt, 0, FldrDtls)
    Debug.Print "Emails"
    For InxF = 1 To FldrDtls.Count
      Debug.Print PadL(FldrDtls(InxF)(2), 5) & _
                  Space(1 + FldrDtls(InxF)(1) * 2) & FldrDtls(InxF)(0)
    Next
  Next

End Sub
Public Function PadL(ByVal Str As String, ByVal PadLen As Long, _
                     Optional ByVal PadChr As String = " ") As String

  ' Pad Str with leading PadChr to give a total length of PadLen
  ' If the length of Str exceeds PadLen, Str will not be truncated

  '   Sep15 Coded
  ' 20Dec15 Added code so overlength strings are not truncated
  ' 10Jun16 Added PadChr so could pad with characters other than space

  If Len(Str) >= PadLen Then
    ' Do not truncate over length strings
    PadL = Str
  Else
    PadL = Right$(String(PadLen, PadChr) & Str, PadLen)
  End If

End Function

Amend the Set Fldr1, Set Fldr2 and Set Fldr3 statements to reference folders on your system. I have started with a folder at the bottom of the hierarchy then a folder in the middle and then a folder at the top. I suggest you pick a similar set of folders. Study the output to the Immediate Window and consider how the sequence of the list has been created.

Is this the routine you want?

It uses a Collection instead of a Dictionary? Does this matter? If my understanding of Dictionaries is correct, a Dictionary would be inappropriate.

You use an array and ReDim Preserve. A Collection is a good choice when you have no idea how many entries will be required. ReDim Preserve is an expensive command in terms of time and memory. The interpreter has to find a new block of memory big enough for the enlarged array. It has to copy values from the old array to the new and initialise the new elements. Finally, it has to release the old array for garbage collection. If I need the final result to be in an array then, with this type of problem, I normally build the list in a collection, size my array according to the size of the collection and then copy data from the collection to the array.

The count of emails against a folder does not include emails in its subfolders. This appears to be a requirement. You cannot amend an entry in a collection so, if this is a requirement, I would handle it as part of the conversion to an array.

Subfolders are not listed in alphabetic sequence. I have never investigated properly but I suspect subfolders are listed in the sequence created. If this is unsatisfactory, you will need a sort. There are several possible approaches. Given there will normally be a small number of subfolders per folder, I suspect the simplest approach will be the best. If you need something a lot more powerful, I have an implementation of Quick Sort that uses indices to avoid sorting the source list.

Upvotes: 0

Related Questions