给定特定的DateTime值,如何显示相对时间,例如:

2小时前3天前一个月前


当前回答

Nuget上还有一个名为Humanizr的软件包,它实际上运行得很好,并且在.NET Foundation中。

DateTime.UtcNow.AddHours(-30).Humanize() => "yesterday"
DateTime.UtcNow.AddHours(-2).Humanize() => "2 hours ago"

DateTime.UtcNow.AddHours(30).Humanize() => "tomorrow"
DateTime.UtcNow.AddHours(2).Humanize() => "2 hours from now"

TimeSpan.FromMilliseconds(1299630020).Humanize() => "2 weeks"
TimeSpan.FromMilliseconds(1299630020).Humanize(3) => "2 weeks, 1 day, 1 hour"

Scott Hanselman在他的博客上写了一篇文章

其他回答

这里是Jeffs Script for PHP的重写:

define("SECOND", 1);
define("MINUTE", 60 * SECOND);
define("HOUR", 60 * MINUTE);
define("DAY", 24 * HOUR);
define("MONTH", 30 * DAY);
function relativeTime($time)
{   
    $delta = time() - $time;

    if ($delta < 1 * MINUTE)
    {
        return $delta == 1 ? "one second ago" : $delta . " seconds ago";
    }
    if ($delta < 2 * MINUTE)
    {
      return "a minute ago";
    }
    if ($delta < 45 * MINUTE)
    {
        return floor($delta / MINUTE) . " minutes ago";
    }
    if ($delta < 90 * MINUTE)
    {
      return "an hour ago";
    }
    if ($delta < 24 * HOUR)
    {
      return floor($delta / HOUR) . " hours ago";
    }
    if ($delta < 48 * HOUR)
    {
      return "yesterday";
    }
    if ($delta < 30 * DAY)
    {
        return floor($delta / DAY) . " days ago";
    }
    if ($delta < 12 * MONTH)
    {
      $months = floor($delta / DAY / 30);
      return $months <= 1 ? "one month ago" : $months . " months ago";
    }
    else
    {
        $years = floor($delta / DAY / 365);
        return $years <= 1 ? "one year ago" : $years . " years ago";
    }
}    
public static string RelativeDate(DateTime theDate)
{
    Dictionary<long, string> thresholds = new Dictionary<long, string>();
    int minute = 60;
    int hour = 60 * minute;
    int day = 24 * hour;
    thresholds.Add(60, "{0} seconds ago");
    thresholds.Add(minute * 2, "a minute ago");
    thresholds.Add(45 * minute, "{0} minutes ago");
    thresholds.Add(120 * minute, "an hour ago");
    thresholds.Add(day, "{0} hours ago");
    thresholds.Add(day * 2, "yesterday");
    thresholds.Add(day * 30, "{0} days ago");
    thresholds.Add(day * 365, "{0} months ago");
    thresholds.Add(long.MaxValue, "{0} years ago");
    long since = (DateTime.Now.Ticks - theDate.Ticks) / 10000000;
    foreach (long threshold in thresholds.Keys) 
    {
        if (since < threshold) 
        {
            TimeSpan t = new TimeSpan((DateTime.Now.Ticks - theDate.Ticks));
            return string.Format(thresholds[threshold], (t.Days > 365 ? t.Days / 365 : (t.Days > 0 ? t.Days : (t.Hours > 0 ? t.Hours : (t.Minutes > 0 ? t.Minutes : (t.Seconds > 0 ? t.Seconds : 0))))).ToString());
        }
    }
    return "";
}

我更喜欢这个版本,因为它简洁,并且能够添加新的刻度点。这可以用Timespan的Latest()扩展来封装,而不是长的1行,但为了发布的简洁,这可以。这修复了一小时前、一小时前的问题,提供了一个小时直到两小时过去

我是这样做的

var ts = new TimeSpan(DateTime.UtcNow.Ticks - dt.Ticks);
double delta = Math.Abs(ts.TotalSeconds);

if (delta < 60)
{
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
}
if (delta < 60 * 2)
{
  return "a minute ago";
}
if (delta < 45 * 60)
{
  return ts.Minutes + " minutes ago";
}
if (delta < 90 * 60)
{
  return "an hour ago";
}
if (delta < 24 * 60 * 60)
{
  return ts.Hours + " hours ago";
}
if (delta < 48 * 60 * 60)
{
  return "yesterday";
}
if (delta < 30 * 24 * 60 * 60)
{
  return ts.Days + " days ago";
}
if (delta < 12 * 30 * 24 * 60 * 60)
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
return years <= 1 ? "one year ago" : years + " years ago";

建议?评论?如何改进此算法?

jquery.timeago插件

Jeff,因为Stack Overflow广泛使用jQuery,所以我建议使用jQuery.timeago插件。

优点:

即使页面是在10分钟前打开的,也不要使用“1分钟前”的时间戳;timeago自动刷新。您可以充分利用web应用程序中的页面和/或片段缓存,因为时间戳不是在服务器上计算的。你可以像酷孩子一样使用微格式。

只需将其附加到DOM就绪的时间戳:

jQuery(document).ready(function() {
    jQuery('abbr.timeago').timeago();
});

这将在标题中使用timeago类和ISO 8601时间戳转换所有缩写元素:

<abbr class="timeago" title="2008-07-17T09:24:17Z">July 17, 2008</abbr>

变成这样:

<abbr class="timeago" title="July 17, 2008">4 months ago</abbr>

结果:4个月前。随着时间的推移,时间戳将自动更新。

免责声明:我写了这个插件,所以我有偏见。

文森特接受的答案做出了许多武断的决定。为什么45分钟舍入为一小时,而45秒不舍入为一分钟?在年和月的计算中,它的圈复杂度增加了,这使得遵循逻辑变得更加复杂。它假设TimeSpan是相对于过去(2天前)的,而它很可能是在未来(2天后)。它定义了不必要的常量,而不是使用TimeSpan.TicksPerSecond等。

此实现解决了上述问题,并更新了语法以使用开关表达式和关系模式

/// <summary>
/// Convert a <see cref="TimeSpan"/> to a natural language representation.
/// </summary>
/// <example>
/// <code>
/// TimeSpan.FromSeconds(10).ToNaturalLanguage();
/// // 10 seconds
/// </code>
/// </example>
public static string ToNaturalLanguage(this TimeSpan @this)
{
    const int daysInWeek = 7;
    const int daysInMonth = 30;
    const int daysInYear = 365;
    const long threshold = 100 * TimeSpan.TicksPerMillisecond;
    @this = @this.TotalSeconds < 0
        ? TimeSpan.FromSeconds(@this.TotalSeconds * -1)
        : @this;
    return (@this.Ticks + threshold) switch
    {
        < 2 * TimeSpan.TicksPerSecond => "a second",
        < 1 * TimeSpan.TicksPerMinute => @this.Seconds + " seconds",
        < 2 * TimeSpan.TicksPerMinute => "a minute",
        < 1 * TimeSpan.TicksPerHour => @this.Minutes + " minutes",
        < 2 * TimeSpan.TicksPerHour => "an hour",
        < 1 * TimeSpan.TicksPerDay => @this.Hours + " hours",
        < 2 * TimeSpan.TicksPerDay => "a day",
        < 1 * daysInWeek * TimeSpan.TicksPerDay => @this.Days + " days",
        < 2 * daysInWeek * TimeSpan.TicksPerDay => "a week",
        < 1 * daysInMonth * TimeSpan.TicksPerDay => (@this.Days / daysInWeek).ToString("F0") + " weeks",
        < 2 * daysInMonth * TimeSpan.TicksPerDay => "a month",
        < 1 * daysInYear * TimeSpan.TicksPerDay => (@this.Days / daysInMonth).ToString("F0") + " months",
        < 2 * daysInYear * TimeSpan.TicksPerDay => "a year",
        _ => (@this.Days / daysInYear).ToString("F0") + " years"
    };
}

/// <summary>
/// Convert a <see cref="DateTime"/> to a natural language representation.
/// </summary>
/// <example>
/// <code>
/// (DateTime.Now - TimeSpan.FromSeconds(10)).ToNaturalLanguage()
/// // 10 seconds ago
/// </code>
/// </example>
public static string ToNaturalLanguage(this DateTime @this)
{
    TimeSpan timeSpan = @this - DateTime.Now;
    return timeSpan.TotalSeconds switch
    {
        >= 1 => timeSpan.ToNaturalLanguage() + " until",
        <= -1 => timeSpan.ToNaturalLanguage() + " ago",
        _ => "now",
    };
}

可以使用NUnit对其进行如下测试:

[TestCase("a second", 0)]
[TestCase("a second", 1)]
[TestCase("2 seconds", 2)]
[TestCase("a minute", 0, 1)]
[TestCase("5 minutes", 0, 5)]
[TestCase("an hour", 0, 0, 1)]
[TestCase("2 hours", 0, 0, 2)]
[TestCase("a day", 0, 0, 24)]
[TestCase("a day", 0, 0, 0, 1)]
[TestCase("6 days", 0, 0, 0, 6)]
[TestCase("a week", 0, 0, 0, 7)]
[TestCase("4 weeks", 0, 0, 0, 29)]
[TestCase("a month", 0, 0, 0, 30)]
[TestCase("6 months", 0, 0, 0, 6 * 30)]
[TestCase("a year", 0, 0, 0, 365)]
[TestCase("68 years", int.MaxValue)]
public void NaturalLanguageHelpers_TimeSpan(
    string expected,
    int seconds,
    int minutes = 0,
    int hours = 0,
    int days = 0
)
{
    // Arrange
    TimeSpan timeSpan = new(days, hours, minutes, seconds);

    // Act
    string result = timeSpan.ToNaturalLanguage();

    // Assert
    Assert.That(result, Is.EqualTo(expected));
}

[TestCase("now", 0)]
[TestCase("10 minutes ago", 0, -10)]
[TestCase("10 minutes until", 10, 10)]
[TestCase("68 years until", int.MaxValue)]
[TestCase("68 years ago", int.MinValue)]
public void NaturalLanguageHelpers_DateTime(
    string expected,
    int seconds,
    int minutes = 0,
    int hours = 0,
    int days = 0
)
{
    // Arrange
    TimeSpan timeSpan = new(days, hours, minutes, seconds);
    DateTime now = DateTime.Now;
    DateTime dateTime = now + timeSpan;

    // Act
    string result = dateTime.ToNaturalLanguage();

    // Assert
    Assert.That(result, Is.EqualTo(expected));
}

或者作为要点:https://gist.github.com/StudioLE/2dd394e3f792e79adc927ede274df56e