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