asp.net 具有.Years和.Months的真实的时间跨度对象

3duebb1j  于 2023-01-22  发布在  .NET
关注(0)|答案(8)|浏览(104)

请考虑以下两种情况:情景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对象需要知道这个相对日期,并可以计算出以下内容:

  • 三月有十天,
  • 一月有一天(从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月,给了我们一个完全不同的时间跨度,整整偏离了2天。实际上,它欺骗了我们。
问题是,人们会相信这一点,谁知道他们可能会有什么感知,他们对过去的感知可能会如何变化,以及当他们试图在自己的头脑中重建过去的事件时可能会做出的决定和生活选择,而从来没有注意到或理解的缺点和固有的失败表示时间是如此普遍的今天。他们将不会理解编程语言不'我没有意识到(或不在乎)上个月有31天,而不是30天、29天或28天--反之亦然,而且当你增加TimeSpan时,这些天就会加起来。
这是这篇文章的核心问题,我明白大多数人不会在意这个差异(但要确保我们中的一些人这样做,不能有这个在我们的背上),如果这不打扰你,没关系。我希望它没有打扰我,我会节省自己一些时间,压力和失望。如果这不是一个麻烦,您可以使用该函数来高效地文本显示相对时间(可定制为1到6个节点,从秒到年),而不是使用它来提供通常可以忽略不计的精确度。
令我失望的是,我注意到没有真实的的时间跨度对象,如果你得到一个时间跨度,然后执行.years.months,你将一无所获,你只会得到.days和更小的值,因为timeSpan对象没有任何东西来告诉它timeSpan是在哪一年或哪一月创建的,因此它永远不会知道它到底有多少个月。It“自从一年中每个月的天数不同,甚至在闰年中更是如此。
为了回应这个问题,我将发布一个我开发的函数,以获得准确的读数,并能够在我的ASP.NET网页上返回如下内容...
4年3个月14天15小时18分24秒前发表
我以为会有...
timeSpan.GetActualNumberOf[Months/Days/Hours/etc](当然必须提供基准日期)
...此数据类型上的类型方法,但没有。
您真正需要做的就是在timeSpan对象上创建另一个属性,为它提供一个计算差值的基准日期,然后上面可爱的字符串将非常容易计算,并且.year & .month将存在!
更新:我已经显着扩大和更新了我的官方答案和代码使用细节在我的答案如下,100%的工作答案和代码(完整),准确和确切的相对时间/日期,没有近似-谢谢。

hgqdbh6s

hgqdbh6s1#

下面是如何用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);
    }
}
drnojrws

drnojrws2#

您要查找的内容确实不是TimeSpan所表示的内容。TimeSpan将间隔表示为刻度计数,而不考虑基数DateTimeCalendar
新的DateDifference类型在这里可能更有意义,它的构造函数或工厂方法接受一个基DateTime、一个目标DateTime和可选的Calendar(默认为CultureInfo.CurrentCulture),使用它们来计算各种差异分量(年、月等)。

**编辑:**在我看来,Noda Time可能拥有您所需的工具-Period类“[r]代表一段时间,以人类的时间顺序表示:小时、天、周、月等等”,特别是Period.Between(then, now, PeriodUnits.AllUnits)似乎是你所要求的精确计算--但它必然是一个比TimeSpan复杂得多的类。野田时间维基上的Key Concepts page解释了“人类如何把时间弄得一团糟”:

撇开天文学和相对论的微妙之处不谈,人类仍然让时间变得难以谈判。如果我们都用Unix时代的滴答声来谈论时间,就不需要像野田时间这样的图书馆了。
但是不,我们喜欢用年、月、天、周来说话--出于某种原因,我们喜欢把下午12点(令人困惑的是,这是下午1点之前的时间)大致定为太阳最高的时间......所以我们有“时区”。
不仅如此,我们对月份的划分也不尽相同。不同的文明有不同的划分方法,也有不同的年份数字。这些就是“日历系统”。

ep6jt1vc

ep6jt1vc3#

好吧,我想最好迟一点;)
C# function giving everything
这是我修改过的版本:

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 " : "");}
uidvcgyl

uidvcgyl4#

Here is the main answer with code, please note that you can get any number of dates/times accuracy, seconds & minutes, or seconds, minutes and days, anywhere up to years (which would contain 6 parts/segments). If you specify top two and it's over a year old, it will return "1 year and 3 months ago" and won't return the rest because you've requested two segments. if it's only a few hours old, then it will only return "2 hours and 1 minute ago". Of course, same rules apply if you specify 1, 2, 3, 4, 5 or 6 segmets (maxes out at 6 because seconds, minutes, hours, days, months, years only make 6 types). It will also correct grammer issues like "minutes" vs "minute" depending on if it's 1 minute or more, same for all types, and the "string" generated will always be grammatically correct.
Here are some examples for use: 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" (won't include hours, minutes and seconds as the top 3 time categories are returned), if however, the date was a newer date, such as something a few days ago, specifying the same segments (3) will return "4 days, 1 hour and 13 minutes ago" instead, so it takes everything into account!
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" , but, be reminded that it will NEVER RETURN something like this "0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago" as it understands there is no date data in the top 3 segments and ignores them, even if you specify 6 segments, so don't worry :). Of course, if there is a segment with 0 in it, it will take that into account when forming the string, and will display as "3 days and 4 seconds ago" and ignoring the "0 hours" part! Enjoy and please comment if you like.

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

Of course, you will need a "ReplaceLast" function, which takes a source string, and an argument specifying what needs to be replaced, and another arg specifying what you want to replace it with, and it only replaces the last occurance of that string... i've included my one if you don't have one or dont want to implement it, so here it is, it will work "as is" with no modification needed. I know the reverseit function is no longer needed (exists in .net) but the ReplaceLast and the ReverseIt func are carried over from the pre-.net days, so please excuse how dated it may look (still works 100% tho, been using em for over ten years, can guarante they are bug free)... :). Also, if you are using VB6, you can use StrReverse (wrapping it around the string extended with the .ReverseIt extension method), instead of using the ReverseIt() function (provided as an extension method). So, instead of doing sReplacable.ReverseIt, you'd do StrReverse(sReplacable) as StrReverse() is a built in VB6 function (and does the exact same thing, reverses a given string, and does nothing more). If you use StrReverse() instead of my generic ReverseIt function, feel free to delete the ReverseIt function/extension. StrReverse() function should be available in .NET as long as you are importing the legacy ms-visualbasic-dll library. Makes no difference either way, I had written ReverseIt() before I even know a StrReverse() function had existed, and had been using it ever since out of habit (no real reason to use mine as opposed to the in-built generic function StrReverse) - in fact, I'm sure StrReverse (or a similar, newer .NET specific version of a string reversing function) would be written to be more efficient :). cheers.

<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
qcuzuvrc

qcuzuvrc5#

使用.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);
klh5stk1

klh5stk16#

我认为当前TimeSpan是一个真实的时间跨度对象,即2008年1月1日1:31 a.m.到2008年2月3日6:45 a.m.之间的时间量与2008年2月5日1:45 p.m.到2008年3月9日6:59 p.m.之间的时间量相同。实际上,您要查找的是两个日期时间之间的差值。
至于.MakeMagicHappen.gimmeSomethingPretty.surelyMShasThoughtAboutThisDilema来满足你的系统的特定需求,这就是为什么人们雇佣你作为程序员。如果你使用的框架做了所有的事情,你的公司只需要按下一个按钮,他们的系统就会完全成形,你就会和我们其他程序员沿着站在失业线上。

ukxgm1gy

ukxgm1gy7#

我相信下面的方法是非常可信和直接的,因为它基于框架的日期计算,并返回可读的运行时间字符串,就像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());
    }
}
sy5wg1nm

sy5wg1nm8#

我接受了被接受的答案,并将其从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);
}

相关问题