Login


Formatting DateTime Ranges

By Jonathan Wood on 7/30/2014
Language: C#
Technology: .NET
Platform: Windows
License: CPOL
Views: 3,078
General Programming » Time & Date » General » Formatting DateTime Ranges

DateTimeRangeFormatter Demo Application

Download Sample Source Code Download Sample Source Code

Introduction

The .NET framework provides many options for formatting dates and times. However, it doesn't provide any intelligent, user-friendly way to print date/time ranges.

You can do something along the lines of:

string.Format("{0} - {1}", startDate, endDate);

But this simply prints all of startDate and all of endDate with a dash between them. If, for example, both the start and end dates are in the same year, it doesn't make sense to print the year twice. And if both the start and end dates have the same month, there's no reason to print the month twice. Here are some examples of how you might want to format date and time ranges:

July 30, 2014 (Starts and ends on the same day)
July 30, 2014 7:00 PM - 9:00 PM
July 30 - 31, 2014
Wednesday, July 30 - Thursday 31, 2014
Wednesday, July 30 8:00 PM - Thursday 31, 2014 7:00 AM

I recently had a need to display a date/time range like this so I wrote a small class to format them the way I wanted.

My Code to Format a DateTime Range

Listing 1 shows my DateTimeRangeFormatter class. This class will format a date/time range in a way that avoids printing unnecessary information, such as the year for both the start and ending dates when they are in the same year.

Listing 1: The DateTimeRangeFormatter Class

/// <summary>
/// Class to format a start and end date/time.
/// </summary>
public class DateTimeRangeFormatter
{
    /// <summary>
    /// Sets or gets whether full month names are used.
    /// </summary>
    public bool UseFullMonthName { get; set; }

    /// <summary>
    /// Sets or gets whether day, month format is used.
    /// Otherwise, month, day format is used.
    /// </summary>
    public bool UseDayMonthFormat { get; set; }

    /// <summary>
    /// Sets or gets whether the day of the week is displayed.
    /// </summary>
    public bool ShowDayOfWeek { get; set; }

    /// <summary>
    /// Sets or gets whether the time of day is displayed.
    /// </summary>
    public bool ShowTime { get; set; }

    /// <summary>
    /// If true, and the end time is 12:00 AM of a date that is after the
    /// start date, a day is subtracted from the end date so that midnight
    /// is not counted as another day.
    /// </summary>
    public bool TreatMidnightAsSameDay { get; set; }

    public DateTimeRangeFormatter()
    {
        // Set defaults
        UseFullMonthName = true;
        UseDayMonthFormat = false;
        ShowDayOfWeek = false;
        ShowTime = false;
        TreatMidnightAsSameDay = false;
    }

    /// <summary>
    /// Formats a date range using the current settings.
    /// </summary>
    /// <param name="startDate">Start date/time</param>
    /// <param name="endDate">End date/time</param>
    public string FormatDateRange(DateTime startDate, DateTime endDate)
    {
        // Ensure that start date is not after the end date
        if (endDate < startDate)
            endDate = startDate;

        // Special handling for events that end at midnight
        if (TreatMidnightAsSameDay &&
            endDate.TimeOfDay.Ticks == 0 &&
            startDate.Date < endDate.Date)
            endDate = endDate.AddDays(-1);

        string result;
        if (startDate.Date == endDate.Date)
        {
            result = FormatDateTime(startDate,
                DateTimeParts.Day | DateTimeParts.Month | DateTimeParts.Year);
            if (ShowTime)
                result += " {0} - {1}";
        }
        else
        {
            result = String.Format(ShowTime ?
                "{0} {{0}} - {1} {{1}}" :
                "{0} - {1}",
                FormatDateTime(startDate,
                    startDate.Year == endDate.Year ?
                    DateTimeParts.Day | DateTimeParts.Month :
                    DateTimeParts.Day | DateTimeParts.Month | DateTimeParts.Year),
                FormatDateTime(endDate,
                    startDate.Year == endDate.Year && startDate.Month == endDate.Month ?
                    DateTimeParts.Day | DateTimeParts.Year :
                    DateTimeParts.Day | DateTimeParts.Month | DateTimeParts.Year));
        }
        return (ShowTime) ?
            String.Format(result, startDate.ToShortTimeString(), endDate.ToShortTimeString()) :
            result;
    }

    /// <summary>
    /// Used by FormatDateTime to specify which date/time parts
    /// are to be displayed.
    /// </summary>
    [Flags]
    private enum DateTimeParts
    {
        Day = 0x01,
        Month = 0x02,
        Year = 0x04,
    }

    /// <summary>
    /// Formats a single date/time value using the current settings.
    /// </summary>
    private string FormatDateTime(DateTime dt, DateTimeParts parts)
    {
        StringBuilder sb = new StringBuilder();

        if (parts.HasFlag(DateTimeParts.Day) && ShowDayOfWeek)
            sb.Append(parts.HasFlag(DateTimeParts.Month) ? "dddd, " : "dddd ");
        if (parts.HasFlag(DateTimeParts.Day))
        {
            if (parts.HasFlag(DateTimeParts.Month))
            {
                if (UseDayMonthFormat)
                    sb.Append(UseFullMonthName ? "d MMMM" : "d MMM");
                else
                    sb.Append(UseFullMonthName ? "MMMM d" : "MMM d");
            }
            else sb.Append("%d");
        }
        if (parts.HasFlag(DateTimeParts.Year))
            sb.Append(UseDayMonthFormat ? " yyyy" : ", yyyy");
        return dt.ToString(sb.ToString());
    }
}

Customizing the Date/Time Range Format

The DateTimeRangeFormatter class offers a number of properties, which specify how the result should be formatted. Here are those properties along with a description of what they do.

UseFullMonthName: If true, the full month names are displayed. Otherwise, the month names are abbreviated.

UseDayMonthFormat: If true, dates are displayed with the day before the month. This is the preferred date format in some countries. If this property is false, the month is displayed before the day.

ShowDayOfWeek: If true, the full name of the day of the week is displayed in the result. This is in addition to the day number of the date.

ShowTime: If true, the starting and ending times are also displayed.

TreatMidnightAsSameDay: When working with this code, I ran across an issue. Let's say you have an event that starts at 9:00 PM and ends at midnight. Technically, 12:00 AM midnight is on a new day (the following day), but logically I don't view that as a multi-day event.

You can set the TreatMidnightAsSameDay to address this issue. If this property is true, it will display the end date as the day before the end date you specified, as long as the end date/time is at midnight and the end date/time is not on the same day as the start date.

Conclusion

This class is simple, but did require a bit of thought to produce the results I wanted, and also to provide as many options as it has for controlling the output.

Hopefull, someone else can benefit from this code as well.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software/website developer working out of the greater Salt Lake City area in Utah. I've developed many websites including Black Belt Coder, Insider Articles, and others.

I hike each week with my dogs Suki and Sasha. You can see my hiking blog at Hiking Salt Lake.