В Microsoft .NET Framework 4 структура TimeSpan была расширена добавлением поддержки форматирования и разбора, сравнимой с аналогичной поддержкой для значений DateTime. В этой статье я дам обзор новых средств форматирования и разбора, а также некоторые полезные советы по работе со значениями TimeSpan.
Форматирование в .NET Framework 3.5 и более ранних версиях
В Microsoft .NET Framework 3.5 и более ранних версиях единственный способ форматирования временных интервалов — использование метода TimeSpan.ToString, не принимающего никаких параметров. Точный формат возвращаемой строки зависит от значения TimeSpan. Как минимум, он включает часы, минуты и секунды из значения TimeSpan. День также включается, если он отличен от 0. Если в значении TimeSpan присутствует компонент с долями секунды, то возвращаются все семь цифр тактового значения. В качестве разделителя между днями, часами, секундами и долями секунд используется точка.
Расширенная поддержка форматирования в .NET Framework 4
Хотя метод TimeSpan.ToString по умолчанию ведет себя идентично и в .NET Framework 4, у него есть две перегруженные версии. Первая принимает единственный параметр, который может быть стандартной или собственной форматирующей строкой, определяющей формат конечной строки. Вторая принимает два параметра: стандартную или собственную форматирующую строку и реализацию IFormatProvider, представляющую культуру, из которой берется информация о форматировании. Кстати, этот метод предоставляет реализацию IFormattable для структуры TimeSpan; это позволяет использовать значения TimeSpan с такими методами, как String.Format, которые поддерживают составное форматирование (composite formatting).
Кроме того форматирующие строки теперь могут быть зависимыми от культуры. Две стандартные форматирующие строки — «g» (универсальный спецификатор краткого формата) и «G» (универсальный спецификатор полного формата) — используют соглашения о формате либо текущей, либо определенной культуры в конечной строке. Примеры форматов на рис. 1 дают общее представление, выводя на экран конечную строку с временным интервалом, размеченную форматирующей строкой «G» для культур en-US и fr-FR.
Рис. 1. Временной интервал, размеченный форматирующей строкой «G» (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
Рис. 1. Временной интервал, размеченный форматирующей строкой «G» (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));
}
}
Код с рис. 1 дает следующий вывод:
en-US: 1:12:42:30.5660000
fr-FR: 1:12:42:30,5660000
Разбор в .NET Framework 3.5 и более ранних версиях
В .NET Framework 3.5 и более ранних версиях разбор временных интервалов обеспечивался статическими методами System.TimeSpan.Parse и System.TimeSpan.TryParse, которые поддерживали ограниченного количество инвариантных форматов. В примере на рис. 2 разбирается строковое представление временного интервала в каждом формате, распознаваемом методом.
Рис. 2. Разбор строки временного интервала в нескольких форматах (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
Рис. 2. Разбор строки временного интервала в нескольких форматах (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);
}
}
}
Код с рис. 2 дает следующий вывод:
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
Как показывает вывод, метод может разбирать единственное целое значение, которое он интерпретирует как число дней во временном интервале (подробности — чуть позже). В ином случае он требует, чтобы разбираемая строка включала хотя бы часы и минуты.
Расширенная поддержка разбора в .NET Framework 4
В .NET Framework 4 и Silverlight 4 поддержка разбора строковых представлений временных интервалов была расширена и теперь сравнима с поддержкой разбора строк дат и времени. Структура TimeSpan в настоящее время включает новую перегруженную версию методов Parse и TryParse, а также совершенно новые методы ParseExact и TryParseExact, у каждого из которых есть по четыре перегруженные версии. Эти методы разбора поддерживают стандартные и собственные форматирующие строки, а также обеспечивают некоторую поддержку форматирования в зависимости от культуры. Две стандартные форматирующие строки («g» и «G») зависят от культуры, тогда как остальные стандартные форматирующие строки («c», «t» и «T»), а также собственные форматирующие строки являются инвариантными. Поддержка разбора и форматирования временных интервалов будет еще больше расширена в следующих версиях .NET Framework.
Пример на рис. 3 иллюстрирует, как использовать метод ParseExact для разбора даты в .NET Framework 4. Он определяет массив из семи собственных форматирующих строк; если разбираемое строковое представление временного интервала не соответствует одному из этих форматов, метод завершается неудачей и генерирует исключение.
Рис. 3. Разбор даты с помощью метода ParseExact (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
Рис. 3. Разбор даты с помощью метода ParseExact (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");
}
}
}
}
Код с рис. 3 дает следующий вывод:
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
Создание экземпляра TimeSpan с единственным числовым значением
Любопытно, что, если те же семь строк временных интервалов передать в метод TimeSpan.Parse(String) в любой версии .NET Framework, все они были бы успешно разобраны, но в четырех случаях результаты получались бы разные. Вызов TimeSpan.Parse(String) с этими строками дает такой вывод:
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
Основное различие между вызовами методов TimeSpan.Parse(String) и TimeSpan.ParseExact(String, String[], IFormatProvider) заключается в обработке строк, представляющих целые значения. Первый интерпретирует их как дни. Интерпретация целых значений методом TimeSpan.ParseExact(String, String[], IFormatProvider) зависит от собственных форматирующих строк, передаваемых в параметре — строковом массиве. В данном примере строки с одним или двумя разрядами целого значения интерпретируются как часы, строки с четырьмя разрядами — как часы и минуты, а строки с пятью разрядами как дробное значение секунды.
Во многих случаях приложения .NET Framework принимают строки, содержащие данные о временных интервалах в произвольном формате (например, целые значения, представляющие число миллисекунд, или целые значения, представляющие количество часов). В предыдущих версиях .NET Framework такие данные нужно было предварительно обрабатывать, чтобы привести их к приемлемому формату до передачи в метод TimeSpan.Parse. В .NET Framework 4 вы можете использовать собственные форматирующие строки, определяющие интерпретацию строк временных интервалов, которые содержат только целые значения, и предварительная обработка строковых данных не обязательна. Код на рис. 4 иллюстрирует сказанное на примере различных представлений целых чисел с 1–5 разрядами.
Рис. 4. Представление целых с помощью 1–5 разрядов (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
Рис. 4. Представление целых с помощью 1–5 разрядов (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);
}
}
}
Код с рис. 4 дает следующий вывод:
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
Обработка исключений OverflowException при разборе временных интервалов
Новые средства разбора и форматирования TimeSpan, введенные в .NET Framework 4, сохраняют одно поведение, которое многие заказчики находят неудобным. Для обратной совместимости методы разбора TimeSpan генерируют OverflowException в следующих обстоятельствах:
- если значение компонента часов превышает 23;
- если значение компонента минут превышает 59;
- если значение компонента секунд превышает 59.
Обойти генерацию исключения можно разными способами. Вместо вызова метода TimeSpan.Parse вы могли бы использовать метод Int32.Parse для преобразования индивидуальных компонентов строки в целые значения, которые потом передали бы одному из конструкторов класса TimeSpan. В отличие от методов разбора TimeSpan конструкторы TimeSpan не генерируют исключение OverflowException, если значения часов, минут или секунд выходят за допустимый диапазон.
Это вполне приемлемое решение, хотя у него есть одно ограничение: перед вызовом конструктора TimeSpan нужно разбирать и преобразовывать в целые значения все строки. Если большая часть данных при операциях разбора не вызывает переполнения, это решение приводит к большому объему лишней обработки.
Другая альтернатива — попытаться разобрать данные, а затем обработать OverflowException, генерируемое, когда компоненты временного интервала выходят за допустимые диапазоны. Это тоже вполне приемлемое решение, хотя лишняя обработка исключений в приложении может оказаться дорогостоящей.
Лучшее решение — использовать метод TimeSpan.TryParse для начального разбора данных и манипулировать индивидуальными компонентами временного интервала, только если этот метод возвращает false. Если операция разбора заканчивается неудачей, вы можете применить метод String.Split, чтобы разбить строковое представление временного интервала на индивидуальные компоненты и затем передать их конструктору TimeSpan(Int32, Int32, Int32, Int32, Int32). Пример простой реализации этого способа показан на рис. 5.
Рис. 5. Обработка нестандартных строк временных интервалов (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
Рис. 5. Обработка нестандартных строк временных интервалов (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;
}
}
Как показано в следующем выводе, код с рис. 5 успешно обрабатывает значения часов, превышающие 23, а также значения минут и секунд, большие 59:
‘37:16:45.33’ --> 1.13:16:45.0330000
‘0:128:16.324’ --> 02:08:16.3240000
‘120:08’ --> 5.00:08:00
Совместимость приложений
Как ни парадоксально, но расширенная поддержка форматирования значений TimeSpan в .NET Framework 4 нарушила работу некоторых приложений, которые форматировали такие значения в предыдущих версиях .NET Framework. Следующий код, например, нормально выполняется в .NET Framework 3.5, но генерирует исключение FormatException в .NET Framework 4:
string result = String.Format("{0:r}", new TimeSpan(4, 23, 17));
Чтобы форматировать каждый аргумент в своем списке параметров, метод String.Format определяет, реализует ли данный объект интерфейс IFormattable. Если да, то вызывает его реализацию IFormattable.ToString. Нет — отбрасывает любую форматирующую строку в элементе индекса (index item) и вызывает не параметризованный метод ToString объекта.
В .NET Framework 3.5 и более ранних версиях TimeSpan ни реализовал IFormattable, ни поддерживал форматирующие строки. Поэтому форматирующая строка «r» игнорировалась, и вызывался метод TimeSpan.ToString без параметров. Но в .NET Framework 4 вызывается TimeSpan.ToString(String, IFormatProvider) с передачей этой неподдерживаемой форматирующей строки, что приводит к генерации исключения.
Если есть возможность, такой код нужно модифицировать вызовом метода TimeSpan.ToString без параметров или передачей допустимой форматирующей строки в метод форматирования. Но, если это неприемлемо, то можно добавить элемент <TimeSpan_LegacyFormatMode> в конфигурационный файл приложения примерно так:
<?xml version ="1.0"?>
<configuration>
<runtime>
<TimeSpan_LegacyFormatMode enabled="true" />
</runtime>
</configuration>
Установив его атрибут enabled в true, вы сможете быть уверенным, что TimeSpan использует прежнюю (устаревшую) функциональность форматирования.