# Cal.pm -- a calendar package
# Andrew M Greene

# Hebrew calendar stuff based on Reingold. Ein chadash tachat hashemesh.

# use strict refs;

package Cal;

sub new { return bless {}; }

sub SetAbsolute # takes aboslute date
{
  my $self = shift;
  $self->{date} = shift;
}

sub GetAbsolute
{
  shift->{date};
}

@DaysBeforeMonth = (zero, 0, 31, 59, 90, 120, 151, 181, 
                          212, 243, 273, 304, 334, 365, 365);

sub SetGregorian # takes Gregorian y, m, d
{
  my $self = shift;
  my ($y, $m, $d) = @_;
  my ($yy) = $y - 1;

  $self->{date} = # number of days in years gone by
                  $yy * 365 + int($yy/4) - int($yy/100) + int($yy/400) +
		  # number of days in months gone by
		  # int($m * 30.5 + int($m/8)*.06 - ($m<3? 30 : 32)) +
		  $DaysBeforeMonth[$m] + 
		  # if this was a leap year, add 1
		  (($y%4 == 0 and ($y%100 > 0 or $y%400 == 0) and $m>2) ? 1 : 0)
		  # number of days gone by this month
		  + $d;
  $self;
}

sub GetGregorian # returns (y, m, d)
{
  my ($self) = shift;
  my ($y, $m, $d, $acc);
  $acc = $self->{date};

  $y  = int($acc/146097)*400;	$acc %= 146097;
  $y += int($acc/ 36524)*100;	$acc %=  36524;
  $y += int($acc/  1461)*  4;	$acc %=   1461;
  $y += int($acc/   365)*  1;   $acc %=    365;

  # sometimes, near the end of the year, we get ahead of ourselves.
  $y-- if $self->{date} <=
          $y * 365 + int($y/4) - int($y/100) + int($y/400);

  # set the accumulator to the day within this year.

  $acc = $self->{date} -
         ($y * 365 + int($y/4) - int($y/100) + int($y/400));

  $y++; # convert to human form

  # guess the month

  $m = int($acc / 29) + 1;
  # sometimes, near the end of the month, we get ahead of ourselves...
  $m-- if $DaysBeforeMonth[$m] >= $acc;

  $d = $acc - $DaysBeforeMonth[$m];

  # if this was a leap year, subtract 1
  $d-- if $y%4 == 0 and ($y%100 > 0 or $y%400 == 0) and $m>2;
  if ($d == 0)
  {
    $m--;
    $d=$DaysBeforeMonth[$m+1] - $DaysBeforeMonth[$m] + ($m==2? 1 : 0);
  }

  ($y, $m, $d);
}

sub GetHebrew # returns (y, m, d). Nisan = 1, Tishre = 7, Adar = 12, Adar B = 13
{
  my $self = shift;

  # We need the preceding and following years' Rosh Hashanna
  my $prevRH, $nextRH;

  my $days = $self->{date};
  $days += $heboffset;
  my $months = int($days / (29 + 12/24 + 793/1080/24));

  # guess the year. At worst, we'll be one year too early
  my $year   = int($months / 235) * 19 + int(($months % 235)/12);

#  print "I guess that it's $days days ($months months) from Creation, year $year\n";

  $nextRH = RoshHashannah($year+1);
  if ($nextRH > $self->{date})
  { $prevRH = RoshHashannah($year); }
  else
  { $prevRH = $nextRH;
    $nextRH = RoshHashannah($year+2);
    $year++;
  }

  $days -= $heboffset;

  # 1 Nisan is always 59*3 = 177 days before Rosh haShannah
  my $nisan1 = $nextRH - 177;
  $nisan1 = $prevRH - 177 if $days < $nisan1;

#  print "prevRH, nisan1, days, nextRH: $prevRH, $nisan1, $days, $nextRH\n";

  $days -= $nisan1;

  # Now, from 1 Nisan until 29 Cheshvan, we can rely on the fact that
  # the months alternate between 29 and 30 days. 
  # So if $days < 8 * 29.5 = 4 * 59 = 236, we're in good shape.
  # After that, things get a little complicated. 
  # If the year is "full", (yearlen % 10 == 5) then we add a 30 Cheshvan
  # If the year is "lean", (yearlen % 10 == 3) then we remove 30 Kislev
  # If the year is "leap", (yearlen > 355)     then we add Adar I

  # So, first let's figure out whether we need to adjust or not.

  my $adjustment = 0;

  # full year, after 29 Cheshvan, needs adjustment
  $adjustment =  1 if (($nextRH - $prevRH) % 10 == 5) and $days > 235;

  # short year, after 29 Kislev, needs adjustment
  $adjustment = -1 if (($nextRH - $prevRH) % 10 == 3) and $days > 264;

  my ($adarI30, $cheshvan30);

  $adarI30 = 1    if $days == 354 + $adjustment;
  $cheshvan30 = 1 if $adjustment == 1 and $days == 236;

  # leap year, after 29 Adar I, needs adjustment
  $adjustment++ if  (($nextRH - $prevRH) > 354) 
                and $days >= 354 + $adjustment;

#  print "$days:$adjustment\t";

  # So the month is the adjusted date divided by 29.5. 
  my $month = int (($days - $adjustment) / 29.5);

  # And the day is the adjusted date modulo 29.5, except for 30 Cheshvan.
  # (I think)

  # How many days between the first day of the current month and 1 Nisan?
  $firstofmonth = int($month * 29.5 + .5) + $adjustment;

  $days = $days - $firstofmonth

#  $days = ($days - $adjustment) - int($month * 29.5 + .5 + $adjustment)
	+ ($adarI30 or $cheshvan30? 1 : 0);

  return ($year, $month+1, $days+1);
}

@HebrewMonthNames = (zero, Nisan, Iyar, Sivan, Tammuz, Av, Elul,
                     Tishre, Cheshvan, Kislev, Tevet, Shvat, Adar, "V'Adar");

sub GetHebrewMonthName { $HebrewMonthNames[shift]; }

# Tishre 30
# Chesvan 29/30
# Kislev 30/29
# Tevet 29
# Shevat 30
# (Adar I 30)
# Adar (II) 29
# Nisan 30
# Iyar 29
# Sivan 30
# Tammuz 29
# Av 30
# Elul 29

sub IsHebrewLeapYear # takes hebrew year hy
{
  my ($hy) = shift;
  return (($hy * 7) + 1) % 19 < 7;
}

sub RoshHashannah # takes hebrew year hy
{
  my ($hy) = shift;

  # How many months precede this year?
  my ($months);
  $hy--;
  $months = 235 * int ($hy / 19) + # complete cycles
             12 * ($hy % 19)     + # regular months in this cycle
        int ((7 * ($hy % 19) + 1) / 19);  # leap months this cycle
  $hy++;

  # each molad is 29d 12h 793p long
  # the first one ("molad shel tohu") was at 2d 5h 204p
  my ($days, $hours, $parts); # chelakim is too much to type over and over...

  $parts = 204 + $months * 793 ;
  $hours =   5 + $months * 12   + int ($parts / 1080);
  $days =    2 + $months * 29   + int ($hours / 24);

  $hours %= 24;
  $parts %= 1080;

  # That is the time of the molad of Tishre. But RH may be deferred.
#  print "Molad Tishre $hy, ", 
#        $days,
#	" days ($months months) from Creation, is Yom ", $days % 7, 
#	", ${days}d ${hours}h ${parts}p\n";

  # deferrals

  $hp = $hours * 1080 + $parts;
  $wd = $days  % 7;

  $days++ if
    $hp >= 19440 		      # at or after noon, or
                                   or
    ($wd == 3 and $hp >= 9924 and     # Yom Shlishi, at or after 9h 204ch ...
     not IsHebrewLeapYear($hy))       # ... in a common year
                                   or
    ($wd == 2 and $hp >= 16789 and    # Yom Sheni at or after 15h 589ch ...
     IsHebrewLeapYear($hy-1));        # ... after a leap year

  $days++ if $wd%7 == 1 or $wd%7 == 4 or $wd%7 == 6; # Lo Adu Rosh

  return $days - $heboffset;
}

$heboffset = 1373429;

#             Name              Last Daf (remember that first daf is 2)
@Masechtot = ("Berachot",	64,
	      "Shabbat",	157,
	      "Eruvin",		105,
	      "Pesachim",	121,
	      "Yoma",		88,
	      "Sukkah",		56,
	      "Betzah",		40,
	      "Rosh Hashannah",	35,
	      "Taanit",		31,
	      "Megillah",	32,
	      "Moed Katan",	29,
	      "Chagigah",	27,
	      "Yevamot",	122,
	      "Ketubot",	112,
	      "Nedarim",	91,
	      "Nazir",		66,
	      "Sotah",		49,
	      "Gittin",		90,
	      "Kiddushin",	82,
	      "B. Kamma",	119,
	      "B. Metziah",	119,
	      "B. Batra",	176,
	      "Sanhedrin",	113,
	      "Makkot",		24,
	      "Shevuot",	49,
	      "Avoda Zara",	76,
	      "Horayot",	14,
	      "Zevachim",	120,
	      "Menachot",	110,
	      "Chullin",	141,
	      "Bechorot",	61,
	      "Arachin",	34,
	      "Temura",		34,
	      "Kritot",		29,
	      "Meilah",		22,
	      "Tamid",		8,
	      "Niddah",		73);

for (@Masechtot) { $totaldafim += $_>0? $_-1 : 0; }

sub GetDafYomi # takes date
{
  my ($self) = shift;
  my ($dafoffset) = ($self->{date} + 488) % ($totaldafim + 1);
  for ($i=0; $i<@Masechtot; $i++)
  {
    if ($dafoffset < $Masechtot[$i*2+1])
      { return $Masechtot[$i*2] . " " . ($dafoffset+1); } 
    $dafoffset -= $Masechtot[$i*2+1] - 1;
  }
  return "ERROR!";
}

package Cal::Event;

sub new { return bless {criterion => "0"}; }

sub SetCriterion
{
  my ($self) = shift;
  $self->{criterion} = shift;
}

sub IsDayEvent
{
  my ($self) = shift;
  my ($date) = shift;
  return eval $self->{criterion};
}

1;
