home *** CD-ROM | disk | FTP | other *** search
/ NetNews Usenet Archive 1993 #3 / NN_1993_3.iso / spool / comp / lang / perl / 8021 < prev    next >
Encoding:
Text File  |  1993-01-28  |  21.8 KB  |  878 lines

  1. Path: sparky!uunet!ferkel.ucsb.edu!taco!gatech!paladin.american.edu!howland.reston.ans.net!sol.ctr.columbia.edu!ursa!buzz
  2. From: buzz@bear.com (Buzz Moschetti)
  3. Newsgroups: comp.lang.perl
  4. Subject: date.pl - A date handling library
  5. Message-ID: <BUZZ.93Jan27115829@lion.bear.com>
  6. Date: 27 Jan 93 16:58:29 GMT
  7. Sender: news@bear.com
  8. Reply-To: buzz@bear.com (Buzz Moschetti)
  9. Organization: Bear, Stearns & Co. - FAST
  10. Lines: 866
  11.  
  12. This is a library of routines that manipulate dates.  Features:
  13.  
  14.     * No spawning of date or cal for superior performance.
  15.     * Dates may be compared and have days, months, or years 
  16.       added to them.
  17.     * Dates recognized in a variety of formats, including
  18.       MM/DD/YYYY, DD MON YYYY, etc.
  19.     * Full support for leap years, pre-Gregorian calendars,
  20.       and September 1752.
  21.  
  22. Comments, suggestions, etc. are welcome.
  23.  
  24. #
  25. #  DEXID
  26. #    date.3PL
  27. #
  28. #  NAME
  29. #    date - perl date handling library
  30. #
  31. #  SYNOPSIS
  32. #    require "date.pl";
  33. #
  34. #    $days = &date_cmp($date1, $date2);
  35. #
  36. #    $days = &date_cmp_months($date1, $date2);
  37. #
  38. #    ($m, $d, $y) = &date_to_mdy($date);
  39. #
  40. #    $date = &date_mdy_to_date($m, $d, $y);
  41. #
  42. #    $date = &date_plus_days($date, $days);
  43. #
  44. #    $date = &date_plus_months($date, $months);
  45. #
  46. #    $date = &date_plus_years($date, $years);
  47. #
  48. #    $rc = &date_is_leap($date)
  49. #
  50. #    $day = &date_day_of_week($date);
  51. #
  52. #    &date_set_date_style($type);
  53. #
  54. #
  55. #  DESCRIPTION
  56. #    The date library is a collection of routines to 
  57. #    manipulate dates.  Functions calling for date-type arguments may
  58. #    be passed simple strings in any one of these forms:
  59. #
  60. #        MM/DD/YYYY
  61. #        YYYYMMDD
  62. #        DD-MON-YYYY
  63. #        DD MON YYYY
  64. #        MONTH DD, YYYY
  65. #
  66. #    MON and MONTH may be mixed case (e.g. aUgUsT) on input but will be
  67. #    uppercased, lowercased, or capitalized on return based on the case
  68. #    of the first and second characters.  If char 1 is lowercase, then
  69. #    the whole name will be lowercased.  If char 1 and char 2 are
  70. #    uppercase, then the whole name will be uppercased.  In all other
  71. #    permutations, the name will be capitalized (e.g. April).
  72. #
  73. #    The forms may be mixed in those functions calling for two
  74. #    date-type arguments.  For example, 
  75. #
  76. #        $diff = &date_cmp("12/4/1964", "01-Jul-1992");
  77. #
  78. #    is a valid statement.
  79. #
  80. #    Functions that return date-type scalar strings will format them in
  81. #    the same format as their first date-tyoe argument.  Examples:
  82. #        
  83. #        "12/05/1992" = &date_plus_days("12/03/1992", 2);
  84. #        "9-NOV-1992" = &date_plus_months("9-OCT-1992", 1);
  85. #    The exception is &date_mdy_to_date(), which will use the format of
  86. #    the last such command since it is passed individual month, day,
  87. #    and year.
  88. #
  89. #    Leap years, post-Gregorian calendars, and the Sep. 1752 anomaly are
  90. #    all taken into account when performing date calculations.
  91. #
  92. #
  93. #    &date_cmp() returns the difference in days between two dates.  The
  94. #    number is < 0 if $date1 is before $date2, 0 if they are the same
  95. #    date, or > 0 if $date1 is after $date2.
  96. #
  97. #    &date_cmp_months() is similar to &date_cmp() except that the
  98. #    difference in months is returned.
  99. #
  100. #    &date_to_mdy() takes a date-type scalar string and returns a list
  101. #    containing the month, day, and year.  The m/d/y values are in
  102. #    "regular" format; that is, month is an integer from 1 to 12 and
  103. #    the year is the actual year, centuries included.  Fans of the Un*x
  104. #    (struct tm) datatype may find this confounding at first since
  105. #    in the (struct tm) resource months are returned as (real month - 1)
  106. #    and years are returned as (real year - 1900).
  107. #
  108. #    &date_mdy_to_date() returns a date-type scalar string given a list
  109. #    containing month, day, and year.  The format of the string will be
  110. #    the same as the last date-type passed as the first argument to one
  111. #    of the other functions that takes a date-type scalar string as an
  112. #    argument.  NOTE:  If &date_mdy_to_date() is called before any
  113. #    function that requires a date-type scalar string, then the format
  114. #    will default to MM/DD/YYYY.
  115. #
  116. #    &date_plus_days(), &date_plus_months(), and &date_plus_years() add
  117. #    an integer number of days, months, or years, respectively, to a
  118. #    given date and return the new date.  The integer may be negative,
  119. #    resulting in a new date that is before the given date.
  120. #
  121. #    &date_is_leap() returns 1 if the given date is a leap year or 0 if
  122. #    it is not.
  123. #
  124. #    &date_day_of_week() returns the ordinal day of the week with
  125. #    Sunday being 0.
  126. #
  127. #    &date_set_date_style() forces all date-type scalar strings
  128. #    returned by other functions to match the format in the input
  129. #    argument.  Any valid date may be used.  If no argument is
  130. #    supplied then the current style is "unset"; that is, the returned
  131. #    strings will once again match the input format.
  132. #
  133. #  NOTES
  134. #    perl constantly creates doubles (floating point) during division.
  135. #    We are only interested in the integer component, so we must call
  136. #    int() to strip the fractional component off.
  137. #
  138. #    The algorithms have been optimized somewhat for performance sake
  139. #    at the expence of clarity.  For example,
  140. #
  141. #         if($epoch_day < ((1752-1)*365 + (1752-1)/4)) {
  142. #
  143. #    which lets you know it's targeting the special year 1752 and
  144. #    checks for leap years {(1752-1)/4} has been replaced with
  145. #
  146. #        if($epoch_day < 639552) {
  147. #
  148. #    The un-optimized code has been commented out so you can see what
  149. #    is going on.
  150. #
  151. #  BUGS
  152. #    No NULL/undef date handling as of yet.
  153. #
  154. #
  155. #  COPYRIGHT
  156. #    Copyright 1993  Buzz Moschetti
  157. #
  158. #    Permission to use, copy, modify, distribute, and sell this
  159. #    software and its documentation for any purpose is hereby granted
  160. #    without fee, provided that this copyright notice appear in all
  161. #    copies and that both that copyright notice and this permission
  162. #    notice appear in supporting documentation, and that the name of
  163. #    Buzz Moschetti or contractions or abbreviations of the same not be
  164. #    used in advertising or publicity pertaining to distribution of the
  165. #    software without prior specific, written permission.  The author
  166. #    makes no representations about the suitability of this software
  167. #    for any purpose.  It is provided "as is" without express or
  168. #    implied warranty.
  169. #
  170. #    BUZZ MOSCHETTI (THE "AUTHOR") DISCLAIMS ALL WARRANTIES WITH REGARD
  171. #    TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
  172. #    MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE
  173. #    LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  174. #    DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  175. #    WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
  176. #    ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  177. #    PERFORMANCE OF THIS SOFTWARE.
  178. #
  179. #  AUTHOR
  180. #    Buzz Moschetti
  181.  
  182.  
  183.  
  184. package date;
  185.  
  186.  
  187. #
  188. #  Various resources:
  189. #
  190. @abbrev_month_names = ('JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC');
  191. @full_month_names = ('JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER');
  192.  
  193. @month_names = @abbrev_month_names;        # default
  194.  
  195. #
  196. #  month styles (case of returned month name)
  197. #    1 - lowercase
  198. #    2 - uppercase
  199. #    3 - caps
  200. #
  201. $set_month_style = 0;
  202. $default_month_style = 3;
  203.  
  204.  
  205. #
  206. #  date styles:
  207. #    1 - MM/DD/YYYY
  208. #    2 - YYYYMMDD
  209. #    3 - DD-MON-YYYY
  210. #    4 - DD MON YYYY
  211. #    5 - DD MONTH YYYY
  212. #    6 - MONTH DD, YYYY
  213. #
  214. $set_date_style = 0;  # no style has been set.
  215. $default_date_style = 1;
  216.  
  217.  
  218. @norm_upto = (0, 31, 59,                        # Jan, Feb, Mar 
  219.           90, 120, 151,                     # Apr, May, Jun
  220.           181, 212, 243,                    # Jul, Aug, Sep
  221.           273, 304, 334,                    # Oct, Nov, Dec
  222.           365);                             # end of year
  223.         
  224. @leap_upto = (0, 31, 60,                        
  225.           91, 121, 152,                     
  226.           182, 213, 244,                    
  227.           274, 305, 335,                    
  228.           366);
  229.  
  230. # That odd-ball year, 1752.  Note September!
  231. @y1752_upto = (0, 31, 60,
  232.           91, 121, 152,
  233.           182, 213, 244,
  234.           263, 294, 324,
  235.           355);
  236.  
  237. #  End of month dates for normal and leap year months
  238. @norm_year = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  239. @leap_year = (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  240.  
  241.  
  242.  
  243. #
  244. #  $days = &date_cmp($date1, $date2);
  245. #
  246. sub date_cmp {
  247.     local($d1, $d2) = @_;
  248.     local($e1, $e2);
  249.  
  250.     $e1 = &date_mdy2epod(&date_to_mdy($d1));
  251.     $e2 = &date_mdy2epod(&date_to_mdy($d2));
  252.  
  253.     return $e1 - $e2;
  254. }
  255.  
  256. #
  257. #  $days = &date_cmp_months($date1, $date2);
  258. #
  259. sub date_cmp_months {
  260.     local($d1, $d2) = @_;
  261.     local(@mdy1, @mdy2);
  262.  
  263.     @mdy1 = &date_to_mdy($date1);
  264.     @mdy2 = &date_to_mdy($date2);
  265.  
  266.     return $mdy1[1] - $mdy2[1];
  267. }
  268.  
  269. #
  270. #  ($m, $d, $y) = &date_to_mdy($date);
  271. #
  272. #  In addition to being a handy app-level function in its own right,
  273. #  &date_to_mdy() is used internally by many of the date routines
  274. #  since it is capable of taking a variable-format scalar string and
  275. #  converting into a well-defined array containing discrete entries
  276. #  for month, day, and year.
  277. #
  278. sub date_to_mdy {
  279.     local($date) = @_;
  280.     local($m, $d, $y) = (-1,-1,-1);
  281.  
  282.     if($date =~ /(\d+)\/(\d+)\/(\d{4,4})/) {
  283.         $m = $1;
  284.         $d = $2;
  285.         $y = $3;
  286.         $default_date_style = 1;
  287.     } elsif($date =~ /(\d{4,4})(\d{2,2})(\d{2,2})/) {
  288.         $m = $2;
  289.         $d = $3;
  290.         $y = $1;
  291.         $default_date_style = 2;
  292.     } elsif($date =~ /(\d+)-(\w{3,3})-(\d{4,4})/) {
  293.         $m = $2;
  294.         @month_names = @abbrev_month_names;
  295.  
  296.         #  month name string $m will become month number $m:
  297.         if(($m = &month_to_idx($m, @month_names)) != -1) {
  298.             $m += 1;
  299.             $y = $3;    
  300.             $d = $1;
  301.  
  302.             &setup_month_style($2);
  303.             $default_date_style = 3;
  304.         }
  305.     } elsif($date =~ /(\d+)\s+(\w{3,3})\s+(\d{4,4})/) {
  306.         $m = $2;
  307.         @month_names = @abbrev_month_names;
  308.  
  309.         #  month name string $m will become month number $m:
  310.         if(($m = &month_to_idx($m, @month_names)) != -1) {
  311.             $m += 1;
  312.             $y = $3;    
  313.             $d = $1;
  314.  
  315.             &setup_month_style($2);
  316.             $default_date_style = 4;
  317.         }
  318.  
  319.     }  elsif($date =~ /(\d+)\s+(\w{4,9})\s+(\d{4,4})/) {
  320.         $m = $2;
  321.         @month_names = @full_month_names;
  322.  
  323.         #  month name string $m will become month number $m:
  324.         if(($m = &month_to_idx($m, @month_names)) != -1) {
  325.             $m += 1;
  326.             $y = $3;    
  327.             $d = $1;
  328.  
  329.             &setup_month_style($2);
  330.             $default_date_style = 5;
  331.         }
  332.     } elsif($date =~ /(\w{4,9})\s+(\d+),\s*(\d{4,4})/) {
  333.         $m = $1;
  334.         @month_names = @full_month_names;
  335.  
  336.         #  month name string $m will become month number $m:
  337.         if(($m = &month_to_idx($m, @month_names)) != -1) {
  338.             $m += 1;
  339.             $y = $3;    
  340.             $d = $2;
  341.  
  342.             &setup_month_style($1);
  343.             $default_date_style = 6;
  344.         }
  345.     }
  346.  
  347.  
  348.     @rc = ($m, $d, $y);
  349. }
  350.  
  351. #
  352. #  $date = &date_mdy_to_date($m, $d, $y)
  353. #
  354. sub date_mdy_to_date {
  355.     local($m,$d,$y) = @_;
  356.     local($date_fmt);
  357.  
  358.     if($set_date_style) {
  359.         $date_fmt = $set_date_style;
  360.     } else {
  361.         $date_fmt = $default_date_style;
  362.     }
  363.     
  364.     if($date_fmt == 1) {
  365.         $rc = sprintf("%02d/%02d/%4d", $m, $d, $y) ;
  366.     } elsif($date_fmt == 2) {
  367.         $rc = sprintf("%4d%02d%02d", $y, $m, $d) ;
  368.     } elsif($date_fmt == 3) {
  369.         $rc = sprintf("%02d-%s-%4d", $d, &make_month_name($m), $y);
  370.     } elsif($date_fmt == 4) {
  371.         $rc = sprintf("%02d %s %4d", $d, &make_month_name($m), $y);
  372.     } elsif($date_fmt == 5) {
  373.         $rc = sprintf("%d %s %4d", $d, &make_month_name($m), $y);
  374.     } elsif($date_fmt == 6) {
  375.         $rc = sprintf("%s %d, %4d", &make_month_name($m), $d, $y);
  376.     }
  377.  
  378.     $rc;
  379. }
  380.  
  381.  
  382.  
  383. #
  384. #    $ndate = &date_plus_months($date, $months);
  385. #
  386. sub date_plus_months {
  387.     local($d1, $months) = @_;
  388.  
  389.     @mdy = &date_to_mdy($d1);
  390.  
  391.     $mdy[0] += $months;
  392.  
  393.     if($mdy[0] > 0) {
  394.         while ($mdy[0] >= 12) {
  395.             $mdy[0] -= 12;
  396.             $mdy[2]++;
  397.         }
  398.     } else {
  399.         while ($mdy[0] < 0) {
  400.             $mdy[0] += 12;
  401.             $mdy[2]--;
  402.         }
  403.     }
  404.  
  405.     #
  406.     #  Dont go past the valid end-of-month.  Also, notice the adjustment
  407.     #  of the month[] index ($mdy[0] - 1).  Don't worry about 
  408.     #  Sep, 1752:  The last day of that weird month it *still* 30, so  
  409.     #  we can use @norm_year or @leap_year.
  410.     #
  411.     if(&date_is_leap(&date_mdy_to_date(@mdy))) {
  412.     @month = @leap_year;
  413.     } else {
  414.     @month = @norm_year;
  415.     }
  416.  
  417.     if ($month[$mdy[0] - 1] < $mdy[1]) {
  418.     $mdy[1] = $month[$mdy[0] - 1];
  419.     }
  420.  
  421.     $rc = &date_mdy_to_date(@mdy);
  422. }
  423.  
  424.  
  425.  
  426.  
  427. #
  428. #  $ndate = &date_plus_days($date, $days);
  429. #
  430. sub date_plus_days {
  431.     local($date, $days) = @_;
  432.     local($edays);
  433.  
  434.     $edays = &date_mdy2epod(&date_to_mdy($date));
  435.     $edays += $days;
  436.  
  437.     $rc = &date_mdy_to_date(&date_epod2mdy($edays));
  438. }
  439.  
  440.  
  441. #
  442. #  $rc = &date_is_leap($d1)
  443. #
  444. sub date_is_leap {
  445.     local($d1) = @_;
  446.  
  447.     @mdy = &date_to_mdy($d1);
  448.     
  449.     $y = $mdy[2];
  450.  
  451.     $rc = ((($y)<1752 && ($y)%4) ||(($y)>1752 && (($y)%4==0&&(($y)%100!=0||($y)%400==0))))
  452. }
  453.  
  454.  
  455. #
  456. #  $ndate = &date_plus_years($date, $years);
  457. #
  458. sub date_plus_years {
  459.     local($d1, $years) = @_;
  460.  
  461.     @mdy = &date_to_mdy($d1);
  462.  
  463.     $mdy[2] += $years;
  464.  
  465.     #
  466.     #  Make sure we dont add years simply to 29th feb on a leap year
  467.     #
  468.     if (! &date_is_leap(&date_mdy_to_date(@mdy)) &&
  469.         $mdy[1] == 1 &&
  470.         $mdy[2] == 29) {
  471.         $mdy[1] = 28;
  472.     }
  473.  
  474.     $rc = &date_mdy_to_date(@mdy);
  475. }
  476.  
  477.  
  478.  
  479. sub date_day_of_week {
  480.     local($date) = @_;
  481.  
  482.     $rc = (&date_mdy2epod(&date_to_mdy($date)) + 5) % 7;
  483.  
  484.     if($rc < 0) {
  485.         $rc += 7;
  486.     }
  487.  
  488.     $rc;
  489. }
  490.  
  491.  
  492. sub date_set_date_style {
  493.     local($date) = @_;
  494.  
  495.     if(!$date) {
  496.         $set_date_style = 0;
  497.         $set_month_style = 0;
  498.     } else {
  499.         #  Force the styles to be set:
  500.         &date_to_mdy($date);
  501.  
  502.         #  Assign them permanently;
  503.         $set_date_style = $default_date_style;
  504.         $set_month_style = $default_month_style;
  505.     }
  506. }
  507.  
  508.  
  509. #
  510. #
  511. #   P R I V A T E   F U N C T I O N S
  512. #
  513. #
  514. #
  515. sub setup_month_style {
  516.     local($m) = @_;
  517.  
  518.     #  Ultra-simple rules:
  519.     #  If first char is lower case, then the whole thing is
  520.     #  lower case.
  521.     #  If the first char is upper case and the second char is
  522.     #  lower case, then the month is capitalized.
  523.     #  If the first char is upper case and the second char is
  524.     #  also upper case, then the month is upper cased.
  525.     #
  526.     $c1 = substr($m, 0, 1);
  527.     $c2 = substr($m, 1, 1);
  528.  
  529.     if($c1 ge 'a' && $c1 le 'z') {
  530.         $default_month_style = 1;  # lowercase
  531.     } elsif($c1 ge 'A' && $c1 le 'Z' &&
  532.         $c2 ge 'A' && $c2 le 'Z') {
  533.         $default_month_style = 2;  # uppercase
  534.     } else {
  535.         $default_month_style = 3;  # caps (default)
  536.     }
  537. }
  538.  
  539. #  Assumes @month_names and $month_style have been set!
  540. sub make_month_name {
  541.     local($m) = @_;
  542.     local($style, $str);
  543.  
  544.     #  $m is "correct" number; subtract 1 to get offset!
  545.     $str = @month_names[$m - 1];
  546.  
  547.     #  If month_style is 2 (all uppercase), then don't do anything.
  548.     #  Yes, this is not particularly robust coding...
  549.     if($set_month_style) {
  550.         $style = $set_month_style;
  551.     } else {
  552.         $style = $default_month_style;    
  553.     }
  554.  
  555.     if($style == 1) {
  556.         $str =~ tr/A-Z/a-z/;
  557.     } elsif($style == 3) {
  558.         $str[0] =~ tr/a-z/A-Z/;
  559.         substr($str, 1) = (substr($str, 1) =~ tr/A-Z/a-z/);
  560.     }
  561.  
  562.     $str;
  563. }
  564.  
  565.     
  566.  
  567. sub month_to_idx {
  568.     local($str, @m_arr) = @_;
  569.  
  570.     # Force uppercase for comparison!
  571.         $str =~ tr/a-z/A-Z/;
  572.  
  573.     for($i = 0; $i < 12; $i++) {
  574.         if($m_arr[$i] eq $str) {
  575.             return $i;
  576.         }
  577.     }
  578.  
  579.     -1;
  580. }
  581.  
  582.  
  583. sub date_mdy2epod {
  584.  
  585.     local($m, $d, $y) = @_;
  586.  
  587.  
  588.     @upto = @norm_upto;
  589.  
  590.  
  591.     #  Adjust for struct tm conventions:
  592.     $m--;
  593.  
  594.  
  595.     if($y<1 || $y>9999 || $m<0 || $m>11 || $d<1 || $d>31) {
  596.         return -1;
  597.     }
  598.  
  599.     
  600.     $day = ($y-1) * 365;    #  Well, most years have at 
  601.                 #  least this many days
  602.  
  603.     $day += int(($y-1)/4);        #  account for all leap years 
  604.  
  605.     if ($y > 1800) {        #  Adjust Post-Gregorian correction
  606.      $day -= int(($y-1701)/100);    #  less 1 day per century
  607.      $day += int(($y-1601)/400);    #  but one more per 400 years
  608.     }
  609.  
  610.  
  611.  
  612.     if($y < 1752) {            # pre strange year
  613.     if ($y % 4 == 0) {        # choose leap year table
  614.         @upto = @leap_upto;
  615.     }
  616.     } elsif($y > 1752) {        # after strange year 
  617.     if($y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0)) {
  618.         @upto = @leap_upto;    #  new leap year
  619.     }
  620.     $day -= 11;        # account for 1752 
  621.     } else {        # year must be 1752 
  622.     
  623.     @upto = @y1752_upto;
  624.  
  625.     if($m == 8) {
  626.         if($d > 2 && $d < 14) {
  627.         warn("InternalError; invalid day $d in Sep. 1752");
  628.         return -1;
  629.          } elsif($d >= 14)    {    # fake day of month 
  630.         $d -= 11;
  631.         }
  632.     }
  633.     }
  634.  
  635.     if($d > ($upto[$m + 1] - $upto[$m])) {    # check for bad day of month 
  636.     warn("InternalError; calc error; $d not valid day of month.\n");
  637.     return -1;
  638.     }
  639.     
  640.     return $day + $upto[$m] + $d;
  641. }
  642.  
  643.  
  644.  
  645. sub date_epod2mdy {
  646.     local($epoch_day) = @_;
  647.  
  648.     $tmp_year = 0;
  649.     @upto = @norm_upto;
  650.  
  651.  
  652.      # Converting in this direction is immensely more difficult
  653.      # than the preceding routine. We have to keep pecking away
  654.      # until we figure out which year the day falls in, then
  655.      # find the month and day. All of this works more easily if
  656.      # the is day 0-based, rather than 1-based, so we start
  657.      # by subtracting 1.
  658.      #
  659.  
  660.     $epoch_day--;
  661.  
  662.     # Years up to 1752
  663. ##    if($epoch_day < ((1752-1)*365 + int((1752-1)/4))) {
  664.     if($epoch_day < 639552) {
  665.     ($epoch_day, $tmp_year) = &date_doleap($epoch_day, $tmp_year, 3); # simple Julian procedure; 
  666.                        # first leap year is 3 hence 
  667.     if($tmp_year % 4 == 3) {           # leap year (zero-based) 
  668.         @upto = @leap_upto;      # An integer array in dates.h 
  669.     }
  670.     }
  671.      # Year 1752 only
  672. ##    elsif($epoch_day < ((1753-1)*365 + int((1753-1)/4) - 11)) {
  673.     elsif($epoch_day < 639907) {
  674. ##    $epoch_day -= (1752-1)*365 + int((1752-1)/4);
  675.     $epoch_day -= 639552;
  676. ##    $tmp_year = 1752 - 1;    # years are zero-based 
  677.     $tmp_year = 1751;    # years are zero-based 
  678.     @upto = @y1752_upto;
  679.     }
  680.  
  681.      # Years 1753 to 1800
  682. ##   elsif($epoch_day < ((1800-1)*365 + int((1800-1)/4) - 11)) {
  683.     elsif($epoch_day < 657073) {
  684.  
  685.      # Treat this like pre-1752,
  686.      # since no funny leap year until 1800.
  687.      # Therefore, cheat by adding back missing 11 days.
  688.  
  689.     $epoch_day += 11;
  690.  
  691.     # first leap yr is 1756 
  692.  
  693. ##    ($epoch_day, $tmp_year) = &date_doleap($epoch_day, $tmp_year, 1756-1753);
  694.     ($epoch_day, $tmp_year) = &date_doleap($epoch_day, $tmp_year, 3);
  695.  
  696.     if($tmp_year % 4 == 3) {    # use leap year table 
  697.         @upto = @leap_upto;   
  698.     }
  699.     }
  700.  
  701.      # Years 1800 to 2000.
  702.      # Deduct century year correction, leave 1752
  703.      # correction.
  704. ##   elsif($epoch_day < ((2000-1)*365 + int((2000-1)/4) - 2 - 11)) {
  705.     elsif($epoch_day < 730121) {
  706.  
  707.      # Change basis to be first day of 1800
  708.  
  709. ##    $epoch_day -= (1800-1)*365 + int((1800-1)/4) - 11;
  710.     $epoch_day -= 657073;
  711.     $tmp_year = 1799;        # zero-based, remember 
  712.  
  713.     ($epoch_day, $tmp_year) = &date_do100($epoch_day, $tmp_year); # century handling
  714.  
  715.     if($tmp_year % 4 == 3 && $tmp_year % 100 != 99) {
  716.         @upto = @leap_upto;     # year is a leap year 
  717.     }
  718.     }
  719.  
  720.      # Years 2000 and after.
  721.      # Base things from first day of year 2000.
  722.     else {
  723. ##    $epoch_day -= (2000-1)*365 + int((2000-1)/4) - 2 - 11;
  724.     $epoch_day -= 730121;
  725.     $tmp_year = 1999;
  726.     # get accurate year, day :
  727.     ($epoch_day, $tmp_year) = &date_do400($epoch_day, $tmp_year);
  728.  
  729.      # check for leap year
  730.     if($tmp_year % 4 == 3 &&
  731.             ($tmp_year % 100 != 99 || $tmp_year % 400 == 399)) {
  732.         @upto = @leap_upto;
  733.     }
  734.     }
  735.  
  736.  
  737.      # At this point the hard stuff is done.
  738.      # "year" is the correct year minus 1.
  739.      # "epoch_day" has changed from being the actual number of days since
  740.      #        Jan 1, 0 to the day of the year within "year" (zero-based:
  741.      #        1/1 is zero), e.g. Feb 5 is 35.
  742.      # 
  743.     for($tmp_month = 1;
  744.     $epoch_day >= $upto[$tmp_month] && $tmp_month <= 12;
  745.     $tmp_month++) {
  746.     }
  747.  
  748.     if($tmp_month > 12) {
  749.     warn("InternalError; calc error: day of year > 365.");
  750.     return (-1,-1,-1);
  751.     }
  752.  
  753.     $tmp_month--;
  754.  
  755.  
  756.     $year = $tmp_year + 1;
  757.     $mon = $tmp_month + 1;
  758.     $day = $epoch_day - $upto[$tmp_month] + 1;
  759.  
  760.  
  761.      # Fudge for 1752 anomaly
  762.     if($tmp_year == 1751 && $tmp_month == 8 && $day > 2) {
  763.     $day += 11;
  764.     }
  765.  
  766.     @rc = ($mon, $day, $year);
  767. }
  768.  
  769.  
  770. # This routine converts an epoch_day day into the relative
  771. # year and day of year components according to a simple Julian
  772. # calendar (ie. leap year every four years). A zero relative year
  773. # is assumed to be a leap year.
  774.  
  775. sub date_doleap {
  776.     local($day, $year, $firstleap) = @_;
  777.  
  778.                     # leap interval 
  779.     if($day < $firstleap*365) {
  780.     $year += int($day/365);    #simple non-leap calc. 
  781.     $day %= 365;
  782.  
  783.     @rc = ($day, $year);
  784.     return @rc;
  785.     } else {
  786.     $year += $firstleap;    # discard first non-leap interval 
  787.     $day -= 365 * $firstleap;
  788.     }
  789.  
  790. ##    $nquad = int($day/(365*4+1));    # number of 4-year hunks 
  791.     $nquad = int($day/1461);    # number of 4-year hunks 
  792.     $nrem = $day % 1461;    # left-over days from such hunks 
  793.  
  794.  
  795.  
  796.     # The 0th of these years is the leap year
  797.     $tmp_year = 0;            # year in hunk 
  798.  
  799. ##    if($nrem > 366-1) {        # check for beyond leap year 
  800.     if($nrem > 365) {        # check for beyond leap year 
  801.     $tmp_year = int(($nrem-366)/365+1);    # number of full years 
  802.     }
  803.  
  804.     $year += $nquad*4 + $tmp_year;    # bump relative year 
  805.     $day = $nrem - 365*$tmp_year - ($tmp_year == 0 ? 0:1);
  806.                     # get day number within year 
  807.  
  808.     @rc = ($day, $year);
  809. }
  810.  
  811.  
  812. # This routine converts an epoch_day day into the relative year
  813. # and day in year simply assuming that years on 100 year boundaries
  814. # are not leap years. The zero year is assumed to be divisible by 100.
  815.  
  816. sub date_do100 {
  817.     local($days, $year) = @_;
  818.  
  819.  
  820. ##    $ncentury = int($days/(100*365+100/4-1));
  821.     $ncentury = int($days/36524);
  822.                     # number of centuries covered 
  823.  
  824. ##    $days %= int((100*365+100/4-1));    # get new number of days 
  825.     $days %= 36524;            # get new number of days 
  826.         $year += 100 * $ncentury;        # bump year 
  827.  
  828.     ($days, $year) = &date_doleap($days, $year, 4);    # handle the rest for leap years;
  829.                     # note we won't hit the century 
  830.                     # boundary anomaly; first following 
  831.                     # leap year is 4 away 
  832.  
  833.     @rc = ($days, $year);
  834. }
  835.  
  836.  
  837. # This routine converts an epoch_day day into the relative year
  838. # and day in year simply assuming that we have to be concerned with
  839. # 100 and 400 year anomalies. Leap years are taken into account.
  840. # The zero year is assumed to be divisible by 400.
  841.  
  842. sub date_do400 {
  843.     local($days, $year) = @_;
  844.     
  845.       $n100 = 0;
  846. ##    $n400 = int($days/(400*365+400/4-4+1));
  847.     $n400 = int($days/146097);
  848.  
  849. ##    $days %= int((400*365+400/4-4+1));    # update number of days 
  850.     $days %= 146097;                # update number of days 
  851.  
  852.  
  853.      # First century of tetra-centennial has all its leap years
  854. ##    if($days > 100*365 + int(100/4)) {
  855.     if($days > 36525) {
  856.  
  857. ##    $n100 = ($days-(100*365+ int(100/4)))/(100*365+int(100/4)-1)+1;
  858.     $n100 = int(($days-36525)/36524) + 1;        # number of centuries
  859.  
  860. ##   $days -= $n100*(100*365+int(100/4)-1)+1;
  861.     $days -= ($n100 * 36524) + 1;             # gives day in century
  862.     }
  863.  
  864.     $year += (100 * $n100) + (400 * $n400);    # update year 
  865.     ($days, $year) = &date_doleap($days, $year, ($n100 == 0)?0:4);
  866.                     # handle century this way; 
  867.                     # first leap year is current 
  868.                     # year if 400 boundary 
  869.                     # else 4 away 
  870.  
  871.     @rc = ($days, $year);
  872. }
  873.  
  874. 1;
  875.  
  876.  
  877.