How to produce "human readable" strings to represent a TimeSpan
Asked Answered
W

13

35

I have a TimeSpan representing the amount of time a client has been connected to my server. I want to display that TimeSpan to the user. But I don't want to be overly verbose to displaying that information (ex: 2hr 3min 32.2345sec = too detailed!)

For example: If the connection time is...

> 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

And of course, in cases where the numeral is 1 (ex: 1 seconds, 1 minutes, 1 hours, 1 days), I would like to make the text singular (ex: 1 second, 1 minute, 1 hour, 1 day).

Is there anyway to easily implement this without a giant set of if/else clauses? Here is what I'm currently doing.

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;
}
Westward answered 22/5, 2013 at 10:30 Comment(2)
May be helpful: https://mcmap.net/q/45143/-calculate-relative-time-in-cYeaton
possible duplicate of C# Library to parse human readable time spansAddams
K
36

To get rid of the complex if and switch constructs you can use a Dictionary lookup for the correct format string based on TotalSeconds and a CustomFormatter to format the supplied Timespan accordingly.

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;
   }   
}
Kirkuk answered 8/2, 2014 at 17:39 Comment(0)
B
21

Why not simply something like this?

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

If you prefer two units of time (e.g. minutes plus seconds), that would be very simple to add.

Buffington answered 24/3, 2016 at 0:55 Comment(0)
M
12

Reviving an old post, but...

Try the Humanizer library which can do this very easily:

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

By default it gives you rounded whole value of the time span. But you can also ask for a more precision:

TimeSpan.FromDays(16).Humanize(2) => "2 weeks, 2 days"
Monogram answered 10/9, 2021 at 20:25 Comment(0)
M
10

I built upon Bjorn's answer to fit my needs, wanted to share in case anyone else saw this issue. May save them time. The accepted answer is a bit heavyweight for my needs.

    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}";
    }
Melanosis answered 31/1, 2017 at 20:28 Comment(0)
L
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();
    }

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

Lewd answered 3/7, 2017 at 10:10 Comment(0)
V
3

Here's my take - a bit simpler than the accepted answer, don't you think? Also, no string splitting/parsing.

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)));
Vernice answered 29/4, 2016 at 17:53 Comment(0)
P
2

I would have prefered something like this which is more "readable" I think :

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;
}
Plimsoll answered 21/12, 2015 at 8:30 Comment(0)
F
2

We have created an implementation of it, which also displays years, months and weeks.
A maximum of 2 units are displayed.
Since the number of days in a month are not always the same, there may be inaccuracies.

Examples:

  • 1 day
  • 2 weeks
  • 1 week 3 days
  • 1 year 1 month
  • 1 year 1 week
  • 1 year 1 day
  • ...

Code:


/// <summary>
///     Format a <see cref="TimeSpan" /> to a human readable string.
/// </summary>
[NoReorder]
public static class TimeSpanHumanReadable
{
#pragma warning disable CS8618
    private static TimeValueClass TimeValue;
    private static StringBuilder  DateStringBuilder;
#pragma warning restore CS8618

    /// <summary>
    ///     Format the given <paramref name="timeSpan" /> to a human readable format.
    /// </summary>
    /// <param name="timeSpan">The value to format</param>
    /// <returns>The formatted value</returns>
    [Pure]
    public static string ToHumanReadableString(this TimeSpan timeSpan)
    {
        TimeValue         = new TimeValueClass(timeSpan);
        DateStringBuilder = new StringBuilder();

        ProcessTimeValue();

        return DateStringBuilder.ToString().Trim();
    }

    // ReSharper disable once CognitiveComplexity
    private static void ProcessTimeValue()
    {
        if (TimeValue.Years is not 0)
        {
            // 1 year
            AddYears();
            AddSpace();

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

            return;
        }

        if (TimeValue.Months is not 0)
        {
            // 1 month
            AddMonths();
            AddSpace();

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

            return;
        }

        if (TimeValue.Weeks is not 0)
        {
            AddWeeks();
            AddSpace();
            AddDays();
            return;
        }

        if (TimeValue.Days is not 0)
        {
            AddDays();
            AddSpace();
            AddHours();
            return;
        }

        if (TimeValue.Hours is not 0)
        {
            AddHours();
            AddSpace();
            AddMinutes();
            return;
        }

        if (TimeValue.Minutes is not 0)
        {
            AddMinutes();
            AddSpace();
            AddSeconds();
            return;
        }

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

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

        DateStringBuilder.Append("000 ms");
    }

    private static void AddSpace()
    {
        DateStringBuilder.Append(' ');
    }

    private static void AddYears()
    {
        if (TimeValue.Years is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Years);
        DateStringBuilder.Append(TimeValue.Years is 1 ? " year" : " years");
    }

    private static void AddMonths()
    {
        if (TimeValue.Months is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Months);
        DateStringBuilder.Append(TimeValue.Months is 1 ? " month" : " months");
    }

    private static void AddWeeks()
    {
        if (TimeValue.Weeks is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Weeks);
        DateStringBuilder.Append(TimeValue.Weeks is 1 ? " week" : " weeks");
    }

    private static void AddDays()
    {
        if (TimeValue.Days is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Days);
        DateStringBuilder.Append(TimeValue.Days is 1 ? " day" : " days");
    }

    private static void AddHours()
    {
        if (TimeValue.Hours is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Hours);
        DateStringBuilder.Append(TimeValue.Hours is 1 ? " hour" : " hours");
    }

    private static void AddMinutes()
    {
        if (TimeValue.Minutes is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Minutes);
        DateStringBuilder.Append(" min");
    }

    private static void AddSeconds()
    {
        if (TimeValue.Seconds is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Seconds);
        DateStringBuilder.Append(" sec");
    }

    private static void AddMilliseconds()
    {
        if (TimeValue.Milliseconds is 0)
        {
            return;
        }

        DateStringBuilder.Append(TimeValue.Milliseconds.ToString().PadLeft(3, '0'));
        DateStringBuilder.Append(" ms");
    }

    /// <remarks>
    ///     With help from https://mcmap.net/q/428677/-format-a-timespan-with-years
    /// </remarks>
    private class TimeValueClass
    {
        private const double DaysPerMonth = 30.4375;
        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 remaining days
            days -= (int)(years * DaysPerYear);

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

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

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

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

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

                days -= (int)(weeks * DaysPerMonth);
            }

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

                days -= (int)(months * DaysPerMonth);
            }

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

Unit Tests:


/// <summary>
///     Test class for <see cref="Utils.Data.TimeSpanHumanReadable.ToHumanReadableString" />
/// </summary>
[NoReorder]
[TestFixture]
public class TimeSpanHumanReadableTests : AbstractTest
{
    [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")]
    public void ToHumanReadableString_DayValues_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), timeSpan.ToString());
    }

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

    [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.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), timeSpan.ToString());
    }

    [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.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), 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.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), 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 + 60,    "1 year 2 months")]
    public void ToHumanReadableString_DaysForYears_ReturnsHumanReadableString(int days, string expected)
    {
        TimeSpan timeSpan = new(days, 0, 0, 0, 0);
        Assert.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), timeSpan.ToString());
    }

    [TestCase(1, 0,  0,  0,  0,  "1 day")]
    [TestCase(0, 1,  0,  0,  0,  "1 hour")]
    [TestCase(0, 0,  1,  0,  0,  "1 min")]
    [TestCase(0, 0,  0,  1,  0,  "1 sec")]
    [TestCase(0, 0,  0,  0,  1,  "001 ms")]
    [TestCase(0, 15, 0,  0,  0,  "15 hours")]
    [TestCase(0, 0,  15, 0,  0,  "15 min")]
    [TestCase(0, 0,  0,  15, 0,  "15 sec")]
    [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
    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.AreEqual(expected, TimeSpanHumanReadable.ToHumanReadableString(timeSpan), timeSpan.ToString());
    }
}
Flaminius answered 2/2, 2023 at 14:57 Comment(0)
B
1

Another approach (In German language)

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;
}
Barby answered 22/3, 2016 at 7:37 Comment(0)
P
1

Here's mine, very simple -

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";
Pouch answered 13/10, 2018 at 1:19 Comment(0)
S
1

I prefer to throw away the details -- for instance, once you're up to a number of months, then the number of seconds is irrelevant. As such, I use natural switching points to reach the next level, and I throw away all decimal values. Of course, this also eliminates the singular/plural problem entirely.

    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;
    }
Scalise answered 24/1, 2022 at 16:21 Comment(0)
P
0

Another stab at this. Deals with the pluralising of units (and omitting zero units) more coherently:

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;
    }
Panathenaea answered 20/4, 2018 at 9:55 Comment(0)
S
0

Here's yet another option.

Test cases:

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)
    },
};

Code:

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";
}
Scop answered 11/8, 2022 at 7:35 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.