Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-devel / 2006-04

Re: [Fwd: Re: Magic numbers used in TryEncodeDate]

5 messages · 3 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Graeme Geldenhuys

Wow! Thanks for that excellent explanation. As I mentioned to a collegue earlier this week. I never knew dates can become such a complex thing!

I will use your email to document those functions, otherwise someone else might feel like I did before reading your explanation of the magic numbers.

Now to add another problem. What happens with dates like 1752-09 which only has 20 days. Shouldn't this affect the DeltaDate? Should Encode/DecodeDate know that for that month, 1752-09, there never existed the days 3rd thru 13th. The correct days for September was 1, 2, 14, 15, ... or should that be the worries of the Calendar Component displaying that month and not Encode/DecodeDate's problem?

Regards,
- Graeme -

Bram Kuijvenhoven wrote 178 lines
I posted this earlier today, but haven't seen it on the list yet. In fact, I have seen no messages at all at the list. Anyway, you have it now :) -------- Original Message -------- Subject: Re: [fpc-devel] Magic numbers used in TryEncodeDate Date: Fri, 21 Apr 2006 13:47:46 +0200 From: Bram Kuijvenhoven To: FPC developers' list References: Hi, Here is the explanation I promised: Graeme Geldenhuys wrote: > What is the meaning of these magic numbers used in the TryEncodeDate method? > I would like to document them for FPC once I know the answer. > > 146097 > 1461 > 153 > 693900 > > I saw the constant DateDelta, but that is never used in TryEncodeDate > (whereas in Delphi it is). > [...] > Function TryEncodeDate(Year,Month,Day : Word; Var Date : TDateTime) : Boolean; > var > c, ya: cardinal; > begin > Result:=(Year>0) and (Year<10000) and > (Month in [1..12]) and > (Day>0) and (Day<=MonthDays[IsleapYear(Year),Month]); > If Result then > begin > if month > 2 then > Dec(Month,3) > else > begin > Inc(Month,9); > Dec(Year); > end; > c:= Year DIV 100; > ya:= Year - 100*c; > Date := (146097*c) SHR 2 + (1461*ya) SHR 2 + > (153*cardinal(Month)+2) DIV 5 + cardinal(Day) - 693900; > end > end; Given are Year, Month and Day. We intend to convert this to the number of days since a certain data, such as 1/1/1, 1/1/1900 or 1/1/1970, or 30/12/1899 for TDateTime. Obviously, if we have some formula that enumerates the days, regardless of the starting date, we can always add or subtract a 'delta' to change the starting date. Intuitively we might think of a formula that looks like: 365 * Year + 30 * Month + Day Of course this formula is terribly wrong, and that is (mainly) because of two reasons: 1) not every year has 365 days in it 2) not every month has 30 days in it Let us first deal with 2). As noted, not every month has 30 days, but even worse, some have 30, some 31 and one has 28/29: February. We are looking for a more accurate formula than the 30 * Month above. It should represent the number of days in the months before Month in the year. A simple trick that will allow us to give a simple formula, and will help us dealing with leap Februaries, is: let a year run from March until February. The following map is taken: December 2005 -> 'month' 9 of 'year' 2005 January 2006 -> 'month' 10 of 'year' 2005 February 2006 -> 'month' 11 of 'year' 2005 March 2006 -> 'month' 0 of 'year' 2006 April 2006 -> 'month' 1 of 'year' 2006 ... December 2006 -> 'month' 9 of 'year' 2006 January 2007 -> 'month' 10 of 'year' 2006 February 2007 -> 'month' 11 of 'year' 2006 March 2007 -> 'month' 0 of 'year' 2007 (I will use 'year' and 'month' instead of year and month to differentiate them.) Now, the number of days in the current 'year' in the 'months' 0 .. Month-1 are given by: March: 0 -> 0 April: 1 -> 31 (the 31 days of March) May: 2 -> 61 (the 31 days of March + the 30 days of April) June: 3 -> 92 (the 31 days of March + the 30 days of April + the 30 days of May) July: 4 -> 122 (... + 30 days of June) August: 5 -> 153 (... + 31 days of July) September: 6 -> 184 (... + 31 days of August) October: 7 -> 214 (... + 30 days of September) November: 8 -> 245 (... + 31 days of October) December: 9 -> 275 (... + 30 days of November) January: 10 -> 306 (... + 31 days of December) February: 11 -> 337 (... + 31 days of January) This function is almost linear. The trick is to round a linear function, such that the increments are sometimes 30 and sometimes 31. For example take floor( 30.6 * Month + 0.4 ) = ( 153*Month + 2 ) DIV 5 This formula exactly gives the above function! To get the day of the 'year', we thus have the formula floor( 30.6 * Month + 0.4 ) + Day Let us now deal with 1): we need a formula for the number of days in the previous 'years'. Remember that a year, called a leap year, has 366 days instead of 365 when: it is a multiple of 4, but not a multiple of 100, unless it is a multiple of 400. So 2000 and 2004 are leap years, but 1900 is not. So every period of 400 years has exactly the same number of days, because the leap year rule is periodic with period 400. The number of days is 365*400 + 400/4 - 400/100 + 400/400 = 146097 So every century has about 146097/4 = 36524.25 days in it. Most centuries have 36524 days in it; only every fourth century has 36525 days in it. Similarly, every fourth year (within a century, e.g. 1901..1999) has 366 instead of 365 days in it. Recall that we used 'years' running from March until February the next year. This means that the leap day of a leap year is actually the last day before that year. For example, the 'year' 2000 runs from March 2000 until February 2001, and the leap day of 200 is 29 February 2000, the last day of the 'year' 1999. We now dissect a Year into two pieces: the century (c) and the year-within-the-century (ya). That is, we take Year = 100*c + ya, where 0 <= ya < 100. (Note: This does not correspond with the usual definition of century, which says e.g. that the 21th century runs from 2001 until 2100.) We now write the number of days in the years before Year = = the number of days in the centuries before c + the number of days in years before ya in the current century For us, a century starts at March c*100 and ends at February (c+1)*100. E.g. c = 19 runs from 1 March 1900 until 29 February 2000. Hence, the extra leap year of every fourth century has its leap day not in e.g. c = 20, but as the last day of c = 19. Since we are looking at the quantity 'the number of days in the centuries before c', this leap day appears first in this quantity for c's divisible by 4, e.g. for c = 20. This explains in detail why 'the number of days in the centuries before c' can be given by floor( 36524.25 * c ) = (146097 * c) SHR 2 This function 'jumps' by 36525 instead of 36524 exactly when c is a multiple of 4. Similarly, the leap day of every fourth year within a century is part of the 'year' before it. E.g. our 'year' 1903 runs (from 1 March 1903) until the leap day 29 February 1904. So now we see that 'the number of days in years before ya in the current century' can be given by floor( 365.25 * ya ) = ( (365*4+1) * ya ) SHR 2 = (1461 * ya) SHR 2 This function 'jumps' by 366 instead of 365 exactly when ya is a multiple of 4. Putting this together, we find that 'the number of days in the years before Year' is given by floor( 36524.25 * c ) + floor( 365.25 * ya ) = (146097 * c) SHR 2 + (1461 * ya) SHR 2 Putting 1) and 2) together we find that the day enumeration is given by (146097 * c) SHR 2 + (1461 * ya) SHR 2 + ( 153*Month + 2 ) DIV 5 + Day + Delta, where Delta depends on the choice of the starting date. For TDateTime, we want 31 December 1899 to be day 1 (by definition). Note that this date has has 'year' 1899 and 'month' 9, so solve Delta from 1 = (146097 * 18) SHR 2 + (1461 * 99) SHR 2 + ( 153*9 + 2 ) DIV 5 + 31 + Delta = = 657436 + 36159 + 275 + 31 + Delta = = 693901 + Delta, hence Delta = 693900 The DateDelta in Delphi is used to convert between Delphi 1 and Delphi 2 TDateTime. In D1, TDateTime was the number of days elapsed since 1 January 1; in D2 and onwards it is the number of days elapsed since 30 December 1899. So they introduced DateDelta, the number of days from 1/1/0001 to 31/12/1899, which is 693594. The DateDelta comes close to our 693900, but it is not exactly the same, as it also incorporates the shift of a year by two months and the inclusion of the year 0 in our (146097*c) SHR 2 term. (Sidenote: actually the year 0 never existed; the year before 1 AD is 1 BC and not a magic year 0.) Looking closer at the routine, things go wrong there for pre-12 December 1899 dates. The constants in the formula are ok, but the calculation is done using cardinals, which are unsigned! EncodeDate(1,1,1) returns 4294273703 instead of the negative number -693593. Another note, about Math.DivMod: it seems to be implemented only for Words at the moment: procedure DivMod(Dividend: Integer; Divisor: Word; var Result, Remainder: Word); This is no real problem for this routine -- we can make c and ya Words and cast them to Longints in the formula (this also solves the pre-30/12/1899 problem, though that can also be done to cast the whole part before the minus sign at the end) -- but why is there no Cardinal (or Longint) version? And: is it {$INLINE}? Or does the compiler already optimize things like result :=a div b; remainder:=a mod b; or the here used result :=a div b; remainder:=a-result*b; BTW, we might need to check the DecodeDate function and others too :) Regards, Bram PS Sorry if this mail arrives twice; I'm resending it because I haven't seen it appearing on the list yet in about an hour; also I used another email address than the one I'm subscribed with at the list, but funny enough yesterday, when I did that too, it worked :S
There's no place like 127.0.0.1

Reply by email

Graeme Geldenhuys
Bram Kuijvenhoven wrote
IIRC this is commonly solved by differentiating between th Gregorian and Julian Calendar. I.e. the EncodeDate function etc. work with the Gregorian Calendar. Before that date in 1752 people used the Julian Calendar, but we map all TDateTimes to Gregorian dates.

This makes sense. As far as I know, in most Calender components, there is an option to specify which calender system to use (Gregorian, Julian, etc..) and Gregorian being the default.

Bottom Line:
*Don't* invent a time travel machine! :-)

Regards,
- Graeme -

There's no place like 127.0.0.1

Reply by email

Bram Kuijvenhoven
to Graeme Geldenhuys

Oops, I see that in the posting mess of this thread I replied to the wrong email address -- I sent the below to Graeme's personal email instead of the list. (Note: a few minutes ago I suddenly saw my other messages appear in the list, along with some other messages posted in the past days; in the light of Florian's 'Test, please ignore' message I assume something was up with the mailing list.)

Earlier message 28 lines
-------- Original Message -------- Subject: Re: [Fwd: Re: [fpc-devel] Magic numbers used in TryEncodeDate] Date: Sat, 22 Apr 2006 12:09:46 +0200 From: Bram Kuijvenhoven To: Graeme Geldenhuys References: (I hope that this email arrives at the mailing list...) Graeme Geldenhuys wrote: > Now to add another problem. What happens with dates like 1752-09 > which only has 20 days. Shouldn't this affect the DeltaDate? Should > Encode/DecodeDate know that for that month, 1752-09, there never > existed the days 3rd thru 13th. The correct days for September was 1, > 2, 14, 15, ... or should that be the worries of the Calendar Component > displaying that month and not Encode/DecodeDate's problem? IIRC this is commonly solved by differentiating between th Gregorian and Julian Calendar. I.e. the EncodeDate function etc. work with the Gregorian Calendar. Before that date in 1752 people used the Julian Calendar, but we map all TDateTimes to Gregorian dates. As far as I know FPC does not have a system like Java, which was designed to work with any calendar. (also think of Chinese, Hebrew, ...) Having such a date system (perhaps in a separate unit) could allow us to have functions mapping things to Julian, Gregorian and mix-type dates (i.e. Julian before 1752/9 and Gregorian after). Note that TDateTime is giving a universal time scale, but that mapping between different calendars needs different routines. Since historical dates, mentioned in historical documents might be Julian-based, it might be useful for some people to be able to deal with more or mixed calendars. But then they might want to be able to deal with BC as well! But I recommend looking these things up somewhere before writing it down in the docs. (i.e. about the precise meaning of the Gregorian vs. the Julian calendar) This is just my IIRC :) Regards, Bram

Reply by email

Marco van de Voort
to Bram Kuijvenhoven
But I recommend looking these things up somewhere before writing it down in the docs. (i.e. about the precise meaning of the Gregorian vs. the Julian calendar) This is just my IIRC :)

There is a very exhaustive calendar faq on the internet. And if you read it, then you will understand that implementing full calendar support will result in an immensely overweight system that is not used 99% of the time.

So the "historic" datetime routines and the normal, 1950+ routines should be kept separate.

This is also because the conversion to the Gregorian calendar is dependant on where you live. Some Orthodox countries only converted in the 20th century.

Most calendar systems only implement a simplified model of the date system (e.g. what can be easily caught with a few equations), and don't tackle the place-specific issues, and the myriads of exceptions. However for real historic date use, you'll need that.

Reply by email

Graeme Geldenhuys

Not sure if this came through the list before....

Graeme .

---------- Forwarded message ----------

Earlier message 23 lines
From: Graeme Geldenhuys Date: 22-Apr-2006 12:31 Subject: Re: [Fwd: Re: [fpc-devel] Magic numbers used in TryEncodeDate] To: FPC developers' list On 22/04/06, Bram Kuijvenhoven wrote: > IIRC this is commonly solved by differentiating between th Gregorian and Julian Calendar. I.e. the EncodeDate function etc. work with the Gregorian Calendar. Before that date in 1752 people used the Julian Calendar, but we map all TDateTimes to Gregorian dates. > This makes sense. As far as I know, in most Calender components, there is an option to specify which calender system to use (Gregorian, Julian, etc..) and Gregorian being the default. Bottom Line: *Don't* invent a time travel machine! :-) Regards, - Graeme - -- There's no place like 127.0.0.1

Reply by email