--- /dev/null
+package HTML::CalendarMonth::DateTool;
+{
+ $HTML::CalendarMonth::DateTool::VERSION = '1.26';
+}
+
+# Base class for determining what date calculation package to use.
+
+use strict;
+use warnings;
+use Carp;
+
+use File::Which qw( which );
+
+my %Toolmap = (
+ 'Time::Local' => 'TimeLocal',
+ 'Date::Calc' => 'DateCalc',
+ 'DateTime' => 'DateTime',
+ 'Date::Manip' => 'DateManip',
+ 'ncal' => 'Ncal',
+ 'cal' => 'Cal',
+);
+
+my %Classmap;
+$Classmap{lc $Toolmap{$_}} = $_ foreach keys %Toolmap;
+
+my($Cal_Cmd, $Ncal_Cmd);
+
+sub _toolmap {
+ shift;
+ my $str = shift;
+ my $tool = $Toolmap{$str};
+ unless ($tool) {
+ foreach (values %Toolmap) {
+ if ($str =~ /^$_$/i) {
+ $tool = $_;
+ last;
+ }
+ }
+ }
+ return unless $tool;
+ join('::', __PACKAGE__, $tool);
+}
+
+sub new {
+ my $class = shift;
+ my $self = {};
+ bless $self, $class;
+ my %parms = @_;
+ $self->{year} = $parms{year};
+ $self->{month} = $parms{month};
+ $self->{weeknum} = $parms{weeknum};
+ $self->{historic} = $parms{historic};
+ if (! $self->{year}) {
+ my @dmy = $self->_dmy_now;
+ $self->{year} = $dmy[2];
+ $self->{month} ||= $dmy[1];
+ }
+ $self->{month} ||= 1;
+ if ($parms{datetool}) {
+ $self->{datetool} = $self->_toolmap($parms{datetool})
+ or croak "Sorry, didn't find a tool for datetool '$parms{datetool}'\n";
+ }
+ my $dc = $self->_summon_date_class;
+ unless (eval "require $dc") {
+ croak "Problem loading $dc ($@)\n";
+ }
+ # rebless into new class
+ bless $self, $dc;
+}
+
+sub year { shift->{year} }
+sub month { shift->{month} }
+sub weeknum { shift->{weeknum} }
+sub historic { shift->{historic} }
+sub datetool { shift->{datetool} }
+
+sub _name {
+ my $class = shift;
+ $class = ref $class || $class;
+ lc((split(/::/, $class))[-1]);
+}
+
+sub _cal_cmd {
+ my $self = shift;
+ if (! defined $Cal_Cmd) {
+ $Cal_Cmd = which('cal') || '';
+ if ($Cal_Cmd) {
+ my @out = grep { ! /^\s*$/ } `$Cal_Cmd 9 1752`;
+ # September 1752
+ #Su Mo Tu We Th Fr Sa
+ # 1 2 14 15 16
+ #17 18 19 20 21 22 23
+ #24 25 26 27 28 29 30
+ my @pat = (
+ qr/^\s*\S+\s+\d+$/,
+ qr/^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s*$/,
+ qr/^\s*\d+\s+\d+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s*$/,
+ );
+ if (@out == @pat) {
+ for my $i (0 .. $#out) {
+ if ($out[$i] !~ $pat[$i]) {
+ $Cal_Cmd = '';
+ last;
+ }
+ }
+ }
+ else {
+ $Cal_Cmd = '';
+ }
+ }
+ }
+ $Cal_Cmd;
+}
+
+sub _ncal_cmd {
+ my $self = shift;
+ if (! defined $Ncal_Cmd) {
+ $Ncal_Cmd = which('ncal') || '';
+ if ($Ncal_Cmd) {
+ my @out = grep { ! /^\s*$/ } map { s/^\s*//; $_ } `$Ncal_Cmd 9 1752`;
+ # September 1752
+ #Mo 18 25
+ #Tu 1 19 26
+ #We 2 20 27
+ #Th 14 21 28
+ #Fr 15 22 29
+ #Sa 16 23 30
+ #Su 17 24
+ my @pat = (
+ qr/^\s*\S+\s+\d+$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s+\d+\s*$/,
+ qr/^\s*\S+\s+\d+\s+\d+\s*$/,
+ );
+ if (@out == @pat) {
+ for my $i (0 .. $#out) {
+ if ($out[$i] !~ $pat[$i]) {
+ $Ncal_Cmd = '';
+ last;
+ }
+ }
+ }
+ else {
+ $Ncal_Cmd = '';
+ }
+ }
+ }
+ $Ncal_Cmd;
+}
+
+sub day_epoch {
+ # in case our subclasses are lazy
+ my($self, $day, $month, $year) = @_;
+ $month ||= $self->month;
+ $year ||= $self->year;
+ Time::Local::timegm(0,0,0,1,$month,$year);
+}
+
+sub _skips {
+ my $self = shift;
+ @_ ? $self->{skips} = shift : $self->{skips};
+}
+
+sub dow1st { (shift->dow1st_and_lastday)[0] }
+
+sub lastday { (shift->dow1st_and_lastday)[1] }
+
+sub _dmy_now {
+ my $self = shift;
+ my $ts = @_ ? shift : time;
+ my($d, $m, $y) = (localtime($ts))[3,4,5];
+ ++$m; $y += 1900;
+ ($d, $m, $y);
+}
+
+sub _dom_now {
+ my $self = shift;
+ my $ts = @_ ? shift : time;
+ my($d, $m, $y);
+ if ($ts =~ /^\d+$/) {
+ if (length $ts <= 2) {
+ ($d, $m, $y) = ($ts, $self->month, $self->year);
+ croak "invalid day of month (1 .. " . $self->lastday . ") '$ts'"
+ unless $ts >= 1 && $ts <= $self->lastday;
+ }
+ else {
+ ($d, $m, $y) = $self->_dmy_now($ts);
+ }
+ }
+ else {
+ ($y, $m, $d) = $ts =~ m{^(\d+)/(\d\d)/(\d\d)$};
+ croak "invalid yyyy/mm/dd date string '$ts'" unless defined $d;
+ }
+ my($cy, $cm) = ($self->year, $self->month);
+ my $first = sprintf("%04d/%02d/%02d", $cy, $cm, 1);
+ my $last = sprintf("%04d/%02d/%02d", $cy, $cm, $self->lastday);
+ my $pivot = sprintf("%04d/%02d/%02d", $y, $m, $d);
+ return -1 if $pivot gt $last;
+ return 0 if $pivot lt $first;
+ $d;
+}
+
+sub _summon_date_class {
+ my $self = shift;
+ my @tools;
+ if (my $c = $self->datetool) {
+ eval "use $c";
+ die "invalid date tool $c : $@" if $@;
+ @tools = $c->_name;
+ }
+ else {
+ @tools = qw( timelocal datecalc datetime datemanip ncal cal );
+ }
+ my($dc, @fails);
+ for my $tool (@tools) {
+ my $method = join('_', '', lc($tool), 'fails');
+ if (my $f = $self->$method) {
+ push(@fails, [$tool, $f]);
+ }
+ else {
+ $dc = $self->_toolmap($tool);
+ last;
+ }
+ }
+ return $dc if $dc;
+ if (@tools == 1) {
+ croak "invalid date tool " . join(': ', @{$fails[0]});
+ }
+ else {
+ croak join("\n",
+ "no valid date tool found:",
+ map(sprintf("%11s: %s", @$_), @fails),
+ "\n"
+ );
+ }
+}
+
+sub _dump_tests {
+ my $self = shift;
+ print "Time::Local : ", $self->_timelocal_fails || 1, "\n";
+ print " Date::Calc : ", $self->_datecalc_fails || 1, "\n";
+ print " DateTime : ", $self->_datetime_fails || 1, "\n";
+ print "Date::Manip : ", $self->_datemanip_fails || 1, "\n";
+ print " ncal : ", $self->_ncal_fails || 1, "\n";
+ print " cal : ", $self->_cal_fails || 1, "\n";
+}
+
+sub _is_julian {
+ my $self = shift;
+ my $y = $self->year;
+ $y < 1752 || ($y == 1752 && $self->month <= 9);
+}
+
+sub _timelocal_fails {
+ my $self = shift;
+ return "not installed" unless $self->_timelocal_present;
+ return "week-of-year numbering unsupported" if $self->weeknum;
+ my $y = $self->year;
+ return "only years between 1970 and 2038 supported"
+ if $y < 1970 || $y >= 2038;
+ return;
+}
+
+sub _ncal_fails {
+ my $self = shift;
+ return "command not found" unless $self->_ncal_present;
+ return "week-of-year numbering not supported prior to 1752/09"
+ if $self->weeknum && $self->_is_julian;
+ return;
+}
+
+sub _cal_fails {
+ my $self = shift;
+ return "command not found" unless $self->_cal_present;
+ return "week-of-year numbering not supported" if $self->weeknum;
+ return;
+}
+
+sub _datecalc_fails {
+ my $self = shift;
+ return "not installed" unless $self->_datecalc_present;
+ return "historic mode prior to 1752/09 not supported"
+ if $self->historic && $self->_is_julian;
+ return;
+}
+
+sub _datetime_fails {
+ my $self = shift;
+ return "not installed" unless $self->_datetime_present;
+ return "historic mode prior to 1752/09 not supported"
+ if $self->historic && $self->_is_julian;
+ return;
+}
+
+sub _datemanip_fails {
+ my $self = shift;
+ return "not installed" unless $self->_datemanip_present;
+ return "historic mode prior to 1752/09 not supported"
+ if $self->historic && $self->_is_julian;
+ eval { require Date::Manip && Date::Manip::Date_Init() };
+ return "init failure: $@" if $@;
+ return;
+}
+
+sub _timelocal_present { eval "require Time::Local"; return !$@ }
+sub _datecalc_present { eval "require Date::Calc"; return !$@ }
+sub _datetime_present { eval "require DateTime"; return !$@ }
+sub _datemanip_present { eval "require Date::Manip"; return !$@ }
+sub _ncal_present { shift->_ncal_cmd }
+sub _cal_present { shift->_cal_cmd };
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+HTML::CalendarMonth::DateTool - Base class for determining which date package to use for calendrical calculations.
+
+=head1 SYNOPSIS
+
+ my $date_tool = HTML::CalendarMonth::DateTool->new(
+ year => $YYYY_year,
+ month => $one_thru_12_month,
+ weeknum => $weeknum_mode,
+ historic => $historic_mode,
+ datetool => $specific_datetool_if_desired,
+ );
+
+=head1 DESCRIPTION
+
+This module attempts to utilize the best date calculation package
+available on the current system. For most contemporary dates this
+usually ends up being the internal Time::Local package of perl. For more
+exotic dates, or when week number of the years are desired, other
+methods are attempted including DateTime, Date::Calc, Date::Manip, and
+the linux/unix 'ncal' or 'cal' commands. Each of these has a specific
+subclass of this module offering the same utility methods needed by
+HTML::CalendarMonth.
+
+=head1 METHODS
+
+=over
+
+=item new()
+
+Constructor. Takes the following parameters:
+
+=over
+
+=item year
+
+Year of calendar in question (required). If you are rendering exotic
+dates (i.e. dates outside of 1970 to 2038) then something besides
+Time::Local will be used for calendrical calculations.
+
+=item month
+
+Month of calendar in question (required). 1 through 12.
+
+=item weeknum
+
+Optional. When specified, will limit class excursions to those that are
+currently set up for week of year calculations.
+
+=item historic
+
+Optional. If the the ncal or cal commands are available, use one of them
+rather than other available date modules since these utilities
+accurately handle some specific historical artifacts such as the
+transition from Julian to Gregorian.
+
+=item datetool
+
+Optional. Mostly for debugging, this option can be used to indicate a
+specific HTML::CalendarMonth::DateTool subclass for instantiation. The
+value can be either the actual utility class, e.g., Date::Calc, or the
+name of the CalendarMonth handler leaf class, e.g. DateCalc. Use 'ncal'
+or 'cal', respectively, for the wrappers around those commands.
+
+=back
+
+=back
+
+There are number of methods automatically available:
+
+=over
+
+=item month()
+
+=item year()
+
+=item weeknum()
+
+=item historical()
+
+=item datetool()
+
+Accessors for the parameters provided to C<new()> above.
+
+=item dow1st()
+
+Returns the day of week number for the 1st of the C<year> and C<month>
+specified during the call to C<new()>. Relies on the presence of
+C<dow1st_and_lastday()>. Should be 0..6 starting with Sun.
+
+=item lastday()
+
+Returns the last day of the month for the C<year> and C<month> specified
+during the call to C<new()>. Relies on the presence of
+C<dow1st_and_lastday()>.
+
+=back
+
+=head1 Overridden methods
+
+Subclasses of this module must provide at least the C<day_epoch()> and
+C<dow1st_and_lastday()> methods.
+
+=over
+
+=item dow1st_and_lastday()
+
+Required. Provides a list containing the day of the week of the first
+day of the month (0..6 starting with Sun) along with the last day of
+the month.
+
+=item day_epoch()
+
+Optional unless interested in epoch values for wacky dates. For a given
+day, and optionally C<month> and C<year> if they are different from
+those specified in C<new()>, provide the unix epoch in seconds for that
+day at midnight.
+
+=back
+
+If the subclass is expected to provide week of year numbers, three more
+methods are necessary:
+
+=over
+
+=item dow()
+
+For a given day, and optionally C<month> and C<year> if they are
+different from those specified in C<new()>, provide the day of week
+number. (0=Sunday, 6=Saturday).
+
+=item add_days($days, $delta, $day, [$month], [$year])
+
+For a given day, and optionally C<month> and C<year> if they are
+different from those specified in C<new()>, provide a list of year,
+month, and day once C<delta> days have been added.
+
+=item week_of_year($day, [$month], [$year])
+
+For a given day, and optionally C<month> and C<year> if they are
+different from those specified in C<new()>, provide a list with the week
+number of the year along with the year. (some days of a particular year
+can end up belonging to the prior or following years).
+
+=back
+
+=head1 AUTHOR
+
+Matthew P. Sisk, E<lt>F<sisk@mojotoad.com>E<gt>
+
+=head1 COPYRIGHT
+
+Copyright (c) 2010 Matthew P. Sisk. All rights reserved. All wrongs
+revenged. This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+HTML::CalendarMonth(3), Time::Local(3), DateTime(3), Date::Calc(3),
+Date::Manip(3), cal(1)