February 2010

Volume 25 Number 02

CLR Inside Out - Formatting and Parsing Time Intervals in the .NET Framework 4

By Ron Petrusha | February 2010

In the Microsoft .NET Framework 4, the TimeSpan structure has been enhanced by adding support for both formatting and parsing that is comparable to the formatting and parsing support for DateTime values. In this article, I’ll survey the new formatting and parsing features, as well as provide some helpful tips for working with TimeSpan values.

Formatting in the .NET Framework 3.5 and Earlier Versions

In the Microsoft .NET Framework 3.5 and earlier versions, the single formatting method for time intervals is the parameterless TimeSpan.ToString method. The exact format of the returned string depends on the TimeSpan value. At a minimum, it includes the hours, minutes and seconds components of a TimeSpan value. If it is non-zero, the day component is included as well. And if there is a fractional seconds component, all seven digits of the ticks component are included. The period (“.”) is used as the separator between days and hours and between seconds and fractional seconds.

Expanded Support for Formatting in the .NET Framework 4

While the default TimeSpan.ToString method behaves identically in the .NET Framework 4, there are now two additional overloads. The first has a single parameter, which can be either a standard or custom format string that defines the format of the result string. The second has two parameters: a standard or custom format string, and an IFormatProvider implementation representing the culture that supplies formatting information. This method, incidentally, provides the IFormattable implementation for the TimeSpan structure; it allows TimeSpan values to be used with methods, such as String.Format, that support composite formatting.

In addition to including standard and custom format strings and providing an IFormattable implementation, formatted strings can now be culture-sensitive. Two standard format strings, “g” (the general short format specifier) and “G” (the general long format specifier) use the formatting conventions of either the current culture or a specific culture in the result string. The example formats in Figure 1 provide an illustration by displaying the result string for a time interval formatted using the “G” format string and the en-US and fr-FR cultures.

Figure 1 Time Interval Formatted Using “G” Format String (VB)

Visual Basic
Imports System.Globalization

Module Example
   Public Sub Main()
      Dim interval As New TimeSpan(1, 12, 42, 30, 566)
      Dim cultures() As CultureInfo = { New CultureInfo("en-US"), 
                                        New CultureInfo("fr-FR") }
      For Each culture As CultureInfo In cultures
         Console.WriteLine("{0}: {1}", culture, interval.ToString(
            "G", culture))
      Next                                  
   End Sub
End Module

Figure 1 Time Interval Formatted Using “G” Format String (C#)

using System;
using System.Globalization;

public class Example
{
   public static void Main()
   {
      TimeSpan interval = new TimeSpan(1, 12, 42, 30, 566);
      CultureInfo[] cultures = { new CultureInfo("en-US"), 
                                 new CultureInfo(“"fr-FR") };
      foreach (CultureInfo culture in cultures)
         Console.WriteLine("{0}: {1}", culture, interval.ToString( _
            "G", culture));
   }
}

The example in Figure 1 displays the following output:

  en-US: 1:12:42:30.5660000
  fr-FR: 1:12:42:30,5660000

Parsing in the .NET Framework 3.5 and Earlier Versions

In the .NET Framework 3.5 and earlier versions, support for parsing time intervals is handled by the static System.TimeSpan.Parse and System.TimeSpan.TryParse methods, which support a limited number of invariant formats. The example in Figure 2 parses the string representation of a time interval in each format recognized by the method.

Figure 2 Parsing Time Interval String in Multiple Formats (VB)

Module Example
   Public Sub Main()
      Dim values() As String = {"12", "12.16:07", "12.16:07:32", _
                                "12.16:07:32.449", "12.16:07:32.4491522", 
_
                                "16:07", "16:07:32", "16:07:32.449" }
      
      For Each value In values
         Try
            Console.WriteLine("Converted {0} to {1}", _
                              value, TimeSpan.Parse(value))
         Catch e As OverflowException
            Console.WriteLine("Overflow: {0}", value)
         Catch e As FormatException
            Console.WriteLine("Bad Format: {0}", value)
         End Try
      Next
   End Sub

Figure 2 Parsing Time Interval String in Multiple Formats (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] values = { "12", "12.16:07", "12.16:07:32", 
                          "12.16:07:32.449", "12.16:07:32.4491522", 
                          "16:07", "16:07:32", "16:07:32.449" };
      
      foreach (var value in values)
         try {
            Console.WriteLine("Converted {0} to {1}", 
                              value, TimeSpan.Parse(value));}
         catch (OverflowException) {
            Console.WriteLine("Overflow: {0}", value); }
         catch (FormatException) {
            Console.WriteLine("Bad Format: {0}", value);
         }
   }
}

The example in Figure 2 displays the following output:

  Converted 12 to 12.00:00:00
  Converted 12.16:07 to 12.16:07:00
  Converted 12.16:07:32 to 12.16:07:32
  Converted 12.16:07:32.449 to 12.16:07:32.4490000
  Converted 12.16:07:32.4491522 to 12.16:07:32.4491522
  Converted 16:07 to 16:07:00
  Converted 16:07:32 to 16:07:32
  Converted 16:07:32.449 to 16:07:32.4490000

As the output shows, the method can parse a single integer, which it interprets as the number of days in a time interval (more about this later). Otherwise, it requires that the string to be parsed includes at least an hour and a minute value.

Expanded Support for Parsing in the .NET Framework 4

In the .NET Framework 4 and Silverlight 4, support for parsing the string representations of time intervals has been enhanced and is now comparable to support for parsing date and time strings. The TimeSpan structure now includes a new overload for the Parse and TryParse methods, as well as completely new ParseExact and TryParseExact methods, each of which has four overloads. These parsing methods support standard and custom format strings, and offer some support for culture-sensitive formatting. Two standard format strings (“g” and “G”) are culture-sensitive, while the remaining standard format strings (“c”, “t” and “T”) as well as all custom format strings are invariant. Support for parsing and formatting time intervals will be further enhanced in future releases of the .NET Framework.

The example in Figure 3 illustrates how you can use the ParseExact method to parse time interval data in the .NET Framework 4. It defines an array of seven custom format strings; if the string representation of the time interval to be parsed does not conform to one of these formats, the method fails and throws an exception.

Figure 3 Parsing Time Interval Data with ParseExact Method (VB)

Module modMain
   Public Sub Main()
      Dim formats() As String = { "hh", "%h", "h\:mm", "hh\:mm",
                                  "d\.hh\:mm\:ss", "fffff", "hhmm" }
      Dim values() As String = { "16", "1", "16:03", "1:12", 
                                 "1.13:34:15", "41237", "0609" }
      Dim interval As TimeSpan
      
      For Each value In values
         Try
            interval = TimeSpan.ParseExact(value, formats, Nothing)
            Console.WriteLine("Converted '{0}' to {1}", 
                              value, interval)
         Catch e As FormatException
            Console.WriteLine("Invalid format: {0}", value)
         Catch e As OverflowException
            Console.WriteLine("Overflow: {0}", value)
         Catch e As ArgumentNullException
            Console.WriteLine("No string to parse")
         End Try         
      Next
   End Sub
End Module

Figure 3 Parsing Time Interval Data with ParseExact Method (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] formats = { "hh", "%h", @"h\:mm", @"hh\:mm", 
                           @"d\.hh\:mm\:ss", "fffff", "hhmm" };
      string[] values = { "16", "1", "16:03", '1:12', 
                          "1.13:34:15", "41237", "0609" };
      TimeSpan interval;
      
      foreach (var value in values)
      {
         try {
            interval = TimeSpan.ParseExact(value, formats, null);
            Console.WriteLine("Converted '{0}' to {1}", value, 
                              interval); }
         catch (FormatException) {
            Console.WriteLine("Invalid format: {0}", value); }
         catch (OverflowException) {
            Console.WriteLine("Overflow: {0}", value); }
         catch (ArgumentNullException) {
            Console.WriteLine("No string to parse");
         }         
      }
   }
}

The example in Figure 3 displays the following output:

  Converted ‘16’ to 16:00:00
 Converted ‘1’ to 01:00:00
 Converted ‘16:03’ to 16:03:00
 Converted ‘1:12’ to 01:12:00
 Converted ‘1.13:34:15’ to 1.13:34:15
 Converted ‘41237’ to 00:00:00.4123700
 Converted ‘0609’ to 06:09:00

Instantiating a TimeSpan with a Single Numeric Value

Interestingly, if these same seven time interval strings were passed to the TimeSpan.Parse(String) method in any version of the .NET Framework, they would all parse successfully, but in four cases, they would return a different result. Calling TimeSpan.Parse(String) with these strings produces the following output:

  Converted ‘16’ to 16.00:00:00
  Converted ‘1’ to 1.00:00:00
  Converted ‘16:03’ to 16:03:00
  Converted ‘1:12’ to 01:12:00
  Converted ‘1.13:34:15’ to 1.13:34:15
  Converted ‘41237’ to 41237.00:00:00
  Converted ‘0609’ to 609.00:00:00

The major difference in the TimeSpan.Parse(String) and TimeSpan.ParseExact(String, String[], IFormatProvider) method calls lies in the handling of strings that represent integer values. The TimeSpan.Parse(String) method interprets them as days. The interpretation of integers by the TimeSpan.ParseExact(String, String[], IFormatProvider) method depends on the custom format strings supplied in the string array parameter. In this example, strings that have only one or two integer digits are interpreted as the number of hours, strings with four digits are interpreted as the number of hours and minutes, and strings that have five integer digits are interpreted as a fractional number of seconds.

In many cases, .NET Framework applications receive strings containing time interval data in an arbitrary format (such as integers representing a number of milliseconds, or integers representing a number of hours). In previous versions of the .NET Framework, it was necessary to manipulate this data so that it would be in an acceptable format before passing it to the TimeSpan.Parse method. In the .NET Framework 4, you can use custom format strings to define the interpretation of time interval strings that contain only integers, and preliminary manipulation of string data is not necessary. The example in Figure 4 illustrates this by providing different representations for integers that have from one to five digits.

Figure 4 Representations of Integers with 1 to 5 Digits (VB)

Module Example
   Public Sub Main()
      Dim formats() As String = { "%h", "hh", "fff", "ffff', 'fffff' }
      Dim values() As String = { "3", "17", "192", "3451", 
                                 "79123", "01233" }

      For Each value In values
         Dim interval As TimeSpan
         If TimeSpan.TryParseExact(value, formats, Nothing, interval) Then
            Console.WriteLine("Converted '{0}' to {1}",  
                              value, interval.ToString())
         Else
            Console.WriteLine("Unable to parse {0}.", value)
         End If       
      Next
   End Sub
End Module

Figure 4 Representations of Integers with 1 to 5 Digits (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] formats = { "%h", "hh", "fff", "ffff", "fffff" };
      string[] values = { "3", "17", "192", "3451", "79123", "01233" };

      foreach (var value in values)
      {
         TimeSpan interval;
         if (TimeSpan.TryParseExact(value, formats, null, out interval))
            Console.WriteLine("Converted '{0}' to {1}", 
                              value, interval.ToString());
         else
            Console.WriteLine("Unable to parse {0}.", value);    
      }
   }
}

The example in Figure 4 displays the following output:

  Converted ‘3’ to 03:00:00
  Converted ‘17’ to 17:00:00
  Converted ‘192’ to 00:00:00.1920000
  Converted ‘3451’ to 00:00:00.3451000
  Converted ‘79123’ to 00:00:00.7912300
  Converted ‘01233’ to 00:00:00.0123300

Handling OverflowExceptions When Parsing Time Intervals

These new TimeSpan parsing and formatting features introduced in the .NET Framework 4 retain one behavior that some customers have found inconvenient. For backward compatibility, the TimeSpan parsing methods throw an OverflowException under the following conditions:

  • If the value of the hours component exceeds 23.
  • If the value of the minutes component exceeds 59.
  • If the value of the seconds component exceeds 59.

There are a number of ways to handle this. Instead of calling the TimeSpan.Parse method, you could use the Int32.Parse method to convert the individual string components to integer values, which you can then pass to one of the TimeSpan class constructors. Unlike the TimeSpan parsing methods, the TimeSpan constructors do not throw an OverflowException if the hours, minutes or seconds value passed to the constructor is out of range.

This is an acceptable solution, although it does have one limitation: It requires that all strings be parsed and converted to integers before calling the TimeSpan constructor. If most of the data to be parsed does not overflow during the parsing operation, this solution involves unnecessary processing.

Another alternative is to try to parse the data, and then handle the OverflowException that is thrown when individual time interval components are out of range. Again, this is an acceptable solution, although unnecessary exception handling in an application can be expensive.

The best solution is to use the TimeSpan.TryParse method to initially parse the data, and then to manipulate the individual time interval components only if the method returns false. If the parse operation fails, you can use the String.Split method to separate the string representation of the time interval into its individual components, which you can then pass to the TimeSpan(Int32, Int32, Int32, Int32, Int32) constructor. The example in Figure 5 provides a simple implementation:

Figure 5 Handling Nonstandard Time Interval Strings (VB)

Module Example
   Public Sub Main()
      Dim values() As String = { "37:16:45.33", "0:128:16.324", 
                                 "120:08" }
      Dim interval As TimeSpan
      For Each value In values
         Try
            interval = ParseIntervalWithoutOverflow(value)
            Console.WriteLine("'{0}' --> {1}", value, interval)
         Catch e As FormatException
            Console.WriteLine("Unable to parse {0}.", value)
         End Try   
      Next
   End Sub
   
   Private Function ParseIntervalWithoutOverflow(value As String) 
                    As TimeSpan   
      Dim interval As TimeSpan
      If Not TimeSpan.TryParse(value, interval) Then
         Try 
            ‘ Handle failure by breaking string into components.
            Dim components() As String = value.Split( {"."c, ":"c } )
            Dim offset As Integer = 0
            Dim days, hours, minutes, seconds, milliseconds As Integer
            ‘ Test whether days are present.
            If value.IndexOf(".") >= 0 AndAlso 
                     value.IndexOf(".") < value.IndexOf(":") Then 
               offset = 1
               days = Int32.Parse(components(0))
            End If
            ‘ Call TryParse to parse values so no exceptions result.
            hours = Int32.Parse(components(offset))
            minutes = Int32.Parse(components(offset + 1))
            If components.Length >= offset + 3 Then
               seconds = Int32.Parse(components(offset + 2))
            End If
            If components.Length >= offset + 4 Then
               milliseconds = Int32.Parse(components(offset + 3))                              
            End If
            ‘ Call constructor.
            interval = New TimeSpan(days, hours, minutes, 
                                    seconds, milliseconds)
         Catch e As FormatException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As ArgumentOutOfRangeException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As OverflowException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As ArgumentNullException
            Throw New ArgumentNullException("value cannot be null.",
                                            e)
         End Try      
      End If         
      Return interval
   End Function
End Module

Figure 5 Handling Nonstandard Time Interval Strings (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] values = { "37:16:45.33", "0:128:16.324", "120:08" };
      TimeSpan interval;
      foreach (var value in values)
      {
         try {
            interval = ParseIntervalWithoutOverflow(value);
            Console.WriteLine("'{0}' --> {1}", value, interval);
         }
         catch (FormatException) {  
            Console.WriteLine("Unable to parse {0}.", value);
         }
      }   
   }

   private static TimeSpan ParseIntervalWithoutOverflow(string value)
   {   
      TimeSpan interval;
      if (! TimeSpan.TryParse(value, out interval))
      {
         try {   
            // Handle failure by breaking string into components.
            string[] components = value.Split( 
                                  new Char[] {'.', ':' } );
   
            int offset = 0;
            int days = 0;
            int hours = 0;
            int minutes = 0;
            int seconds = 0;
            int milliseconds = 0;
            // Test whether days are present.
            if (value.IndexOf(".") >= 0 &&  
                     value.IndexOf(".") < value.IndexOf(":")) 
            {
               offset = 1;
               days = Int32.Parse(components[0]);
            }
            // Call TryParse to parse values so no exceptions result.
            hours = Int32.Parse(components[offset]);
            minutes = Int32.Parse(components[offset + 1]);
            if (components.Length >= offset + 3)
               seconds = Int32.Parse(components[offset + 2]);
   
            if (components.Length >= offset + 4)
               milliseconds = Int32.Parse(components[offset + 3]);                              
   
            // Call constructor.
            interval = new TimeSpan(days, hours, minutes, 
                                    seconds, milliseconds);
         }
         catch (FormatException e) {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }   
         catch (ArgumentOutOfRangeException e) {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }
         catch (OverflowException e)
         {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }   
         catch (ArgumentNullException e)
         {
            throw new ArgumentNullException("value cannot be null.",
                                            e);
         }      
      }         
      return interval;
   }   
}

As the following output shows, the example in Figure 5 successfully handles hour values that are greater than 23, as well as minute and second values that are greater than 59:

  ‘37:16:45.33’ --> 1.13:16:45.0330000
  ‘0:128:16.324’ --> 02:08:16.3240000
  ‘120:08’ --> 5.00:08:00

Application Compatibility

Paradoxically, enhanced formatting support for TimeSpan values in the .NET Framework 4 has broken some applications that formatted TimeSpan values in previous versions of the .NET Framework. The following code, for example, executes normally in the .NET Framework 3.5, but throws a FormatException in the .NET Framework 4:

string result = String.Format("{0:r}", new TimeSpan(4, 23, 17));

To format each argument in its parameter list, the String.Format method determines whether the object implements IFormattable. If it does, it calls the object’s IFormattable.ToString implementation. If it does not, it discards any format string supplied in the index item and calls the object’s parameterless ToString method.

In the .NET Framework 3.5 and earlier versions, TimeSpan does not implement IFormattable, nor does it support format strings. Therefore, the  “r” format string is ignored, and the parameterless TimeSpan.ToString method is called. In the .NET Framework 4, on the other hand, TimeSpan.ToString(String, IFormatProvider) is called and passed the unsupported format string, which causes the exception.

If possible, this code should be modified by calling the parameterless TimeSpan.ToString method, or by passing a valid format string to a formatting method. If that is not possible, however, a <TimeSpan_LegacyFormatMode> element can be added to the application’s configuration file so that it resembles the following:

<?xml version ="1.0"?>
<configuration>
   <runtime>
      <TimeSpan_LegacyFormatMode enabled="true"/>
   </runtime>
</configuration>

By setting its enabled attribute to true, you can ensure that TimeSpan uses legacy formatting behavior.


Ron Petrusha *is a programming writer on the .NET Framework Base Class Library team. He is also the author of numerous programming books and articles on Windows programming, Web programming and programming with VB.NET. *

Thanks to the following technical experts for reviewing this article: Melitta Andersen and Josh Free