两个日期之间的月数差异

422

在C#中如何计算两个日期之间的月份差异?

是否有等效于VB的DateDiff()方法的C#方法。我需要找到年份不同的两个日期之间的月份差异。文档中说我可以使用TimeSpan,例如:

TimeSpan ts = date1 - date2;

但是这样会给我返回以天为单位的数据。我不想将这个数字除以30,因为并非每个月都有30天,而且由于这两个操作数之间相差很大,我担心除以30可能会给我错误的值。

有什么建议吗?


31
请定义“月份差”,“2010年5月1日”和“2010年6月16日”之间的月份差是1.5、1还是其他数? 答案是1.5个月。月份差是指两个日期之间相差的月份数,其中不足一个月按照0计算,超过一个月但不满两个月按照0.5计算,以此类推。在这个例子中,“2010年5月1日”到“2010年6月1日”是一个完整的月,而从“2010年6月1日”到“2010年6月16日”是不足一个月的时间,因此月份差为1.5个月。 - Cheng Chen
8
再强调一下,2010年12月31日和2011年1月1日之间相差几个月?这取决于具体时间,可能只有1秒钟的差别,你会把它视为一个月的差距吗? - stakx - no longer contributing
15
Danny: 1个月15天。stakx: 0个月1天。重点是获取“月”这个部分。我认为这很明显,也是一个不错的问题。 - Kirk Woll
DateDiff 实现:http://referencesource.microsoft.com/#Microsoft.VisualBasic/DateAndTime.vb,229。 - dovid
4
我认为既然OP提到了vb的DateDiff,所有这些问题都已经得到回答。答案恰好与SQL Server的datediff相同。只需回答问题即可... 明确一点,它是两个日期之间跨越的月边界数(包括在内)。 - greg
显示剩余3条评论
45个回答

582

假设月份中的日期无关紧要(即2011.1.1与2010.12.31之间的差值为1),其中date1 > date2时返回正值,date2 > date1时返回负值。

((date1.Year - date2.Year) * 12) + date1.Month - date2.Month

或者假设你想要获取两个日期之间大致的“平均月数”,以下代码适用于除非日期差距非常大的情况。

date1.Subtract(date2).Days / (365.25 / 12)

请注意,如果您使用后一种解决方案,则您的单元测试应该声明应用程序可以处理的最宽日期范围,并相应地验证计算结果。


更新(感谢 Gary

如果使用“平均月份”方法,则“每年平均天数”的更准确数字是365.2425


3
@Kurru - 365/12只是一个月平均天数的近似测量值,它是一种不准确的度量。对于小日期范围,可以容忍这种不准确性,但对于非常大的日期范围,这种不准确性可能变得显著。 - Adam Ralph
35
我认为考虑日期组件是必要的。类似这样:(date1.Year - date2.Year) * 12 + date1.Month - date2.Month + (date1.Day >= date2.Day ? 0 : -1) - DrunkCoder
2
@DrunkCoder 这取决于特定系统的要求。在某些情况下,您的解决方案可能确实是最佳选择。例如,重要的是考虑当两个日期跨越31天的月份、30天的月份、28天的二月或29天的二月时会发生什么。如果您的公式结果符合系统要求,那么显然是正确的选择。如果不符合,则需要其他解决方案。 - Adam Ralph
6
跟随Adam的说法,我花了多年时间为保险精算师编写代码。一些计算是“按天数除以数量,向上取整30天以获得每月数字”。有时候,计算月份要假设“每个日期都从当月的第一天开始,相应地计算整个月份”。在计算日期方面并没有“最佳”方法。除非你是为之编写代码的客户端,否则将此问题反馈上级并进行澄清,可能需要通过客户的会计师来解决。 - Binary Worrier
3
365.2425是公历中一年的略微更精确的天数,如果您正在使用公历的话。然而,到DateTime.MaxValue(公元10000年1月1日)只有大约59天的差异。此外,一年的定义可以因您的视角而大不相同。https://en.wikipedia.org/wiki/Year。 - Gary
显示剩余15条评论

237

下面是一个全面的解决方案,用于返回一个DateTimeSpan,类似于TimeSpan,但它还包括所有日期组件以及时间组件。

用法:

void Main()
{
    DateTime compareTo = DateTime.Parse("8/13/2010 8:33:21 AM");
    DateTime now = DateTime.Parse("2/9/2012 10:10:11 AM");
    var dateSpan = DateTimeSpan.CompareDates(compareTo, now);
    Console.WriteLine("Years: " + dateSpan.Years);
    Console.WriteLine("Months: " + dateSpan.Months);
    Console.WriteLine("Days: " + dateSpan.Days);
    Console.WriteLine("Hours: " + dateSpan.Hours);
    Console.WriteLine("Minutes: " + dateSpan.Minutes);
    Console.WriteLine("Seconds: " + dateSpan.Seconds);
    Console.WriteLine("Milliseconds: " + dateSpan.Milliseconds);
}

输出:

年:1
月:5
日:27
小时:1
分钟:36
秒:50
毫秒:0

为了方便起见,我已将逻辑合并到DateTimeSpan结构体中,但您可以将方法CompareDates移动到任何您认为合适的位置。此外,请注意,两个日期的先后顺序无所谓。

public struct DateTimeSpan
{
    public int Years { get; }
    public int Months { get; }
    public int Days { get; }
    public int Hours { get; }
    public int Minutes { get; }
    public int Seconds { get; }
    public int Milliseconds { get; }

    public DateTimeSpan(int years, int months, int days, int hours, int minutes, int seconds, int milliseconds)
    {
        Years = years;
        Months = months;
        Days = days;
        Hours = hours;
        Minutes = minutes;
        Seconds = seconds;
        Milliseconds = milliseconds;
    }

    enum Phase { Years, Months, Days, Done }

    public static DateTimeSpan CompareDates(DateTime date1, DateTime date2)
    {
        if (date2 < date1)
        {
            var sub = date1;
            date1 = date2;
            date2 = sub;
        }

        DateTime current = date1;
        int years = 0;
        int months = 0;
        int days = 0;

        Phase phase = Phase.Years;
        DateTimeSpan span = new DateTimeSpan();
        int officialDay = current.Day;

        while (phase != Phase.Done)
        {
            switch (phase)
            {
                case Phase.Years:
                    if (current.AddYears(years + 1) > date2)
                    {
                        phase = Phase.Months;
                        current = current.AddYears(years);
                    }
                    else
                    {
                        years++;
                    }
                    break;
                case Phase.Months:
                    if (current.AddMonths(months + 1) > date2)
                    {
                        phase = Phase.Days;
                        current = current.AddMonths(months);
                        if (current.Day < officialDay && officialDay <= DateTime.DaysInMonth(current.Year, current.Month))
                            current = current.AddDays(officialDay - current.Day);
                    }
                    else
                    {
                        months++;
                    }
                    break;
                case Phase.Days:
                    if (current.AddDays(days + 1) > date2)
                    {
                        current = current.AddDays(days);
                        var timespan = date2 - current;
                        span = new DateTimeSpan(years, months, days, timespan.Hours, timespan.Minutes, timespan.Seconds, timespan.Milliseconds);
                        phase = Phase.Done;
                    }
                    else
                    {
                        days++;
                    }
                    break;
            }
        }

        return span;
    }
}

2
@KirkWoll谢谢。但是为什么DateTimeSpan返回此日期时间差为34天,实际上应该是35天http://www.timeanddate.com/date/durationresult.html?d26=01&m1=11&y1=2012&d2=31&m2=12&y2=2012 - Deeptechtons
2
我写了一个回答 https://dev59.com/3XNA5IYBdhLWcg3wH6AW#17537472,回答了一个类似的问题,并测试了提出的答案(发现大多数都不起作用)。这个答案是少数几个能够工作的答案之一(根据我的测试套件)。在我的答案中附上了Github链接。 - jwg
1
@KirkWoll - 这个答案似乎不能处理一些边缘情况,例如起始日期的天数大于结束日期的月份或者源日期是闰年的情况。试试 2020-02-292021-06-29 - 它返回 "1y 4m 1d",但实际上应该是 "1y 4m 0d",对吧? - Enigmativity
1
@Enigmativity,感谢您的评论。我已通过在Phase.Months部分处理此场景来更新我的答案。对我来说似乎可以工作。 - Kirk Woll
非常酷,但请注意此算法存在一些边缘情况问题。如果您在日期值中使用DateTime.MinValueDateTime.MaxValue,则会出现问题。例如:System.ArgumentOutOfRangeException': "The added or subtracted value results in an un-representable DateTime." - iCollect.it Ltd
显示剩余3条评论

51

你可以这样做

if ( date1.AddMonths(x) > date2 )

这非常简单,对我来说完美无缺。当计算从一个月末到下一个月末的日期时,如果下个月天数较少,我惊喜地发现它按预期工作。例如... 1-31-2018 + 1个月 = 2018年2月28日。 - lucky.expert
这是更好的解决方案之一。 - barnacle.m
非常简单高效的解决方案!最佳答案提出。 - Cedric Arnould
3
如果date1 = 2018-10-28,date2 = 2018-12-21,那么答案将会是2。但正确的答案应该是3,因为日期范围为3个月。如果我们只计算月份而忽略天数,那么2并不是正确答案。 - Tommix
更合理的做法是:if ( date1.AddMonths(x).Month == date2.Month ),然后您只需使用 x + 1 作为月份计数。 - Tommix
2
我是否漏掉了什么...这是一个真/假检查,检查日期是否至少相差给定的月数,而不是计算那个月数,这是我认为o/p所要求的。 - Chris Peacock

37
如果您想要精确的全月数,始终为正数(2000年1月15日,2000年2月14日返回0),考虑到一个完整的月份是指当您在下个月达到同一天时(类似于年龄计算)。
public static int GetMonthsBetween(DateTime from, DateTime to)
{
    if (from > to) return GetMonthsBetween(to, from);

    var monthDiff = Math.Abs((to.Year * 12 + (to.Month - 1)) - (from.Year * 12 + (from.Month - 1)));

    if (from.AddMonths(monthDiff) > to || to.Day < from.Day)
    {
        return monthDiff - 1;
    }
    else
    {
        return monthDiff;
    }
}

编辑原因:旧代码在某些情况下不正确,例如:

new { From = new DateTime(1900, 8, 31), To = new DateTime(1901, 8, 30), Result = 11 },

Test cases I used to test the function:

var tests = new[]
{
    new { From = new DateTime(1900, 1, 1), To = new DateTime(1900, 1, 1), Result = 0 },
    new { From = new DateTime(1900, 1, 1), To = new DateTime(1900, 1, 2), Result = 0 },
    new { From = new DateTime(1900, 1, 2), To = new DateTime(1900, 1, 1), Result = 0 },
    new { From = new DateTime(1900, 1, 1), To = new DateTime(1900, 2, 1), Result = 1 },
    new { From = new DateTime(1900, 2, 1), To = new DateTime(1900, 1, 1), Result = 1 },
    new { From = new DateTime(1900, 1, 31), To = new DateTime(1900, 2, 1), Result = 0 },
    new { From = new DateTime(1900, 8, 31), To = new DateTime(1900, 9, 30), Result = 0 },
    new { From = new DateTime(1900, 8, 31), To = new DateTime(1900, 10, 1), Result = 1 },
    new { From = new DateTime(1900, 1, 1), To = new DateTime(1901, 1, 1), Result = 12 },
    new { From = new DateTime(1900, 1, 1), To = new DateTime(1911, 1, 1), Result = 132 },
    new { From = new DateTime(1900, 8, 31), To = new DateTime(1901, 8, 30), Result = 11 },
};

为了避免其他人的困惑,我认为这个解决方案是不正确的。使用测试用例:new { From = new DateTime(2015, 12, 31), To = new DateTime(2015, 6, 30), Result = 6 }测试将失败,因为结果应该是5。 - Cristian Badila
我在这里添加了一个快速要点,其中包含我提出的修复方案链接 - Cristian Badila
3
在我看来,这是可以预期的行为,至少这是我所期望的行为。我指出,一个完整的月份是当你到达相同的日期(或下个月的同一天,就像在这种情况下一样)。 - Guillaume86
我理解,这就是为什么我说“我认为解决方案不正确”的原因。我仍然试图提供一个替代方案,以减少混淆。在我看来,一个月是一个具有可变天数的日历子单位,当它的最后一天到达时,考虑一个月是完整的是有意义的。作为另一个例子(考虑到二月份可能有28/29天),使用您的代码计算2016年2月29日和2017年2月28日之间的差异将是11个月,这对我来说是“令人惊讶的”。 - Cristian Badila
我想这只是个人喜好的问题。我本来还想提到闰年的情况来证明我的选择,但这也不是普遍适用的。有些国家对于闰年生日的规定是一种方式,而另一些则是另一种方式,我在这种情况下支持英国:https://en.wikipedia.org/wiki/February_29#Legal_status。 - Guillaume86
显示剩余3条评论

25

我通过 MSDN 检查了这种方法在 VB.NET 中的用法,发现它有很多用途。 在 C# 中没有这样的内置方法。(即使这不是一个好主意)您可以在 C# 中调用 VB。

  1. Microsoft.VisualBasic.dll 添加到您的项目中作为参考。
  2. 在您的代码中使用 Microsoft.VisualBasic.DateAndTime.DateDiff

7
你为什么认为这不是一个好主意?直觉上,我会猜测该库只是运行时的“另一个 .NET 库”。注意,这里我是在拿出反面论点,虽然我也不太愿意这样做(感觉有点作弊),但我想知道是否有任何令人信服的技术原因不这样做。 - Adam Ralph
3
@AdamRalph:没有任何理由不这样做。这些库都是用100%托管代码实现的,因此与其他所有内容一样。唯一可以想象的区别是必须加载Microsoft.VisualBasic.dll模块,但执行该操作所需的时间微不足道。没有理由放弃经过全面测试和有用的功能,只是因为您选择使用C#编写程序。(这也适用于类似My.Application.SplashScreen之类的内容。) - Cody Gray
4
如果你知道这是用C#编写的,你会改变主意吗?实际上是这样的。按照同样的逻辑,使用System.Data和PresentationFramework也是作弊,因为其中很大一部分是用C++/CLI编写的。 - Hans Passant
3
@AdamRalph:你有没有想到任何具体的“奇怪包袱”?还是说这只是假设?虽然这可能会影响一些写了大量代码来完成某项任务的C#伙伴们的思维,但只要使用正确的“using”语句,你就可以用一行代码完成同样的任务。我不认为会有任何严重的损害。 - Cody Gray
1
@Cody Gray:同意,正如你所示,这个例子很简单。我想避免的是通过调用这种不寻常(从 C# 视角来看)的方法引入的额外代码“噪音”。在一个组织良好的团队中,这些东西会在代码审查中被发现并且可以很容易地避免。顺便说一句 - 我并不是在攻击 VB6/VB.NET。我之所以将这样的方法描述为“奇怪”,只是因为从 .NET 视角来看,没有理由存在 DateAndTime.Year(),因为 DateTime 已经有了一个 Year 属性。它的存在只是为了让 VB.NET 看起来更像 VB6。作为一名前 VB6 程序员,我能够理解这一点 ;-) - Adam Ralph
显示剩余6条评论

11
要获取相差的月份(包括开始和结束日期),与日期无关:
DateTime start = new DateTime(2013, 1, 1);
DateTime end = new DateTime(2014, 2, 1);
var diffMonths = (end.Month + end.Year * 12) - (start.Month + start.Year * 12);

5
想象一下 startend 是相同的。那么你会得到一个结果为 1。这怎么对呢?为什么要将结果加 1 呢?谁给这个答案点了赞 :-/ ? - paul
对于相同的日期,它将输出1。基本上,它将计算包括起始月份和结束月份在内的所有月份。 - Chirag
3
对我来说,这听起来不像是两个物品之间的区别。2和2之间有什么区别吗?它真的是1吗?我认为这两个数的差别是0。 - paul
我无法理解这是如何运作的,但我将其放入Excel中并且它确实有效。我在末尾添加了+1,因为根据我的需求,8/2023 - 8/2023 等于1个月。他的解决方案确实提到了“包括开始和结束”。 - flashsplat

11

使用Noda Time

LocalDate start = new LocalDate(2013, 1, 5);
LocalDate end = new LocalDate(2014, 6, 1);
Period period = Period.Between(start, end, PeriodUnits.Months);
Console.WriteLine(period.Months); // 16

(示例来源)


8

我只需要一个简单的方法来处理只输入了月份/年份的就业日期,所以希望得到区分年份和月份的工作时长。这是我使用的代码,仅供参考。

public static YearsMonths YearMonthDiff(DateTime startDate, DateTime endDate) {
    int monthDiff = ((endDate.Year * 12) + endDate.Month) - ((startDate.Year * 12) + startDate.Month) + 1;
    int years = (int)Math.Floor((decimal) (monthDiff / 12));
    int months = monthDiff % 12;
    return new YearsMonths {
        TotalMonths = monthDiff,
            Years = years,
            Months = months
    };
}

.NET Fiddle


4
你可以使用.NET时间段库DateDiff类:
// ----------------------------------------------------------------------
public void DateDiffSample()
{
  DateTime date1 = new DateTime( 2009, 11, 8, 7, 13, 59 );
  DateTime date2 = new DateTime( 2011, 3, 20, 19, 55, 28 );
  DateDiff dateDiff = new DateDiff( date1, date2 );

  // differences
  Console.WriteLine( "DateDiff.Months: {0}", dateDiff.Months );
  // > DateDiff.Months: 16

  // elapsed
  Console.WriteLine( "DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths );
  // > DateDiff.ElapsedMonths: 4

  // description
  Console.WriteLine( "DateDiff.GetDescription(6): {0}", dateDiff.GetDescription( 6 ) );
  // > DateDiff.GetDescription(6): 1 Year 4 Months 12 Days 12 Hours 41 Mins 29 Secs
} // DateDiffSample

4

以下是我对获取月份差异的贡献,我认为这种方法非常精确:

namespace System
{
     public static class DateTimeExtensions
     {
         public static Int32 DiffMonths( this DateTime start, DateTime end )
         {
             Int32 months = 0;
             DateTime tmp = start;

             while ( tmp < end )
             {
                 months++;
                 tmp = tmp.AddMonths( 1 );
             }

             return months;
        }
    }
}

使用方法:

Int32 months = DateTime.Now.DiffMonths( DateTime.Now.AddYears( 5 ) );

您可以创建另一个名为DiffYears的方法,并应用与上述完全相同的逻辑,除了在while循环中使用AddYears而不是AddMonths。


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