File Coverage

blib/lib/Date/Convert.pm
Criterion Covered Total %
statement 245 257 95.3
branch 51 70 72.8
condition 44 84 52.3
subroutine 32 33 96.9
pod 3 5 60.0
total 375 449 83.5


line stmt bran cond sub pod time code
1              
2             package Date::Convert;
3              
4 5     5   4584 use Carp;
  5         8  
  5         1632  
5              
6             $VERSION="0.16";
7              
8              
9             $VERSION=$VERSION; # to make -w happy. :)
10              
11             # methods that every class should have:
12             # initialize, day, date, date_string
13              
14             # methods that are recommended if applicable:
15             # year, month, day, is_leap
16              
17              
18             $BEGINNING=1721426; # 1 Jan 1 in the Gregorian calendar, although technically,
19             # the Gregorian calendar didn't exist at the time.
20             $VERSION_TODAY=2450522; # today in JDN, when I wrote this.
21              
22              
23             sub new { # straight out of the perlobj manpage:
24 17     17 1 348 my $class = shift;
25 17         36 my $self = {};
26 17         44 bless $self, $class;
27 17         59 $self->initialize(@_);
28 17         55 return $self;
29             }
30              
31              
32             sub initialize {
33 2     2 1 3 my $self = shift;
34 2   33     8 my $val = shift || $VERSION_TODAY;
35 2 50       8 carp "Date::Convert is not reliable before Absolute $BEGINNING"
36             if $val < $BEGINNING;
37 2         14 $$self{absol}=$val;
38             }
39              
40              
41              
42             sub clean {
43 15     15 0 20 my $self = shift;
44 15         16 my $key;
45 15         54 foreach $key (keys %$self) {
46 62 100       166 delete $$self{$key} unless $key eq 'absol';
47             }
48             }
49              
50              
51              
52             sub convert {
53 15     15 1 169 my $class = shift;
54 15         18 my $self = shift;
55 15         73 $self->clean;
56 15         55 bless $self, $class;
57             }
58              
59              
60              
61              
62             sub absol {
63 0     0 0 0 my $self = shift;
64 0         0 return $$self{absol};
65             }
66              
67              
68              
69              
70              
71             package Date::Convert::Gregorian;
72              
73 5     5   26 use Carp;
  5         6  
  5         5058  
74             @ISA = qw ( Date::Convert );
75              
76             $GREG_BEGINNING=1721426; # 1 Jan 1 in the Gregorian calendar, although
77             # technically, the Gregorian calendar didn't exist at
78             # the time.
79             @MONTHS_SHORT = qw ( nil Jan Feb Mar Apr May Jun July Aug Sep Oct Nov Dec );
80             @MONTH_ENDS = qw ( 0 31 59 90 120 151 181 212 243 273 304 334 365 );
81             @LEAP_ENDS = qw ( 0 31 60 91 121 152 182 213 244 274 305 335 366 );
82              
83             $NORMAL_YEAR = 365;
84             $LEAP_YEAR = $NORMAL_YEAR + 1;
85             $FOUR_YEARS = 4 * $NORMAL_YEAR + 1; # one leap year every four years
86             $CENTURY = 25 * $FOUR_YEARS - 1; # centuries aren't leap years . . .
87             $FOUR_CENTURIES = 4 * $CENTURY + 1; # . . .except every four centuries.
88              
89              
90             sub year {
91 28     28   32 my $self = shift;
92 28 100       101 return $$self{year} if exists $$self{year}; # no point recalculating.
93 8         13 my $days;
94             my $year;
95             # note: years and days are initially days *before* today, rather than
96             # today's date. This is because of fenceposts. :)
97 8         22 $days = $$self{absol} - $GREG_BEGINNING;
98 8 50       22 if (($days+1) % $FOUR_CENTURIES) { # normal case
99 8         21 $year = int ($days / $FOUR_CENTURIES) * 400;
100 8         10 $days %= $FOUR_CENTURIES;
101 8         16 $year += int ($days / $CENTURY) * 100; # years.
102 8         8 $days %= $CENTURY;
103 8         14 $year += int ($days / $FOUR_YEARS) * 4;
104 8         10 $days %= $FOUR_YEARS;
105 8 50       16 if (($days+1) % $FOUR_YEARS) {
106 8         11 $year += int ($days / $NORMAL_YEAR); # fence post from year 1
107 8         46 $days %= $NORMAL_YEAR;
108 8         9 $days += 1; # today
109 8         12 $year += 1;
110             } else {
111 0         0 $year += int ($days / $NORMAL_YEAR + 1) - 1;
112 0         0 $days = $LEAP_YEAR;
113             }
114             } else { # exact four century boundary. Uh oh. . .
115 0         0 $year = int ($days / $FOUR_CENTURIES + 1) * 400;
116 0         0 $days = $LEAP_YEAR; # correction for later.
117             }
118 8         11 $$self{year}=$year;
119 8         13 $$self{days_into_year}=$days;
120 8         14 return $year;
121             }
122              
123              
124              
125              
126             sub is_leap {
127 17     17   22 my $self = shift;
128 17   66     64 my $year = shift || $self->year; # so is_leap can be static or method
129 17 100 66     87 return 0 if (($year %4) || (($year % 400) && !($year % 100)));
      66        
130 3         10 return 1;
131             }
132              
133              
134             sub month {
135 15     15   567 my $self = shift;
136 15 100       72 return $$self{month} if exists $$self{month};
137 10         20 my $year = $self -> year;
138 10         18 my $days = $$self{days_into_year};
139 10         14 my $MONTH_REF = \@MONTH_ENDS;
140 10 100       24 $MONTH_REF = \@LEAP_ENDS if ($self->is_leap);
141 10         19 my $month= 13 - (grep {$days <= $_} @$MONTH_REF);
  130         199  
142 10         16 $$self{month} = $month;
143 10         21 $$self{day} = $days-@$MONTH_REF[$month-1];
144 10         20 return $month;
145             }
146              
147              
148              
149             sub day {
150 15     15   20 my $self = shift;
151 15 50       62 return $$self{day} if exists $$self{day};
152 0         0 $self->month; # calculates day as a side-effect
153 0         0 return $$self{day};
154             }
155              
156              
157              
158             sub date {
159 3     3   5 my $self = shift;
160 3         8 return ($self->year, $self->month, $self->day);
161             }
162              
163              
164              
165             sub date_string {
166 12     12   60 my $self = shift;
167 12         33 my $year = $self->year;
168 12         38 my $month = $self->month;
169 12         32 my $day = $self->day;
170 12         65 return "$year $MONTHS_SHORT[$month] $day";
171             }
172              
173              
174              
175              
176             sub initialize {
177 6     6   12 my $self = shift;
178 6   50     33 my $year = shift || return Date::Convert::initialize;
179 6   33     28 my $month= shift ||
180             croak "Date::Convert::Gregorian::initialize needs more args";
181 6   33     19 my $day = shift ||
182             croak "Date::Convert::Gregorian::initialize needs more args";
183 6 50       24 warn "These routines don't work well for Gregorian before year 1"
184             if $year<1;
185 6         11 my $absol = $GREG_BEGINNING;
186 6         31 $$self{'year'} = $year;
187 6         48 $$self{'month'}= $month;
188 6         13 $$self{'day'} = $day;
189 6         25 my $is_leap = is_leap Date::Convert::Gregorian $year;
190 6         8 $year --; #get years *before* this year. Makes math easier. :)
191             # first, convert year into days. . .
192 6         19 $absol += int($year/400)*$FOUR_CENTURIES;
193 6         10 $year %= 400;
194 6         12 $absol += int($year/100)*$CENTURY;
195 6         9 $year %= 100;
196 6         14 $absol += int($year/4)*$FOUR_YEARS;
197 6         10 $year %= 4;
198 6         9 $absol += $year*$NORMAL_YEAR;
199             # now, month into days.
200 6 50 33     41 croak "month number $month out of range"
201             if $month < 1 || $month >12;
202 6         14 my $MONTH_REF=\@MONTH_ENDS;
203 6 100       19 $MONTH_REF=\@LEAP_ENDS if $is_leap;
204 6 50 33     52 croak "day number $day out of range for month $month"
205             if $day<1 || $day+$$MONTH_REF[$month-1]>$$MONTH_REF[$month];
206 6         15 $absol += $day+$$MONTH_REF[$month-1]-1;
207 6         18 $$self{absol}=$absol;
208             }
209              
210              
211              
212              
213              
214             package Date::Convert::Hebrew;
215 5     5   33 use Carp;
  5         23  
  5         7277  
216             @ISA = qw ( Date::Convert );
217              
218             $HEBREW_BEGINNING = 347996; # 1 Tishri 1
219              
220             # @MONTH = (29, 12, 793);
221             @NORMAL_YEAR = (354, 8, 876); # &part_mult(12, @MONTH);
222             @LEAP_YEAR = (383, 21, 589); # &part_mult(13, @MONTH);
223             @CYCLE_YEARS = (6939, 16, 595); # &part_mult(235, @MONTH);
224             @FIRST_MOLAD = ( 1, 5, 204);
225             @LEAP_CYCLE = qw ( 3 6 8 11 14 17 0 );
226              
227             @MONTHS = ('Nissan', 'Iyyar', 'Sivan', 'Tammuz', 'Av',
228             'Elul', 'Tishrei', 'Cheshvan', 'Kislev', 'Teves',
229             'Shevat', 'Adar', 'Adar II' );
230              
231             # In the Hebrew calendar, the year starts in the seventh month, there can
232             # be a leap month, and there are two months with a variable number of days.
233             # Rather than calculate do the actual math, let's set up lookup tables based
234             # on year length. :)
235              
236             %MONTH_START=
237             ('353' => [177, 207, 236, 266, 295, 325, 1, 31, 60, 89, 118, 148],
238             '354' => [178, 208, 237, 267, 296, 326, 1, 31, 60, 90, 119, 149],
239             '355' => [179, 209, 238, 268, 297, 327, 1, 31, 61, 91, 120, 150],
240             '383' => [207, 237, 266, 296, 325, 355, 1, 31, 60, 89, 118, 148, 178],
241             '384' => [208, 238, 267, 297, 326, 356, 1, 31, 60, 90, 119, 149, 179],
242             '385' => [209, 239, 268, 298, 327, 357, 1, 31, 61, 91, 120, 150, 180]);
243              
244             sub is_leap {
245 972     972   1426 my $self = shift;
246 972         948 my $year = shift;
247 972 50       1472 $year=$self->year if ! defined $year;
248 972         904 my $mod=$year % 19;
249 972         1083 return scalar(grep {$_==$mod} @LEAP_CYCLE);
  6804         11195  
250             }
251              
252              
253             sub initialize {
254 6     6   10 my $self = shift;
255 6   50     17 my $year = shift || return Date::Convert::initialize;
256 6   33     17 my $month= shift ||
257             croak "Date::Convert::Hebrew::initialize needs more args";
258 6   33     19 my $day = shift ||
259             croak "Date::Convert::Hebrew::initialize needs more args";
260 6 50       20 warn "These routines don't work well for Hebrew before year 1"
261             if $year<1;
262 6         25 $$self{year}=$year; $$self{$month}=$month; $$self{day}=$day;
  6         17  
  6         64  
263 6         19 my $rosh=$self->rosh;
264 6         23 my $year_length=(rosh Date::Convert::Hebrew ($year+1))-$rosh;
265 6 50       32 carp "Impossible year length" unless defined $MONTH_START{$year_length};
266 6         10 my $months_ref=$MONTH_START{$year_length};
267 6         15 my $days=$$months_ref[$month-1]+$day-1;
268 6         15 $$self{days}=$days;
269 6         9 my $absol=$rosh+$days-1;
270 6         22 $$self{absol}=$absol;
271             }
272              
273              
274              
275             sub year {
276 30     30   38 my $self = shift;
277 30 100       177 return $$self{year} if exists $$self{year};
278 4         6 my $days=$$self{absol};
279 4         10 my $year=int($days/365)-3*365; # just an initial guess, but a good one.
280 4 50       11 warn "Date::Convert::Hebrew isn't reliable before the beginning of\n".
281             "\tthe Hebrew calendar" if $days < $HEBREW_BEGINNING;
282 4         23 $year++ while rosh Date::Convert::Hebrew ($year+1)<=$days;
283 4         19 $$self{year}=$year;
284 4         19 $$self{days}=$days-(rosh Date::Convert::Hebrew $year)+1;
285 4         27 return $year;
286             }
287              
288              
289             sub month {
290 8     8   14 my $self = shift;
291 8 50       42 return $$self{month} if exists $$self{month};
292 8         637 my $year_length=
293             rosh Date::Convert::Hebrew ($self->year+1) -
294             rosh Date::Convert::Hebrew $self->year;
295 8 50       39 carp "Impossible year length" unless defined $MONTH_START{$year_length};
296 8         14 my $months_ref=$MONTH_START{$year_length};
297 8         43 my $days=$$self{days};
298 8         16 my ($n, $month)=(1);
299 8         10 my $day=31; # 31 is too large. Good. :)
300 8 100 100     17 grep {if ($days>=$_ && $days-$_<$day)
  101         302  
  9         16  
301 9         13 {$day=$days-$_+1;$month=$n}
302 101         121 $n++} @$months_ref;
303 8         17 $$self{month}=$month;
304 8         13 $$self{day}=$day;
305 8         47 return $month;
306             }
307              
308              
309              
310              
311             sub day {
312 8     8   9 my $self = shift;
313 8 50       77 return $$self{day} if exists $$self{day};
314 0         0 $self->month; # calculates day as a side-effect.
315 0         0 return $$self{day};
316             }
317              
318              
319             sub date {
320 3     3   4 my $self = shift;
321 3         9 return ($self->year, $self->month, $self->day);
322             }
323              
324              
325             sub date_string {
326 5     5   46 my $self=shift;
327 5         16 return $self->year." $MONTHS[$self->month-1] ".$self->day;
328             }
329              
330              
331             sub rosh {
332 623     623   869 my $self = shift;
333 623   66     1062 my $year = shift || $self->year;
334 623         1005 my @molad= @FIRST_MOLAD;
335 623         1375 @molad = &part_add(@molad, &part_mult(int(($year-1)/19),@CYCLE_YEARS));
336 623         928 my $offset=($year-1)%19;
337 623         731 my $num_leaps=(grep {$_<=$offset} @LEAP_CYCLE) - 1;
  4361         5666  
338 623         1086 @molad = &part_add(@molad, &part_mult($num_leaps, @LEAP_YEAR));
339 623         1300 @molad = &part_add(@molad, &part_mult($offset-$num_leaps,
340             @NORMAL_YEAR));
341 623         813 my $day=shift @molad;
342 623         639 my $hour=shift @molad;
343 623         592 my $part= shift @molad;
344 623         659 my $guess=$day%7;
345 623 100 100     1672 if (($hour>=18) # molad zoken al tidrosh
      66        
      66        
      66        
      100        
      66        
      66        
      33        
346             or
347             ((is_leap Date::Convert::Hebrew $year) and # gatrad b'shanah
348             ($guess==2) and # p'shutah g'rosh
349             (($hour>9)or($hour==9 && $part>=204)))
350             or
351             ((is_leap Date::Convert::Hebrew $year-1) and # b'to takfat achar
352             ($guess==1) and # ha'ibur akor
353             (($hour>15)or($hour==15&&$part>589)))){ # mi-lishorsh
354 197         197 $guess++;
355 197         186 $day++;
356             }
357 623         691 $guess%=7;
358 623 100       656 if (scalar(grep {$guess==$_} (0, 3, 5))) { # lo ad"o rosh
  1869         2911  
359 271         231 $guess++;
360 271         242 $day++;
361             }
362 623         568 $guess%=7;
363 623         2369 return ($day+1+$HEBREW_BEGINNING);
364             }
365              
366              
367              
368              
369             sub part_add {
370 1869     1869   2241 my ($day1, $hour1, $part1)=(shift, shift, shift);
371 1869         2130 my ($day2, $hour2, $part2)=(shift, shift, shift);
372 1869         1900 my $part=$part1+$part2;
373 1869         1694 my $hour=$hour1+$hour2;
374 1869         1654 my $day =$day1 +$day2;
375 1869 100       2977 if ($part>1080) {
376 645         602 $part-=1080;
377 645         614 $hour++;
378             }
379 1869 100       2829 if ($hour>24) {
380 726         656 $hour-=24;
381 726         657 $day++;
382             }
383 1869         4060 return ($day, $hour, $part);
384             }
385              
386              
387             sub part_mult {
388 1869     1869   1753 my $scalar = shift;
389 1869         2030 my $day= ((0+ shift) * $scalar);
390 1869         1971 my $hour=((0+ shift) * $scalar);
391 1869         1914 my $part=((0+ shift) * $scalar);
392 1869         1672 my $tmp;
393 1869 100       2987 if ($part>1080) {
394 1617         1628 $tmp=int($part/1080);
395 1617         1464 $part%=1080;
396 1617         1642 $hour+=$tmp;
397             }
398 1869 100       2825 if ($hour>24) {
399 1557         1533 $tmp=int($hour/24);
400 1557         1411 $hour%=24;
401 1557         1427 $day+=$tmp;
402             }
403 1869         3910 return($day, $hour, $part);
404             }
405              
406              
407             # Here's a quickie, based on the base class.
408              
409             package Date::Convert::Absolute;
410 5     5   59 use Date::Convert;
  5         9  
  5         698  
411             @ISA = qw ( Date::Convert );
412              
413             sub initialize {
414 2     2   10 return Date::Convert::initialize @_;
415             }
416              
417              
418             sub date {
419 4     4   14 my $self=shift;
420 4         32 return $$self{'absol'};
421             }
422              
423             sub date_string {
424 2     2   15 my $self=shift;
425 2         5 my $date=$self->date; # just a scalar
426 2         6 return "$date";
427             }
428              
429              
430              
431             # Julian is kinda like Gregorian, but the leap year rule is easier.
432              
433             package Date::Convert::Julian;
434              
435 5     5   32 use Carp;
  5         9  
  5         3181  
436             @ISA = qw ( Date::Convert::Gregorian Date::Convert );
437             # we steal useful constants from Gregorian
438             $JULIAN_BEGINNING=$Date::Convert::Gregorian::GREG_BEGINNING - 2;
439             $NORMAL_YEAR= $Date::Convert::Gregorian::NORMAL_YEAR;
440             $LEAP_YEAR= $Date::Convert::Gregorian::LEAP_YEAR;
441             $FOUR_YEARS= $Date::Convert::Gregorian::FOUR_YEARS;
442              
443             @MONTH_ENDS = @Date::Convert::Gregorian::MONTH_ENDS;
444             @LEAP_ENDS = @Date::Convert::Gregorian::LEAP_ENDS;
445              
446             sub initialize {
447 3   33 3   16 my $self=shift ||
448             croak "Date::Convert::Julian::initialize needs more args";
449 3   50     7 my $year=shift || return Date::Convert::initialize;
450 3   33     82 my $month=shift ||
451             croak "Date::Convert::Julian::initialize needs more args";
452 3   33     8 my $day=shift ||
453             croak "Date::Convert::Julian::initialize needs more args";
454              
455 3 50       12 warn "These routines don't work well for Julian before year 1"
456             if $year<1;
457 3         5 my $absol = $JULIAN_BEGINNING;
458 3         5 $$self{'year'} = $year;
459 3         6 $$self{'month'}= $month;
460 3         4 $$self{'day'} = $day;
461 3         8 my $is_leap = is_leap Date::Convert::Gregorian $year;
462 3         5 $year --; #get years *before* this year. Makes math easier. :)
463             # first, convert year into days. . .
464 3         6 $absol += int($year/4)*$FOUR_YEARS;
465 3         4 $year %= 4;
466 3         4 $absol += $year*$NORMAL_YEAR;
467             # now, month into days.
468 3 50 33     15 croak "month number $month out of range"
469             if $month < 1 || $month >12;
470 3         5 my $MONTH_REF=\@MONTH_ENDS;
471 3 50       7 $MONTH_REF=\@LEAP_ENDS if $is_leap;
472 3 50 33     18 croak "day number $day out of range for month $month"
473             if $day<1 || $day+$$MONTH_REF[$month-1]>$$MONTH_REF[$month];
474 3         5 $absol += $day+$$MONTH_REF[$month-1]-1;
475 3         6 $$self{absol}=$absol;
476             }
477              
478              
479             sub year {
480 7     7   9 my $self = shift;
481 7 100       31 return $$self{year} if exists $$self{year};
482 2         3 my ($days, $year);
483             # To avoid fenceposts, year and days are initially *before* today.
484             # the next code is stolen directly form the ::Gregorian code. Good thing
485             # I'm the one who wrote it. . .
486 2         15 $days=$$self{absol}-$JULIAN_BEGINNING;
487 2         7 $year = int ($days / $FOUR_YEARS) * 4;
488 2         4 $days %= $FOUR_YEARS;
489 2 50       6 if (($days+1) % $FOUR_YEARS) { # Not on a four-year boundary. Good!
490 2         4 $year += int ($days / $NORMAL_YEAR); # fence post from year 1
491 2         3 $days %= $NORMAL_YEAR;
492 2         3 $days += 1; # today
493 2         2 $year += 1;
494             } else {
495 0         0 $year += int ($days / $NORMAL_YEAR + 1) - 1;
496 0         0 $days = $LEAP_YEAR;
497             }
498 2         4 $$self{year}=$year;
499 2         4 $$self{days_into_year}=$days;
500 2         9 return $year;
501             }
502              
503              
504              
505             sub is_leap {
506 2     2   3 my $self = shift;
507 2   33     8 my $year = shift || $self->year; # so is_leap can be static or method
508 2 100       7 return 0 if ($year %4);
509 1         4 return 1;
510             }
511              
512              
513             # OK, we're done. Everything else just gets inherited from Gregorian.
514              
515              
516             1;
517              
518             __END__