如何在年/月/周/日中获得两个日期之间的差异?

Ahm*_*med 46 .net c# datetime

如何以有效的方式在年/月/周/日中获得两个日期之间的差异?

例如.两个日期之间的差异是1年,2个月,3个星期,4天.

差异表示两个日期之间的年(s),月(s),周(s)和日(s)的计数.

Jon*_*eet 34

这实际上非常棘手.不同的总天数可能会产生相同的结果.例如:

  • 2008年6月19日至2010年6月19日= 2年,也是365*2天

  • 2006年6月19日至2008年6月19日= 2年,但由于闰年也是365 + 366天

你可能想要减去年数,直到你得到两个相隔不到一年的日期.然后减去几个月,直到你得到两个相隔不到一个月的日期.

进一步混淆:当你可能以"3月30日"开始时减去(或增加)月份是棘手的 - 比这更早一个月?

更进一步的混乱(可能不相关):即使一天也不总是24小时.夏令时任何人?

更进一步的混乱(几乎肯定相关):即使一分钟也不总是60秒.闰秒非常令人困惑......

我现在没有时间找出正确的方法 - 这个答案主要是为了提出这个事实并不像听起来那么简单.

编辑:不幸的是,我没有足够的时间来完全回答这个问题.我建议你先定义一个代表一个结构的结构Period:

public struct Period
{
    private readonly int days;
    public int Days { get { return days; } }
    private readonly int months;
    public int Months { get { return months; } }
    private readonly int years;
    public int Years { get { return years; } }

    public Period(int years, int months, int days)
    {
        this.years = years;
        this.months = months;
        this.days = days;
    }

    public Period WithDays(int newDays)
    {
        return new Period(years, months, newDays);
    }

    public Period WithMonths(int newMonths)
    {
        return new Period(years, newMonths, days);
    }

    public Period WithYears(int newYears)
    {
        return new Period(newYears, months, days);
    }

    public static DateTime operator +(DateTime date, Period period)
    {
        // TODO: Implement this!
    }

    public static Period Difference(DateTime first, DateTime second)
    {
        // TODO: Implement this!
    }
}
Run Code Online (Sandbox Code Playgroud)

我建议你首先实现+运算符,它应该通知Difference方法 - 你应该确保first + (Period.Difference(first, second)) == second所有first/ second值.

首先编写一系列单元测试 - 最初是"简单"的案例,然后转向涉及闰年的棘手问题.我知道正常的方法是一次编写一个测试,但在开始任何实现工作之前,我会亲自集体讨论它们.

让自己有一天正确实施.这是棘手的事情.

请注意,我在这里省略了几周 - 这个值至少很容易,因为它总是7天.因此,给定(正)期间,您将拥有:

int years = period.Years;
int months = period.Months;
int weeks = period.Days / 7;
int daysWithinWeek = period.Days % 7;
Run Code Online (Sandbox Code Playgroud)

(我建议你甚至不要考虑消极时期 - 确保所有时间都是正面的.)

  • 我渴望看到你'做正确的'正确方法'. (4认同)
  • @DavidBasarab - [Noda Time](http://nodatime.org)自从这篇文章最初创作以来已经走了很长一段路.它现在可以通过`Period.Between(d1,d2)`精确地处理这个要求. (3认同)

jwg*_*jwg 17

部分是为了尝试正确地回答这个问题(甚至可能是最终的......),部分是为了检查一个人可以信任粘贴在SO上的代码,并且部分地作为查找错误的练习,我创建了一堆对此问题进行单元测试,并将其应用于此页面中的许多建议解决方案以及几个重复项.

结果是决定性的:没有一个代码贡献准确地回答了这个问题.更新:我现在有四个正确的解决方案,包括我自己的,请参阅下面的更新.

代码测试

从这个问题,我测试了以下用户的代码:Mohammed Ijas Nasirudeen,ruffin,Malu MN,Dave,pk.,Jani,lc.

这些都是他们的代码中提供了三年,几个月和几天的所有答案.请注意,其中两个,Dave和Jani,给出了总天数和月数,而不是计算年数后剩余的总月数,以及计算月数后剩余的总天数.我认为答案在OP似乎想要的方面是错误的,但在这些情况下,单元测试显然不会告诉你太多.(请注意,在Jani的情况下,这是我的错误,他的代码实际上是正确的 - 请参阅下面的更新4)

Jon Skeet,Aghasoleimani,Mukesh Kumar,Richard,Colin,Sheir的答案,我看到的,Chalkey和Andy,都是不完整的.这并不意味着答案没有任何好处,实际上其中一些答案对解决方案是有用的贡献.它只是意味着没有代码占用两个DateTimes并返回3 int秒,我可以正确测试.然而,其中四个谈论使用TimeSpan.正如许多人所提到的,TimeSpan不会返回任何大于天的数量.

我测试的其他答案来自

  • 问题3054715 - LukeH,ho1和this.___curious_geek
  • 问题6260372 - Chuck Rostance和Jani(与这个问题的答案相同)
  • 问题9(!) - Dylan Hayes,Jon和Rajeshwaran SP

这个.___ curious_geek的回答是他链接到的页面上的代码,我不认为他写的.Jani的答案是唯一一个使用外部库,即.Net的时间段库.

所有这些问题的所有其他答案似乎都不完整.问题9是关于年龄的年龄,三个答案是超过简要和计算的年,月和日的答案.如果有人发现此问题的进一步重复,请告诉我.

我是如何测试的

很简单:我做了一个界面

public interface IDateDifference
{
  void SetDates(DateTime start, DateTime end);
  int GetYears();
  int GetMonths();
  int GetDays();

}
Run Code Online (Sandbox Code Playgroud)

对于每个答案,我编写了一个实现此接口的类,使用复制和粘贴的代码作为基础.当然,我必须调整具有不同签名等功能,但我尝试进行最小编辑,保留所有逻辑代码.

我在抽象泛型类中写了一堆NUnit测试

[TestFixture]
public abstract class DateDifferenceTests<DDC> where DDC : IDateDifference, new()
Run Code Online (Sandbox Code Playgroud)

并添加了一个空的派生类

public class Rajeshwaran_S_P_Test : DateDifferenceTests<Rajeshwaran_S_P>
{
}
Run Code Online (Sandbox Code Playgroud)

to the source file for each IDateDifference class.

NUnit is clever enough to do the rest.

The tests

A couple of these were written in advance and the rest were written to try and break seemingly working implementations.

[TestFixture]
public abstract class DateDifferenceTests<DDC> where DDC : IDateDifference, new()
{
  protected IDateDifference ddClass;

  [SetUp]
  public void Init()
  {
    ddClass = new DDC();
  }

  [Test]
  public void BasicTest()
  {
    ddClass.SetDates(new DateTime(2012, 12, 1), new DateTime(2012, 12, 25));
    CheckResults(0, 0, 24);
  }

  [Test]
  public void AlmostTwoYearsTest()
  {
    ddClass.SetDates(new DateTime(2010, 8, 29), new DateTime(2012, 8, 14));
    CheckResults(1, 11, 16);
  }

  [Test]
  public void AlmostThreeYearsTest()
  {
    ddClass.SetDates(new DateTime(2009, 7, 29), new DateTime(2012, 7, 14));
    CheckResults(2, 11, 15);
  }

  [Test]
  public void BornOnALeapYearTest()
  {
    ddClass.SetDates(new DateTime(2008, 2, 29), new DateTime(2009, 2, 28));
    CheckControversialResults(0, 11, 30, 1, 0, 0);
  }

  [Test]
  public void BornOnALeapYearTest2()
  {
    ddClass.SetDates(new DateTime(2008, 2, 29), new DateTime(2009, 3, 1));
    CheckControversialResults(1, 0, 0, 1, 0, 1);
  }


  [Test]
  public void LongMonthToLongMonth()
  {
    ddClass.SetDates(new DateTime(2010, 1, 31), new DateTime(2010, 3, 31));
    CheckResults(0, 2, 0);
  }

  [Test]
  public void LongMonthToLongMonthPenultimateDay()
  {
    ddClass.SetDates(new DateTime(2009, 1, 31), new DateTime(2009, 3, 30));
    CheckResults(0, 1, 30);
  }

  [Test]
  public void LongMonthToShortMonth()
  {
    ddClass.SetDates(new DateTime(2009, 8, 31), new DateTime(2009, 9, 30));
    CheckControversialResults(0, 1, 0, 0, 0, 30);
  }

  [Test]
  public void LongMonthToPartWayThruShortMonth()
  {
    ddClass.SetDates(new DateTime(2009, 8, 31), new DateTime(2009, 9, 10));
    CheckResults(0, 0, 10);
  }

  private void CheckResults(int years, int months, int days)
  {
    Assert.AreEqual(years, ddClass.GetYears());
    Assert.AreEqual(months, ddClass.GetMonths());
    Assert.AreEqual(days, ddClass.GetDays());
  }

  private void CheckControversialResults(int years, int months, int days,
    int yearsAlt, int monthsAlt, int daysAlt)
  {
    // gives the right output but unhelpful messages
    bool success = ((ddClass.GetYears() == years
                     && ddClass.GetMonths() == months
                     && ddClass.GetDays() == days)
                    ||
                    (ddClass.GetYears() == yearsAlt
                     && ddClass.GetMonths() == monthsAlt
                     && ddClass.GetDays() == daysAlt));

    Assert.IsTrue(success);
  }
}
Run Code Online (Sandbox Code Playgroud)

Most of the names are slightly silly and don't really explain why code might fail the test, however looking at the two dates and the answer(s) should be enough to understand the test.

There are two functions that do all the Asserts, CheckResults() and CheckControversialResults(). These work well to save typing and give the right results, but unfortunately they make it harder to see exactly what went wrong (because the Assert in CheckControversialResults() will fail with "Expected true", rather than telling you which value was incorrect. If anyone has a better way to do this (avoid writing the same checks each time, but have more useful error messages) please let me know.

CheckControversialResults() is used for a couple of cases where there seem to be two different opinions on what is right. I have an opinion of my own, but I thought I should be liberal in what I accepted here. The gist of this is deciding whether one year after Feb 29 is Feb 28 or Mar 1.

These tests are the crux of the matter, and there could very well be errors in them, so please do comment if you find one which is wrong. It would be also good to hear some suggestions for other tests to check any future iterations of answers.

No test involves time of day - all DateTimes are at midnight. Including times, as long as it's clear how rounding up and down to days works (I think it is), might show up even more flaws.

The results

The complete scoreboard of results is as follows:

ChuckRostance_Test 3 failures               S S S F S S F S F
Dave_Test 6 failures                        F F S F F F F S S
Dylan_Hayes_Test 9 failures                 F F F F F F F F F
ho1_Test 3 failures                         F F S S S S F S S
Jani_Test 6 failures                        F F S F F F F S S
Jon_Test 1 failure                          S S S S S S F S S
lc_Test 2 failures                          S S S S S F F S S
LukeH_Test 1 failure                        S S S S S S F S S
Malu_MN_Test 1 failure                      S S S S S S S F S
Mohammed_Ijas_Nasirudeen_Test 2 failures    F S S F S S S S S
pk_Test 6 failures                          F F F S S F F F S
Rajeshwaran_S_P_Test 7 failures             F F S F F S F F F
ruffin_Test 3 failures                      F S S F S S F S S
this_curious_geek_Test 2 failures           F S S F S S S S S
Run Code Online (Sandbox Code Playgroud)

But note that Jani's solution was actually correct and passed all tests - see update 4 below.

The columns are in alphabetical order of test name:

  • AlmostThreeYearsTest
  • AlmostTwoYearsTest
  • BasicTest
  • BornOnALeapYearTest
  • BornOnALeapYearTest2
  • LongMonthToLongMonth
  • LongMonthToLongMonthPenultimateDay
  • LongMonthToPartWayThruShortMonth
  • LongMonthToShortMonth

Three answers failed only 1 test each, Jon's, LukeH's and Manu MN's. Bear in mind these tests were probably written specifically to address flaws in those answers.

Every test was passed by at least one piece of code, which is slightly reassuring that none of the tests are erroneous.

Some answers failed a lot of tests. I hope no-one feels this is a condemnation of that poster's efforts. Firstly the number of successes is fairly arbitrary as the tests don't evenly cover the problem areas of the question space. Secondly this is not production code - answers are posted so people can learn from them, not copy them exactly into their programs. Code which fails a lot of tests can still have great ideas in it. At least one piece which failed a lot of tests had a small bug in it which I didn't fix. I'm grateful to anyone who took the time to share their work with everyone else, for making this project so interesting.

My conclusions

There are three:

  1. Calendars are hard. I wrote nine tests, including three where two answers are possible. Some of the tests where I only had one answer might not be unanimously agreed with. Just thinking about exactly what we mean when we say '1 month later' or '2 years earlier' is tricky in a lot of situations. And none of this code had to deal with all the complexities of things like working out when leap years are. All of it uses library code to handle dates. If you imagine the 'spec' for telling time in days, weeks, months and years written out, there's all sorts of cruft. Because we know it pretty well since primary school, and use it everyday, we are blind to many of the idiosyncracies. The question is not an academic one - various types of decomposition of time periods into years, quarters and months are essential in accounting software for bonds and other financial products.

  2. Writing correct code is hard. There were a lot of bugs. In slightly more obscure topics or less popular questions than the chances of a bug existing without having been pointed out by a commenter are much, much higher than for this question. You should really never, never copy code from SO into your program without understanding exactly what it does. The flipside of this is that you probably shouldn't write code in your answer that is ready to be copied and pasted, but rather intelligent and expressive pseudo-code that allows someone to understand the solution and implement their own version (with their own bugs!)

  3. Unit tests are helpful. I am still meaning to post my own solution to this when I get round to it (for someone else to find the hidden, incorrect assumptions in!) Doing this was a great example of 'saving the bugs' by turning them into unit tests to fix the next version of the code with.

Update

The whole project is now at https://github.com/jwg4/date-difference This includes my own attempt jwg.cs, which passes all the tests I currently have, including a few new ones which check for proper time of day handling. Feel free to add either more tests to break this and other implementations or better code for answering the question.

Update 2

@MattJohnson has added an implementation which uses Jon Skeet's NodaTime. It passes all the current tests.

Update 3

@KirkWoll's answer to Difference in months between two dates has been added to the project on github. It passes all the current tests.

Update 4

@Jani pointed out in a comment that I had used his code wrongly. He did suggest methods that counted the years, months and days correctly, (alongside some which count the total number of days and months, not the remainders) however I mistakenly used the wrong ones in my test code. I have corrected my wrapper around his code and it now passes all tests. There are now four correct solutions, of which Jani's was the first. Two use libraries (Intenso.TimePeriod and NodaTime) and two are written from scratch.


小智 16

多年来/月/周的正确差计算,日历中的CultureInfo的必须考虑:

  • 飞跃与非闰年
  • 不同天数的月份
  • 具有不同周数的年份(根据一周的第一天和日历周规则而变化)

.NET时间段库DateDiff类尊重所有这些因素:

// ----------------------------------------------------------------------
public void DateDiffSample()
{
  DateTime date1 = new DateTime( 2009, 11, 8, 7, 13, 59 );
  Console.WriteLine( "Date1: {0}", date1 );
  // > Date1: 08.11.2009 07:13:59
  DateTime date2 = new DateTime( 2011, 3, 20, 19, 55, 28 );
  Console.WriteLine( "Date2: {0}", date2 );
  // > Date2: 20.03.2011 19:55:28

  DateDiff dateDiff = new DateDiff( date1, date2 );

  // differences
  Console.WriteLine( "DateDiff.Years: {0}", dateDiff.Years );
  // > DateDiff.Years: 1
  Console.WriteLine( "DateDiff.Quarters: {0}", dateDiff.Quarters );
  // > DateDiff.Quarters: 5
  Console.WriteLine( "DateDiff.Months: {0}", dateDiff.Months );
  // > DateDiff.Months: 16
  Console.WriteLine( "DateDiff.Weeks: {0}", dateDiff.Weeks );
  // > DateDiff.Weeks: 70
  Console.WriteLine( "DateDiff.Days: {0}", dateDiff.Days );
  // > DateDiff.Days: 497
  Console.WriteLine( "DateDiff.Weekdays: {0}", dateDiff.Weekdays );
  // > DateDiff.Weekdays: 71
  Console.WriteLine( "DateDiff.Hours: {0}", dateDiff.Hours );
  // > DateDiff.Hours: 11940
  Console.WriteLine( "DateDiff.Minutes: {0}", dateDiff.Minutes );
  // > DateDiff.Minutes: 716441
  Console.WriteLine( "DateDiff.Seconds: {0}", dateDiff.Seconds );
  // > DateDiff.Seconds: 42986489

  // elapsed
  Console.WriteLine( "DateDiff.ElapsedYears: {0}", dateDiff.ElapsedYears );
  // > DateDiff.ElapsedYears: 1
  Console.WriteLine( "DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths );
  // > DateDiff.ElapsedMonths: 4
  Console.WriteLine( "DateDiff.ElapsedDays: {0}", dateDiff.ElapsedDays );
  // > DateDiff.ElapsedDays: 12
  Console.WriteLine( "DateDiff.ElapsedHours: {0}", dateDiff.ElapsedHours );
  // > DateDiff.ElapsedHours: 12
  Console.WriteLine( "DateDiff.ElapsedMinutes: {0}", dateDiff.ElapsedMinutes );
  // > DateDiff.ElapsedMinutes: 41
  Console.WriteLine( "DateDiff.ElapsedSeconds: {0}", dateDiff.ElapsedSeconds );
  // > DateDiff.ElapsedSeconds: 29

  // description
  Console.WriteLine( "DateDiff.GetDescription(1): {0}", dateDiff.GetDescription( 1 ) );
  // > DateDiff.GetDescription(1): 1 Year
  Console.WriteLine( "DateDiff.GetDescription(2): {0}", dateDiff.GetDescription( 2 ) );
  // > DateDiff.GetDescription(2): 1 Year 4 Months
  Console.WriteLine( "DateDiff.GetDescription(3): {0}", dateDiff.GetDescription( 3 ) );
  // > DateDiff.GetDescription(3): 1 Year 4 Months 12 Days
  Console.WriteLine( "DateDiff.GetDescription(4): {0}", dateDiff.GetDescription( 4 ) );
  // > DateDiff.GetDescription(4): 1 Year 4 Months 12 Days 12 Hours
  Console.WriteLine( "DateDiff.GetDescription(5): {0}", dateDiff.GetDescription( 5 ) );
  // > DateDiff.GetDescription(5): 1 Year 4 Months 12 Days 12 Hours 41 Mins
  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
Run Code Online (Sandbox Code Playgroud)

DateDiff还计算Quarters的差异.


lc.*_*lc. 15

闰年和不均衡的月份实际上使这成为一个非常重要的问题.我确信有人可以提出一种更有效的方法,但这里有一个选项 - 首先在小方面进行近似调整(未经测试):

public static void GetDifference(DateTime date1, DateTime date2, out int Years, 
    out int Months, out int Weeks, out int Days)
{
    //assumes date2 is the bigger date for simplicity

    //years
    TimeSpan diff = date2 - date1;
    Years = diff.Days / 366;
    DateTime workingDate = date1.AddYears(Years);

    while(workingDate.AddYears(1) <= date2)
    {
        workingDate = workingDate.AddYears(1);
        Years++;
    }

    //months
    diff = date2 - workingDate;
    Months = diff.Days / 31;
    workingDate = workingDate.AddMonths(Months);

    while(workingDate.AddMonths(1) <= date2)
    {
        workingDate = workingDate.AddMonths(1);
        Months++;
    }

    //weeks and days
    diff = date2 - workingDate;
    Weeks = diff.Days / 7; //weeks always have 7 days
    Days = diff.Days % 7;
}
Run Code Online (Sandbox Code Playgroud)


小智 10

使用System.Data.Linq命名空间及其SqlMethods.DateDiffMonth方法怎么样?

例如,说:

DateTime starDT = {01-Jul-2009 12:00:00 AM}
DateTime endDT = {01-Nov-2009 12:00:00 AM}
Run Code Online (Sandbox Code Playgroud)

然后:

int monthDiff = System.Data.Linq.SqlClient.SqlMethods.DateDiffMonth(startDT, endDT);
Run Code Online (Sandbox Code Playgroud)

==> 4

类中还有其他DateDiff静态方法SqlMethods.