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

2小时前3天前一个月前


当前回答

iPhone Objective-C版本

+ (NSString *)timeAgoString:(NSDate *)date {
    int delta = -(int)[date timeIntervalSinceNow];

    if (delta < 60)
    {
        return delta == 1 ? @"one second ago" : [NSString stringWithFormat:@"%i seconds ago", delta];
    }
    if (delta < 120)
    {
        return @"a minute ago";
    }
    if (delta < 2700)
    {
        return [NSString stringWithFormat:@"%i minutes ago", delta/60];
    }
    if (delta < 5400)
    {
        return @"an hour ago";
    }
    if (delta < 24 * 3600)
    {
        return [NSString stringWithFormat:@"%i hours ago", delta/3600];
    }
    if (delta < 48 * 3600)
    {
        return @"yesterday";
    }
    if (delta < 30 * 24 * 3600)
    {
        return [NSString stringWithFormat:@"%i days ago", delta/(24*3600)];
    }
    if (delta < 12 * 30 * 24 * 3600)
    {
        int months = delta/(30*24*3600);
        return months <= 1 ? @"one month ago" : [NSString stringWithFormat:@"%i months ago", months];
    }
    else
    {
        int years = delta/(12*30*24*3600);
        return years <= 1 ? @"one year ago" : [NSString stringWithFormat:@"%i years ago", years];
    }
}

其他回答

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行,但为了发布的简洁,这可以。这修复了一小时前、一小时前的问题,提供了一个小时直到两小时过去

鉴于全世界和她的丈夫似乎都在发布代码样本,这是我不久前根据这些答案写的。

我特别需要这个代码可以本地化。所以我有两个类——Grammar,它指定了可本地化的术语,FuzzyDateExtensions,它包含一系列扩展方法。我不需要处理未来的日期时间,因此不尝试使用此代码处理它们。

为了简洁起见,我在源代码中保留了一些XMLdoc,但删除了大部分(显而易见的地方)。我也没有把每个班级成员都包括在这里:

public class Grammar
{
    /// <summary> Gets or sets the term for "just now". </summary>
    public string JustNow { get; set; }
    /// <summary> Gets or sets the term for "X minutes ago". </summary>
    /// <remarks>
    ///     This is a <see cref="String.Format"/> pattern, where <c>{0}</c>
    ///     is the number of minutes.
    /// </remarks>
    public string MinutesAgo { get; set; }
    public string OneHourAgo { get; set; }
    public string HoursAgo { get; set; }
    public string Yesterday { get; set; }
    public string DaysAgo { get; set; }
    public string LastMonth { get; set; }
    public string MonthsAgo { get; set; }
    public string LastYear { get; set; }
    public string YearsAgo { get; set; }
    /// <summary> Gets or sets the term for "ages ago". </summary>
    public string AgesAgo { get; set; }

    /// <summary>
    ///     Gets or sets the threshold beyond which the fuzzy date should be
    ///     considered "ages ago".
    /// </summary>
    public TimeSpan AgesAgoThreshold { get; set; }

    /// <summary>
    ///     Initialises a new <see cref="Grammar"/> instance with the
    ///     specified properties.
    /// </summary>
    private void Initialise(string justNow, string minutesAgo,
        string oneHourAgo, string hoursAgo, string yesterday, string daysAgo,
        string lastMonth, string monthsAgo, string lastYear, string yearsAgo,
        string agesAgo, TimeSpan agesAgoThreshold)
    { ... }
}

FuzzyDateString类包含:

public static class FuzzyDateExtensions
{
    public static string ToFuzzyDateString(this TimeSpan timespan)
    {
        return timespan.ToFuzzyDateString(new Grammar());
    }

    public static string ToFuzzyDateString(this TimeSpan timespan,
        Grammar grammar)
    {
        return GetFuzzyDateString(timespan, grammar);
    }

    public static string ToFuzzyDateString(this DateTime datetime)
    {
        return (DateTime.Now - datetime).ToFuzzyDateString();
    }

    public static string ToFuzzyDateString(this DateTime datetime,
       Grammar grammar)
    {
        return (DateTime.Now - datetime).ToFuzzyDateString(grammar);
    }


    private static string GetFuzzyDateString(TimeSpan timespan,
       Grammar grammar)
    {
        timespan = timespan.Duration();

        if (timespan >= grammar.AgesAgoThreshold)
        {
            return grammar.AgesAgo;
        }

        if (timespan < new TimeSpan(0, 2, 0))    // 2 minutes
        {
            return grammar.JustNow;
        }

        if (timespan < new TimeSpan(1, 0, 0))    // 1 hour
        {
            return String.Format(grammar.MinutesAgo, timespan.Minutes);
        }

        if (timespan < new TimeSpan(1, 55, 0))    // 1 hour 55 minutes
        {
            return grammar.OneHourAgo;
        }

        if (timespan < new TimeSpan(12, 0, 0)    // 12 hours
            && (DateTime.Now - timespan).IsToday())
        {
            return String.Format(grammar.HoursAgo, timespan.RoundedHours());
        }

        if ((DateTime.Now.AddDays(1) - timespan).IsToday())
        {
            return grammar.Yesterday;
        }

        if (timespan < new TimeSpan(32, 0, 0, 0)    // 32 days
            && (DateTime.Now - timespan).IsThisMonth())
        {
            return String.Format(grammar.DaysAgo, timespan.RoundedDays());
        }

        if ((DateTime.Now.AddMonths(1) - timespan).IsThisMonth())
        {
            return grammar.LastMonth;
        }

        if (timespan < new TimeSpan(365, 0, 0, 0, 0)    // 365 days
            && (DateTime.Now - timespan).IsThisYear())
        {
            return String.Format(grammar.MonthsAgo, timespan.RoundedMonths());
        }

        if ((DateTime.Now - timespan).AddYears(1).IsThisYear())
        {
            return grammar.LastYear;
        }

        return String.Format(grammar.YearsAgo, timespan.RoundedYears());
    }
}

我想实现的一件关键事情,以及本地化,就是“今天”只意味着“这个日历日”,所以IsToday、IsThisMonth和IsThisYear方法如下:

public static bool IsToday(this DateTime date)
{
    return date.DayOfYear == DateTime.Now.DayOfYear && date.IsThisYear();
}

舍入方法如下(我已包括RoundedMonths,因为这有点不同):

public static int RoundedDays(this TimeSpan timespan)
{
    return (timespan.Hours > 12) ? timespan.Days + 1 : timespan.Days;
}

public static int RoundedMonths(this TimeSpan timespan)
{
    DateTime then = DateTime.Now - timespan;

    // Number of partial months elapsed since 1 Jan, AD 1 (DateTime.MinValue)
    int nowMonthYears = DateTime.Now.Year * 12 + DateTime.Now.Month;
    int thenMonthYears = then.Year * 12 + then.Month;                    

    return nowMonthYears - thenMonthYears;
}

我希望人们觉得这很有用和/或有趣:o)

文森特接受的答案做出了许多武断的决定。为什么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

下面是我作为DateTime类的扩展方法添加的一个实现,它处理未来和过去的日期,并提供了一个近似选项,允许您指定要查找的详细程度(“3小时前”与“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();
}
public string getRelativeDateTime(DateTime date)
{
    TimeSpan ts = DateTime.Now - date;
    if (ts.TotalMinutes < 1)//seconds ago
        return "just now";
    if (ts.TotalHours < 1)//min ago
        return (int)ts.TotalMinutes == 1 ? "1 Minute ago" : (int)ts.TotalMinutes + " Minutes ago";
    if (ts.TotalDays < 1)//hours ago
        return (int)ts.TotalHours == 1 ? "1 Hour ago" : (int)ts.TotalHours + " Hours ago";
    if (ts.TotalDays < 7)//days ago
        return (int)ts.TotalDays == 1 ? "1 Day ago" : (int)ts.TotalDays + " Days ago";
    if (ts.TotalDays < 30.4368)//weeks ago
        return (int)(ts.TotalDays / 7) == 1 ? "1 Week ago" : (int)(ts.TotalDays / 7) + " Weeks ago";
    if (ts.TotalDays < 365.242)//months ago
        return (int)(ts.TotalDays / 30.4368) == 1 ? "1 Month ago" : (int)(ts.TotalDays / 30.4368) + " Months ago";
    //years ago
    return (int)(ts.TotalDays / 365.242) == 1 ? "1 Year ago" : (int)(ts.TotalDays / 365.242) + " Years ago";
}

一个月和一年中的天数的转换值取自谷歌。