如何生成“人类可读”字符串来表示时间跨度

问题描述 投票:0回答:13

我有一个

TimeSpan
代表客户端连接到我的服务器的时间。我想向用户显示
TimeSpan
。但我不想过于冗长地显示该信息(例如:2hr 3min 32.2345sec = 太详细了!)

例如:如果连接时间是...

> 0 seconds and < 1 minute   ----->  0 Seconds
> 1 minute  and < 1 hour     ----->  0 Minutes, 0 Seconds
> 1 hour    and < 1 day      ----->  0 Hours, 0 Minutes
> 1 day                      ----->  0 Days, 0 Hours

当然,如果数字为 1(例如:1 秒、1 分钟、1 小时、1 天),我想将文本设为单数(例如:1 秒、1 分钟、1 小时、1 天) ).

有没有办法在没有大量 if/else 子句的情况下轻松实现这一点?这是我目前正在做的事情。

public string GetReadableTimeSpan(TimeSpan value)
{
    string duration;

    if (value.TotalMinutes < 1)
        duration = value.Seconds + " Seconds";
    else if (value.TotalHours < 1)
        duration = value.Minutes + " Minutes, " + value.Seconds + " Seconds";
    else if (value.TotalDays < 1)
        duration = value.Hours + " Hours, " + value.Minutes + " Minutes";
    else
        duration = value.Days + " Days, " + value.Hours + " Hours";

    if (duration.StartsWith("1 Seconds") || duration.EndsWith(" 1 Seconds"))
        duration = duration.Replace("1 Seconds", "1 Second");

    if (duration.StartsWith("1 Minutes") || duration.EndsWith(" 1 Minutes"))
        duration = duration.Replace("1 Minutes", "1 Minute");

    if (duration.StartsWith("1 Hours") || duration.EndsWith(" 1 Hours"))
        duration = duration.Replace("1 Hours", "1 Hour");

    if (duration.StartsWith("1 Days"))
        duration = duration.Replace("1 Days", "1 Day");

    return duration;
}
c# .net string-formatting timespan
13个回答
36
投票

要摆脱复杂的 if 和 switch 结构,您可以使用字典查找基于 TotalSeconds 的正确格式字符串,并使用 CustomFormatter 来相应地格式化提供的时间跨度。

public string GetReadableTimespan(TimeSpan ts)
{
     // formats and its cutoffs based on totalseconds
     var cutoff = new SortedList<long, string> { 
       {59, "{3:S}" }, 
       {60, "{2:M}" },
       {60*60-1, "{2:M}, {3:S}"},
       {60*60, "{1:H}"},
       {24*60*60-1, "{1:H}, {2:M}"},
       {24*60*60, "{0:D}"},
       {Int64.MaxValue , "{0:D}, {1:H}"}
     };

     // find nearest best match
     var find = cutoff.Keys.ToList()
                   .BinarySearch((long)ts.TotalSeconds);
     // negative values indicate a nearest match
     var near = find<0?Math.Abs(find)-1:find;
     // use custom formatter to get the string
     return String.Format(
         new HMSFormatter(), 
         cutoff[cutoff.Keys[near]], 
         ts.Days, 
         ts.Hours, 
         ts.Minutes, 
         ts.Seconds);
}

// formatter for forms of
// seconds/hours/day
public class HMSFormatter:ICustomFormatter, IFormatProvider
{
    // list of Formats, with a P customformat for pluralization
    static Dictionary<string, string> timeformats = new Dictionary<string, string> {
        {"S", "{0:P:Seconds:Second}"},
        {"M", "{0:P:Minutes:Minute}"},
        {"H","{0:P:Hours:Hour}"},
        {"D", "{0:P:Days:Day}"}
    };

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        return String.Format(new PluralFormatter(),timeformats[format], arg);
    }

    public object GetFormat(Type formatType)
    {
        return formatType == typeof(ICustomFormatter)?this:null;
    }   
}

// formats a numeric value based on a format P:Plural:Singular
public class PluralFormatter:ICustomFormatter, IFormatProvider
{

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
     if (arg !=null)
     {
         var parts = format.Split(':'); // ["P", "Plural", "Singular"]

         if (parts[0] == "P") // correct format?
         {
            // which index postion to use
            int partIndex = (arg.ToString() == "1")?2:1;
            // pick string (safe guard for array bounds) and format
            return String.Format("{0} {1}", arg, (parts.Length>partIndex?parts[partIndex]:""));               
         }
     }
     return String.Format(format, arg);
   }

   public object GetFormat(Type formatType)
   {
       return formatType == typeof(ICustomFormatter)?this:null;
   }   
}

21
投票

为什么不简单地像这样呢?

public static class TimespanExtensions
{
    public static string ToHumanReadableString (this TimeSpan t)
    {
        if (t.TotalSeconds <= 1) {
            return $@"{t:s\.ff} seconds";
        }
        if (t.TotalMinutes <= 1) {
            return $@"{t:%s} seconds";
        }
        if (t.TotalHours <= 1) {
            return $@"{t:%m} minutes";
        }
        if (t.TotalDays <= 1) {
            return $@"{t:%h} hours";
        }

        return $@"{t:%d} days";
    }
}

如果您更喜欢两个时间单位(例如分钟加秒),添加起来会非常简单。


12
投票

恢复旧帖子,但是...

尝试使用 Humanizer 库,它可以非常轻松地做到这一点:

TimeSpan.FromMilliseconds(1).Humanize() => "1 millisecond"
TimeSpan.FromMilliseconds(2).Humanize() => "2 milliseconds"
TimeSpan.FromDays(1).Humanize() => "1 day"
TimeSpan.FromDays(16).Humanize() => "2 weeks"

默认情况下,它会为您提供时间跨度的四舍五入的整数值。但您也可以要求更精确:

TimeSpan.FromDays(16).Humanize(2) => "2 weeks, 2 days"

10
投票

我根据 Bjorn 的答案来满足我的需求,想分享以防其他人看到这个问题。可以节省他们的时间。接受的答案对于我的需求来说有点重量级。

    private static string FormatTimeSpan(TimeSpan timeSpan)
    {
        Func<Tuple<int,string>, string> tupleFormatter = t => $"{t.Item1} {t.Item2}{(t.Item1 == 1 ? string.Empty : "s")}";
        var components = new List<Tuple<int, string>>
        {
            Tuple.Create((int) timeSpan.TotalDays, "day"),
            Tuple.Create(timeSpan.Hours, "hour"),
            Tuple.Create(timeSpan.Minutes, "minute"),
            Tuple.Create(timeSpan.Seconds, "second"),
        };

        components.RemoveAll(i => i.Item1 == 0);

        string extra = "";

        if (components.Count > 1)
        {
            var finalComponent = components[components.Count - 1];
            components.RemoveAt(components.Count - 1);
            extra = $" and {tupleFormatter(finalComponent)}";
        }

        return $"{string.Join(", ", components.Select(tupleFormatter))}{extra}";
    }

5
投票
public string ToHumanDuration(TimeSpan? duration, bool displaySign = true)
    {
        if (duration == null) return null;

        var builder = new StringBuilder();
        if (displaySign)
        {
            builder.Append(duration.Value.TotalMilliseconds < 0 ? "-" : "+");
        }

        duration = duration.Value.Duration();

        if (duration.Value.Days > 0)
        {
            builder.Append($"{duration.Value.Days}d ");
        }

        if (duration.Value.Hours > 0)
        {
            builder.Append($"{duration.Value.Hours}h ");
        }

        if (duration.Value.Minutes > 0)
        {
            builder.Append($"{duration.Value.Minutes}m ");
        }

        if (duration.Value.TotalHours < 1)
        {
            if (duration.Value.Seconds > 0)
            {
                builder.Append(duration.Value.Seconds);
                if (duration.Value.Milliseconds > 0)
                {
                    builder.Append($".{duration.Value.Milliseconds.ToString().PadLeft(3, '0')}");
                }

                builder.Append("s ");
            }
            else
            {
                if (duration.Value.Milliseconds > 0)
                {
                    builder.Append($"{duration.Value.Milliseconds}ms ");
                }
            }
        }

        if (builder.Length <= 1)
        {
            builder.Append(" <1ms ");
        }

        builder.Remove(builder.Length - 1, 1);

        return builder.ToString();
    }

来源:https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Dashboard/HtmlHelper.cs


3
投票

这是我的看法 - 比公认的答案简单一点,你不觉得吗?另外,没有字符串分割/解析。

var components = new List<Tuple<int, string>> {
    Tuple.Create((int)span.TotalDays, "day"),
    Tuple.Create(span.Hours, "hour"),
    Tuple.Create(span.Minutes, "minute"),
    Tuple.Create(span.Seconds, "second"),
};

while(components.Any() && components[0].Item1 == 0)
{
    components.RemoveAt(0);
}

var result = string.Join(", ", components.Select(t => t.Item1 + " " + t.Item2 + (t.Item1 != 1 ? "s" : string.Empty)));

3
投票

我们已经创建了它的实现,它还显示年、月和周。
最多显示 2 个单位。
由于一个月的天数并不总是相同,因此可能会存在误差。

示例:

  • 1天
  • 2周
  • 1周3天
  • 1年1个月
  • 1年1周
  • 1年1天
  • ...

代码:


/// <summary>
///     Formats a <see cref="TimeSpan" /> to a human-readable string.
/// </summary>
public static class TimeSpanHumanReadable
{
    /// <summary>
    ///     Formats the given <paramref name="timeSpan" /> to a human-readable format.
    /// </summary>
    /// <remarks>
    ///     This method intelligently formats the given <paramref name="timeSpan" /> to produce a result
    ///     that combines years, months, weeks, days, hours, minutes, seconds, and milliseconds, ensuring
    ///     the output is both accurate and intuitive for a wide range of durations.<br />
    /// </remarks>
    /// <param name="timeSpan">The value to format.</param>
    /// <returns>A human-readable <see cref="TimeSpan" />. (See Example)</returns>
    /// <example>
    ///     Example outputs for various <paramref name="timeSpan" /> values:
    ///     <ul>
    ///         <li>1 day</li>
    ///         <li>2 weeks</li>
    ///         <li>1 week 3 days</li>
    ///         <li>1 year 1 month</li>
    ///         <li>1 year 1 week</li>
    ///         <li>1 year 1 day</li>
    ///         <li>1 hour</li>
    ///         <li>1 hour 15 min</li>
    ///         <li>1 min 15 sec</li>
    ///         <li><c>TimeSpan.FromDays(400)</c>: 1 year 1 month</li>
    ///         <li><c>TimeSpan.FromHours(30)</c>: 1 day 6 hours</li>
    ///         <li><c>TimeSpan.FromMilliseconds(12345)</c>: 12 sec</li>
    ///         <li><c>TimeSpan.FromMinutes(90)</c>: 1 hour 30 min</li>
    ///     </ul>
    /// </example>
    [Pure]
    public static string ToHumanReadableString(this TimeSpan timeSpan)
    {
        TimeValueClass timeValue = new(timeSpan);
        StringBuilder  builder   = new();

        ProcessTimeValue(builder, timeValue);

        return builder.ToString();
    }

    // ReSharper disable once CognitiveComplexity
    private static void ProcessTimeValue(this StringBuilder builder, TimeValueClass timeValue)
    {
        if (timeValue.Years is not 0)
        {
            // 1 year
            builder.AddYears(timeValue);

            if (timeValue.Months is not 0)
            {
                // 1 year 1 month
                builder.AddSpace()
                       .AddMonths(timeValue);
            }
            else if (timeValue.Weeks is not 0)
            {
                // 1 year 1 week
                builder.AddSpace()
                       .AddWeeks(timeValue);
            }
            else if (timeValue.Days is not 0)
            {
                // 1 year 1 day
                builder.AddSpace()
                       .AddDays(timeValue);
            }

            return;
        }

        if (timeValue.Months is not 0)
        {
            // 1 month
            builder.AddMonths(timeValue);

            if (timeValue.Weeks is not 0)
            {
                // 1 month 1 week
                builder.AddSpace()
                       .AddWeeks(timeValue);
            }
            else if (timeValue.Days is >= 3 and <= 6)
            {
                // 1 month 3 days
                builder.AddSpace()
                       .AddDays(timeValue);
            }

            return;
        }

        if (timeValue.Weeks is not 0)
        {
            builder.AddWeeks(timeValue);

            if (timeValue.Days is not 0)
            {
                builder.AddSpace()
                       .AddDays(timeValue);
            }

            return;
        }

        if (timeValue.Days is not 0)
        {
            builder.AddDays(timeValue);

            if (timeValue.Hours is not 0)
            {
                builder.AddSpace()
                       .AddHours(timeValue);
            }

            return;
        }

        if (timeValue.Hours is not 0)
        {
            builder.AddHours(timeValue);

            if (timeValue.Minutes is not 0)
            {
                builder.AddSpace()
                       .AddMinutes(timeValue);
            }

            return;
        }

        if (timeValue.Minutes is not 0)
        {
            builder.AddMinutes(timeValue);

            if (timeValue.Seconds is not 0)
            {
                builder.AddSpace()
                       .AddSeconds(timeValue);
            }

            return;
        }

        if (timeValue.Seconds is not 0)
        {
            builder.AddSeconds(timeValue);
            return;
        }

        if (timeValue.Milliseconds is not 0)
        {
            builder.AddMilliseconds(timeValue);
            return;
        }

        builder.Append("000 ms");
    }

    private static StringBuilder AddSpace(this StringBuilder builder)
    {
        return builder.Append(' ');
    }

    private static StringBuilder AddYears(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Years, " year", " years");

    private static StringBuilder AddMonths(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Months, " month", " months");

    private static StringBuilder AddWeeks(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Weeks, " week", " weeks");

    private static StringBuilder AddDays(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Days, " day", " days");

    private static StringBuilder AddHours(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Hours, " hour", " hours");

    private static StringBuilder AppendValueAndUnit(this StringBuilder builder, int value, string singular, string plural)
    {
        return builder.Append(value)
                      .Append(value is 1 or -1 ? singular : plural);
    }

    private static StringBuilder AddMinutes(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Minutes, " min");

    private static StringBuilder AddSeconds(this StringBuilder builder, TimeValueClass timeValue)
        => builder.AppendValueAndUnit(timeValue.Seconds, " sec");

    private static StringBuilder AppendValueAndUnit(this StringBuilder builder, int value, string unit)
    {
        return builder.Append(value)
                      .Append(unit);
    }

    private static StringBuilder AddMilliseconds(this StringBuilder builder, TimeValueClass timeValue)
    {
        // We show ms with leading zeros. So we have to add the '-' here.
        if (timeValue.Milliseconds < 0)
        {
            builder.Append('-');
        }

        builder.Append(Math.Abs(timeValue.Milliseconds).ToString().PadLeft(3, '0'));
        builder.Append(" ms");
        return builder;
    }

    /// <remarks>
    ///     With help from https://stackoverflow.com/a/21260317/1847143
    /// </remarks>
    private class TimeValueClass
    {
        private const double DaysPerMonth = 30.4375; // Average days per month.
        private const double DaysPerWeek  = 7;
        private const double DaysPerYear  = 365;

        public int Days         { get; }
        public int Hours        { get; }
        public int Milliseconds { get; }
        public int Minutes      { get; }
        public int Months       { get; }
        public int Seconds      { get; }
        public int Weeks        { get; }

        public int Years { get; }

        public TimeValueClass(TimeSpan timeSpan)
        {
            // Calculate the span in days
            int days = timeSpan.Days;

            // 362 days == 11 months and 4 weeks. 4 weeks => 1 month and 12 months => 1 year. So we have to exclude this value
            bool has362Days = days % 362 == 0;

            // Calculate years
            int years = (int)(days / DaysPerYear);

            // Decrease the remaining days
            days -= (int)(years * DaysPerYear);

            // Calculate months
            int months = (int)(days / DaysPerMonth);

            // Decrease the remaining days
            days -= (int)(months * DaysPerMonth);

            // Calculate weeks
            int weeks = (int)(days / DaysPerWeek);

            // Decrease the remaining days
            days -= (int)(weeks * DaysPerWeek);

            // 4 weeks is 1 month
            if (weeks is 4 && has362Days is false)
            {
                weeks = 0;
                months++;
            }
            else if (weeks is -4 && has362Days is false)
            {
                weeks = 0;
                months--;
            }

            // 12 months is 1 year
            if (months == 12)
            {
                months = 0;
                years++;
            }

            Years        = years;
            Months       = months;
            Weeks        = weeks;
            Days         = days;
            Hours        = timeSpan.Hours;
            Minutes      = timeSpan.Minutes;
            Seconds      = timeSpan.Seconds;
            Milliseconds = timeSpan.Milliseconds;
        }
    }
}

单元测试:



/// <summary>
///     Test class for <see cref="Utils.Data.TimeSpanHumanReadable.ToHumanReadableString" />
/// </summary>
[NoReorder]
[TestFixture]
public class TimeSpanHumanReadableTests : AbstractTestBase
{
    [TestCase(-1, "-1 day")]
    [TestCase(1,  "1 day")]
    [TestCase(2,  "2 days")]
    [TestCase(3,  "3 days")]
    [TestCase(4,  "4 days")]
    [TestCase(5,  "5 days")]
    [TestCase(6,  "6 days")]
    [TestCase(7,  "1 week")]
    [TestCase(-7, "-1 week")]
    public void ToHumanReadableString_DayValues_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(28,  "1 month")]
    [TestCase(-28, "-1 month")]
    [TestCase(29,  "1 month")]
    [TestCase(-29, "-1 month")]
    [TestCase(30,  "1 month")]
    [TestCase(-30, "-1 month")]
    [TestCase(31,  "1 month")]
    [TestCase(-31, "-1 month")]
    [TestCase(32,  "1 month")]
    [TestCase(-32, "-1 month")]
    public void ToHumanReadableString_DaysFor1Month_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(-58, "-2 months")]
    [TestCase(58,  "2 months")]
    [TestCase(59,  "2 months")]
    [TestCase(60,  "2 months")]
    [TestCase(61,  "2 months")]
    [TestCase(62,  "2 months")]
    public void ToHumanReadableString_DaysFor2Months_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(-8, "-1 week -1 day")]
    [TestCase(8,  "1 week 1 day")]
    [TestCase(16, "2 weeks 2 days")]
    public void ToHumanReadableString_DaysForWeeks_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(30,         "1 month")]
    [TestCase(30 + 1,     "1 month")]
    [TestCase(30 + 2,     "1 month")]
    [TestCase(30 + 3,     "1 month 3 days")]
    [TestCase(30 + 4,     "1 month 4 days")]
    [TestCase(30 + 5,     "1 month 5 days")]
    [TestCase(30 + 6,     "1 month 6 days")]
    [TestCase(30 + 7,     "1 month 1 week")]
    [TestCase(30 + 7 + 1, "1 month 1 week")]
    [TestCase(32 + 7 + 2, "1 month 1 week")]
    [TestCase(32 + 7 + 3, "1 month 1 week")]
    public void ToHumanReadableString_DaysForMonths_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(365,          "1 year")]
    [TestCase(365  + 1,     "1 year 1 day")]
    [TestCase(365  + 2,     "1 year 2 days")]
    [TestCase(365  + 3,     "1 year 3 days")]
    [TestCase(365  + 4,     "1 year 4 days")]
    [TestCase(365  + 5,     "1 year 5 days")]
    [TestCase(365  + 6,     "1 year 6 days")]
    [TestCase(365  + 7,     "1 year 1 week")]
    [TestCase(365  + 7 + 1, "1 year 1 week")]
    [TestCase(365  + 7 + 2, "1 year 1 week")]
    [TestCase(365  + 7 + 3, "1 year 1 week")]
    [TestCase(365  + 7 + 4, "1 year 1 week")]
    [TestCase(365  + 7 + 5, "1 year 1 week")]
    [TestCase(365  + 7 + 6, "1 year 1 week")]
    [TestCase(365  + 14,    "1 year 2 weeks")]
    [TestCase(365  + 30,    "1 year 1 month")]
    [TestCase(-365 - 30,    "-1 year -1 month")]
    [TestCase(365  + 60,    "1 year 2 months")]
    [TestCase(-365 - 60,    "-1 year -2 months")]
    public void ToHumanReadableString_DaysForYears_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }

    [TestCase(1,    0,   0,   0,   0,     "1 day")]
    [TestCase(-1,   0,   0,   0,   0,     "-1 day")]
    [TestCase(0,    1,   0,   0,   0,     "1 hour")]
    [TestCase(0,    -1,  0,   0,   0,     "-1 hour")]
    [TestCase(0,    0,   1,   0,   0,     "1 min")]
    [TestCase(0,    0,   -1,  0,   0,     "-1 min")]
    [TestCase(0,    0,   0,   1,   0,     "1 sec")]
    [TestCase(0,    0,   0,   -1,  0,     "-1 sec")]
    [TestCase(0,    0,   0,   0,   1,     "001 ms")]
    [TestCase(0,    0,   0,   0,   -1,    "-001 ms")]
    [TestCase(0,    15,  0,   0,   0,     "15 hours")]
    [TestCase(0,    -15, 0,   0,   0,     "-15 hours")]
    [TestCase(0,    0,   15,  0,   0,     "15 min")]
    [TestCase(0,    0,   -15, 0,   0,     "-15 min")]
    [TestCase(0,    0,   0,   15,  0,     "15 sec")]
    [TestCase(0,    0,   0,   -15, 0,     "-15 sec")]
    [TestCase(0,    0,   0,   0,   15,    "015 ms")]
    [TestCase(0,    0,   0,   0,   -15,   "-015 ms")]
    [TestCase(1,    1,   0,   0,   0,     "1 day 1 hour")]
    [TestCase(2,    2,   0,   0,   0,     "2 days 2 hours")]
    [TestCase(5,    5,   5,   5,   5,     "5 days 5 hours")]
    [TestCase(0,    1,   1,   0,   0,     "1 hour 1 min")]
    [TestCase(0,    2,   2,   0,   0,     "2 hours 2 min")]
    [TestCase(0,    0,   1,   1,   0,     "1 min 1 sec")]
    [TestCase(0,    0,   2,   2,   0,     "2 min 2 sec")]
    [TestCase(0,    0,   0,   1,   1,     "1 sec")]  // With ms
    [TestCase(0,    0,   0,   2,   2,     "2 sec")]  // With ms
    [TestCase(0,    0,   0,   0,   0,     "000 ms")] // With ms
    [TestCase(400,  0,   0,   0,   0,     "1 year 1 month")]
    [TestCase(0,    30,  0,   0,   0,     "1 day 6 hours")]
    [TestCase(0,    0,   0,   0,   12345, "12 sec")]
    [TestCase(0,    0,   90,  0,   0,     "1 hour 30 min")]
    [TestCase(5000, 0,   90,  0,   0,     "13 years 8 months")]
    public void ToHumanReadableString_TimeValues_ReturnsHumanReadableString(int days, int hours, int minutes, int seconds, int milliseconds, string expected)
    {
        TimeSpan timeSpan = new(days, hours, minutes, seconds, milliseconds);
        Assert.That(TimeSpanHumanReadable.ToHumanReadableString(timeSpan), Is.EqualTo(expected), timeSpan.ToString());
    }
}

2
投票

我更喜欢这样的东西,我认为它更“可读”:

public string GetReadableTimeSpan(TimeSpan value)
{
 string duration = "";

 var totalDays = (int)value.TotalDays;
 if (totalDays >= 1)
 {
     duration = totalDays + " day" + (totalDays > 1 ? "s" : string.Empty);
     value = value.Add(TimeSpan.FromDays(-1 * totalDays));
 }

 var totalHours = (int)value.TotalHours;
 if (totalHours >= 1)
 {
     if (totalDays >= 1)
     {
         duration += ", ";
     }
     duration += totalHours + " hour" + (totalHours > 1 ? "s" : string.Empty);
     value = value.Add(TimeSpan.FromHours(-1 * totalHours));
 }

 var totalMinutes = (int)value.TotalMinutes;
 if (totalMinutes >= 1)
 {
     if (totalHours >= 1)
     {
         duration += ", ";
     }
     duration += totalMinutes + " minute" + (totalMinutes > 1 ? "s" : string.Empty);
 }

 return duration;
}

1
投票

另一种方法(德语)

public static string GetReadableTimeSpan(TimeSpan span)
{
    var formatted = string.Format("{0}{1}{2}{3}",
        span.Duration().Days > 0
            ? $"{span.Days:0} Tag{(span.Days == 1 ? string.Empty : "e")}, "
            : string.Empty,
        span.Duration().Hours > 0
            ? $"{span.Hours:0} Stunde{(span.Hours == 1 ? string.Empty : "n")}, "
            : string.Empty,
        span.Duration().Minutes > 0
            ? $"{span.Minutes:0} Minute{(span.Minutes == 1 ? string.Empty : "n")}, "
            : string.Empty,
        span.Duration().Seconds > 0
            ? $"{span.Seconds:0} Sekunde{(span.Seconds == 1 ? string.Empty : "n")}"
            : string.Empty);

    if (formatted.EndsWith(", ")) formatted = formatted.Substring(0, formatted.Length - 2);

    return string.IsNullOrEmpty(formatted) ? "0 Sekunden" : ReplaceLastOccurrence(formatted, ",", " und ").Replace("  ", " ");
}

private static string ReplaceLastOccurrence(string source, string find, string replace)
{
    var place = source.LastIndexOf(find, StringComparison.Ordinal);

    if (place == -1)
        return source;

    var result = source.Remove(place, find.Length).Insert(place, replace);
    return result;
}

1
投票

这是我的,非常简单-

TimeSpan timeElapsed = DateTime.Now - referenceTime_;
string timeString = "";
if (timeElapsed.Hours > 0)
    timeString = timeElapsed.Hours.ToString() + " hour(s), " + timeElapsed.Minutes.ToString() + " minutes, " + timeElapsed.Seconds.ToString() + " seconds";
else if (timeElapsed.Minutes > 0)
    timeString = timeElapsed.Minutes.ToString() + " minutes, " + timeElapsed.Seconds.ToString() + " seconds";
else
    timeString = timeElapsed.Seconds.ToString() + " seconds";

1
投票

我更喜欢抛弃细节——例如,一旦你达到了几个月,那么秒数就无关紧要了。因此,我使用自然切换点来达到下一个级别,并丢弃所有小数值。当然,这也完全消除了单复数问题。

    private static string LastFetched(TimeSpan ago)
    {
        string lastFetched = "last fetched ";
        if (ago.TotalDays >= 90)
            lastFetched += $"{(int)ago.TotalDays / 30} months ago";
        else if (ago.TotalDays >= 14)
            lastFetched += $"{(int)ago.TotalDays / 7} weeks ago";
        else if (ago.TotalDays >= 2)
            lastFetched += $"{(int)ago.TotalDays} days ago";
        else if (ago.TotalHours >= 2)
            lastFetched += $"{(int)ago.TotalHours} hours ago";
        else if (ago.TotalMinutes >= 2)
            lastFetched += $"{(int)ago.TotalMinutes} minutes ago";
        else if (ago.TotalSeconds >= 10)
            lastFetched += $"{(int)ago.TotalSeconds} seconds ago";
        else
            lastFetched += $"just now";

        return lastFetched;
    }

0
投票

再次尝试这一点。 更连贯地处理单位的复数(并省略零单位):

private string GetValueWithPluralisedUnits(int value, string units, int prefix_value)
    {
        if (value != 0)
        {
            return (prefix_value == 0 ? "" : ", ") + value.ToString() + " " + units + (value == 1 ? "" : "s");
        }

        return "";
    }

    private string GetReadableTimeSpan(TimeSpan value)
    {
        string duration;

        if (value.TotalMinutes < 1)
        {
            if (value.Seconds > 0)
            {
                duration = GetValueWithPluralisedUnits(value.Seconds, "Second", 0);
            }
            else
            {
                duration = "";
            }
        }
        else if (value.TotalHours < 1)
        {
            duration = GetValueWithPluralisedUnits(value.Minutes, "Minute", 0) + GetValueWithPluralisedUnits(value.Seconds, "Second", value.Minutes);
        }
        else if (value.TotalDays < 1)
        {
            duration = GetValueWithPluralisedUnits(value.Hours, "Hour", 0) + GetValueWithPluralisedUnits(value.Minutes, "Minute", value.Hours);
        }
        else
        {
            int days_left = (int)value.TotalDays;
            int years = days_left / 365;
            days_left -= years * 365;
            int months = days_left / 12;
            days_left -= months * 12;

            duration = GetValueWithPluralisedUnits(years, "Year", 0) + GetValueWithPluralisedUnits(months, "Month", years) + GetValueWithPluralisedUnits(days_left, "Day", years + months);
        }

        return duration;
    }

0
投票

还有另一种选择。

测试用例:

var testCases = new List<HumanReadableTimeStringTestCase>
{
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "1.88s",
        ExpectedLong = "1.88 seconds",
        Span = TimeSpan.FromMilliseconds(1880)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "90s",
        ExpectedLong = "90 seconds",
        Span = TimeSpan.FromSeconds(90.4)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "90s",  // No rounding for seconds
        ExpectedLong = "90 seconds",
        Span = TimeSpan.FromSeconds(90.7)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "90m",
        ExpectedLong = "90 minutes",
        Span = TimeSpan.FromMinutes(90.4)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "119m",
        ExpectedLong = "119 minutes",
        Span = TimeSpan.FromMinutes(119.4)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "2h",
        ExpectedLong = "2 hours, 0 minutes",
        Span = TimeSpan.FromMinutes(120)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "3h", // rounded
        ExpectedLong = "2 hours, 55 minutes",
        Span = TimeSpan.FromMinutes(120 + 55)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "24h",
        ExpectedLong = "24 hours, 3 minutes",
        Span = new TimeSpan(days: 1, hours: 0, minutes: 3, seconds: 0)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "26h",
        ExpectedLong = "26 hours, 3 minutes",
        Span = new TimeSpan(days: 1, hours: 2, minutes: 3, seconds: 0)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "27h",
        ExpectedLong = "26 hours, 31 minutes",
        Span = new TimeSpan(days: 1, hours: 2, minutes: 31, seconds: 0)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "24h",
        ExpectedLong = "24 hours, 3 minutes",
        Span = new TimeSpan(days: 1, hours: 0, minutes: 3, seconds: 0)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "2d,0h",
        ExpectedLong = "2 days, 0 hours",
        Span = new TimeSpan(days: 2, hours: 0, minutes: 3, seconds: 0)
    },
    new HumanReadableTimeStringTestCase
    {
        ExpectedShort = "2d,4h",
        ExpectedLong = "2 days, 4 hours",
        Span = new TimeSpan(days: 2, hours: 4, minutes: 3, seconds: 0)
    },
};

代码:

public static string ToHumanReadableString(TimeSpan t)
{
    if (t.TotalSeconds < 2)
    {
        return $@"{t.TotalSeconds:.##} seconds";
    }

    if (t.TotalMinutes < 2)
    {
        return $@"{(int)t.TotalSeconds} seconds";
    }

    if (t.TotalHours < 2)
    {
        return $@"{(int)Math.Round(t.TotalMinutes, MidpointRounding.AwayFromZero)} minutes";
    }

    if (t.TotalDays < 2)
    {
        return $@"{(int)(t.TotalMinutes / 60)} hours, {t:%m} minutes";
    }

    return $@"{t:%d} days, {t:%h} hours";
}

public static string ToHumanReadableStringShort(TimeSpan t)
{
    if (t.TotalSeconds < 2)
    {
        return $@"{t.TotalSeconds:0.##}s";
    }

    if (t.TotalMinutes < 2)
    {
        return $@"{(int)t.TotalSeconds}s";
    }

    if (t.TotalHours < 2)
    {
        return $@"{(int)Math.Round(t.TotalMinutes, MidpointRounding.AwayFromZero)}m";
    }

    if (t.TotalDays < 2)
    {
        return $@"{(int)Math.Round(t.TotalHours, MidpointRounding.AwayFromZero)}h";
    }

    return $@"{t:%d}d,{t:%h}h";
}
© www.soinside.com 2019 - 2024. All rights reserved.