44

Say I convert some seconds into the TimeSpan object like this:

Dim sec = 1254234568
Dim t As TimeSpan = TimeSpan.FromSeconds(sec)

How do I format the TimeSpan object into a format like the following:

>105hr 56mn 47sec

Is there a built-in function or do I need to write a custom function?

Maxd
  • 445
  • 1
  • 4
  • 5

10 Answers10

70

Well, the simplest thing to do is to format this yourself, e.g.

return string.Format("{0}hr {1}mn {2}sec",
                     (int) span.TotalHours,
                     span.Minutes,
                     span.Seconds);

In VB:

Public Shared Function FormatTimeSpan(span As TimeSpan) As String
    Return String.Format("{0}hr {1}mn {2}sec", _
                         CInt(Math.Truncate(span.TotalHours)), _
                         span.Minutes, _
                         span.Seconds)
End Function

I don't know whether any of the TimeSpan formatting in .NET 4 would make this simpler.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    You may want to give VB code, though, as the code in the question looks like it :-) – Joey Aug 17 '10 at 17:37
  • 2
    Am I missing something? I get nervous when my answer disagrees with Jon Skeet :-) – Jason Williams Aug 17 '10 at 17:38
  • 3
    `((int) span.TotalMinutes) % 60` can be replaced by `span.Minutes`. The same thing with seconds. – Lasse Espeholt Aug 17 '10 at 17:43
  • Oops, yes, got carried away after the TotalHours - which *is* required. – Jon Skeet Aug 17 '10 at 17:48
  • HHow do I check if total hour > 1 then append "hrs" ortherwise, just append "hr" within the String.Format statement? – Maxd Aug 17 '10 at 18:02
  • @Maxd: Well, you could add an extra format parameter which ended up as "s" or "" depending on whether TotalHours was greater than 1. I don't have time to provide sample code right now I'm afraid. – Jon Skeet Aug 17 '10 at 18:05
  • @Joh Skeet I think, when you cast TotalHours to Integer, by default the TotalHours is rounded up. So, 34526 sec will be 10hr 35mn 26sec, which is not correct. 34526 sec = 9hr 35mn 26sec. Math.Floor(t.TotalHours) will do the job. – Tola Aug 17 '10 at 18:35
  • @Angkor: Wrong. Integral casts will truncate. – SLaks Aug 17 '10 at 19:01
  • @SLaks, I did try the sample he wrote. 34526 sec = 10hr 35mn 26sec, which is NOT correct. Did you try it? – Tola Aug 17 '10 at 19:03
  • @Angkor: Fixed, thanks. Blech - as far as I can tell there's no simple "convert and truncate" operator in VB, the equivalent of casting in C#. – Jon Skeet Aug 17 '10 at 19:24
  • @Angkor: Sorry; I was thinking in C#. – SLaks Aug 17 '10 at 22:12
  • Any particular reason for choosing `Math.Truncate` over `Math.Floor`? Also, `String.Format` did the right thing for me without the extra `CInt`. I assume this situation hasn't been remedied with `TimeSpan` formatting in 4.0? – Jeff B Dec 16 '14 at 15:36
  • 2
    @JeffBridgman: If the timespan is -30 minutes, you wouldn't want it to show as -1 hour, would you? Without the CInt, I'd have *expected* it to show as something like "1.0" rather than "1", but I suspect i didn't check it. I'm not sure what you mean by "I assume this situation hasn't been" - but yes, custom formatting in .NET 4 would indeed have made this simpler. – Jon Skeet Dec 16 '14 at 15:38
  • @JonSkeet Thanks. I meant the there wasn't an awesome format string that would grab `TotalHours` instead of `Hours`. I think you still have to do something like "String.Format({0}hr {1:mm}mn {1:ss}sec", (int)t.TotalHours, t)` instead of `duration.ToString("xx:mm:ss")` where `xx` would have given me total hours. – Jeff B Dec 16 '14 at 15:45
  • @Jeff: ah, yes, for multiday values. Don't think so, although I could be wrong. Noda Time handles this better :) – Jon Skeet Dec 16 '14 at 15:56
15

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 TimeSpans 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 ValueTuples 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....

dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
  • 1
    Your short and sweet answer takes into account leading zeros, beautiful! Was stuck with the likes of "47:33:4" for a while.. better than Skeet himself! – Murphybro2 Oct 09 '15 at 14:26
  • 2
    I really love this solution. Unfortunately it won't be usable in two way binding scenario, because there is no implementation of `TimeSpan.Parse` that would take an instance of `ICustomFormatter`. – Martin Braun Nov 19 '15 at 16:51
  • @modiX Would you post an example of how you're using it? I might be able to finagle a solution, and I want to start working with the correct context. Between extension methods and [`DateTime.ParseExact(input, format, formatProvder)`](https://msdn.microsoft.com/en-us/library/dd992370(v=vs.110).aspx), there may be a solution. – dx_over_dt Feb 24 '16 at 16:48
4

string.Format("{0}hr {1}mn {2}sec", (int) t.TotalHours, t.Minutes, t.Seconds);

Jason Williams
  • 56,972
  • 11
  • 108
  • 137
3

You can try this:

TimeSpan ts = TimeSpan.FromSeconds(1254234568);
Console.WriteLine($"{((int)ts.TotalHours).ToString("d2")}hr {ts.Minutes.ToString("d2")}mm {ts.Seconds.ToString("d2")}sec");
daniell89
  • 1,832
  • 16
  • 28
2

As per (https://msdn.microsoft.com/en-us/library/1ecy8h51(v=vs.110).aspx), the default ToString() method for a TimeSpan object uses the "c" formatting, which means that by default, a timespan longer than 24 hours looks something like "1.03:14:56" when output to a razor view. This caused some confusion with my customers who do not understand that the "1." represents one day.

So, if you can use interpolated strings (C#6+), an easy way that I came up with to preserve the default formatting as much as possible, while using TotalHours instead of Days+Hours is to provide a get property to output the time as a formatted string in, like so:

public TimeSpan SystemTime { get; set; }
public string SystemTimeAsString
{
    get
    {
        // Note: ignoring fractional seconds.
        return $"{(int)SystemTime.TotalHours}:SystemTime.Minutes.ToString("00")}:SystemTime.Seconds.ToString("00")}";
    }
}

The result of this using the same time as above will be "27:14:56".

1

You may need to calculate the hours. The range for hours in TimeSpan.ToString is only 0-23.

The worst you'll need is to do raw string formatting a la Jon Skeet.

John
  • 15,990
  • 10
  • 70
  • 110
0

You may wish to consider using Noda Time's Duration type.

For example:

Duration d = Duration.FromSeconds(sec);

Or

Duration d = Duration.FromTimeSpan(ts);

You can then simply format it as a string, like this:

string result = d.ToString("H'hr' m'mn' s'sec'", CultureInfo.InvariantCulture);

Alternatively, you can use the pattern-based API instead:

DurationPattern p = DurationPattern.CreateWithInvariantCulture("H'hr' m'mn' s'sec'");
string result = p.Format(d);

The advantage with the pattern API is that you only need to create the pattern once. If you have a lot of values to parse or format, there can be a significant performance benefit.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
0

My solution is:

string text = Math.Floor(timeUsed.TotalHours) + "h " + ((int)timeUsed.TotalMinutes) % 60 + "min";
alansiqueira27
  • 8,129
  • 15
  • 67
  • 111
0

MS Excel has other format different to .NET.

Check this Link http://www.paragon-inc.com/resources/blogs-posts/easy_excel_interaction_pt8

I create a simple function that convert a TimeSpan in a DateTime with the MS Excel format

    public static DateTime MyApproach(TimeSpan time)
    {
        return new DateTime(1900, 1, 1).Add(time).AddDays(-2);
    }

and you need to format the cell like this:

col.Style.Numberformat.Format = "[H]:mm:ss";
oaamados
  • 666
  • 10
  • 17
0

Try this Function:

Public Shared Function GetTimeSpanString(ByVal ts As TimeSpan) As String
        Dim output As New StringBuilder()

        Dim needsComma As Boolean = False

        If ts = Nothing Then

            Return "00:00:00"

        End If

        If ts.TotalHours >= 1 Then
            output.AppendFormat("{0} hr", Math.Truncate(ts.TotalHours))
            If ts.TotalHours > 1 Then
                output.Append("s")
            End If
            needsComma = True
        End If

        If ts.Minutes > 0 Then
            If needsComma Then
                output.Append(", ")
            End If
            output.AppendFormat("{0} m", ts.Minutes)
            'If ts.Minutes > 1 Then
            '    output.Append("s")
            'End If
            needsComma = True
        End If

        Return output.ToString()

 End Function       

Convert A Timespan To Hours And Minutes

Tola
  • 2,401
  • 10
  • 36
  • 60