题 计算C#中的相对时间


鉴于具体情况 DateTime 值,我如何显示相对时间,如:

  • 2小时前
  • 3天前
  • 一个月前

1344
2017-09-08 14:02


起源


如果你想计算从现在到未来的相对时间怎么办? - Jhonny D. Cano -Leftware-
moment.js是一个非常好的日期解析库。您可以考虑使用它(服务器端或客户端),具体取决于您的需求。只是因为没人在这里提到它 - code ninja
有.net包 github.com/NickStrupat/TimeAgo 这几乎就是要问的问题。 - Rossco


答案:


杰夫, 你的代码 很好,但可以使用常量更清晰(如代码完成中所示)。

const int SECOND = 1;
const int MINUTE = 60 * SECOND;
const int HOUR = 60 * MINUTE;
const int DAY = 24 * HOUR;
const int MONTH = 30 * DAY;

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

if (delta < 1 * MINUTE)
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";

if (delta < 2 * MINUTE)
  return "a minute ago";

if (delta < 45 * MINUTE)
  return ts.Minutes + " minutes ago";

if (delta < 90 * MINUTE)
  return "an hour ago";

if (delta < 24 * HOUR)
  return ts.Hours + " hours ago";

if (delta < 48 * HOUR)
  return "yesterday";

if (delta < 30 * DAY)
  return ts.Days + " days ago";

if (delta < 12 * MONTH)
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
else
{
  int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
  return years <= 1 ? "one year ago" : years + " years ago";
}

891
2017-09-08 14:02



我充满激情地讨厌这些常数。这看起来对任何人都不对吗? Thread.Sleep(1 * MINUTE)?因为这是错误的1000倍。 - Roman Starkov
const int SECOND = 1; 所以很奇怪一秒钟就是一秒钟。 - seriousdev
这种类型的代码几乎不可能本地化。如果您的应用只需要保留英语,那么很好。但是,如果你跳到其他语言,你会讨厌做这样的逻辑。所以你们都知道...... - Nik Reiman
我认为如果常量被重命名以准确描述其中的值,那么它将更容易理解。所以SecondsPerMinute = 60; MinutesPerHour = 60; SecondsPerHour = MinutesPerHour * SecondsPerHour;只要将其称为MINUTE = 60,就不允许读者确定该值是什么。 - slolife
为什么没有人(除了乔)关心错误的'昨天'或'天前'价值???昨天不是一小时计算,而是一天一天的计算。所以,是的,这是一个错误的代码,至少在两个常见的情况下。 - CtrlX


jquery.timeago插件

Jeff,因为Stack Overflow广泛使用jQuery,我推荐 jquery.timeago插件

优点:

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

只需将它附加到DOM准备好的时间戳上:

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

这将全部转变 abbr 具有一类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个月前。随着时间的推移,时间戳将自动更新。

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


345



Seb,如果禁用了Javascript,则会显示最初放在abbr标记之间的字符串。通常,这只是您希望的任何格式的日期或时间。 Timeago优雅地退化。它并没有变得简单多了。 - Ryan McGeary
Ryan,我建议SO之前使用timeago。杰夫的回答让我哭了,我建议你坐下来: stackoverflow.uservoice.com/pages/1722-general/suggestions/... - Rob Fonseca-Ensor
嘿,谢谢Rob。没关系。它几乎没有引起注意,特别是在转换期间只有一个数字发生变化时,虽然SO页面有很多时间戳。我原以为他至少会欣赏页面缓存的好处,即使他选择避免自动更新。我相信Jeff本可以提供反馈来改进插件。我知道网站喜欢安慰 arstechnica.com 用它。 - Ryan McGeary
@Rob Fonseca-Ensor - 现在它让我哭了。如何每分钟更新一次,以显示准确的信息, 以任何方式 有关文字每秒闪烁一次? - Daniel Earwicker
问题是关于C#,我没看到jQuery插件是如何相关的。 - BartoszKP


我就是这样做的

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 < 120)
{
  return "a minute ago";
}
if (delta < 2700) // 45 * 60
{
  return ts.Minutes + " minutes ago";
}
if (delta < 5400) // 90 * 60
{
  return "an hour ago";
}
if (delta < 86400) // 24 * 60 * 60
{
  return ts.Hours + " hours ago";
}
if (delta < 172800) // 48 * 60 * 60
{
  return "yesterday";
}
if (delta < 2592000) // 30 * 24 * 60 * 60
{
  return ts.Days + " days ago";
}
if (delta < 31104000) // 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";

建议?注释?如何改进这种算法?


320



“<48 * 60 * 60s”是“昨天”的一个非常规的定义。如果是星期三上午9点,你真的会想到星期一上午9点​​01分为“昨天”。我曾经想过,在午夜之前/之后应考虑昨天或“n天前”的算法。 - Joe
编译器通常非常擅长预先计算常量表达式,如24 * 60 * 60,因此您可以直接使用它们而不是自己计算它为86400并将原始表达式放在注释中 - zvolkov
注意到这个功能不包括周 - jray
@bzlm我想我做的是我正在做的一个项目。我的动机是提醒其他人这个代码示例中省略了几周。至于如何做到这一点,对我来说似乎很直接。 - jray
我认为改进算法的好方法是显示2个单位,如“2个月21天前”,“1小时40分钟前”,以提高准确性。 - Evgeny Levin


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

我更喜欢这个版本的简洁性,以及添加新刻度点的能力。 这可以用a封装 Latest() 扩展到Timespan而不是那个长1的班轮,但为了简洁发布,这样做。 这修复了一小时前,1小时前,提供了一个小时,直到2个小时过去


84



我正在使用此函数遇到各种问题,例如,如果你模拟'theDate = DateTime.Now.AddMinutes(-40);'我正在'40小时前',但是有了迈克尔的重构代码响应,它在'40分钟前返回正确'? - GONeale
我认为你错过零,试试:long from =(DateTime.Now.Ticks - theDate.Ticks)/ 10000000; - robnardo
嗯,虽然这段代码可能有效但假设词典中键的顺序将按特定顺序排列是不正确的。 Dictionary使用Object.GetHashCode(),它不返回long但是int!。如果您希望对它们进行排序,那么您应该使用SortedList <long,string>。在if / else if /.../ else中评估阈值有什么问题?您获得相同数量的比较。仅供参考,long.MaxValue的散列与int.MinValue相同! - CodeMonkeyKing
OP忘了t.Days> 30? t.Days / 30: - Lars Holm Jensen
要解决@CodeMonkeyKing提到的问题,您可以使用 SortedDictionary 而不是平原 Dictionary:用法相同,但它确保键被排序。但即使这样,该算法也存在缺陷,因为 RelativeDate(DateTime.Now.AddMonths(-3).AddDays(-3)) 回报 “95个月前”,无论你使用哪种字典类型,这是不正确的(它应该返回“3个月前”或“4个月前”,具体取决于你正在使用的阈值) - 即使-3在过去没有创建日期一年(我已经在十二月对此进行了测试,因此在这种情况下不应该发生这种情况)。 - Matt


这里是来自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";
    }
}    

68



问题是 C#标记 为什么 PHP代码 ? - Kiquenet


public static string ToRelativeDate(DateTime input)
{
    TimeSpan oSpan = DateTime.Now.Subtract(input);
    double TotalMinutes = oSpan.TotalMinutes;
    string Suffix = " ago";

    if (TotalMinutes < 0.0)
    {
        TotalMinutes = Math.Abs(TotalMinutes);
        Suffix = " from now";
    }

    var aValue = new SortedList<double, Func<string>>();
    aValue.Add(0.75, () => "less than a minute");
    aValue.Add(1.5, () => "about a minute");
    aValue.Add(45, () => string.Format("{0} minutes", Math.Round(TotalMinutes)));
    aValue.Add(90, () => "about an hour");
    aValue.Add(1440, () => string.Format("about {0} hours", Math.Round(Math.Abs(oSpan.TotalHours)))); // 60 * 24
    aValue.Add(2880, () => "a day"); // 60 * 48
    aValue.Add(43200, () => string.Format("{0} days", Math.Floor(Math.Abs(oSpan.TotalDays)))); // 60 * 24 * 30
    aValue.Add(86400, () => "about a month"); // 60 * 24 * 60
    aValue.Add(525600, () => string.Format("{0} months", Math.Floor(Math.Abs(oSpan.TotalDays / 30)))); // 60 * 24 * 365 
    aValue.Add(1051200, () => "about a year"); // 60 * 24 * 365 * 2
    aValue.Add(double.MaxValue, () => string.Format("{0} years", Math.Floor(Math.Abs(oSpan.TotalDays / 365))));

    return aValue.First(n => TotalMinutes < n.Key).Value.Invoke() + Suffix;
}

http://refactormycode.com/codes/493-twitter-esque-relative-dates

C#6版本:

static readonly SortedList<double, Func<TimeSpan, string>> offsets = 
   new SortedList<double, Func<TimeSpan, string>>
{
    { 0.75, _ => "less than a minute"},
    { 1.5, _ => "about a minute"},
    { 45, x => $"{x.TotalMinutes:F0} minutes"},
    { 90, x => "about an hour"},
    { 1440, x => $"about {x.TotalHours:F0} hours"},
    { 2880, x => "a day"},
    { 43200, x => $"{x.TotalDays:F0} days"},
    { 86400, x => "about a month"},
    { 525600, x => $"{x.TotalDays / 30:F0} months"},
    { 1051200, x => "about a year"},
    { double.MaxValue, x => $"{x.TotalDays / 365:F0} years"}
};

public static string ToRelativeDate(this DateTime input)
{
    TimeSpan x = DateTime.Now - input;
    string Suffix = x.TotalMinutes > 0 ? " ago" : " from now";
    x = new TimeSpan(Math.Abs(x.Ticks));
    return offsets.First(n => x.TotalMinutes < n.Key).Value(x) + Suffix;
}

61



这是非常好的IMO :)这也可以作为扩展方法重构?字典是否会变为静态,所以它只创建一次并从之后引用? - Pure.Krome
Pure.Krome: stackoverflow.com/questions/11/how-do-i-calculate-relative-time/... - Chris Charabaruk
您可能希望将该字典拉出到字段中,以便减少实例化和GC流失。你必须改变 Func<string> 至 Func<double>。 - Drew Noakes
在javascript中 - jsfiddle.net/drzaus/eMUzF - drzaus


这是我作为DateTime类的扩展方法添加的一个实现,它处理未来和过去的日期,并提供一个近似选项,允许您指定您要查找的详细程度(“3小时前”vs“3小时, 23分钟12秒前“):

using System.Text;

/// <summary>
/// Compares a supplied date to the current date and generates a friendly English 
/// comparison ("5 days ago", "5 days from now")
/// </summary>
/// <param name="date">The date to convert</param>
/// <param name="approximate">When off, calculate timespan down to the second.
/// When on, approximate to the largest round unit of time.</param>
/// <returns></returns>
public static string ToRelativeDateString(this DateTime value, bool approximate)
{
    StringBuilder sb = new StringBuilder();

    string suffix = (value > DateTime.Now) ? " from now" : " ago";

    TimeSpan timeSpan = new TimeSpan(Math.Abs(DateTime.Now.Subtract(value).Ticks));

    if (timeSpan.Days > 0)
    {
        sb.AppendFormat("{0} {1}", timeSpan.Days,
          (timeSpan.Days > 1) ? "days" : "day");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Hours > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty,
          timeSpan.Hours, (timeSpan.Hours > 1) ? "hours" : "hour");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Minutes > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty, 
          timeSpan.Minutes, (timeSpan.Minutes > 1) ? "minutes" : "minute");
        if (approximate) return sb.ToString() + suffix;
    }
    if (timeSpan.Seconds > 0)
    {
        sb.AppendFormat("{0}{1} {2}", (sb.Length > 0) ? ", " : string.Empty, 
          timeSpan.Seconds, (timeSpan.Seconds > 1) ? "seconds" : "second");
        if (approximate) return sb.ToString() + suffix;
    }
    if (sb.Length == 0) return "right now";

    sb.Append(suffix);
    return sb.ToString();
}

48