Edit Oct 2018: C# 6/VB 14 introduced interpolated strings which may or may not be simpler than the first code segment of my original answer. Thankfully, the syntax for interpolation is identical for both languages: a preceding $
.
C# 6
TimeSpan t = new TimeSpan(105, 56, 47);
Console.WriteLine($"{(int)t.TotalHours}h {t:mm}mn {t:ss}sec");
Visual Basic 14
dim t As New TimeSpan(105, 56, 47)
Console.WriteLine($"{CInt(Math.Truncate(t.TotalHours))}h {t:mm}mn {t:ss}sec")
Edit Nov 2021: The above answer only works for positive TimeSpan
s and negative ones less than or equal to -1
hour. If you have a negative TimeSpan
in the range (-1, 0]hr
, you'll need to manually insert the negative yourself. Note, this is also true of the original answer.
TimeSpan t = TimeSpan.FromSeconds(-30 * 60 - 10); // -(30mn 10 sec)
Console.WriteLine($"{(ts.Ticks < 0 && (int)ts.TotalHours == 0 ? "-" : "")}{(int)t.TotalHours}h {t:mm}mn {t:ss}sec");
Since this is cumbersome, I recommend creating a helper function.
string Neg(TimeSpan ts)
{
return ts.Ticks < 0 && (int)ts.TotalHours == 0 ? "-" : "";
}
TimeSpan t = TimeSpan.FromSeconds(-30 * 60 - 10); // -(30mn 10 sec)
Console.WriteLine($"{Neg(ts)}{(int)t.TotalHours}h {t:mm}mn {t:ss}sec");
I don't know VB well enough to write the equivalent version.
See a quick example of C# here including the ValueTuple
s feature introduced in C# 7. Alas, the only C#7 online compiler I could find runs .NET Core, which is super cumbersome for small examples, but rest assured it works exactly the same in a .NET Framework project.
Original Answer
Microsoft doesn't (currently) have a simple format string shortcut for this. The easiest options have already been shared.
C#
string.Format("{0}hr {1:mm}mn {1:ss}sec", (int)t.TotalHours, t);
VB
String.Format("{0}hr {1:mm}mn {1:ss}sec", _
CInt(Math.Truncate(t.TotalHours)), _
t)
However, an overly-thorough option is to implement your own ICustomFormatter
for TimeSpan
. I wouldn't recommend it unless you use this so often that it would save you time in the long run. However, there are times where you DO write a class where writing your own ICustomFormatter
is appropriate, so I wrote this one as an example.
/// <summary>
/// Custom string formatter for TimeSpan that allows easy retrieval of Total segments.
/// </summary>
/// <example>
/// TimeSpan myTimeSpan = new TimeSpan(27, 13, 5);
/// string.Format("{0:th,###}h {0:mm}m {0:ss}s", myTimeSpan) -> "27h 13m 05s"
/// string.Format("{0:TH}", myTimeSpan) -> "27.2180555555556"
///
/// NOTE: myTimeSpan.ToString("TH") does not work. See Remarks.
/// </example>
/// <remarks>
/// Due to a quirk of .NET Framework (up through version 4.5.1),
/// <code>TimeSpan.ToString(format, new TimeSpanFormatter())</code> will not work; it will always call
/// TimeSpanFormat.FormatCustomized() which takes a DateTimeFormatInfo rather than an
/// IFormatProvider/ICustomFormatter. DateTimeFormatInfo, unfortunately, is a sealed class.
/// </remarks>
public class TimeSpanFormatter : IFormatProvider, ICustomFormatter
{
/// <summary>
/// Used to create a wrapper format string with the specified format.
/// </summary>
private const string DefaultFormat = "{{0:{0}}}";
/// <remarks>
/// IFormatProvider.GetFormat implementation.
/// </remarks>
public object GetFormat(Type formatType)
{
// Determine whether custom formatting object is requested.
if (formatType == typeof(ICustomFormatter))
{
return this;
}
return null;
}
/// <summary>
/// Determines whether the specified format is looking for a total, and formats it accordingly.
/// If not, returns the default format for the given <para>format</para> of a TimeSpan.
/// </summary>
/// <returns>
/// The formatted string for the given TimeSpan.
/// </returns>
/// <remarks>
/// ICustomFormatter.Format implementation.
/// </remarks>
public string Format(string format, object arg, IFormatProvider formatProvider)
{
// only apply our format if there is a format and if the argument is a TimeSpan
if (string.IsNullOrWhiteSpace(format) ||
formatProvider != this || // this should always be true, but just in case...
!(arg is TimeSpan) ||
arg == null)
{
// return the default for whatever our format and argument are
return GetDefault(format, arg);
}
TimeSpan span = (TimeSpan)arg;
string[] formatSegments = format.Split(new char[] { ',' }, 2);
string tsFormat = formatSegments[0];
// Get inner formatting which will be applied to the int or double value of the requested total.
// Default number format is just to return the number plainly.
string numberFormat = "{0}";
if (formatSegments.Length > 1)
{
numberFormat = string.Format(DefaultFormat, formatSegments[1]);
}
// We only handle two-character formats, and only when those characters' capitalization match
// (e.g. 'TH' and 'th', but not 'tH'). Feel free to change this to suit your needs.
if (tsFormat.Length != 2 ||
char.IsUpper(tsFormat[0]) != char.IsUpper(tsFormat[1]))
{
return GetDefault(format, arg);
}
// get the specified time segment from the TimeSpan as a double
double valAsDouble;
switch (char.ToLower(tsFormat[1]))
{
case 'd':
valAsDouble = span.TotalDays;
break;
case 'h':
valAsDouble = span.TotalHours;
break;
case 'm':
valAsDouble = span.TotalMinutes;
break;
case 's':
valAsDouble = span.TotalSeconds;
break;
case 'f':
valAsDouble = span.TotalMilliseconds;
break;
default:
return GetDefault(format, arg);
}
// figure out if we want a double or an integer
switch (tsFormat[0])
{
case 'T':
// format Total as double
return string.Format(numberFormat, valAsDouble);
case 't':
// format Total as int (rounded down)
return string.Format(numberFormat, (int)valAsDouble);
default:
return GetDefault(format, arg);
}
}
/// <summary>
/// Returns the formatted value when we don't know what to do with their specified format.
/// </summary>
private string GetDefault(string format, object arg)
{
return string.Format(string.Format(DefaultFormat, format), arg);
}
}
Note, as in the remarks in the code, TimeSpan.ToString(format, myTimeSpanFormatter)
will not work due to a quirk of the .NET Framework, so you'll always have to use string.Format(format, myTimeSpanFormatter) to use this class. See How to create and use a custom IFormatProvider for DateTime?.
EDIT:
If you really, and I mean really, want this to work with TimeSpan.ToString(string, TimeSpanFormatter)
, you can add the following to the above TimeSpanFormatter
class:
/// <remarks>
/// Update this as needed.
/// </remarks>
internal static string[] GetRecognizedFormats()
{
return new string[] { "td", "th", "tm", "ts", "tf", "TD", "TH", "TM", "TS", "TF" };
}
And add the following class somewhere in the same namespace:
public static class TimeSpanFormatterExtensions
{
private static readonly string CustomFormatsRegex = string.Format(@"([^\\])?({0})(?:,{{([^(\\}})]+)}})?", string.Join("|", TimeSpanFormatter.GetRecognizedFormats()));
public static string ToString(this TimeSpan timeSpan, string format, ICustomFormatter formatter)
{
if (formatter == null)
{
throw new ArgumentNullException();
}
TimeSpanFormatter tsFormatter = (TimeSpanFormatter)formatter;
format = Regex.Replace(format, CustomFormatsRegex, new MatchEvaluator(m => MatchReplacer(m, timeSpan, tsFormatter)));
return timeSpan.ToString(format);
}
private static string MatchReplacer(Match m, TimeSpan timeSpan, TimeSpanFormatter formatter)
{
// the matched non-'\' char before the stuff we actually care about
string firstChar = m.Groups[1].Success ? m.Groups[1].Value : string.Empty;
string input;
if (m.Groups[3].Success)
{
// has additional formatting
input = string.Format("{0},{1}", m.Groups[2].Value, m.Groups[3].Value);
}
else
{
input = m.Groups[2].Value;
}
string replacement = formatter.Format(input, timeSpan, formatter);
if (string.IsNullOrEmpty(replacement))
{
return firstChar;
}
return string.Format("{0}\\{1}", firstChar, string.Join("\\", replacement.ToCharArray()));
}
}
After this, you may use
ICustomFormatter formatter = new TimeSpanFormatter();
string myStr = myTimeSpan.ToString(@"TH,{000.00}h\:tm\m\:ss\s", formatter);
where {000.00}
is however you want the TotalHours int or double to be formatted. Note the enclosing braces, which should not be there in the string.Format() case. Also note, formatter
must be declared (or cast) as ICustomFormatter
rather than TimeSpanFormatter
.
Excessive? Yes. Awesome? Uhhh....