28

考虑以下 2 个场景:场景 1)。今天是 2012 年 5 月 1 日,场景 2)。今天是 2012 年 9 月 1 日。

现在,考虑我们在网页上写下关于某人留下的评论的以下内容:“此评论是在 3 个月零 12 天前写的”。即使陈述完全相同,这两种情况下的天数也总是不同的。在场景 1 中,“3 个月零 12 天”等于102 days。但是,在场景 2 中,“3 个月零 12 天”将是104 days

现在,为了说明我的观点,让我们使用一个不同的例子,假设有人在 2013 年 1 月 30 日在我们的网站上发表了评论,而今天是 2013 年 3 月 10 日。我们的真实 TimeSpan 对象需要知道这个相对日期,并且可以计算出以下内容:

  • 三月有10天,
  • 1 月有 1 天(从 30 日到 31 日)。
  • 二月是一个月,不管它有多少天(即使它是 28 天)。

因此,这意味着 10 天 + 1 天 + 1 个月总计,转换为This comment was posted 1 Month and 11 Days ago.

现在,如果您使用 MS 样式的 TimeSpan 对象(或任何语言的任何 TimeSpan 对象),它将为您提供从 1 月 30 日到 3 月 10 日(39 天)的天数,并且因为 TimeSpan 对象不存储相对日期(我们减去的基本/初始日期以获得 TimeSpan),如果你问它有多少个月和多少天,它会假设一个月有 30 天,甚至最坏,平均值大于 30 天, 并在几天内返回其余的,所以要达到 39 天,它会告诉你已经 1 个月零 9 天,你会得到This comment was posted 1 Month and 9 Days ago信息。请记住,这两种情况都有相同的开始日期和相同的当前/结束日期,是的,Microsoft TimeSpan 对象不允许我们告诉它应该考虑 2013 年 2 月的月份,它给了我们一个完全不同的 TimeSpan,相差一个整2天。实际上,它对我们撒了谎。

问题是,人们会相信这一点,谁知道他们可能有什么看法,他们对过去的看法可能会如何改变,以及他们在试图在自己的脑海中重建过去的事件时可能做出的决定和生活选择,而从来没有注意到或理解代表时间的缺点和固有的失败,这在今天无处不在。他们不会明白编程语言没有意识到(或关心)上个月有 31 天,与 30、29 或 28 天相反——反之亦然,当你增加 TimeSpan 时,这会增加。

这是这篇文章的核心问题。我知道大多数人不会关心这种差异(但请确保我们中的一些人会关心,并且不能把它放在我们的背上),如果这不打扰你,那没关系。我希望它不会打扰我,我会为自己节省一些时间、压力和失望。如果这不麻烦,您可以使用该功能来有效地以文本显示相对时间(可自定义为从秒到年的 1 到 6 个节点),而不是使用它来提供通常可以忽略不计的精度。

令我失望的是,我注意到没有真正的时间跨度对象,如果你得到一个时间跨度,然后做一个,.years否则.months你什么也得不到,你只会得到.days和降低,因为时间跨度对象没有任何东西告诉它哪个月或创建 timeSpan 的年份。因此,它永远不会真正知道它有多少个月,因为每个月的天数在一年内甚至在闰年都不同。

作为对此的回应,我将发布一个我开发的函数,以便获得准确的读数并能够在我的 ASP.NET 网页上返回如下内容...

发表于 4 年 3 个月 14 天 15 小时 18 分 24 秒前

我以为会有一个…

timeSpan.GetActualNumberOf[Months/Days/Hours/etc](当然必须提供基准日期)

......这个数据类型的类型方法,但没有。

您真正需要做的就是在 timeSpan 对象上创建另一个属性,为其提供计算差异的基准日期,然后上面可爱的字符串将很容易计算,并且.year&.month将存在!

更新:我在下面的答案中显着扩展和更新了我的官方答案和代码使用细节,100% 工作答案和代码(完整),准确和准确的相对时间/日期,没有近似值 - 谢谢。

4

8 回答 8

29

以下是如何使用 C# 使用平均值为此添加一些扩展方法:

public static class TimeSpanExtensions
{
    public static int GetYears(this TimeSpan timespan)
    {
        return (int)(timespan.Days/365.2425);
    }
    public static int GetMonths(this TimeSpan timespan)
    {
        return (int)(timespan.Days/30.436875);
    }
}
于 2009-12-18T00:32:35.750 回答
11

你所寻找的确实不是TimeSpan代表的东西。 TimeSpan将间隔表示为滴答计数,而不考虑基数DateTimeCalendar.

一种新DateDifference类型在这里可能更有意义,构造函数或工厂方法采用 base DateTime、 targetDateTime和可选的Calendar(默认为 CultureInfo.CurrentCulture)来计算各种差异组件(年、月等)

编辑:在我看来,Noda Time可能有你需要的工具——Period类“[r]表示以人类时间顺序表示的一段时间:小时、天、周、月等”,特别是Period.Between(then, now, PeriodUnits.AllUnits)似乎是您要求的精确计算 - 但它必然是一个比TimeSpan. Noda Time wiki 上的Key Concepts 页面解释了“人类如何使时间变得混乱”:

撇开天文学和相对论的棘手问题不谈,人类仍然很难进行谈判。如果我们都使用 Unix 时代的刻度来谈论时间,那么就不需要像 Noda Time 这样的库了。

但是不,我们喜欢以年、月、日、周为单位进行交谈——出于某种原因,我们喜欢中午 12 点(令人困惑的是下午 1 点之前)大致是太阳最高的时间......所以我们有时区

不仅如此,而且我们并不都同意有多少个月。不同的文明提出了不同的年份划分方式,以及不同的年份起始数字。这些是日历系统

于 2009-12-18T20:06:47.157 回答
6

好吧,我想迟到更好;)

C# 函数提供一切

这是我的修改版本:

private string GetElapsedTime(DateTime from_date, DateTime to_date) {
int years;
int months;
int days;
int hours;
int minutes;
int seconds;
int milliseconds;

//------------------
// Handle the years.
//------------------
years = to_date.Year - from_date.Year;

//------------------------
// See if we went too far.
//------------------------
DateTime test_date = from_date.AddMonths(12 * years);

if (test_date > to_date)
{
    years--;
    test_date = from_date.AddMonths(12 * years);
}

//--------------------------------
// Add months until we go too far.
//--------------------------------
months = 0;

while (test_date <= to_date)
{
    months++;
    test_date = from_date.AddMonths(12 * years + months);
}

months--;

//------------------------------------------------------------------
// Subtract to see how many more days, hours, minutes, etc. we need.
//------------------------------------------------------------------
from_date = from_date.AddMonths(12 * years + months);
TimeSpan remainder = to_date - from_date;
days = remainder.Days;
hours = remainder.Hours;
minutes = remainder.Minutes;
seconds = remainder.Seconds;
milliseconds = remainder.Milliseconds;

return (years > 0 ? years.ToString() + " years " : "") +
       (months > 0 ? months.ToString() + " months " : "") +
       (days > 0 ? days.ToString() + " days " : "") +
       (hours > 0 ? hours.ToString() + " hours " : "") +
       (minutes > 0 ? minutes.ToString() + " minutes " : "");}
于 2015-07-10T11:15:24.420 回答
5

这是代码的主要答案,请注意,您可以获得任意数量的日期/时间精度、秒和分钟,或秒、分钟和天,最长可达数年(其中包含 6 个部分/段)。如果您指定前两个并且它已超过一年,它将返回“1 年零 3 个月前”并且不会返回其余部分,因为您请求了两个片段。如果它只有几个小时,那么它只会返回“2 小时 1 分钟前”。当然,如果您指定 1、2、3、4、5 或 6 个段(最大为 6,因为秒、分钟、小时、天、月、年仅产生 6 种类型),则同样的规则适用。它还将纠正语法问题,例如“分钟”与“分钟”,具体取决于它是否为 1 分钟或更长,所有类型都相同,以及“字符串”

以下是一些使用示例: bAllowSegments 标识要显示多少段...即:如果为 3,则返回字符串将是(作为示例)... "3 years, 2 months and 13 days"(不包括小时、分钟和秒作为前 3 时间类别被返回),但是,如果日期是较新的日期,例如几天前的日期,则指定相同的段 (3) 将"4 days, 1 hour and 13 minutes ago"改为返回,因此它会考虑所有因素!

如果 bAllowSegments 为 2 它将返回"3 years and 2 months",如果 6 (最大值)将返回"3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds",但是,请注意它会NEVER RETURN像这样"0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago",因为它知道前 3 个段中没有日期数据并忽略它们,即使您指定 6 个段,所以别担心:)。当然,如果有一个段中包含 0,它会在形成字符串时考虑到这一点,并且会显示为"3 days and 4 seconds ago"并忽略“0小时”部分!如果您喜欢,请享受并发表评论。

 Public Function RealTimeUntilNow(ByVal dt As DateTime, Optional ByVal bAllowSegments As Byte = 2) As String
  ' bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)...
  ' "3 years, 2 months and 13 days" the top 3 time categories are returned, if bAllowSegments is 2 it would return
  ' "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"
  Dim rYears, rMonths, rDays, rHours, rMinutes, rSeconds As Int16
  Dim dtNow = DateTime.Now
  Dim daysInBaseMonth = Date.DaysInMonth(dt.Year, dt.Month)

  rYears = dtNow.Year - dt.Year
  rMonths = dtNow.Month - dt.Month
  If rMonths < 0 Then rMonths += 12 : rYears -= 1 ' add 1 year to months, and remove 1 year from years.
  rDays = dtNow.Day - dt.Day
  If rDays < 0 Then rDays += daysInBaseMonth : rMonths -= 1
  rHours = dtNow.Hour - dt.Hour
  If rHours < 0 Then rHours += 24 : rDays -= 1
  rMinutes = dtNow.Minute - dt.Minute
  If rMinutes < 0 Then rMinutes += 60 : rHours -= 1
  rSeconds = dtNow.Second - dt.Second
  If rSeconds < 0 Then rSeconds += 60 : rMinutes -= 1

  ' this is the display functionality
  Dim sb As StringBuilder = New StringBuilder()
  Dim iSegmentsAdded As Int16 = 0

  If rYears > 0 Then sb.Append(rYears) : sb.Append(" year" & If(rYears <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMonths > 0 Then sb.AppendFormat(rMonths) : sb.Append(" month" & If(rMonths <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rDays > 0 Then sb.Append(rDays) : sb.Append(" day" & If(rDays <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rHours > 0 Then sb.Append(rHours) : sb.Append(" hour" & If(rHours <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMinutes > 0 Then sb.Append(rMinutes) : sb.Append(" minute" & If(rMinutes <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rSeconds > 0 Then sb.Append(rSeconds) : sb.Append(" second" & If(rSeconds <> 1, "s", "") & "") : iSegmentsAdded += 1

parseAndReturn:

  ' if the string is entirely empty, that means it was just posted so its less than a second ago, and an empty string getting passed will cause an error
  ' so we construct our own meaningful string which will still fit into the "Posted * ago " syntax...

  If sb.ToString = "" Then sb.Append("less than 1 second")

  Return ReplaceLast(sb.ToString.TrimEnd(" ", ",").ToString, ",", " and")

 End Function

当然,您将需要一个“ReplaceLast”函数,它接受一个源字符串和一个指定需要替换的参数,以及另一个指定要替换它的参数,它只替换该字符串的最后一次出现...如果您没有或不想实现它,我已经包含了我的一个,所以在这里,它可以“按原样”工作,无需修改。我知道 reverseit 函数不再需要(存在于 .net 中),但 ReplaceLast 和 ReverseIt 函数是从 pre-.net 时代遗留下来的,所以请原谅它看起来多么过时(仍然 100% 有效,一直在使用em 使用了十多年,可以保证它们没有错误)... :)。此外,如果您使用的是 VB6,则可以使用 StrReverse(将其包裹在使用 .ReverseIt 扩展方法扩展的字符串周围),而不是使用 ReverseIt() 函数(作为扩展方法提供)。因此,不要执行 sReplacable.ReverseIt,而是执行 StrReverse(sReplacable),因为 StrReverse() 是内置的 VB6 函数(并且执行完全相同的操作,反转给定的字符串,仅此而已)。如果您使用 StrReverse() 而不是我的通用 ReverseIt 函数,请随意删除 ReverseIt 函数/扩展。只要您要导入旧版 ms-visualbasic-dll 库,StrReverse() 函数就应该在 .NET 中可用。无论哪种方式都没有区别,我什至在知道 StrReverse() 函数存在之前就编写了 ReverseIt(),并且从那时起就出于习惯使用它(没有真正的理由使用我的而不是内置的通用函数StrReverse)——事实上,我确信 StrReverse (或类似的、更新的 . NET 特定版本的字符串反转函数)将被编写为更有效:)。干杯。

<Extension()> _ 
Public Function ReplaceLast(ByVal sReplacable As String, ByVal sReplaceWhat As String, ByVal sReplaceWith As String) As String 
    ' let empty string arguments run, incase we dont know if we are sending and empty string or not. 
    sReplacable = sReplacable.ReverseIt 
    sReplacable = Replace(sReplacable, sReplaceWhat.ReverseIt, sReplaceWith.ReverseIt, , 1) ' only does first item on reversed version! 
    Return sReplacable.ReverseIt.ToString 
End Function 

<Extension()> _ 
Public Function ReverseIt(ByVal strS As String, Optional ByVal n As Integer = -1) As String 
    Dim strTempX As String = "", intI As Integer 

    If n > strS.Length Or n = -1 Then n = strS.Length 

    For intI = n To 1 Step -1 
        strTempX = strTempX + Mid(strS, intI, 1) 
    Next intI 

    ReverseIt = strTempX + Right(strS, Len(strS) - n) 

End Function 
于 2009-12-24T01:32:59.517 回答
2

使用 .Net 4.5 和CultureInfo类,可以将月份和年份添加到给定日期。

DateTime datetime = DateTime.UtcNow;
int years = 15;
int months = 7;

DateTime yearsAgo = CultureInfo.InvariantCulture.Calendar.AddYears(datetime, -years);
DateTime monthsInFuture = CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);

由于输入很多,我更喜欢创建扩展方法:

public static DateTime AddYears(this DateTime datetime, int years)
{
    return CultureInfo.InvariantCulture.Calendar.AddYears(datetime, years);
}

public static DateTime AddMonths(this DateTime datetime, int months)
{
    return CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);
}

DateTime yearsAgo = datetime.AddYears(-years);
DateTime monthsInFuture = datetime.AddMonths(months);
于 2014-10-14T21:45:32.250 回答
1

我会说当前 TimeSpan 是一个真实的时间跨度对象,即 2008 年 1 月 1 日凌晨 1:31 和 2008 年 2 月 3 日早上 6:45 之间的时间量与 2 月 5 日之间的时间量相同,2008 年下午 1:45 和 2008 年 3 月 9 日下午 6:59。您正在寻找的实际上是两个日期时间之间的差异。

至于 .MakeMagicHappen.gimmeSomethingPretty.surelyMShasThoughtAboutThisDilema 来满足您系统的特定需求,这就是人们雇用您作为程序员的原因。如果您使用的框架绝对可以完成所有工作,那么您的公司只需按下一个按钮,他们的系统就会完全成型,您将与我们其他程序员一起处于失业线上。

于 2009-12-16T18:32:20.893 回答
1

我相信以下方法非常可靠且简单,因为它基于框架日期计算并返回一个可读的经过时间字符串,如 Facebook 的。对葡萄牙语的小词和复数处理感到抱歉,就我而言,这是必要的。

public static string ElapsedTime(DateTime dtEvent)
{
    TimeSpan TS = DateTime.Now - dtEvent;

    int intYears = TS.Days / 365;
    int intMonths = TS.Days / 30;
    int intDays = TS.Days;
    int intHours = TS.Hours;
    int intMinutes = TS.Minutes;
    int intSeconds = TS.Seconds;

    if (intYears > 0) return String.Format("há {0} {1}", intYears, (intYears == 1) ? "ano" : "anos");
    else if (intMonths > 0) return String.Format("há {0} {1}", intMonths, (intMonths == 1) ? "mês" : "meses");
    else if (intDays > 0) return String.Format("há {0} {1}", intDays, (intDays == 1) ? "dia" : "dias");
    else if (intHours > 0) return String.Format("há ± {0} {1}", intHours, (intHours == 1) ? "hora" : "horas");
    else if (intMinutes > 0) return String.Format("há ± {0} {1}", intMinutes, (intMinutes == 1) ? "minuto" : "minutos");
    else if (intSeconds > 0) return String.Format("há ± {0} {1}", intSeconds, (intSeconds == 1) ? "segundo" : "segundos");
    else
    {
        return String.Format("em {0} às {1}", dtEvent.ToShortDateString(), dtEvent.ToShortTimeString());
    }
}
于 2011-05-23T19:01:40.387 回答
0

我接受了接受的答案并将其从 VB.Net 转换为 C#,并进行了一些修改/改进。我摆脱了用于替换字符串的最后一个实例的字符串反转,并使用了一种更直接地查找和替换字符串的最后一个实例的扩展方法。

如何调用该方法的示例:

PeriodBetween(#2/28/2011#, DateTime.UtcNow, 6)

主要方法:

public static string PeriodBetween(DateTime then, DateTime now, byte numberOfPeriodUnits = 2)
{
    // Translated from VB.Net to C# from: https://stackoverflow.com/a/1956265

    // numberOfPeriodUnits identifies how many time period units to show.
    // If numberOfPeriodUnits = 3, function would return:
    //      "3 years, 2 months and 13 days"
    // If numberOfPeriodUnits = 2, function would return:
    //      "3 years and 2 months"
    // If numberOfPeriodUnits = 6, (maximum value), function would return:
    //      "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"

    if (numberOfPeriodUnits > 6 || numberOfPeriodUnits < 1)
    {
        throw new ArgumentOutOfRangeException($"Parameter [{nameof(numberOfPeriodUnits)}] is out of bounds. Valid range is 1 to 6.");
    }

    short Years = 0;
    short Months = 0;
    short Days = 0;
    short Hours = 0;
    short Minutes = 0;
    short Seconds = 0;
    short DaysInBaseMonth = (short)(DateTime.DaysInMonth(then.Year, then.Month));

    Years = (short)(now.Year - then.Year);

    Months = (short)(now.Month - then.Month);
    if (Months < 0)
    {
        Months += 12;
        Years--; // add 1 year to months, and remove 1 year from years.
    }

    Days = (short)(now.Day - then.Day);
    if (Days < 0)
    {
        Days += DaysInBaseMonth;
        Months--;
    }

    Hours = (short)(now.Hour - then.Hour);
    if (Hours < 0)
    {
        Hours += 24;
        Days--;
    }

    Minutes = (short)(now.Minute - then.Minute);
    if (Minutes < 0)
    {
        Minutes += 60;
        Hours--;
    }

    Seconds = (short)(now.Second - then.Second);
    if (Seconds < 0)
    {
        Seconds += 60;
        Minutes--;
    }

    // This is the display functionality.
    StringBuilder TimePeriod = new StringBuilder();
    short NumberOfPeriodUnitsAdded = 0;

    if (Years > 0)
    {
        TimePeriod.Append(Years);
        TimePeriod.Append(" year" + (Years != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Months > 0)
    {
        TimePeriod.AppendFormat(Months.ToString());
        TimePeriod.Append(" month" + (Months != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Days > 0)
    {
        TimePeriod.Append(Days);
        TimePeriod.Append(" day" + (Days != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Hours > 0)
    {
        TimePeriod.Append(Hours);
        TimePeriod.Append(" hour" + (Hours != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Minutes > 0)
    {
        TimePeriod.Append(Minutes);
        TimePeriod.Append(" minute" + (Minutes != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Seconds > 0)
    {
        TimePeriod.Append(Seconds);
        TimePeriod.Append(" second" + (Seconds != 1 ? "s" : "") + "");
        NumberOfPeriodUnitsAdded++;
    }

    ParseAndReturn:
    // If the string is empty, that means the datetime is less than a second in the past.
    // An empty string being passed will cause an error, so we construct our own meaningful
    // string which will still fit into the "Posted * ago " syntax.

    if (TimePeriod.ToString() == "")
    {
        TimePeriod.Append("less than 1 second");
    }

    return TimePeriod.ToString().TrimEnd(' ', ',').ToString().ReplaceLast(",", " and");
}

ReplaceLast 扩展方法:

public static string ReplaceLast(this string source, string search, string replace)
{
    int pos = source.LastIndexOf(search);

    if (pos == -1)
    {
        return source;
    }

    return source.Remove(pos, search.Length).Insert(pos, replace);
}
于 2019-05-20T18:26:05.547 回答