计算两个日期之间的月份差异

149
在C#/.NET中,TimeSpan类有TotalDays、TotalMinutes等属性,但我无法找到计算总月份差异的公式。每个月的天数和闰年让我感到困惑。我该如何获取TotalMonths呢?
编辑:对不清楚之前的表达表示抱歉:我知道实际上无法从TimeSpan类中获取这个值,但我认为使用TotalDays和TotalMinutes可以很好地说明我所寻找的内容...只是我想要获取总月份。
例如:2009年12月25日至2009年10月6日=2个月。10月6日到11月5日等于0个月。11月6日为1个月。12月6日为2个月。

2
你对2009年12月25日至2009年10月6日有什么期待? - Jeff Moser
2
你如何定义TimeSpan的月份? - Aliostad
1
@Aliostad - 没有日期,你可以将一个月定义为30天,并且相当准确。 - ChaosPandion
由于某些原因,它被管理员与这个问题合并了。 - Jamiec
实际上,您需要阅读我的帖子,它回答了这个问题并提供了编码解决方案,https://dev59.com/YHI-5IYBdhLWcg3wTWn4?rq=1 忽略那些喷子(brianary),关注我与supercat通过评论的对话。我们称之为“孤立月份”的是时间跨度开头和结尾的月份,问题归结为如何根据天数定义这些孤立月份 - 一旦您确定了这一点(以及您想如何定义它),其余部分就只是代码(已包含在内)。我的定义基于我认为我的用户会期望什么。 - Erx_VB.NExT.Coder
20-01-2021到20-03-2021之间的总月数是多少?我猜应该是2.0xx,但如果你把二月份看作30天,你会得到不同的答案“1.8xx或者其他什么”。我已经在另一个问题中回答了这个问题,如果你感兴趣可以去看看:https://dev59.com/MYbca4cB1Zd3GeqPZLEd#65284425。 - Wahid Bitar
27个回答

257
你无法从时间间隔TimeSpan中获得这个信息,因为“月”是一个可变的计量单位。你需要自己计算,而且必须确定你想要的具体工作方式。
例如,像2009年7月5日2009年8月4日这样的日期应该产生一个月还是零个月的差异?如果你认为应该产生一个月,那么2009年7月31日2009年8月1日呢?那一个月吗?它是否只是日期的Month值之差,或者与实际时间段更相关?确定所有这些规则的逻辑都是非平凡的,所以你必须确定自己的规则并实现适当的算法。
如果你只想简单地获取月份的差异 - 完全忽略日期值 - 那么你可以使用以下代码:
public static int MonthDifference(this DateTime lValue, DateTime rValue)
{
    return (lValue.Month - rValue.Month) + 12 * (lValue.Year - rValue.Year);
}
请注意,此方法返回的是相对差异值,也就是说,如果rValue大于lValue,则返回值将为负数。如果您需要绝对差异值,可以使用以下代码:
public static int MonthDifference(this DateTime lValue, DateTime rValue)
{
    return Math.Abs((lValue.Month - rValue.Month) + 12 * (lValue.Year - rValue.Year));
}

@Dinah 这只是一个近似值,如果你想知道真正的 .Month 和 .Years - 我刚刚发布了一个答案,你可以去看看。虽然,就近似值而言,这是一个很好的近似值(感谢 Adam Robinson),但你应该记住,如果你使用任何这些近似值,你只是无意中欺骗了你的用户。 - Erx_VB.NExT.Coder
@Erx_VB.NExT.Coder:感谢你的认可,但是虽然你的答案指出了没有一个回答考虑到月份是一种可变的计量单位这一事实,但似乎大多数回答都考虑到了;它们只是没有使用你特定的近似方法。正如我回答中的第一句话所示,这是可变的。任何答案,包括你的答案,都是一种近似,因为它不是一个精确的答案。你的“2个月”的结果对于不同的输入可能意味着不同的含义,因此它是一种近似值。 - Adam Robinson
非常简单的解决方案。就像这样。 - Hensembryan
2
这似乎是 Sql Server DateDiff(month, ...) 函数使用的相同逻辑。它还具有极其简洁和易于解释和理解的优点。我会这样解释它...从一个日期到另一个日期需要翻多少页日历? - JoelFan
20-01-2021到20-03-2021之间的总月数是多少?我猜应该是2.0xx,但如果你把二月份算作30天,你会得到不同的答案“1.8xx或者其他什么”。我已经在另一个问题中回答了这个问题,如果你感兴趣可以去看看:https://dev59.com/MYbca4cB1Zd3GeqPZLEd#65284425。 - Wahid Bitar
显示剩余3条评论

63

我知道这是一个老问题,但是...

在.NET中纯粹地进行这项工作相对来说是很痛苦的。我建议使用我自己的Noda Time库,该库特别设计用于处理此类任务:

LocalDate start = new LocalDate(2009, 10, 6);
LocalDate end = new LocalDate(2009, 12, 25);
Period period = Period.Between(start, end);
int months = period.Months;

还有其他选项,例如,如果您只想计算跨年份的月份数量,您可以使用Period period = Period.Between(start, end, PeriodUnits.Months);


我下载了你的库并复制了你上面写的代码,但是我收到了编译时错误。错误1:运算符“-”不能应用于类型为“NodaTime.LocalDate”和“NodaTime.LocalDate”的操作数。我知道这篇文章已经有5年了,那时候有什么改变导致这段代码不起作用了吗? - Hakan Fıstık
1
@HakamFostok:抱歉 - 在2.0发布之前,它将会起作用,但在那之前你需要使用Period.Between。已经编辑了代码,所以它可以与NodaTime 1.3.1一起使用。 - Jon Skeet
非常感谢NodaTime库,它完全实现了我想要做的事情。我想计算两个日期之间不仅仅是月份,还有剩余的天数,而这正是NodaTime所做的,再次感谢。 - Hakan Fıstık
1
@JonSkeet,您的库真是一门黑魔法。日期总是让我头疼。那段代码片段为我节省了大量时间。 - onefootswill

28

也许你不想了解月份分数;那这段代码呢?


public static class DateTimeExtensions // 日期时间拓展类
{
    public static int TotalMonths(this DateTime start, DateTime end) // 计算两个日期之间相差的月份数
    {
        return (start.Year * 12 + start.Month) - (end.Year * 12 + end.Month); // 返回相差月份数
    }
}
// Console.WriteLine( // DateTime.Now.TotalMonths( // DateTime.Now.AddMonths(-1))); // 输出 "1"

2
我不理解 * 100 是什么意思。它应该是 * 12 吗? - Ruffles

16
我已经写了一个在 DateTimeDateTimeOffset 上非常简单的扩展方法来做这件事。我希望它能够像TimeSpanTotalMonths 属性一样工作:即返回两个日期之间完整月份的计数,忽略任何不足一个月的部分。因为它基于 DateTime.AddMonths() ,所以它尊重不同的月份长度,并返回人们理解的月份期间。
(很遗憾,你无法将其实现为 TimeSpan 的扩展方法,因为它不能保留使用的实际日期知识,而对于月份,它们很重要。)
代码和测试都在 GitHub 上可用。 该代码非常简单:
public static int GetTotalMonthsFrom(this DateTime dt1, DateTime dt2)
{
    DateTime earlyDate = (dt1 > dt2) ? dt2.Date : dt1.Date;
    DateTime lateDate = (dt1 > dt2) ? dt1.Date : dt2.Date;

    // Start with 1 month's difference and keep incrementing
    // until we overshoot the late date
    int monthsDiff = 1;
    while (earlyDate.AddMonths(monthsDiff) <= lateDate)
    {
        monthsDiff++;
    }

    return monthsDiff - 1;
}

而且它通过了所有这些单元测试用例:

// Simple comparison
Assert.AreEqual(1, new DateTime(2014, 1, 1).GetTotalMonthsFrom(new DateTime(2014, 2, 1)));
// Just under 1 month's diff
Assert.AreEqual(0, new DateTime(2014, 1, 1).GetTotalMonthsFrom(new DateTime(2014, 1, 31)));
// Just over 1 month's diff
Assert.AreEqual(1, new DateTime(2014, 1, 1).GetTotalMonthsFrom(new DateTime(2014, 2, 2)));
// 31 Jan to 28 Feb
Assert.AreEqual(1, new DateTime(2014, 1, 31).GetTotalMonthsFrom(new DateTime(2014, 2, 28)));
// Leap year 29 Feb to 29 Mar
Assert.AreEqual(1, new DateTime(2012, 2, 29).GetTotalMonthsFrom(new DateTime(2012, 3, 29)));
// Whole year minus a day
Assert.AreEqual(11, new DateTime(2012, 1, 1).GetTotalMonthsFrom(new DateTime(2012, 12, 31)));
// Whole year
Assert.AreEqual(12, new DateTime(2012, 1, 1).GetTotalMonthsFrom(new DateTime(2013, 1, 1)));
// 29 Feb (leap) to 28 Feb (non-leap)
Assert.AreEqual(12, new DateTime(2012, 2, 29).GetTotalMonthsFrom(new DateTime(2013, 2, 28)));
// 100 years
Assert.AreEqual(1200, new DateTime(2000, 1, 1).GetTotalMonthsFrom(new DateTime(2100, 1, 1)));
// Same date
Assert.AreEqual(0, new DateTime(2014, 8, 5).GetTotalMonthsFrom(new DateTime(2014, 8, 5)));
// Past date
Assert.AreEqual(6, new DateTime(2012, 1, 1).GetTotalMonthsFrom(new DateTime(2011, 6, 10)));

4
乡村风格,但是最好的解决方案。拷贝并粘贴。谢谢。 - Daniel Dolz
1
谢谢,这个绝妙的解决方案完美地满足了我的需求。 - Satinder singh

9
你首先需要定义你所说的TotalMonths的含义。
一个简单的定义是将一个月定为30.4天(365.25 / 12)。
除此之外,任何包括分数的定义似乎都没有用处,更常见的整数值(日期之间的整月数)也取决于非标准的业务规则。

8

您需要自己处理日期时间。如何处理最后的存根日期将取决于您想要使用它的方式。

一种方法是计算月份,然后在末尾进行天数修正。例如:

   DateTime start = new DateTime(2003, 12, 25);
   DateTime end = new DateTime(2009, 10, 6);
   int compMonth = (end.Month + end.Year * 12) - (start.Month + start.Year * 12);
   double daysInEndMonth = (end - end.AddMonths(1)).Days;
   double months = compMonth + (start.Day - end.Day) / daysInEndMonth;

代码写得不错,但是有一个小bug: 因为(2月28日+1个月=3月28日):-) //decimal daysInEndMonth = (end - end.AddMonths(1)).Days; 我建议改成: decimal daysInEndMonth = DateTime.DaysInMonth(end.Year, end.Month) * -1; - Roman Motyka

4
这个问题没有很明确的答案,因为你总是在假设一些东西。
这个解决方案可以计算两个日期之间的月份差值,并且考虑了日期相同的情况(也就是说,在计算中考虑了日期),例如,如果你有一个2012年1月30日的日期,那么2012年2月29日不会被计算为一个月,但是2013年3月1日会被计算为一个月。
它经过了比较充分的测试,我们将在使用它时进行优化,并采用两个日期而不是时间跨度来计算,这可能更好。希望这对其他人有所帮助。
private static int TotalMonthDifference(DateTime dtThis, DateTime dtOther)
{
    int intReturn = 0;
    bool sameMonth = false;

    if (dtOther.Date < dtThis.Date) //used for an error catch in program, returns -1
        intReturn--;

    int dayOfMonth = dtThis.Day; //captures the month of day for when it adds a month and doesn't have that many days
    int daysinMonth = 0; //used to caputre how many days are in the month

    while (dtOther.Date > dtThis.Date) //while Other date is still under the other
    {
        dtThis = dtThis.AddMonths(1); //as we loop, we just keep adding a month for testing
        daysinMonth = DateTime.DaysInMonth(dtThis.Year, dtThis.Month); //grabs the days in the current tested month

        if (dtThis.Day != dayOfMonth) //Example 30 Jan 2013 will go to 28 Feb when a month is added, so when it goes to march it will be 28th and not 30th
        {
            if (daysinMonth < dayOfMonth) // uses day in month max if can't set back to day of month
                dtThis.AddDays(daysinMonth - dtThis.Day);
            else
                dtThis.AddDays(dayOfMonth - dtThis.Day);
        }
        if (((dtOther.Year == dtThis.Year) && (dtOther.Month == dtThis.Month))) //If the loop puts it in the same month and year
        {
            if (dtOther.Day >= dayOfMonth) //check to see if it is the same day or later to add one to month
                intReturn++;
            sameMonth = true; //sets this to cancel out of the normal counting of month
        }
        if ((!sameMonth)&&(dtOther.Date > dtThis.Date))//so as long as it didn't reach the same month (or if i started in the same month, one month ahead, add a month)
            intReturn++;
    }
    return intReturn; //return month
}

3

如果你需要计算不完整的月份,那么接受的答案就不能完全胜任。

以下是我为计算不完整月份所想出的解决方案:

    /// <summary>
    /// Calculate the difference in months.
    /// This will round up to count partial months.
    /// </summary>
    /// <param name="lValue"></param>
    /// <param name="rValue"></param>
    /// <returns></returns>
    public static int MonthDifference(DateTime lValue, DateTime rValue)
    {
        var yearDifferenceInMonths = (lValue.Year - rValue.Year) * 12;
        var monthDifference = lValue.Month - rValue.Month;

        return yearDifferenceInMonths + monthDifference + 
            (lValue.Day > rValue.Day
                ? 1 : 0); // If end day is greater than start day, add 1 to round up the partial month
    }

我也需要计算一年内的部分年份差异。以下是我想出的解决方案:

    /// <summary>
    /// Calculate the differences in years.
    /// This will round up to catch partial months.
    /// </summary>
    /// <param name="lValue"></param>
    /// <param name="rValue"></param>
    /// <returns></returns>
    public static int YearDifference(DateTime lValue, DateTime rValue)
    {
        return lValue.Year - rValue.Year +
               (lValue.Month > rValue.Month // Partial month, same year
                   ? 1
                   : ((lValue.Month = rValue.Month) 
                     && (lValue.Day > rValue.Day)) // Partial month, same year and month
                   ? 1 : 0);
    }

你的 YearDifference 函数存在逻辑错误,当 lValue.Month < rValue.Month 时 - 我已经修复了这个问题,你可能需要重新审查一下... - Stobor

3
我会这样做:
static int TotelMonthDifference(this DateTime dtThis, DateTime dtOther)
{
    int intReturn = 0;

    dtThis = dtThis.Date.AddDays(-(dtThis.Day-1));
    dtOther = dtOther.Date.AddDays(-(dtOther.Day-1));

    while (dtOther.Date > dtThis.Date)
    {
        intReturn++;     
        dtThis = dtThis.AddMonths(1);
    }

    return intReturn;
}

4
这确实是一个算法,但可以大大简化为return (dtOther.Month - dtThis.Month) + 12 * (dtOther.Year - dtThis.Year); - Adam Robinson
1
两个问题:首先,你的起始点是两个日期而不是时间间隔。其次,你计算的是两个月份的第一天之间的时间差,这个定义是非常值得商榷的。虽然有时候或许是正确的。 - H H
@Henk:是的,当然这并不总是正确的,这就是为什么我说这是我会做的方式,而不是每个人都应该这样做的方式。OP没有明确说明计算结果的方式。 @Adam:哇,我又想得太复杂了...这种情况时常发生在我身上。谢谢你的评论,显然你是对的,你的版本更好。从现在开始我会使用你的方法。 - Maximilian Mayerl
@Adam:为什么你不把这个作为一个真正的答案提交呢?这是迄今为止最紧凑的。非常流畅。 - Dinah
@Dinah:我不想假设那就是你真正想要的。如果是的话,我已经编辑了我的先前答案,包括这种方法。 - Adam Robinson

2

虽然这是一个老问题,但可能会对某些人有帮助。我使用了上面@Adam的答案,但是我检查了一下差异是否为1或-1,然后再检查是否为完整的日历月份差异。因此,21/07/55和20/08/55不是一个完整的月份,但是21/07/55和21/07/55则是。

/// <summary>
/// Amended date of birth cannot be greater than or equal to one month either side of original date of birth.
/// </summary>
/// <param name="dateOfBirth">Date of birth user could have amended.</param>
/// <param name="originalDateOfBirth">Original date of birth to compare against.</param>
/// <returns></returns>
public JsonResult ValidateDateOfBirth(string dateOfBirth, string originalDateOfBirth)
{
    DateTime dob, originalDob;
    bool isValid = false;

    if (DateTime.TryParse(dateOfBirth, out dob) && DateTime.TryParse(originalDateOfBirth, out originalDob))
    {
        int diff = ((dob.Month - originalDob.Month) + 12 * (dob.Year - originalDob.Year));

        switch (diff)
        {
            case 0:
                // We're on the same month, so ok.
                isValid = true;
                break;
            case -1:
                // The month is the previous month, so check if the date makes it a calendar month out.
                isValid = (dob.Day > originalDob.Day);
                break;
            case 1:
                // The month is the next month, so check if the date makes it a calendar month out.
                isValid = (dob.Day < originalDob.Day);
                break;
            default:
                // Either zero or greater than 1 month difference, so not ok.
                isValid = false;
                break;
        }
        if (!isValid)
            return Json("Date of Birth cannot be greater than one month either side of the date we hold.", JsonRequestBehavior.AllowGet);
    }
    else
    {
        return Json("Date of Birth is invalid.", JsonRequestBehavior.AllowGet);
    }
    return Json(true, JsonRequestBehavior.AllowGet);
}

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