Login


Creating a User-Friendly TimeSpan String

By Jonathan Wood on 10/8/2012 (Updated on 10/9/2012)
Language: C#
Technology: .NET
Platform: Windows
License: CPOL
Views: 15,960
General Programming » Time & Date » General » Creating a User-Friendly TimeSpan String

Introduction

The .NET TimeSpan class is great for keeping track of the difference between two DateTime instances. However, while there are many options options for formatting a TimeSpan value as a string, I didn't find any that were very user friendly. For example, the default format for a TimeSpan might look something like "1099.05:05:59.9989999". Other format options are available, but I found them all to be about equally non user friendly.

I recently had the need to display a TimeSpan in a more user-friendly format, such as "4 days, 1 hour and 27 minutes." So I wrote up a quick extension method to produce such a string and decided to share it here.

A User-"Friendlier" String

Listing 1 shows my extension method, which adds a ToUserFriendlyString() method to each instance of TimeSpan. This method produces a more user-friendly description of the TimeSpan value.

The code is fairly straight forward. It starts by producing a value description for each of years, months, days, hours, minutes and seconds. Value descriptions are only created for values that are non-zero; however, if all values are zero then the value description for seconds is included no matter what so that the resulting string is never empty.

Once these value descriptions are created, the method then combines them all into a single string. Each value is separated with a comma except for the last two, which are separated with the word "and".

Listing 1: The ToUserFriendlyString() Extension Method

static class TimeSpanHelper
{
    /// <summary>
    /// Constructs a user-friendly string for this TimeSpan instance.
    /// </summary>
    public static string ToUserFriendlyString(this TimeSpan span)
    {
        const int DaysInYear = 365;
        const int DaysInMonth = 30;

        // Get each non-zero value from TimeSpan component
        List<string> values = new List<string>();

        // Number of years
        int days = span.Days;
        if (days >= DaysInYear)
        {
            int years = (days / DaysInYear);
            values.Add(CreateValueString(years, "year"));
            days = (days % DaysInYear);
        }
        // Number of months
        if (days >= DaysInMonth)
        {
            int months = (days / DaysInMonth);
            values.Add(CreateValueString(months, "month"));
            days = (days % DaysInMonth);
        }
        // Number of days
        if (days >= 1)
            values.Add(CreateValueString(days, "day"));
        // Number of hours
        if (span.Hours >= 1)
            values.Add(CreateValueString(span.Hours, "hour"));
        // Number of minutes
        if (span.Minutes >= 1)
            values.Add(CreateValueString(span.Minutes, "minute"));
        // Number of seconds (include when 0 if no other components included)
        if (span.Seconds >= 1 || values.Count == 0)
            values.Add(CreateValueString(span.Seconds, "second"));

        // Combine values into string
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < values.Count; i++)
        {
            if (builder.Length > 0)
                builder.Append((i == (values.Count - 1)) ? " and " : ", ");
            builder.Append(values[i]);
        }
        // Return result
        return builder.ToString();
    }

    /// <summary>
    /// Constructs a string description of a time-span value.
    /// </summary>
    /// <param name="value">The value of this item</param>
    /// <param name="description">The name of this item (singular form)</param>
    private static string CreateValueString(int value, string description)
    {
        return String.Format("{0:#,##0} {1}",
            value, (value == 1) ? description : String.Format("{0}s", description));
    }
}

Using the Code

As an extension method, using the code is very easy. Listing 2 shows some sample code and Figure 1 shows the output from that code. Note that the example also writes the default TimeSpan format, for comparison, using the ToString() method.

Listing 2: A Simple Example Using the ToUserFriendlyString() Extension Method

DateTime end = DateTime.Now.AddYears(3).AddDays(4).AddHours(5).AddMinutes(6);
TimeSpan span = (end - DateTime.Now);
Console.WriteLine(span.ToString());
Console.WriteLine(span.ToUserFriendlyString());

Figure 1: Shows the Output from Listing 1

1099.05:05:59.9989999
3 years, 4 days, 5 hours, 5 minutes and 59 seconds

Notice that the string produced by ToUserFriendlyString() contains a small rounding error (5 minutes and 59 seconds instead of 6 minutes). This is related to TimeSpan and not my extension method. You can confirm this by inspecting the default TimeSpan string "1099.05:05:59.9989999". The "5:59" also appears there. And the trailing "9989999" is the second that wasn't quite a full second.

For my purposes, this was more than accurate enough. If this type of rounding causes a problem for you, you could try adding the line in Listing 3 to the top of my extension method. This line adds half a second to the TimeSpan value to help rounding to the nearest second.

Listing 3: Try Adding this Line at the Top of ToUserFriendlyString() if Rounding Issues are a Concern

span = span.Add(new TimeSpan(TimeSpan.TicksPerSecond / 2));

Conclusion

In addition to possible rounding errors as discussed above, there are some other limitations I should point out. The main one has to do with months. Since not every month has the same number of days, the code may not produce exact results when describing the number of months. My code uses 30 as the number of days in a month. You can easily change this by editing the value of the DaysInMonth constant at the top of the code. But other values are unlikely to work as well as 30.

You might have also noticed that my code doesn't display milliseconds. For me, this just wasn't needed, although you could easily modify the code to display milliseconds.

For other ways to display TimeSpan values, you might want to take a look at my article A Friendly DateTime Formatter. This article presents code to display a DateTime value according to its relationship to the current time. And it accomplishes this by first converting the DateTime value to a TimeSpan value.

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.