TRUEDATE

A Portable Routine for Date Arithmetic

by Stephen R. Ferg

View PUBLIC DOMAIN license information

Introduction

It's not hard to find a date arithmetic routine when you need one. They are available in journal articles, in books, and on electronic bulletin boards.But if you are building a business application, you have a remarkably slim chance of finding a routine that you can actually use.

The reason is that most date arithmetic routines use time-concepts drawn from astronomy, not from the business calendar. For these routines, a day is the time it takes the earth to make one complete rotation on its axis; a year is the time it takes the earth to make one complete revolution around the sun. For such routines, the number of days in a year is not built into the concept year, but is something to be discovered through scientific investigation. As we all know, astronomers have discovered that a year is equal to approximately 365 and 1/4 days.

Astronomy-based routines typically rely on hard-coded "magic numbers": a year is 365.2422 days, a month is 30.6001 days, and so on. They also involve repeated multiplication and division operations designed to force truncation of numbers that are stored internally in a particular format: you might find, for example, a number being multiplied by 12 and then immediately being divided by 12. (The great-grand-daddy of these routines is a FORTRAN algorithm published in 1968 by two astronomers at the U.S. Naval Observatory. See "A Machine Algorithm for Processing Calendar Dates", by Henry F. Fliegel and Thomas C. Van Flandern, Communications of the ACM, Volume 11, Number 10, October 1968.)

These routines have two major drawbacks. First, because they depend on numbers being stored internally in a format determined by a particular language as implemented by a particular compiler for a particular hardware platform, they are often not portable to other languages or platforms. Second, because their magic numbers and algorithms are based on the behavior of the platform they were designed to run on, not on problem-domain concepts such as year and leap year, their algorithms are virtually incomprehensible, and hence unmodifiable.

In order to overcome these problems, I developed a date routine called TRUEDATE that is designed to be both portable and modifiable.

TRUEDATE is portable because its algorithms are independent of any particular language, compiler, or hardware platform. It requires only support for an INTEGER data type of at least seven significant digits, and normal arithmetic operations on integers: addition, subtraction, multiplication, integer division, and division remainder (modulo).

TRUEDATE is comprehensible, and hence modifiable, because its algorithms are based on application-domain date concepts. For TRUEDATE:

TRUEDATE's leapyear scheme

The algorithm that TRUEDATE uses for determining which years are leap years and which are not is clearly represented in its source code. TRUEDATE's leapyear scheme is:

Every year is an ordinary year

According to this scheme:

The TRUEDATE Source Code

The source code for TRUEDATE is shown in Listing 1. It is expressed in a Pascal-like pseudo-code; the pseudo-code can be transformed into compilable Turbo Pascal by using a case-sensitive text editor to change "then" to "THEN BEGIN", "elseif" to "END ELSE IF", "else" to "END ELSE BEGIN", and "endif" to "END;". Translation into C, COBOL, Fortran, REXX, or other languages should not be difficult. Some obvious opportunities for optimization have been ignored in order to leave the pattern of the algorithm clear.

TRUEDATE is intended for business applications, not historical ones. It knows nothing about historical changes in the calendar such as the 10-day jump that occurred when Britain moved from the Julian to the Gregorian calendar in the 18th century. As far as TRUEDATE is concerned, the calendar has followed the same pattern, unchanged, since January 1, 0001.

Central to all date arithmetic routines is the concept of an "absolute" or "true Julian" date: a date expressed as the number of days from some day in the distant past. The apparently arbitrary base date for the Fliegel/Van Flandern algorithm --January 1, 4713 BCE -- reflects its basis in implementation, rather than application-domain, considerations. TRUEDATE's base date is January 1, 0001 (i.e. day 1 of month 1 of year 1). For this base, the absolute date for January 1, 1992 is 727198.

At the heart of TRUEDATE are two routines that convert a calendar date to, and from, an absolute date. With such routines available, doing date arithmetic is easy. For example, to determine an invoice date that is 60 days in the future, one simply translates today's calendar date into an absolute date, adds 60 to the absolute date, and translates the resulting absolute date back into a calendar date. When absolute dates are available, day-of-week determination is similarly easy. Dividing an absolute date by 7 will produce a remainder in the range 0..6 that will tell you the number of the day of the week.

Note that both of TRUEDATE's conversion routines calculate numbers for the leapyear flag and the Julian date (the number of days elapsed since the beginning of the year), and so can make those numbers available to a program that calls them.

When you implement TRUEDATE in a particular language, it is important to remember that TRUEDATE uses integer division. Integer division is an operation that always returns a whole number. In normal division, 7 divided by 2 produces the result 3.5; in integer division the result is 3. In many declarative languages, integer division can be performed simply by declaring all variables to be of type INTEGER. In other languages, however, you will need to be sure that all division operations are being done using the language's integer-division operator. In REXX, for example, the integer division operator is the percent sign (as opposed to the slash, the normal division operatior). In Turbo Pascal, the integer division operator is "div".

Note that these date routines do the minimum amount of data validation necessary to insure that they can execute successfully. The Cal2Absdate routine does not check that its input date is completely valid. It can and will accept and successfuly process, for example, a date for the 50th day of the month, or for the 29th of February in a non-leap year. A simple way to check to see if a calendar date is valid is to convert it to an absolute date, and then convert it back again. If you end up with the same calendar date that you started with, then the date is valid.

These are internal routines. They use variables from the surrounding environment, rather than declaring their own variables and getting input and output via parameter passing.


Listing 1

UNIT Truedate;
{================================================================}
{THIS SOURCE CODE REQUIRES PREPROCESSING BEFORE THE TURBO PASCAL }
{COMPILER CAN COMPILE IT.                                        }
{================================================================}
{If porting program to COBOL, replace LongInt with PIC S9(9) COMP }

INTERFACE

TYPE MonthNameType   = string;
TYPE TDoWNameType    = string;
TYPE TErrMsgType     = string;

{parameter variables}
VAR   AbsDate         : LongInt;
VAR   CalYear         : LongInt;
VAR   CalMonth        : LongInt;
VAR   MonthName       : MonthNameType;
VAR   CalDay          : LongInt;
VAR   JulianDate      : LongInt;
VAR   DayOfWeekNum    : LongInt;
VAR   LeapYearFlag    : LongInt;   {N.B. flag is also used as a number}
VAR   TDoWname        : TDoWNameType;
VAR   TErrMsg         : TErrMsgType;

Function DoWnum        (AbsDate:longint)     :longint;
Function DoWname_US    (DayOfWeekNum:longint):TDoWNameType;
Function DoWname_FR    (DayOfWeekNum:longint):TDoWNameType;
Function DoWname_SP    (DayOfWeekNum:longint):TDoWNameType;
Function DoWname_GR    (DayOfWeekNum:longint):TDoWNameType;
Function DoWname_DK    (DayOfWeekNum:longint):TDoWNameType;

Function MonthName_US  (CalMonth:longint)    :MonthNameType;
Function MonthName_FR  (CalMonth:longint)    :MonthNameType;
Function MonthName_SP  (CalMonth:longint)    :MonthNameType;
Function Monthname_GR  (CalMonth:longint)    :MonthNameType;
Function Monthname_DK  (CalMonth:longint)    :MonthNameType;

Procedure CalDate2AbsDate ;
Procedure AbsDate2CalDate ;

IMPLEMENTATION

CONST MinAbsDate       : LongInt = 1   ;   { JANUARY   1, 0001 }
CONST DaysInOrdinaryYr : LongInt = 365 ;
CONST DaysIn004YrGroup : LongInt = 1461;   {(DaysInOrdinaryYr *  4) + 1}
CONST DaysIn100YrGroup : LongInt = 36524;  {(DaysIn004YrGroup * 25) - 1}
CONST DaysIn400YrGroup : LongInt = 146097; {(DaysIn100YrGroup *  4) + 1}
CONST MaxAbsDate       : LongInt = 3652059;{ DECEMBER 31, 9999 }

CONST JANdays   : LongInt = 31;

CONST FEBshort  : LongInt = 28;
VAR   FEBdays   : LongInt;

CONST MARdays   : LongInt = 31;
CONST APRdays   : LongInt = 30;
CONST MAYdays   : LongInt = 31;
CONST JUNdays   : LongInt = 30;
CONST JULdays   : LongInt = 31;
CONST AUGdays   : LongInt = 31;
CONST SEPdays   : LongInt = 30;
CONST OCTdays   : LongInt = 31;
CONST NOVdays   : LongInt = 30;
CONST DECdays   : LongInt = 31;


VAR   Num400YrGroups , Num400YrModYrs : LongInt;
VAR   Num100YrGroups , Num100YrModYrs : LongInt;
VAR   Num004YrGroups , Num004YrModYrs : LongInt;
VAR   Num001YrGroups , Num001YrModYrs : LongInt;

{}
{==============================================================}
Function  Is_LeapYr(CalYear:longint):longint;
{==============================================================}
VAR   Mod400 : LongInt;
VAR   Mod100 : LongInt;
VAR   Mod004 : LongInt;
VAR   Mod001 : LongInt;

begin {proc/func}

   Mod400 := CalYear mod 400;
if Mod400  = 0 then
   Is_LeapYr := 1;
else

      Mod100 := Mod400 mod 100;
   if Mod100  = 0 then
      Is_LeapYr := 0;
   else

         Mod004 := Mod100 mod 4 ;
      if Mod004  = 0 then
         Is_LeapYr := 1;
      else
         Is_LeapYr := 0;
      endif

   endif
endif

end;  {proc/func}


{===============================================================}
Function DoWnum    (AbsDate:longint):longint;
{Calculate the day of the week from the absolute date}
{===============================================================}
begin {proc/func}

   {add 1, so that DoWnum is in range 1..7 rather than 0..6}
   {DoWnum 1 is Sunday, DoWnum 2 is Monday ... DoWnum 7 is Saturday}

   DoWnum := (AbsDate mod 7) + 1;

end;  {proc/func}


{===============================================================}
Procedure BumpMonth(Monthdays:longint);
{===============================================================}
begin {proc/func}

  CalMonth := CalMonth + 1;
  CalDay   := CalDay   - Monthdays;

end;  {proc/func}
{}
{==============================================================}
Function MonthName_US (CalMonth:longint):MonthNameType;
{===============================================================}
begin {proc/func}
       if  CalMonth = 1  then MonthName_US := 'January'
   elseif  CalMonth = 2  then MonthName_US := 'February'
   elseif  CalMonth = 3  then MonthName_US := 'March'
   elseif  CalMonth = 4  then MonthName_US := 'April'
   elseif  CalMonth = 5  then MonthName_US := 'May'
   elseif  CalMonth = 6  then MonthName_US := 'June'
   elseif  CalMonth = 7  then MonthName_US := 'July'
   elseif  CalMonth = 8  then MonthName_US := 'August'
   elseif  CalMonth = 9  then MonthName_US := 'September'
   elseif  CalMonth = 10 then MonthName_US := 'October'
   elseif  CalMonth = 11 then MonthName_US := 'November'
   elseif  CalMonth = 12 then MonthName_US := 'December'
   endif
end;  {proc/func}
{===============================================================}
Function DoWname_US (DayOfWeekNum:longint):TDoWNameType;
{===============================================================}
begin {proc/func}
       if DayOfWeekNum = 1 then DoWname_US := 'Sunday'
   elseif DayOfWeekNum = 2 then DoWname_US := 'Monday'
   elseif DayOfWeekNum = 3 then DoWname_US := 'Tuesday'
   elseif DayOfWeekNum = 4 then DoWname_US := 'Wednesday'
   elseif DayOfWeekNum = 5 then DoWname_US := 'Thursday'
   elseif DayOfWeekNum = 6 then DoWname_US := 'Friday'
   elseif DayOfWeekNum = 7 then DoWname_US := 'Saturday'
   endif
end;  {proc/func}


{}
{==============================================================}
Function MonthName_FR (CalMonth:longint):MonthNameType;
{FRENCH LANGUAGE: name of day of month }
{===============================================================}
begin {proc/func}
       if  CalMonth = 1  then MonthName_FR := 'janvier'
   elseif  CalMonth = 2  then MonthName_FR := 'fevrier'
   elseif  CalMonth = 3  then MonthName_FR := 'mars'
   elseif  CalMonth = 4  then MonthName_FR := 'avril'
   elseif  CalMonth = 5  then MonthName_FR := 'mai'
   elseif  CalMonth = 6  then MonthName_FR := 'juin'
   elseif  CalMonth = 7  then MonthName_FR := 'juillet'
   elseif  CalMonth = 8  then MonthName_FR := 'aout'
   elseif  CalMonth = 9  then MonthName_FR := 'septembre'
   elseif  CalMonth = 10 then MonthName_FR := 'octobre'
   elseif  CalMonth = 11 then MonthName_FR := 'novembre'
   elseif  CalMonth = 12 then MonthName_FR := 'decembre'
   endif
end;  {proc/func}
{===============================================================}
Function DoWname_FR (DayOfWeekNum:longint):TDoWNameType;
{FRENCH LANGUAGE: name of day of week }
{===============================================================}
begin {proc/func}
       if DayOfWeekNum = 1 then DoWname_FR := 'dimanche'
   elseif DayOfWeekNum = 2 then DoWname_FR := 'lundi'
   elseif DayOfWeekNum = 3 then DoWname_FR := 'mardi'
   elseif DayOfWeekNum = 4 then DoWname_FR := 'mercredi'
   elseif DayOfWeekNum = 5 then DoWname_FR := 'jeudi'
   elseif DayOfWeekNum = 6 then DoWname_FR := 'vendredi'
   elseif DayOfWeekNum = 7 then DoWname_FR := 'samedi'
   endif
end;  {proc/func}




{}
{==============================================================}
Function Monthname_GR (CalMonth:longint):MonthNameType;
{GERMAN LANGUAGE: name of day of month }
{===============================================================}
begin {proc/func}
       if  CalMonth = 1  then Monthname_GR := 'Januar'
   elseif  CalMonth = 2  then Monthname_GR := 'Februar'
   elseif  CalMonth = 3  then Monthname_GR := 'M„rz'
   elseif  CalMonth = 4  then Monthname_GR := 'April'
   elseif  CalMonth = 5  then Monthname_GR := 'Mai'
   elseif  CalMonth = 6  then Monthname_GR := 'Juni'
   elseif  CalMonth = 7  then Monthname_GR := 'Juli'
   elseif  CalMonth = 8  then Monthname_GR := 'August'
   elseif  CalMonth = 9  then Monthname_GR := 'September'
   elseif  CalMonth = 10 then Monthname_GR := 'Oktober'
   elseif  CalMonth = 11 then Monthname_GR := 'November'
   elseif  CalMonth = 12 then Monthname_GR := 'Dezember'
   endif
end;  {proc/func}
{===============================================================}
Function DoWname_GR (DayOfWeekNum:longint):TDoWNameType;
{GERMAN LANGUAGE: name of day of week }
{===============================================================}
begin {proc/func}
       if DayOfWeekNum = 1 then DoWname_GR := 'Sonntag'
   elseif DayOfWeekNum = 2 then DoWname_GR := 'Montag'
   elseif DayOfWeekNum = 3 then DoWname_GR := 'Dienstag'
   elseif DayOfWeekNum = 4 then DoWname_GR := 'Mittwoch'
   elseif DayOfWeekNum = 5 then DoWname_GR := 'Donnerstag'
   elseif DayOfWeekNum = 6 then DoWname_GR := 'Freitag'
   elseif DayOfWeekNum = 7 then DoWname_GR := 'Samstag'
   endif
end;  {proc/func}


{}
{==============================================================}
Function Monthname_DK (CalMonth:longint):MonthNameType;
{DANISH LANGUAGE: name of day of month }
{===============================================================}

{Note that in Danish, the names of the weekdays and the months are
NOT capitalized.}

begin {proc/func}
       if  calmonth = 1  then monthname_dk := 'januar'
   elseif  calmonth = 2  then monthname_dk := 'februar'
   elseif  calmonth = 3  then monthname_dk := 'marts'
   elseif  calmonth = 4  then monthname_dk := 'april'
   elseif  calmonth = 5  then monthname_dk := 'maj'
   elseif  calmonth = 6  then monthname_dk := 'juni'
   elseif  calmonth = 7  then monthname_dk := 'juli'
   elseif  calmonth = 8  then monthname_dk := 'august'
   elseif  calmonth = 9  then monthname_dk := 'september'
   elseif  calmonth = 10 then monthname_dk := 'oktober'
   elseif  calmonth = 11 then monthname_dk := 'november'
   elseif  calmonth = 12 then monthname_dk := 'december'
   endif
end;  {proc/func}


{===============================================================}
Function DoWname_DK (DayOfWeekNum:longint):TDoWNameType;
{DANISH LANGUAGE: name of day of week }
{===============================================================}

{Note that in Danish, the names of the weekdays and the months are
NOT capitalized.}

{In Danish, the names for Saturday and Sunday are spelled with a
special Danish character O_slash.  This character looks like 'o'
with '/' overlaid on it.  In Denmark, codepage 850 is widely used,
and in this codepage, the lowercase O_Slash character has ASCII
code 155.}

CONST Danish_lower_o_slash : string[1] = #155;

begin {proc/func}
       if DayOfWeekNum = 1 then downame_dk := 's'+ Danish_lower_o_slash+ 'ndag'
   elseif DayOfWeekNum = 2 then downame_dk := 'mandag'
   elseif DayOfWeekNum = 3 then downame_dk := 'tirsdag'
   elseif DayOfWeekNum = 4 then downame_dk := 'onsdag'
   elseif DayOfWeekNum = 5 then downame_dk := 'torsdag'
   elseif DayOfWeekNum = 6 then downame_dk := 'fredag'
   elseif DayOfWeekNum = 7 then downame_dk := 'l'+Danish_lower_o_slash+'rdag'
   endif
end;  {proc/func}


{==============================================================}
Function MonthName_SP (CalMonth:longint):MonthNameType;
{SPANISH LANGUAGE: name of day of month }
{===============================================================}
begin {proc/func}
       if  CalMonth = 1  then MonthName_SP := 'enero'
   elseif  CalMonth = 2  then MonthName_SP := 'febrero'
   elseif  CalMonth = 3  then MonthName_SP := 'marzo'
   elseif  CalMonth = 4  then MonthName_SP := 'abril'
   elseif  CalMonth = 5  then MonthName_SP := 'mayo'
   elseif  CalMonth = 6  then MonthName_SP := 'junio'
   elseif  CalMonth = 7  then MonthName_SP := 'julio'
   elseif  CalMonth = 8  then MonthName_SP := 'agosto'
   elseif  CalMonth = 9  then MonthName_SP := 'septiembre'
   elseif  CalMonth = 10 then MonthName_SP := 'octubre'
   elseif  CalMonth = 11 then MonthName_SP := 'noviembre'
   elseif  CalMonth = 12 then MonthName_SP := 'diciembre'
   endif
end;  {proc/func}
{===============================================================}
Function DoWname_SP (DayOfWeekNum:longint):TDoWNameType;
{SPANISH LANGUAGE: name of day of week }
{===============================================================}
begin {proc/func}
       if DayOfWeekNum = 1 then DoWname_SP := 'domingo'
   elseif DayOfWeekNum = 2 then DoWname_SP := 'lunes'
   elseif DayOfWeekNum = 3 then DoWname_SP := 'martes'
   elseif DayOfWeekNum = 4 then DoWname_SP := 'mi‚rcoles'
   elseif DayOfWeekNum = 5 then DoWname_SP := 'jueves'
   elseif DayOfWeekNum = 6 then DoWname_SP := 'viernes'
   elseif DayOfWeekNum = 7 then DoWname_SP := 'sbado'
   endif
end;  {proc/func}
{}
{==============================================================}
Procedure AbsDate2CalDate;
{ Convert an absolute date into a calendar date }
{===============================================================}
begin {proc/func}

TErrMsg         := '';

Num400YrGroups := AbsDate div DaysIn400YrGroup;
Num400YrModYrs := AbsDate mod DaysIn400YrGroup;
if Num400YrModYrs = 0 then
         {absolute date fits exactly into a 400-year group}
         JulianDate := 366;
         CalYear    := (400 * Num400YrGroups)
                     ; {end assignment statement}
else
   Num100YrGroups := Num400YrModYrs div DaysIn100YrGroup;
   Num100YrModYrs := Num400YrModYrs mod DaysIn100YrGroup;
   if Num100YrModYrs = 0 then
         {absolute date fits exactly into a 100-year group}
         JulianDate := 365;
         CalYear    := (400 * Num400YrGroups)
                     + (100 * Num100YrGroups)
                     ; {end assignment statement}
   else
      Num004YrGroups := Num100YrModYrs div DaysIn004YrGroup ;
      Num004YrModYrs := Num100YrModYrs mod DaysIn004YrGroup ;
      if Num004YrModYrs = 0 then
         {absolute date fits exactly into a   4-year group}
         JulianDate := 366;
         CalYear    := (400 * Num400YrGroups)
                     + (100 * Num100YrGroups)
                     + ( 4  * Num004YrGroups)
                     ; {end assignment statement}
      else
         Num001YrGroups := Num004YrModYrs div DaysInOrdinaryYr ;
         Num001YrModYrs := Num004YrModYrs mod DaysInOrdinaryYr ;
         if Num001YrModYrs = 0 then
            {absolute date fits exactly into a   1-year group}
            JulianDate:= 365;
            CalYear := (400 * Num400YrGroups)
                     + (100 * Num100YrGroups)
                     + ( 4  * Num004YrGroups)
                     + ( 1  * Num001YrGroups)
                     ; {end assignment statement}
         else
            {absolute date doesn't fit exactly into any group}
            JulianDate:= Num001YrModYrs;

            {Add 1 to convert a year count into an ordinal year}
            {E.g. Absolute day 5 is Jan. 5 of year 1, not year 0}

            CalYear := (400 * Num400YrGroups)
                     + (100 * Num100YrGroups)
                     + ( 4  * Num004YrGroups)
                     + ( 1  * Num001YrGroups)
                     +   1
                     ; {end assignment statement}
         endif
      endif
   endif
endif

{====================================================}
{ determine number of days in February in this year  }
{====================================================}
LeapYearFlag := Is_LeapYr(CalYear);
FEBdays      := FEBshort + LeapYearFlag;

{Initialize month number to month #1   }
CalMonth := 1;

{Initialize day-of-month to Julian date}
CalDay   := JulianDate;

{Subtract days of elapsed months from day-of-month to get final
day-of-month.
At the same time, increment month-number for each elapsed month.}
if CalDay > JANdays then
  BumpMonth(JANdays) ;
  if CalDay > FEBdays then
      BumpMonth(FEBdays) ;
      if CalDay > MARdays then
        BumpMonth(MARdays) ;
        if CalDay > APRdays then
           BumpMonth(APRdays) ;
           if CalDay > MAYdays then
              BumpMonth(MAYdays) ;
              if CalDay > JUNdays then
                 BumpMonth(JUNdays) ;
                 if CalDay > JULdays then
                    BumpMonth(JULdays) ;
                    if CalDay > AUGdays then
                       BumpMonth(AUGdays) ;
                       if CalDay > SEPdays then
                          BumpMonth(SEPdays) ;
                          if CalDay > OCTdays then
                             BumpMonth(OCTdays) ;
                             if CalDay > NOVdays then
                                BumpMonth(NOVdays) ;
                             endif
                          endif
                       endif
                     endif
                  endif
               endif
            endif
         endif
      endif
   endif
endif

end;  {proc/func}
{

} {==============================================================} Procedure CalDate2AbsDate; { Convert a calendar date into an absolute date } {===============================================================} begin; {proc/func} TErrMsg := ''; if CalMonth < 1 then TErrMsg := 'Month number is less than 01'; exit; elseif CalMonth > 12 then TErrMsg := 'Month number greater than 12'; exit; endif {=============================================================== Subtract 1 to convert an ordinal year number into a count of years elapsed since "the start of time". Examples: During year ONE, ZERO years have actually elapsed from day one. During year TWO, ONE year has actually elapsed from day one. ===============================================================} Num400YrGroups := (CalYear - 1) div 400; Num400YrModYrs := (CalYear - 1) mod 400; Num100YrGroups := Num400YrModYrs div 100; Num100YrModYrs := Num400YrModYrs mod 100; Num004YrGroups := Num100YrModYrs div 4; Num004YrModYrs := Num100YrModYrs mod 4; Num001YrGroups := Num004YrModYrs div 1; {Initialize absolute date to number of days elapsed in previous years} AbsDate := ( Num400YrGroups * DaysIn400YrGroup ) + ( Num100YrGroups * DaysIn100YrGroup ) + ( Num004YrGroups * DaysIn004YrGroup ) + ( Num001YrGroups * DaysInOrdinaryYr ) ; {determine number of days in February in this year} LeapYearFlag := Is_LeapYr(CalYear); FEBdays := FEBshort + LeapYearFlag; {Initialize Julian date to days elapsed in this month} JulianDate := CalDay; {add days of previous months in this year to get final Julian date} if CalMonth > 1 then JulianDate := JulianDate + JANdays endif if CalMonth > 2 then JulianDate := JulianDate + FEBdays endif if CalMonth > 3 then JulianDate := JulianDate + MARdays endif if CalMonth > 4 then JulianDate := JulianDate + APRdays endif if CalMonth > 5 then JulianDate := JulianDate + MAYdays endif if CalMonth > 6 then JulianDate := JulianDate + JUNdays endif if CalMonth > 7 then JulianDate := JulianDate + JULdays endif if CalMonth > 8 then JulianDate := JulianDate + AUGdays endif if CalMonth > 9 then JulianDate := JulianDate + SEPdays endif if CalMonth > 10 then JulianDate := JulianDate + OCTdays endif if CalMonth > 11 then JulianDate := JulianDate + NOVdays endif {add Julian date to days of previous years to get final absolute date} AbsDate := AbsDate + JulianDate; end; {proc/func} End. {unit}

LICENSE INFORMATION
I, Stephen R. Ferg, affirm that I myself developed the truedate date arithmetic algorithm, and the code and text in which it is here described and expressed.

CC0
To the extent possible under law, Stephen R. Ferg has waived all copyright and related or neighboring rights to Truedate date arithmetic algorithm. This work is published from the United States of America.


THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.