Format TimeSpan greater than 24 hour
Asked Answered
L

10

46

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?

Lands answered 17/8, 2010 at 17:33 Comment(0)
H
71

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.

Harriot answered 17/8, 2010 at 17:35 Comment(15)
You may want to give VB code, though, as the code in the question looks like it :-)Insinuate
Am I missing something? I get nervous when my answer disagrees with Jon Skeet :-)Mccahill
((int) span.TotalMinutes) % 60 can be replaced by span.Minutes. The same thing with seconds.Damnation
Oops, yes, got carried away after the TotalHours - which is required.Harriot
HHow do I check if total hour > 1 then append "hrs" ortherwise, just append "hr" within the String.Format statement?Lands
@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.Harriot
@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.Ola
@Angkor: Wrong. Integral casts will truncate.Laforge
@SLaks, I did try the sample he wrote. 34526 sec = 10hr 35mn 26sec, which is NOT correct. Did you try it?Ola
@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#.Harriot
@Angkor: Sorry; I was thinking in C#.Laforge
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?Mckeever
@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.Harriot
@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.Mckeever
@Jeff: ah, yes, for multiday values. Don't think so, although I could be wrong. Noda Time handles this better :)Harriot
B
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....

Bragi answered 15/7, 2014 at 19:56 Comment(3)
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!Sternick
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.Selfimmolation
@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), there may be a solution.Bragi
M
4

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

Mccahill answered 17/8, 2010 at 17:36 Comment(3)
This is only for TimeSpan < 24 hr. And t,Seconds should be t.SecondsOla
You should change that to {0:f0} ..., t.TotalHours, ...Laforge
Still, you need to tweak (int) t.TotalHours, see my comment on Jon Skeet post about rounding.Ola
E
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");
Exclosure answered 5/4, 2017 at 14:36 Comment(1)
toString("d2") with connection with accepted answer gets the best resultRetriever
D
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".

Dhruv answered 30/3, 2018 at 19:39 Comment(0)
D
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.

Diocese answered 17/8, 2010 at 17:39 Comment(0)
O
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

Ola answered 17/8, 2010 at 17:45 Comment(0)
W
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.

Whitebook answered 5/11, 2014 at 17:9 Comment(0)
C
0

My solution is:

string text = Math.Floor(timeUsed.TotalHours) + "h " + ((int)timeUsed.TotalMinutes) % 60 + "min";
Causality answered 18/11, 2014 at 19:35 Comment(0)
C
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";
Chesterfield answered 19/11, 2014 at 22:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.