Share via


How Long Now?

 

Duncan Mackenzie
Microsoft Corporation

July 1, 2004

Applies to:
   Microsoft Visual Basic .NET

Summary: Duncan Mackenzie describes how to calculate the difference between two dates in Visual Basic .NET, and builds an application that counts down to the release of Halo 2. (11 printed pages)

Download the source code for this article.

Introduction

In the past few days on GotDotNet's forums, I've seen the same question come up in at least 5 different ways:

"How do I figure out how many days, weeks, hours, or minutes there are between two dates?"

People have been pretty quick to answer each question with a snippet of source code, usually showing how to use the TimeSpan class or the DateDiff function, but I thought I'd try to discuss the matter in a more general fashion. In this article, I'm going to first quickly run down the ways you can determine the difference between two dates. Then I'll walk through the creation of a simple "countdown" application (see Figure 1) that you can run on your desktop to give you a running update of the time remaining until a specific event occurs.

Figure 1. Just can't wait for Halo 2 to come out? Torture yourself with this running countdown that shows you just how far away it is.

DateDiff vs. TimeSpan

Depending on what book you read, or who you ask for advice, you'll probably hear one of two ways to determine the difference between two dates: through the Microsoft .NET Framework TimeSpan structure, or through the Microsoft Visual Basic DateDiff function. Both methods are capable of determining the difference between two dates, but each has a couple of unique features. Despite their similar functions, the core difference between the two is that DateDiff is a function, so you need to call it every time you need to retrieve a value, whereas TimeSpan is a structure that is created once, and then you just work with its various members as needed. Let's run through some code samples using both methods to produce the number of days, hours, minutes, and seconds between the current date/time and the launch of Halo 2.

Dim Current, Halo2 As Date
Current = Date.Now
Halo2 = #11/9/2004#

'DateDiff
Console.WriteLine("DateDiff")
Console.WriteLine()

Console.WriteLine("{0} Days", _
    DateDiff(DateInterval.Day, Current, Halo2))
Console.WriteLine("{0} Hours", _
    DateDiff(DateInterval.Hour, Current, Halo2))
Console.WriteLine("{0} Minutes", _
    DateDiff(DateInterval.Minute, Current, Halo2))
Console.WriteLine("{0} Seconds", _
    DateDiff(DateInterval.Second, Current, Halo2))
Console.WriteLine()

'TimeSpan
Console.WriteLine("TimeSpan")
Console.WriteLine()

Dim difference As TimeSpan = Halo2.Subtract(Current)
Console.WriteLine("{0} Days", difference.TotalDays)
Console.WriteLine("{0} Hours", difference.TotalHours)
Console.WriteLine("{0} Minutes", difference.TotalMinutes)
Console.WriteLine("{0} Seconds", difference.TotalSeconds)
Console.WriteLine()

The output of the two different methods is nearly identical, except that the TimeSpan properties are returning Doubles, while DateDiff always returns Longs (Int64).

DateDiff

168 Days

4041 Hours

242494 Minutes

14549654 Seconds

TimeSpan (Total<interval>)

168.398780965921 Days

4041.57074318211 Hours

242494.244590927 Minutes

14549654.6754556 Seconds

The fractional values returned from the TimeSpan properties are more meaningful than they might seem at first glance. Each of these properties is returning the complete difference between the two dates, whereas the whole numbers returned by DateDiff only provide a value to within one interval (day, hour, and so on). Using only one of these properties from TimeSpan, it would be possible to figure out any other time interval (with varying degrees of precision, of course), although that calculation would seldom be required in practice.

What is really interesting, though, is that I've only been using one "set" of the properties available from the TimeSpan structure—the Total<interval> properties. These properties (TotalHours, TotalDays, TotalMinutes, and so on), return the complete number, including the fractional number of hours, days, or whatever interval you are returning. This is great when you are trying to do a computation, but not so great for displaying to an actual user. Thankfully, there are another set of properties on the TimeSpan structure that are more useful for displaying information: TimeSpan.Days, TimeSpan.Hours, and so on. Let's take a look at the results of using those properties with our same Halo2 date.

First, here is the code to print out the time between now and the Halo2 launch date:

'TimeSpan (not Totals)
Console.WriteLine("TimeSpan (<interval>)")
Console.WriteLine()

Console.WriteLine("{0} Days", difference.Days)
Console.WriteLine("{0} Hours", difference.Hours)
Console.WriteLine("{0} Minutes", difference.Minutes)
Console.WriteLine("{0} Seconds", difference.Seconds)

And here is the output produced by that code:

TimeSpan (<interval>)

168 Days
9 Hours
34 Minutes
14 Seconds

You can see that these properties produce much more readable output—and this is where the two possible date difference solutions diverge, as this type of output is currently impossible using the DateDiff function. In fact, the TimeSpan structure allows you to produce all of the elements of your time duration output using a single ToString() call. Like so:

Console.WriteLine(difference.ToString)

produces

167.12:07:00.1922416

DateDiff has some unique functionality as well (just to confuse the issue of which option you should use for your calculations), including the ability to calculate the weeks or quarters between two dates. Depending on the type of system you are building, that type of calculation may be essential, and although you can certainly calculate it yourself, it is always nice to have your work done for you already.

Using TimeSpan to Create a Real Solution

For the purposes of creating a sample for this article, I am going to use only the TimeSpan method of calculating the difference between two dates. Combining that with a bit of GDI+ code, the end result should be sufficiently interesting to distract you from your real work and entice you into daydreaming about an upcoming event or release. I am sure you will be able to enhance the application quite a bit, but to start with I will keep it as simple as possible.

Starting from the Bottom Up

I am going to approach this application in a slightly odd way (at least for me), by creating the settings file first and then building up the application that will load that information in.

<?xml version="1.0" encoding="utf-8"?>
<howLongInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <EventName>Halo 2</EventName>
    <TargetDateTime>2004-11-09T08:00:00.0000000-08:00
    </TargetDateTime>
    <Pictures>
       <string>http://www.bungie.net/pic1.jpg</string>
       <string>http://www.bungie.net/pic2.jpg</string>
       <string>http://www.bungie.net/pic3.jpg</string>
    </Pictures>
    <ImageWidth>120</ImageWidth>
    <ImageHeight>90</ImageHeight>
    <ImageURL>http://www.bungie.net/Games/Halo2/</ImageURL>
</howLongInfo>

Of course, creating that file manually wouldn't be all that easy, so I wrote it out by serializing a bare bones settings class called howLongInfo. That class is nothing but simple properties, including a Specialized.StringCollection for the list of pictures, so I won't bother listing it in this article.

Setting Up a Form that Refreshes

Now that I know the set of information I will be working with, I'll start on the real application. First, I need to create a Microsoft Windows Forms application, and set up the automatically created form to be a sizable tool window. Next, I add a timer (with an interval of one second) that will be used to refresh the window and update the time display. Then I've got to switch to code and write the timer's Click event. The timer's Click event handler takes care of calculating the time difference between the current time (Now) and the event time (loaded from the settings file), and then invalidates the form so that the OnPaint routine will be called.

Dim currentTime As String
Dim formatString As String = _
    "{0} Days, {1} Hours, {2} Minutes and {3} Seconds"
Dim timerCount As Integer = 0
Dim switchPictureTimerCount As Integer = 20

Private Sub updateCurrentTime_Tick(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles updateCurrentTime.Tick

    Dim diff As TimeSpan
    diff = Me.howLong.TargetDateTime.Subtract(Date.Now)
    currentTime = String.Format(formatString, _
        diff.Days, diff.Hours, diff.Minutes, diff.Seconds)

    If timerCount < switchPictureTimerCount Then
        timerCount += 1
        Me.Invalidate(New Rectangle(CInt(Me.layoutRectangle.X), _
            CInt(Me.layoutRectangle.Y), _
            CInt(Me.layoutRectangle.Width), _
            CInt(Me.layoutRectangle.Height)))
    Else
        timerCount = 0
        LoadNextPicture()
        Me.Invalidate()
    End If
End Sub

You may have noticed that in the code above, only a portion of the Form is invalidated (Me.layoutRectangle), which is supposed to represent only the text portion of the display. CalculateLayoutRectangle determines the layout of the form, dividing it up into an area for text and an area for the event-related images to be displayed. The two rectangles calculated by this routine are then used throughout the rest of the form (including in the timer's click routine from above).

Private Const border As Integer = 10
Private layoutRectangle As RectangleF
Private graphicRectangle As Rectangle

Private Sub CalculateLayoutRectangle()

    If Me.howLong Is Nothing Then
        LoadNextPicture()
        With Me.graphicRectangle
            .X = border
            .Y = border
            .Width = Me.howLong.ImageWidth
            .Height = Me.howLong.ImageHeight
        End With
    End If

    With layoutRectangle
        .X = Me.howLong.ImageWidth + (border * 2)
        .Y = border
        .Width = Me.Width - .X - border
        .Height = Me.Height - .Y - border
    End With
End Sub

Me.howLong is a variable that represents information from the settings file and is used in several places in this form, including in the previous two routines. You will see how that information is loaded in the next section, as well as how the event-related graphics specified in that settings file are pulled down for use.

Loading Settings and Pictures

The LoadSettings and LoadNextPicture routines support the main timer/painting loop, allowing those code blocks to be made as simple as possible. LoadSettings uses serialization to load in the event information from the settings xml file (which is expected to be found in the same directory as the executable, named the same but with an .xml extension), including the image width and height, and the list of images to be displayed.

Dim howLong As howLongInfo

Private Sub LoadSettings()
    Dim myXMLSerializer As _
        New Xml.Serialization.XmlSerializer( _
        GetType(howLongInfo))
    Dim sr As New IO.StreamReader( _
            IO.Path.Combine(Application.StartupPath, _
            IO.Path.GetFileNameWithoutExtension( _
                Application.ExecutablePath) & ".xml"))
    Me.howLong = DirectCast(myXMLSerializer.Deserialize(sr), _
        howLongInfo)
End Sub

LoadNextPicture inspects the list of pictures retrieved from the settings xml file, randomly selects one item from the list, and then handles the retrieval (and caching) of that image from the web.

Private currentPicture As Image

Private Sub LoadNextPicture()
    If howLong Is Nothing Then
        LoadSettings()
    End If
    If Not howLong Is Nothing AndAlso _
        Me.howLong.Pictures.Count > 0 Then

        Dim max As Integer = Me.howLong.Pictures.Count - 1
        Dim selectedItem As Integer
        selectedItem = New Random().Next(0, max)

        Dim imagePath As String = Me.howLong.Pictures(selectedItem)
        Dim img As Bitmap
        Dim obj As Object
        obj = System.Web.HttpRuntime.Cache.Get(imagePath)
        If obj Is Nothing Then
            Dim imgStream As IO.Stream
            Dim imgRequest As Net.HttpWebRequest = _
                DirectCast(Net.WebRequest.Create(imagePath), _
                    Net.HttpWebRequest)
            Dim imgResponse As Net.HttpWebResponse = _
                DirectCast(imgRequest.GetResponse, _
                    Net.HttpWebResponse)
            If imgResponse.StatusCode = Net.HttpStatusCode.OK Then
                imgStream = imgResponse.GetResponseStream
                img = New Bitmap(imgStream)
                System.Web.HttpRuntime.Cache.Add(imagePath, img, _
                    Nothing, Now.AddHours(1), _
                    System.Web.Caching.Cache.NoSlidingExpiration, _
                    Web.Caching.CacheItemPriority.Normal, Nothing)
            End If
        Else
            img = DirectCast(obj, Bitmap)
        End If
        Me.currentPicture = img
    Else
        Me.currentPicture = Nothing
    End If
End Sub

With the picture loaded and the layout determined, it is time to move onto drawing the actual form contents out by overriding the Form's OnPaint routine.

Drawing Out Time and More in OnPaint

OnPaint uses the GDI+ routines in the System.Drawing namespace to render that time information onto the Form.

Protected Overrides Sub OnPaint( _
    ByVal e As System.Windows.Forms.PaintEventArgs)
    Dim g As Graphics
    g = e.Graphics

    g.Clear(Me.BackColor)
    Dim myFont As New Font("Arial", 24, _
        FontStyle.Bold, GraphicsUnit.Pixel)
    Dim sf As New StringFormat(StringFormatFlags.FitBlackBox)
    sf.Alignment = StringAlignment.Near
    Dim myTextColor As Color = Me.ForeColor
    g.DrawString(currentTime, myFont, _
        New SolidBrush(myTextColor), _
        layoutRectangle, sf)
    If currentPicture Is Nothing Then
        LoadNextPicture()
    End If

    If Not currentPicture Is Nothing Then
        g.DrawImageUnscaled(currentPicture, _
            border, border)
    End If
End Sub

All of that code produces the basic display that I want, with randomly changing images and text displayed and wrapped to fit correctly, but I needed to add a bit more code to make this sample at least slightly interactive.

Making Your Pictures Hot

I wanted the event-related images to be hot, so that clicking on them was possible and produced an effect. I decided to include an EventURL in the settings file, and to open the user's browser to that address when they clicked on the image. Towards that end, I overrode the OnClick routine and watched for mouse clicks within the area by checking clicks for containment within the graphicLayout rectangle.

Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
    Debug.WriteLine(Me.MousePosition.X)
    Dim mousePos As Point = Me.PointToClient(Me.MousePosition)
    If Me.graphicRectangle.Contains(mousePos) Then
        System.Diagnostics.Process.Start(Me.howLong.ImageURL)
    End If
End Sub

To produce the appropriate visual feedback, I also change the cursor appropriately when the mouse is over the clickable image area:

Protected Overrides Sub OnMouseMove( _
    ByVal e As System.Windows.Forms.MouseEventArgs)
    If Me.graphicRectangle.Contains(e.X, e.Y) Then
        Me.Cursor = Cursors.Hand
    Else
        Me.Cursor = Cursors.Default
    End If
End Sub

An interesting change might be to associate a URL with each individual image, versus a single URL for the entire event, or to add some form of tool tip to appear when the user hovers over an image.

Coding Challenge

At the end of some of my Coding4Fun columns, I will have a little coding challenge—something for you to work on if you are interested. For this article, the challenge is to create a .NET application that extends the concept I have detailed in this article to support multiple events, pull event information from a public Web site, or whatever type of enhancement you wish. Just post whatever you produce to GotDotNet and send me an e-mail message (at duncanma@microsoft.com) with an explanation of what you have done and why you feel it is interesting. You can send me your ideas whenever you like, but please just send me links to code samples, not the samples themselves (my inbox thanks you in advance).

Have your own ideas for hobbyist content? Let me know at duncanma@microsoft.com, and happy coding!

 

Coding4Fun

Duncan Mackenzie is the Microsoft Visual Basic .NET Content Strategist for MSDN during the day and a dedicated coder late at night. It has been suggested that he wouldn't be able to do any work at all without his Earl Grey tea, but let's hope we never have to find out. For more on Duncan, see his site.