#!/usr/pkg/bin/perl

=begin metadata

Name: cal
Description: displays a calendar and the date of Easter
Author: Michael E. Schechter, mschechter@earthlink.net
License: gpl

=end metadata

=cut


package
	cal; # hide from PAUSE
use strict;

use Getopt::Std qw(getopts);

use constant EX_SUCCESS => 0;
use constant EX_FAILURE => 1;

my %opts;
getopts('jy', \%opts) or display_help();
$cal::num_args = scalar( @ARGV );

if( $cal::num_args == 0 ) {
	my @tm = localtime(time);
	$cal::year = $tm[5] + 1900;
	$cal::month = $tm[4] + 1 unless $opts{'y'};
} elsif( $cal::num_args == 1 ) {
	$cal::year = &get_year( $ARGV[ 0 ] );
} elsif( $cal::num_args == 2 ) {
	if( $opts{'y'} ) {
		print "INVALID FLAG.  Can not use -y with month and year.\n";
		display_help();
	}
	$cal::month = &get_month( $ARGV[ 0 ] );
	$cal::year = &get_year( $ARGV[ 1 ] );
} else {
	print "TOO MANY ARGUMENTS.\n";
	display_help();
}

if( $cal::year && $cal::month ) {
	my @lines = fmt_month($cal::year, $cal::month, 1);
	print join("\n", @lines), "\n\n";
} else {
	print_title();
	if ($opts{'j'}) {
		print_year_jd();
	} else {
		print_year();
	}
	print "\n";
}
exit EX_SUCCESS;

sub box_width {
	return $opts{'j'} ? 28 : 21;
}

sub box_hcount {
	return $opts{'j'} ? 2 : 3;
}

sub print_title {
	my $width = box_hcount() * box_width() + 1;
	my $margin = int(($width / 2) - (length("$cal::year") / 2));
	print ' ' x $margin, $cal::year, "\n\n";
}

sub print_year_jd {
	my $w = box_width();
	my @month = (1 .. 12);
	while (@month) {
		my @mon = splice @month, 0, 2;
		my @m0 = fmt_month($cal::year, $mon[0], 0);
		my @m1 = fmt_month($cal::year, $mon[1], 0);

		foreach my $i (0 .. 7) {
			$m0[$i] = '' unless defined $m0[$i];
			$m1[$i] = '' unless defined $m1[$i];
			printf "%-${w}s %-${w}s\n", $m0[$i], $m1[$i];
		}
	}
}

sub print_year {
	my $w = box_width();
	my @month = (1 .. 12);
	while (@month) {
		my @mon = splice @month, 0, 3;
		my @m0 = fmt_month($cal::year, $mon[0], 0);
		my @m1 = fmt_month($cal::year, $mon[1], 0);
		my @m2 = fmt_month($cal::year, $mon[2], 0);

		foreach my $i (0 .. 7) {
			$m0[$i] = '' unless defined $m0[$i];
			$m1[$i] = '' unless defined $m1[$i];
			$m2[$i] = '' unless defined $m2[$i];
			printf "%-${w}s %-${w}s %-${w}s\n", $m0[$i], $m1[$i], $m2[$i];
		}
	}
}

sub fmt_month {
	my( $year, $month, $titleyr ) = @_;
	my( @month ) = &make_month_array( $year, $month );
	my( $title, $margin, $end, $x, $buf ) = ();
	my( @months ) = ( '', 'January', 'February', 'March', 'April', 'May',
					  'June', 'July', 'August', 'September', 'October',
					  'November', 'December' );
	my( @days ) = ('Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa' );
	my @lines;

	$title = $months[$month];
	$title .= " $year" if $titleyr;
	$margin = int((box_width() / 2) - (length($title) / 2));
	$title = (' ' x $margin) . $title;
	push @lines, $title;

	$buf = '';
	foreach my $day (@days) {
		if ($opts{'j'}) {
			$buf .= sprintf '%3s ', $day;
		} else {
			$buf .= sprintf '%2s ', $day;
		}
	}

	$end = 0;
	for( $x = 0; $x < scalar(@month); $x++ ) {
		if ($end == 0) {
			push @lines, $buf;
			$buf = '';
		}
		if ($opts{'j'}) {
			$buf .= sprintf '%3s ', $month[$x];
		} else {
			$buf .= sprintf '%2s ', $month[$x];
		}
		$end++;
		if( $end > 6 ) {
			$end = 0;
		}
	}
	push @lines, $buf;
	return @lines;
}

sub make_month_array {
	my( $year, $month ) = @_;
	my( @month_array, $numdays, $x, $y ) = ();
	my( $firstweekday ) = &day_of_week_num( $year, $month, 1 );
	$numdays = &days_in_month( $year, $month );
	if ($opts{'j'}) {
		$y = &day_of_year( $year, $month, 1 );
	} else {
		$y = 1;
	}
	for ($x = 0; $x < $firstweekday; $x++) {
		$month_array[$x] = '';
	}
	if( !(($year == 1752) && ($month == 9)) ) {
		for( $x = 1; $x <= $numdays; $x++, $y++ ) {
			$month_array[$x + $firstweekday - 1] = $y;
		}
	} else {
		for( $x = 1; $x <= $numdays; $x++, $y++ ) {
			$month_array[$x + $firstweekday - 1] = $y;
			if( $y == 2 ) {
				$y = 13;
			}
		}
	}
	return( @month_array );
}

sub day_of_week_num {
	my( $year, $month, $day ) = @_;
	my( $a, $y, $m, $d ) = ();
	$a = int( (14 - $month)/12 );
	$y = $year - $a;
	$m = $month + (12 * $a) - 2;
	if( &is_julian( $year, $month ) ) {
		$d = (5 + $day + $y + int($y/4) + int(31*$m/12)) % 7;
	} else {
		$d = ($day + $y + int($y/4) - int($y/100) + int($y/400) + int(31*$m/12)) % 7;
	}
	return( $d );
}

sub days_in_month {
	my( $year, $month ) = @_;
	my( @month_days ) = ( 0,31,28,31,30,31,30,31,31,30,31,30,31 );
	if( ($month == 2) && (&is_leap_year( $year )) ) {
		$month_days[ 2 ] = 29;
	} elsif ( ($year == 1752) && ($month == 9) ) {
		$month_days[ 9 ] = 19;
	}
	return( $month_days[ $month ] );
}

sub day_of_year {
	my( $year, $month, $day ) = @_;
	my( @days ) = ( 0,31,59,90,120,151,181,212,243,273,304,334,365 );
	my ( $ly, $i ) = ();
	$ly = 0;
	if( (&is_leap_year( $year )) && ($month > 2) ) {
		$ly = 1;
	}
	if( $year == 1752 ) {
		for( $i = 9; $i < 13; $i++ ) {
			$days[ $i ] = $days[ $i ] - 11;
		}
	}
	return( $days[ $month - 1 ] + $day + $ly )
}

sub is_leap_year {
	my( $year ) = @_;
	my( $bool ) = 0;
	if( &is_julian( $year, 1 ) ) {
		if( $year % 4 == 0 ) {
			$bool = 1;
		}
	} else {
		if( (($year % 4 == 0) && ($year % 100 != 0)) || ($year % 400 == 0) ) {
			$bool = 1;
		}
	}
	return( $bool );
}

sub is_julian {
	my( $year, $month ) = @_;
	my( $bool ) = 0;
	if( ($year < 1752) || ($year == 1752 && $month <= 9) ) {
		$bool = 1;
	}
	return( $bool );
}

sub get_month {
	my( $month ) = @_;
	if( $month < 1 || $month > 12 || $month !~ /^\d+$/o ) {
		print "INVALID MONTH ENTERED.\n";
		display_help();
	}
	return( $month );
}

sub get_year {
	my( $year ) = @_;
	if ($year !~ m/\A[0-9]+\z/ ||  $year < 1 || $year > 9999) {
		print "INVALID YEAR ENTERED.\n";
		display_help();
	}
	return( $year );
}

sub display_help {
	print <<END_HELP;
usage: cal [-jy] [[month] year]

    -j : Display calendar using julian days, where each day
         number is the day of the year.
    -y : Display entire year calendar for the current year.
 month : The month of the entered year for which to display
         a calendar.  Valid months are 1 through 12.
  year : The year for which to display the calendar.  If no
         month is entered, will display the calendar for the
         entire entered year.  Valid years are 1 through 9999.

If no arguments are passed, prints the calendar for the current
month and year.
END_HELP
	exit EX_FAILURE;
}

__END__

=head1 NAME

cal - displays a calendar and the date of easter

=head1 SYNOPSIS

    cal [-jy] [[month] year]

If no arguments are supplied, the current month will be displayed.  If
only the -j flag is passed as an argument, the current month will be
displayed with julian day numbering (see below).

=head2 FLAGS

Below is a list of the flags that can be passed to cal.pl, and their
meanings.

=over 4

=item -j

displays the requested date using julian days for the numbers.  That
is, each day number is the day of the year for that day.  Thus, in non
leap-years, December 31 is day 365.

=item -y

displays the calendar for the current year.

=back

=head2 MONTH/YEAR

If only one number is supplied, this number will be used as the year.
If two numbers are supplied, the first will be the month, and the
second will be the year. Valid months are 1 through 12, and valid
years are 1 through 9999.

B<NOTE:> dates must be fully defined.  Entering a year of 99 means 99
AD, not 1999 AD.

=head1 DESCRIPTION

cal generates calendars from 1 AD through 9999 AD, in the following
format.

S<         March 1999
  Su   M  Tu   W  Th   F  Sa
       1   2   3   4   5   6
   7   8   9  10  11  12  13
  14  15  16  17  18  19  20
  21  22  23  24  25  26  27
  28  29  30  31
>

It takes into account the conversion from the Julian Calendar to the
Gregorian Calendar in September, 1752.  Note that this was the date
used in the UK and all of her colonies (including the U.S.).  Thus, in
September, 1752, there were only 19 days. Eleven days had to be
removed from the calendar to make up for inaccuracies in the Julian
Calendar. So, September 2, 1752 was followed by September 14, 1752 in
the UK and all colonies, as shown below.

S<       September 1752
  Su   M  Tu   W  Th   F  Sa
           1   2  14  15  16
  17  18  19  20  21  22  23
  24  25  26  27  28  29  30
>

The Julian calendar has a leap year every 4 years.  The Gregorian
calendar has a leap year based on the following:  If the year is
evenly divisible by 400, it is a leap year, else, if the year is
evenly divisible by 4, and not evenly divisible by 100, it is a leap
year.  All other years are not leap years.  There is some contention
over whether 4 AD was a leap year or not.  Not all the experts agree,
as there was a counting error in the beginning of the Julian calendar
with regards to leap years that had to be corrected, and thus some
years that would normally have been leap years were not.  This
calendar assumes that 4 AD was indeed a leap year.

=head1 FURTHER READING

The calculations used in this calendar, as well as information on when
the calendar was switched over, and the implications thereof, were
obtained from information found at the following URL.  If you would
like to find out more about calendars, I highly suggest taking a look.

F<URL: http://www.pip.dknet.dk/~c-t/calendar.html>

=head1 AUTHOR

Michael E. Schechter mschechter@earthlink.net

=head1 COPYRIGHT INFORMATION

This application is distributed as part of the Perl Power Tools.  Feel
free to copy, modify, delete, or whatever you would like with this
file, under the information contained in the GNU GPL.

=cut

