使用年来格式化TimeSpan

28

我有一个类,其中包含两个日期属性:FirstDayLastDay。其中LastDay是可空的。我想生成一个格式为"x year(s) y day(s)"的字符串。如果总年数少于1,则省略年部分。如果总天数少于1,则省略天部分。如果年份或天数为0,则它们应该分别说"day/year"而不是"days/years"。

示例:
2.2年:            "2 years 73 days"
1.002738年:   "1 year 1 day"
0.2年:             "73 days"
2年:                "2 years"

我的方法可以工作,但是很长:

private const decimal DaysInAYear = 365.242M;

public string LengthInYearsAndDays
{
    get
    {
        var lastDay = this.LastDay ?? DateTime.Today;
        var lengthValue = lastDay - this.FirstDay;

        var builder = new StringBuilder();

        var totalDays = (decimal)lengthValue.TotalDays;
        var totalYears = totalDays / DaysInAYear;
        var years = (int)Math.Floor(totalYears);

        totalDays -= (years * DaysInAYear);
        var days = (int)Math.Floor(totalDays);

        Func<int, string> sIfPlural = value =>
            value > 1 ? "s" : string.Empty;

        if (years > 0)
        {
            builder.AppendFormat(
                CultureInfo.InvariantCulture,
                "{0} year{1}",
                years,
                sIfPlural(years));

            if (days > 0)
            {
                builder.Append(" ");
            }
        }

        if (days > 0)
        {
            builder.AppendFormat(
                CultureInfo.InvariantCulture,
                "{0} day{1}",
                days,
                sIfPlural(days));
        }

        var length = builder.ToString();
        return length;
    }
}

有更简洁但仍可读的方法吗?


2
http://codereview.stackexchange.com/ - Kashif
6个回答

47

由于取决于起始点和终止点,所以TimeSpan没有一个明智的“年”概念。(月份类似 - 29天有多少个月?嗯,这取决于...)

不过,我的Noda Time项目使这变得非常简单:

using System;
using NodaTime;

public class Test
{
    static void Main(string[] args)
    {
        LocalDate start = new LocalDate(2010, 6, 19);
        LocalDate end = new LocalDate(2013, 4, 11);
        Period period = Period.Between(start, end,
                                       PeriodUnits.Years | PeriodUnits.Days);

        Console.WriteLine("Between {0} and {1} are {2} years and {3} days",
                          start, end, period.Years, period.Days);
    }
}

输出:

Between 19 June 2010 and 11 April 2013 are 2 years and 296 days

14
你可以完全相信Jon在时间日期方面的专业知识。 - JDB
8
额,又是一个额外的第三方库——但是Jon Skeet的库相当令人信服。 - tofutim
这根本就没有意义。年份可以通过起始点和结束点来计算。这与它没有意义无关,而是因为微软没有提供支持。可以使用DateTimeOffset,但他们将最小常量值设为0001/01/01,而不是正确地允许0000/00/00,以便人们可以计算差异。 - Shadowblitz16
@Shadowblitz16:“年份可以通过起始点和结束点计算”- 当然,但这不是TimeSpan所代表的。而且,我认为允许0000/00/00并不“正确”或有帮助。 - Jon Skeet
@Jon Skeet 我不同意。在获取两个日期之间的差异时,这实际上是有帮助的。如果天、月或年没有差异,那么它将为零,但 C# 不允许这样做,因此我必须手动分别计算它们。 - Shadowblitz16
1
@Shadowblitz16:不,如果你有两个有效的日期,你不需要一个完全无效的“额外”日期(即0000/00/00)。我不清楚你到底想要做什么,但是如果“让我们在系统中引入一个无效的日期”是实现它的有用方法,那我会感到非常惊讶。(请记住,我花了很多时间考虑日期/时间API。)如果你想进一步探讨这个问题,我建议你提出一个新问题,并更详细地说明你真正想要实现什么。 - Jon Skeet

8
public string GetAgeText(DateTime birthDate)
{
        const double ApproxDaysPerMonth = 30.4375;
        const double ApproxDaysPerYear = 365.25;

        /*
        The above are the average days per month/year over a normal 4 year period
        We use these approximations as they are more accurate for the next century or so
        After that you may want to switch over to these 400 year approximations

           ApproxDaysPerMonth = 30.436875
           ApproxDaysPerYear  = 365.2425 

          How to get theese numbers:
            The are 365 days in a year, unless it is a leepyear.
            Leepyear is every forth year if Year % 4 = 0
            unless year % 100 == 1
            unless if year % 400 == 0 then it is a leep year.

            This gives us 97 leep years in 400 years. 
            So 400 * 365 + 97 = 146097 days.
            146097 / 400      = 365.2425
            146097 / 400 / 12 = 30,436875

        Due to the nature of the leap year calculation, on this side of the year 2100
        you can assume every 4th year is a leap year and use the other approximatiotions

        */
    //Calculate the span in days
    int iDays = (DateTime.Now - birthDate).Days;

    //Calculate years as an integer division
    int iYear = (int)(iDays / ApproxDaysPerYear);

    //Decrease remaing days
    iDays -= (int)(iYear * ApproxDaysPerYear);

    //Calculate months as an integer division
    int iMonths = (int)(iDays / ApproxDaysPerMonth);

    //Decrease remaing days
    iDays -= (int)(iMonths * ApproxDaysPerMonth);

    //Return the result as an string   
    return string.Format("{0} years, {1} months, {2} days", iYear, iMonths, iDays);
}

修改您的最后一行代码为 return (iYear > 0 ? $"{iYear} 年, " : "") + (iMonths > 0 ? $"{iMonths} 月, " : "") + (iDays > 0 ? $"{iDays} 天" : "").Trim(' ', ','); 以清理输出。是一个简单有效的解决方案。 - Thymine
谢谢提供这个数学公式。我已经用它创建了一个 ToHumanReadableString 函数。https://dev59.com/LmQn5IYBdhLWcg3woIUF#75325021 - TheBigNeo

0

我认为这应该可以工作:

public static int DiffYears(DateTime dateValue1, DateTime dateValue2)
{
    var intToCompare1 = Convert.ToInt32(dateValue1.ToString("yyyyMMdd"));
    var intToCompare2 = Convert.ToInt32(dateValue2.ToString("yyyyMMdd"));
    return (intToCompare2 - intToCompare1) / 10000;
}

你在这里渲染月份和日期以进行转换为“int”,但随后除以一万,强制转换为Int。这几乎相当于按精度计算dateValue1.Year - dateValue2.year。结果将是一个单独的数字,指示完全过去了多少年,而不考虑任何可能的354个尾随天数。从语义上讲,这也非常可疑。 - Kana Ki

0
Public Function TimeYMDBetween(StartDate As DateTime, EndDate As DateTime) As String
    Dim Years As Integer = EndDate.Year - StartDate.Year
    Dim Months As Integer = EndDate.Month - StartDate.Month
    Dim Days As Integer = EndDate.Day - StartDate.Day
    Dim DaysLastMonth As Integer

    'figure out how many days were in last month
    If EndDate.Month = 1 Then
        DaysLastMonth = DateTime.DaysInMonth(EndDate.Year - 1, 12)
    Else
        DaysLastMonth = DateTime.DaysInMonth(EndDate.Year, EndDate.Month - 1)
    End If

    'adjust for negative days
    If Days < 0 Then
        Months = Months - 1
        Days = Days + DaysLastMonth 'borrowing from last month
    End If

    'adjust for negative Months
    If Months < 0 Then 'startdate hasn't happend this year yet
        Years = Years - 1
        Months = Months + 12
    End If

    Return Years.ToString() + " Years, " + Months.ToString() + " Months and " + Days.ToString() + " Days"

End Function

在解释代码的工作原理时,最好能够说明原因。回答问题时提供更多细节总是一个好主意。 - Charlie Fish

0

我不会使用 TimeSpan 来做这件事。因为日期计算一旦超过天数就会变得棘手,因为一个月和一年的天数不再是恒定的。这很可能是为什么 TimeSpan 不包含 YearsMonths 属性的原因。相反,我会确定两个 DateTime 值之间的年/月/日等数量,并相应地显示结果。


1
话虽如此,看起来楼主已经就他的目的达成了一个足够的妥协:每年365.242M天。 - JDB
1
一年中并不是恰好有365.242天。有些年份有365天,有些则有366天。平均每年有365.242天,但如果你要比较两个具体日期,这种方法就行不通了。如果只知道天数(这也是TimeSpan能做到的最好的),那么这是一个不错的估计,但在某些情况下可能会误差一天。 - D Stanley
1
我同意你的看法,我只是说对于非公开的个人项目而言,精确性可能(合理地)被方便性所取代。 - JDB
1
我实际上是在用这个来计算国债的利息。 - Dan
2
太酷了!你能给我邮寄四舍五入差额的支票吗?;) - D Stanley
显示剩余2条评论

0
我需要为Core 3做这个。NodaTime似乎依赖于Framework 4.7.2。我编写了下面的方法,它似乎可以将时间跨度格式化为年、月和日,并省略不需要的部分。
public static string ToYearsMonthsAndDays(this TimeSpan span)
    {
        var result = string.Empty;
        var totalYears = span.Days / 364.25;
        var fullYears = Math.Floor(totalYears);

        var totalMonths = (span.Days - (365.24 * fullYears)) / 30;
        var fullMonths = Math.Floor(totalMonths);

        var totalDays = (span.Days - (365.24 * totalYears) - (30 * fullMonths)) / 30;
        var fullDays = Math.Floor(totalDays);
        var sb = new StringBuilder();
        if (fullYears > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullYears + "y");
        }
        if (fullMonths > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullMonths + "m");
        }
        if (fullDays > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullDays + "d");
        }
        return sb.ToString();
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接