Formatting Elapsed Time

VBA provides no support for elapsed times or for displaying formatted elapsed times. You’ll have to take steps on your own if you want to take two dates, find the difference between them, and display the difference formatted the way you want it. The function in this section, dhFormatInterval, in Listing 2.24 (certainly the longest procedure in this chapter), allows you to specify two dates and an optional format specifier and returns a string representing the difference. As the function is currently written, you can use any of the format specifiers listed in Table 2.9. You are invited, of course, to add your own specifiers to the list by modifying the source code. (For information on retrieving the time delimiter programmatically, see the section “Formatting Cumulative Times” later in this chapter.)

Table 2.9: Available Format Specifications for dhFormatInterval

Format Example
D H 3 Days 3 Hours
D H M 3 Days 2 Hours 46 Minutes
D H M S 3 Days 2 Hours 45 Minutes 45 Seconds
D H:MM 3 Days 2:46
D HH:MM 3 Days 02:46
D HH:MM:SS 3 Days 02:45:45
H M 74 Hours 46 Minutes
H:MM 74:46 (leading 0 on minutes, if necessary)
H:MM:SS 74:45:45
M S 4485 Minutes 45 Seconds
M:SS 4485:45 (leading 0 on seconds, if necessary)

Listing 2.24: Format the Interval between Two Dates

Function dhFormatInterval(dtmStart As Date, datend As Date, _
 Optional strFormat As String = "H:MM:SS") As String
    ' Return the difference between two times,
    ' formatted as specified in strFormat.
    Dim lngSeconds As Long
    Dim sngMinutes As Single
    Dim sngHours As Single
    Dim sngDays As Single
    Dim intSeconds As Integer
    Dim intMinutes As Integer
    Dim intHours As Integer
    Dim intRoundedHours As Integer
    Dim intRoundedMinutes As Integer
    Dim strDay As String
    Dim strHour As String
    Dim strMinute As String
    Dim strSecond As String
    Dim strOut As String
    Dim lngFullDays As Long
    Dim lngFullHours As Long
    Dim lngFullMinutes As Long
    Dim strDelim As String
    ' If you don't want to use the local delimiter,
    ' but a specific one, replace the next line with
    ' this:
    ' strDelim = ":"
    strDelim = GetTimeDelimiter()
    ' Calculate the full number of seconds in the interval.
    ' This limits the calculation to 2 billion seconds
    ' (68 years or so), but that's not too bad. Then calculate
    ' the difference in minutes, hours, and days, as well.
    lngSeconds = DateDiff("s", dtmStart, datend)
    sngMinutes = lngSeconds / 60
    sngHours = sngMinutes / 60
    sngDays = sngHours / 24
    ' Get the full hours and minutes, for later display.
    lngFullDays = Int(sngDays)
    lngFullHours = Int(sngHours)
    lngFullMinutes = Int(sngMinutes)

    ' Get the incremental amount of each unit.
    intHours = Int((sngDays - lngFullDays) * 24)
    intMinutes = Int((sngHours - lngFullHours) * 60)
    intSeconds = CInt((sngMinutes - lngFullMinutes) * 60)
    ' In some instances, time values must be rounded.
    ' The next two lines depend on the fact that a true statement
    ' has a value of -1 and a false statement has a value of 0.
    ' The code needs to add 1 to the value if the following
    ' expression is true, and 0 if not.
    intRoundedHours = intHours - (intMinutes > 30)
    intRoundedMinutes = intMinutes - (intSeconds > 30)
    strDay = "Days"
    strHour = "Hours"
    strMinute = "Minutes"
    strSecond = "Seconds"
    If lngFullDays = 1 Then strDay = "Day"
    Select Case strFormat
        Case "D H"
            If intRoundedHours = 1 Then strHour = "Hour"
            strOut = lngFullDays & " " & strDay & " " & _
             intRoundedHours & " " & strHour
        Case "D H M"
            If intHours = 1 Then strHour = "Hour"
            If intRoundedMinutes = 1 Then strMinute = "Minute"
            strOut = lngFullDays & " " & strDay & " " & _
             intHours & " " & strHour & " " & _
             intRoundedMinutes & " " & strMinute
        Case "D H M S"
            If intHours = 1 Then strHour = "Hour"
            If intMinutes = 1 Then strMinute = "Minute"
            If intSeconds = 1 Then strSecond = "Second"
            strOut = lngFullDays & " " & strDay & " " & _
             intHours & " " & strHour & " " & _
             intMinutes & " " & strMinute & " " & _
             intSeconds & " " & strSecond
        Case "D H:MM"      ' 3 Days 2:46"
            strOut = lngFullDays & " " & strDay & " " & _
             intHours & strDelim & Format(intRoundedMinutes, "00")
        Case "D HH:MM"     ' 3 Days 02:46"
            strOut = lngFullDays & " " & strDay & " " & _
             Format(intHours, "00") & strDelim & _
             Format(intRoundedMinutes, "00")
        Case "D HH:MM:SS"  ' 3 Days 02:45:45"
            strOut = lngFullDays & " " & strDay & " " & _
             Format(intHours, "00") & strDelim & _
             Format(intMinutes, "00") & strDelim & _
             Format(intSeconds, "00")
        Case "H M"         ' 74 Hours 46 Minutes"
            If lngFullHours = 1 Then strHour = "Hour"
            If intRoundedMinutes = 1 Then strMinute = "Minute"
            strOut = lngFullHours & " " & strHour & " " & _
             intRoundedMinutes & " " & strMinute
        Case "H:MM"        ' 74:46
            strOut = lngFullHours & strDelim & _
             Format(intRoundedMinutes, "00")
        Case "H:MM:SS"     ' 74:45:45
            strOut = lngFullHours & strDelim & _
             Format(intMinutes, "00") & strDelim & _
             Format(intSeconds, "00")
        Case "M S"         ' 4485 Minutes 45 Seconds
            If lngFullMinutes = 1 Then strMinute = "Minute"
            If intSeconds = 1 Then strSecond = "Second"
            strOut = lngFullMinutes & " " & strMinute & " " & _
             intSeconds & " " & strSecond
        Case "M:SS"        ' 4485:45
            strOut = lngFullMinutes & strDelim & _
             Format(intSeconds, "00")
        Case Else
            strOut = ""
    End Select
    dhFormatInterval = strOut
End Function

For example, to test out the function, you might write a test routine like the sample shown in Listing 2.25. This sample exercises all the predefined format specifiers.

Listing 2.25: Test Routine for dhFormatInterval

Sub TestInterval()
    Dim dtmStart As Date
    Dim dtmEnd As Date
    dtmStart = #1/1/97 12:00:00 PM#
    dtmEnd = #1/4/97 2:45:45 PM#
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D H")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D H M")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D H M S")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D H:MM")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D HH:MM")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "D HH:MM:SS")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "H M")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "H:MM")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "H:MM:SS")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "M S")
    Debug.Print dhFormatInterval(dtmStart, dtmEnd, "M:SS")
End Sub

Let’s face it: the dhFormatInterval function defines the term brute force. Although we attempted to make this routine as simple as possible, it requires several steps to provide all this flexibility.

How does it work? The function first calculates the difference between the two dates in seconds and then calculates the total number of days, hours, minutes, and seconds. In addition, it calculates the number of leftover hours, minutes, and seconds so it can display those, too. Finally, it also calculates rounded values for hours and minutes. That way, if you choose not to display seconds, the minutes value will be rounded accordingly. The same goes for hours: if you decide not to display minutes, the hours value must be rounded to the nearest full hour. Once the routine has those values, it uses a large Select Case statement to determine which type of output string to create and takes the steps to create the correct result.

Because dhFormatInterval calculates the difference between the two dates in seconds and places that value in a long integer, you’re limited to around 68 years between the two dates. Most likely that won’t be a terrible limitation, but you should be aware of it before using this function in a production application.

© 1997 by SYBEX Inc. All rights reserved.