大于24小时的TimeSpan格式化方法

45

假设我像这样将一些秒数转换为TimeSpan对象:

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

我该如何将TimeSpan对象格式化为以下格式:
>105hr 56mn 47sec

有没有内置函数可以用,还是我需要编写自定义函数?

10个回答

70

好的,最简单的方法是自己进行格式化,例如:

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

在 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

我不知道在.NET 4中是否有任何TimeSpan格式化方法可以让这更简单。


1
你可能想提供VB代码,因为问题中的代码看起来像它 :) - Joey
2
我有什么遗漏吗?当我的答案与Jon Skeet不一致时,我会感到紧张 :-) - Jason Williams
3
((int) span.TotalMinutes) % 60可以替换为span.Minutes。对于秒钟也是一样的。 - Lasse Espeholt
选择 Math.Truncate 而不是 Math.Floor 有什么特别的原因吗?此外,String.Format 在没有额外的 CInt 的情况下为我做了正确的事情。 我假设在 4.0 中使用 TimeSpan 格式化时,这种情况还没有得到解决? - Jeff B
2
@JeffBridgman:如果时间跨度为-30分钟,您不希望它显示为-1小时,对吗?如果没有CInt,我本来期望它会显示为类似于“1.0”而不是“1”,但我怀疑我没有检查过。我不确定您所说的“我假设这种情况还没有发生”是什么意思,但是是的,在.NET 4中进行自定义格式化确实会使此操作更简单。 - Jon Skeet
显示剩余10条评论

15

2018年10月编辑:C# 6 / VB 14引入了插值字符串,可能比我原始答案的第一个代码段更简单。幸运的是,两种语言的插值语法相同:前面有$

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")

2021年11月修订:上述答案仅适用于正的TimeSpan和小于或等于-1小时的负数。如果您有一个范围在(-1,0]小时内的负TimeSpan,您需要手动插入负号。请注意,这也适用于原始答案。

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");

由于这样很繁琐,我建议创建一个辅助函数。

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");

我不够熟练,无法编写相应版本的VB代码。

在这里可以看到C#的一个快速示例here,其中包括C# 7中引入的ValueTuples特性。遗憾的是,我找到的唯一一个支持C#7的在线编译器运行的是.NET Core,对于小型示例来说非常麻烦,但请放心,在.NET Framework项目中它的工作方式完全相同。


原始回答

微软目前没有针对此问题的简单格式字符串快捷方式。最简单的选项已经分享。

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)

然而,一种过于彻底的选项是为TimeSpan实现自己的ICustomFormatter。我不建议这样做,除非您经常使用它并且从长远来看可以节省时间。但是,在某些情况下,您确实会编写一个类,编写自己的ICustomFormatter是适当的,因此我编写了这个示例。
/// <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);
    }
}

请注意,在代码中的注释中,TimeSpan.ToString(format, myTimeSpanFormatter)由于.NET Framework的一个怪异问题而无法使用,因此您始终需要使用string.Format(format, myTimeSpanFormatter)来使用这个类。请参阅如何创建和使用自定义IFormatProvider for DateTime?
编辑: 如果您真的(我是说真的)想让TimeSpan.ToString(string, TimeSpanFormatter)与此一起工作,您可以将以下内容添加到上面的TimeSpanFormatter类:
/// <remarks>
/// Update this as needed.
/// </remarks>
internal static string[] GetRecognizedFormats()
{
    return new string[] { "td", "th", "tm", "ts", "tf", "TD", "TH", "TM", "TS", "TF" };
}

请在同一命名空间中添加以下类:
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()));
    }
}

在此之后,您可以使用
ICustomFormatter formatter = new TimeSpanFormatter();
string myStr = myTimeSpan.ToString(@"TH,{000.00}h\:tm\m\:ss\s", formatter);

这里的 {000.00} 是你希望格式化 TotalHours 的整数或小数。注意大括号,它不应该出现在 string.Format() 中。此外,请注意,formatter 必须声明(或强制转换)为 ICustomFormatter 而不是 TimeSpanFormatter

过于冗长了吧?是的。很棒吗?嗯....


1
你的简短而精炼的答案考虑了前导零,太棒了!我曾经被“47:33:4”这样的东西困扰了一段时间...比Skeet本人还要好! - Murphybro2
2
我真的很喜欢这个解决方案。不幸的是,在双向绑定场景中它无法使用,因为没有实现可以接受“ICustomFormatter”实例的“TimeSpan.Parse”。 - Martin Braun
@modiX 你能否提供一个你使用它的例子?我可能能够想出一个解决方案,并且我想开始使用正确的上下文。在扩展方法和DateTime.ParseExact(input, format, formatProvder)之间,可能有一个解决方案。 - dx_over_dt

4

string.Format("{0}小时 {1}分钟 {2}秒", (int) t.TotalHours, t.Minutes, t.Seconds);


2
这仅适用于时间间隔小于24小时的情况。t,Seconds 应该为 t.Seconds。 - Tola
2
你应该将其更改为{0:f0} ..., t.TotalHours, ... - SLaks
不过,你需要调整(int) t.TotalHours,可以参考我在Jon Skeet的帖子中关于四舍五入的评论。 - Tola

3
你可以尝试这个:
TimeSpan ts = TimeSpan.FromSeconds(1254234568);
Console.WriteLine($"{((int)ts.TotalHours).ToString("d2")}hr {ts.Minutes.ToString("d2")}mm {ts.Seconds.ToString("d2")}sec");

使用toString("d2")与接受的答案相结合可以获得最佳结果。 - Grzegorz G.

2
根据(https://msdn.microsoft.com/en-us/library/1ecy8h51(v=vs.110).aspx),TimeSpan对象的默认ToString()方法使用“c”格式,这意味着默认情况下,输出到Razor视图的时间跨度超过24小时时看起来像“1.03:14:56”。这导致我的客户有些困惑,他们不明白“1.”代表一天。
因此,如果您可以使用插值字符串(C#6+),我想到了一种简单的方法来尽可能保留默认格式,同时使用TotalHours而不是Days+Hours来提供一个获取属性以将时间输出为格式化字符串,如下所示:
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")}";
    }
}

使用与上面相同的时间,结果将为“27:14:56”。

1
你可能需要计算小时数。TimeSpan.ToString 的小时范围仅为 0 到 23。
最糟糕的情况是你需要像 Jon Skeet 一样进行原始字符串格式化。

0

您可能希望考虑使用Noda TimeDuration类型。

例如:

Duration d = Duration.FromSeconds(sec);

或者

Duration d = Duration.FromTimeSpan(ts);

然后,您可以将其格式化为字符串,如下所示:

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

或者,您可以使用基于模式的 API 代替:

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

使用模式 API 的好处是您只需要创建一次模式。如果您有许多要解析或格式化的值,则可以获得显着的性能优势。

0

MS Excel有与.NET不同的其他格式。

请查看此链接http://www.paragon-inc.com/resources/blogs-posts/easy_excel_interaction_pt8

我创建了一个简单的函数,将TimeSpan转换为带有MS Excel格式的DateTime

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

而且您需要像这样格式化单元格:

col.Style.Numberformat.Format = "[H]:mm:ss";

0

我的解决方案是:

string text = Math.Floor(timeUsed.TotalHours) + "h " + ((int)timeUsed.TotalMinutes) % 60 + "min";

0

尝试使用这个函数:

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       

将时间间隔转换为小时和分钟


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接