I think some good points are in #63, #67 (personal comments aside), and #73. I think .paul. missed the two points in #69. As of #60 I decided to explicitly state a lack of confidence that I'd hit all edge cases and I'm no longer claiming correctness. I tested many edge cases but knew there were likely several I missed.
My "essays" (ask dbasnett about me, he's seen my *real* essays) are to highlight the points I felt were pertinent. I like word-heavy posts because I think it's nearly criminal to aid and abet copypasta programmers. In this case it doesn't matter; odds are any example won't have the domain-specific logic the programmer wants. Here's what I think is important now:
This is a tough algorithm to get right; simple code is probably a better idea than complicated code.
Days are the only unit easily and accurately counted. All others require difficult logic.
There is no consensus on the "right" answer to certain time spans except when expressed in days. The domain specifies expected behavior.
DateDiff() is often harder to use than rewrite yourself.
I'm not sure if all edge cases have been enumerated; I think that's the best first step.
Based on #5, the best we can do is test *many* edge cases and use that as our confidence all cases are covered.
I'm not interested in a third attempt because of #5. Call me chicken, but at this point I think it'd be a bit hypocritical to post another code sample if I'm not absolutely certain it works. Also, I'm not certain anyone's posted a critique of #60 so I'm going to assume it works until I've run dbasnett's test cases from #74. Plus, I want to see dbasnett's approach; I haven't opened VS 2010 yet today. I wouldn't want to waste time writing something he's already written.
I think the only "good" way to verify this would be a brute-force approach. It's a simple matter to keep adding days to an end date and period, then comparing the results to the expected value. The trick is figuring out how long a time span is adequate for correctness. I think I have an idea and I'm going to work on that test harness instead of any further attempts at a "correct" algorithm.
The concept of counting weeks makes me shiver, but I don't think it's as complicated on its own as it is incorporated to the other counter. Pseudocode:
Code:
Let weeks = 0
Let currentDate = startDate
While currentDate < endDate
Add 7 days to currentDate
If currentDate < endDate Then
Add 1 to weeks
End If
End While
Of course this has the same problems with domain-specific requirements as the rest. You might be picky about whether this is off by 1, or you might only want to count full weeks, or you might only want full business weeks. Each of those has its own complexities.
This is why I think DateDiff() fails. It tries to address *all* of these scenarios rather than addressing very specific scenarios. I don't think it's wise to write a general-purpose *function* to calculate these ranges. A class with several specific functions seems better.
...My "essays" (ask dbasnett about me, he's seen my *real* essays) are to highlight the points I felt were pertinent.
This is a tough algorithm to get right; simple code is probably a better idea than complicated code.
Days are the only unit easily and accurately counted. All others require difficult logic.
There is no consensus on the "right" answer to certain time spans except when expressed in days. The domain specifies expected behavior.
DateDiff() is often harder to use than rewrite yourself.
I'm not sure if all edge cases have been enumerated; I think that's the best first step.
Based on #5, the best we can do is test *many* edge cases and use that as our confidence all cases are covered.
.... Also, I'm not certain anyone's posted a critique of #60 so I'm going to assume it works until I've run dbasnett's test cases from #74. Plus, I want to see dbasnett's approach; I haven't opened VS 2010 yet today. I wouldn't want to waste time writing something he's already written.
I think the only "good" way to verify this would be a brute-force approach. It's a simple matter to keep adding days to an end date and period, then comparing the results to the expected value. The trick is figuring out how long a time span is adequate for correctness. I think I have an idea and I'm going to work on that test harness instead of any further attempts at a "correct" algorithm.
"I'm not sure if all edge cases have been enumerated; I think that's the best first step." I have an idea of what I mean and I will post my definition.
I agree about the brute force method. My attempt at that is in post #58.
I am still cleaning up the code and will post it in post #2 as I work on it. After 5 or 6 days of fooling with this I am in no hurry.
In other news, brute force fell flat. In order for my technique to work, you have to have a way to calculate the "expected" range. Calculating the expected range is what the algorithm is trying to do; you can't test something with itself. I'm not sure I trust #58 either; all you're doing is testing one algorithm for calculating the range against another. Rats.
Haven't spent much time on the revised #2; I can't figure out which pieces are important and which pieces aren't. I'll wait until you've got something presentable while I ruminate on the test cases.
Last edited by Sitten Spynne; Jul 27th, 2011 at 10:31 AM.
In other news, brute force fell flat. In order for my technique to work, you have to have a way to calculate the "expected" range. Calculating the expected range is what the algorithm is trying to do; you can't test something with itself. I'm not sure I trust #58 either; all you're doing is testing one algorithm for calculating the range against another. Rats.
Haven't spent much time on the revised #2; I can't figure out which pieces are important and which pieces aren't. I'll wait until you've got something presentable while I ruminate on the test cases.
In other news, brute force fell flat. In order for my technique to work, you have to have a way to calculate the "expected" range. Calculating the expected range is what the algorithm is trying to do; you can't test something with itself. I'm not sure I trust #58 either; all you're doing is testing one algorithm for calculating the range against another. Rats.
Haven't spent much time on the revised #2; I can't figure out which pieces are important and which pieces aren't. I'll wait until you've got something presentable while I ruminate on the test cases.
1/29/2005 does (did) exist.
Read #42 to see why I think #58 works.
Here is a simple example of how to use the code in #2
Code:
Private Sub Button4_Click(sender As System.Object, e As System.EventArgs) Handles Button4.Click
Try
Dim foo As New AgeCalc(Date.Parse(TextBox1.Text), Date.Parse(TextBox2.Text))
Dim bar As Age = foo.AgeInYearsMonthsDays
Label1.Text = bar.Years.ToString & " " & bar.Months.ToString & " " & bar.Days.ToString
Catch ex As Exception
End Try
End Sub
Try these date pairs for controversy:
1/30/2005 - 4/30/2005
1/31/2005 - 4/30/2005
Last edited by dbasnett; Jul 27th, 2011 at 11:38 AM.
I'm still not convinced. The algorithm you describe in #42 is very similar to my #8. If so, and #8 has flaws, how can you trust checking against a flawed algorithm?
In other news, I just sat down and banged out what I think is a fairly exhaustive list of test cases. I listed 98 (I think!) but could have thought of nearly 150 if I felt really paranoid. To test day calculations, I picked all for month lengths and picked easy dates within the month. To test month calculations, I noted there were several combinations of month lengths. First, I crossed each month boundary (31, 30, 29, and 28 days) and verified the (1m, 0d) date and the two boundary dates. (The * before some tests indicates previous tests have technically covered that case.) For fun, I tested from July-September, one of the two 31-31 boundaries. I did a cursory test of the Dec-Jan boundary to catch erroneous year increments. Then I listed 31-30, 31-29, and 31-28. At this point I felt month calculations were adequately tested, but other possible tests include the boundaries 30-31, 31-31-30, 31-30-31, and 30-31-30 (they would have been more interesting in an algorithm attempting to go from days -> months directly.) Year calculations don't have to be as thorough: I tested Jan-Jan and Jan-Mar (leap and non-leap.) Finally, calculations across leap years are tested in all scenarios I could think of: first year is leap, 1 leap year is spanned, no leap year in range, end year is leap, and multiple leap years spanned.
So far I can't think of any relevant situations not listed.
It's possible I'm off by one on the "expected" dates. I triple-counted using a calendar, but the font was small and I tend to get cross-eyed trying to count dates like that. I'm too lazy to type up a harness that uses them all; I've spent enough time on forums today anyway Maybe I'll unleash it against my stuff later tonight.
For no other reason than, "I started so I'll finish" here is the latest revision of my own method which I am confident will pass any stress tests
I made a better test function to help me conclude this. It specifies a startdate. It then tests this against every possible end date upto 366 days from the start date. It will then increase the startdate by 1 day and cycle this again. It ends when the startdate reaches 366 days from the given date. OK I think i've confused us all enough now!
dbasnett, you will be glad to know I tested it on your code too, and there are no errors in yours either
Here's the code:
VB.NET Code:
Public Class Form1
'Date1 is the lowest date, Date2 is the highest date
Private Sub DateCompare(ByVal Date1 As Date, ByVal Date2 As Date)
Dim Days As Integer = Nothing
Dim Months As Integer = Nothing
Dim Years As Integer = Nothing
'Convert Date1/Date2 months and years into months.
Dim Date1Months As Integer = Date1.Year * 12 + Date1.Month
Dim Date2Months As Integer = Date2.Year * 12 + Date2.Month
'Get the number of Months between the two dates. Subtract 1 (we will add this back later where necessary)
Months = Date2Months - Date1Months - 1
'See how many days are within the Month Date1/Date2 reside.
Dim DaysInDate1Month As Integer = Date.DaysInMonth(Date1.Year, Date1.Month)
Dim DaysInDate2Month As Integer = Date.DaysInMonth(Date2.Year, Date2.Month)
If Date1.Day = DaysInDate1Month And Date2.Day = DaysInDate2Month Then
'If Both Date1 and Date2 occur on the LastDayOfTheMonth.
'We count this as a month rather than x amount of days.
'Therefore add 1 to Months and set Days to zero.
'nb: We subtracted a month when we first grabbed the number of Months. Add it back here.
Months += 1
Days = 0
ElseIf Date1.Day = DaysInDate1Month And Date2.Day < DaysInDate2Month Then
'If Date1 occur's on the LastDayOfTheMonth but Date2 resides on a day prior to the end of it's month.
'We know the number of Days between the two dates will be the number of Days in Date2.
Days = Date2.Day
ElseIf Date1.Day > DaysInDate2Month And Date2.Day = DaysInDate2Month Then
Months += 1
Days = 0
Else
'If Neither Date1 or Date2 occur on the LastDayOfTheMonth then we can do a day count ourselves.
Select Case Date1.Day
Case Is < Date2.Day
'If Date2's Day is higher then Date1's Day then the 'Months' value will represent the number of months upto the Month that Date2 resides.
'The number of days will therefore be the difference between Date1/Date2's Day values.
Days = Date2.Day - Date1.Day
'nb: We subtracted a month when we first grabbed the number of Months. Add it back here.
Months += 1
Case Is > Date2.Day
'If Date2's Day is lower then Date1's Day then the 'Months' value will represent the number of months upto the Month prior to that which Date2 resides.
'First we need to find out the month prior to Date2's month value.
Dim MonthBeforeDate2 As Integer = Date2.Month
'If Date2 occurs in January, then we will also need to use the previous year in our calculation.
Dim Date2Year As Integer = Date2.Year
If MonthBeforeDate2 = 1 Then
MonthBeforeDate2 = 12
Date2Year -= 1
Else : MonthBeforeDate2 -= 1
End If
'If there are more days in Date1 than occur in the month prior to Date2 then we want to avoid calculating the days that remain in that month.
'As that would lead to a neagtive value.
If Date1.Day > Date.DaysInMonth(Date2Year, MonthBeforeDate2) Then
'In this case we only need the number of days in Date2.
Days = Date2.Day
Else
'Next we will see how many days occur between the value of Date1's Day and the end of the month prior to Date2.
Dim DaysLeftInMonthBeforeDate2 As Long = Date.DaysInMonth(Date2Year, MonthBeforeDate2) - Date1.Day
'Finally we can add the number of Days in Date2.
Days = DaysLeftInMonthBeforeDate2 + Date2.Day
End If
Case Is = Date2.Day
'If the Day value in Date1/Date2 matches.
'nb: We subtracted a month when we first grabbed the number of Months. Add it back here.
Months += 1
End Select
End If
'The last step is to work out the number of Years. We can work this out from the 'Months'.
'nb: We left this until last because there may have been a few neccessary changes to the Months value along the way.
If Months >= 12 Then
Do Until Months < 12
Years += 1
Months -= 12
Loop
End If
'Now just format the result:
MsgBox(String.Format("Years: {1}{0}Months: {2}{0}Days: {3}{0}", {vbCrLf, Years, Months, Days}))
@jay - Depending on how you define a leaplings birthday this may or may not be an error
1 years, 0 months, 0 days between 2/29/2004 2/28/2005
This is how I define it
0 years, 11 months, 30 days between 2/29/2004 2/28/2005
1 years, 0 months, 0 days between 2/29/2004 3/1/2005
That's the kind of thing I'm complaining about though; to some people you're wrong and Feb 29 N to Mar 1 N + 1 is a year. If that's the only miscalculation in an algorithm I say the calculation's correct; it's easy enough to correct the "flaw" with an If statement.
@jay - Depending on how you define a leaplings birthday this may or may not be an error
1 years, 0 months, 0 days between 2/29/2004 2/28/2005
This is how I define it
0 years, 11 months, 30 days between 2/29/2004 2/28/2005
1 years, 0 months, 0 days between 2/29/2004 3/1/2005
'Leaplings' can celebrate a birthday on either 28/02 or 01/03 in any standard year. It is common to opt for the latter because it is the day following Feb 28th as was the case of the day of their birth.
I did weigh up the leapling idea, but decided to leave it as is. My reasoning is that if you count the days between these dates:
28/2/2003 - 29/2/2004 = 366 days
29/2/2004 - 28/2/2005 = 365 days
28/2/2005 - 28/2/2006 = 365 days
28/2/2006 - 28/2/2007 = 365 days
28/2/2007 - 29/2/2008 = 366 days
by definition a leap year occurs every 4 years. I take that to mean a year is equal to 365 days for 3 consecutive years, followed by a non-standard year of 366 days.
If we were to take the leapling route then the dates above would be written:
28/2/2003 - 29/2/2004 = 366 days
29/2/2004 - 01/3/2005 = 366 days
Upto now we have 2 consective years that calculate as 366 days. Now since 01/03 has been used to define the end of year that begins on a leapling, do we now consider the start of the next year from the 01/03? If so we would be looking at losing a day in order to re-align the years:
01/3/2005 - 28/2/2006 = 364 days
28/2/2006 - 28/2/2007 = 365 days
28/2/2007 - 29/2/2008 = 366 days
and therefore the consistency of the year by definition has been thrown out of the window. Perhaps my view is wrong though, but as Sitten Spynne has maintained throughout, details such as these really come down to invidual interpretation because there is no set standard to govern this.
A couple of results from your function:
28/2/2003 - 29/2/2004 = 1 year, 1 day (366 days)
29/2/2004 - 28/2/2005 = 11 Months, 30 Days (11 months = 01/03 to 31/01, that is 336 days. Then add the 30.)
Last edited by JayJayson; Jul 28th, 2011 at 12:22 AM.
@jay - However you decide to treat a leapling's birthday is fine with me. I think we all have come to the conclusion that time expressed in anything other than days(TimeSpan accuracy) is arbitrary.