The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Calendar::Any::Util::Calendar;
{
  $Calendar::Any::Util::Calendar::VERSION = '0.5';
}
use Calendar::Any;

require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(
calendar $week_start_day
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our $week_start_day = 0;

sub calendar {
    my ($month, $year, $package) = @_;
    if ( defined($package) && $package =~ /China|Chinese/) {
        chinese_simple($month, $year);
    } else {
        generic_simple(@_);
    }
}

sub generic_simple {
    my ($month, $year) = @_;
    my @month = generic_calendar(@_);
    my $cal = "$Calendar::Any::month_name[$month-1] $year\n\n";
    $cal .= join(" ", map {
        substr($Calendar::Any::weekday_name[$_], 0, 2)
    } $week_start_day..6, 0..($week_start_day-1) ). "\n";
    $cal .= join("\n", map {
        join(" ", map { $_ ? sprintf "%2d", $_ : '  ' } @$_);
    } @month);
    return $cal;
}

sub generic_calendar {
    my ($month, $year, $package) = @_;
    defined($package) || ($package = "Gregorian");
    my $module = "Calendar::Any::$package";
    eval("require $module");
    if ( $@ ) {
        die "Can't load module $module: $@!\n"
    }
    my $start = $module->new(-month => $month, -day => 1, -year => $year);
    my $weekday = ($start->weekday-$week_start_day) % 7;
    my $last = $start->last_day_of_month;
    my @month = ((undef)x$weekday, 1..$last, (undef)x ( 7 - (($weekday+$last)%7  || 7) ));
    return map { [@month[$_*7..$_*7+6]] } 0..(@month/7-1);
}

sub chinese_simple {
    my ($month, $year) = @_;
    my ($months, $newmonth) = chinese_calendar(@_);
    # print it
    my $cal = center_han_string(
        sprintf("%d年%d月  %s年%s", $year, $month,
                      $newmonth->[-1][1]->sexagesimal_name,
                      join(",", map { sprintf "%s%s%d日始", $_->[1]->month_name(),
                                           ($_->[1]->last_day_of_month > 29 ? "大" : "小"),
                                               $_->[0] } @$newmonth)),
        9*7-3) . "\n";
    $cal .= join("   ", map {
        substr($Calendar::Any::weekday_name[$_], 0, 3) . " " . $Calendar::Any::Chinese::weekday_name[$_]
    } $week_start_day..6, 0..($week_start_day-1)) . "\n";
    foreach ( @$months ) {
        foreach ( @$_) {
            my $str;
            if ( $_ ) {
                $str = sprintf("%2d", $_->[0]);
                my $cdate = $_->[1];
                if ( exists $cdate->{jieqi} ) {
                    $str .= $Calendar::Any::Chinese::jieqi_name[$cdate->{jieqi}].' ' x 2;
                } elsif ( $cdate->day == 1 ) {
                    # make sure the month name is less than 3 character. the only
                    # exception is '闰十一月'
                    my $month_name = $cdate->month_name;
                    Encode::_utf8_on($month_name);
                    if (length($month_name) >= 3) {
                        $month_name = substr($month_name, 0, 3);
                    } else {
                        $month_name .= "  ";
                    }
                    Encode::_utf8_off($month_name);
                    $str .= $month_name;
                } else {
                    $str .= $cdate->day_name.' 'x2;
                }
            } else {
                $str = ' ' x 8;
            }
            $cal .= $str . " ";
        }
        $cal .= "\n";
    }
    return $cal;
}

sub chinese_calendar {
    require Encode;
    require Calendar::Any::Chinese;
    my ($month, $year) = @_;
    unless ( $month=~ /^\d+$/ && $year =~ /^\d+$/ && $month>0 && $month<13 ) {
        pod2usage();
    }
    my $tz = Calendar::Any::Chinese::timezone($year);
    my $start = Calendar::Any->new_from_Gregorian($month, 1, $year); # first date of Gregorian
    my $weekday = ($start->weekday - $week_start_day) % 7; # weekday of first date
    my $last = $start->last_day_of_month; # last day of this month
    my @month = ((undef)x$weekday, 1..$last, (undef)x ( 7 - (($weekday+$last)%7  || 7) ));
    my $adate = $start->absolute_date-1;
    my @newmonth;
    foreach ( $weekday..$#month ) {
        last unless defined $month[$_];
        my $cdate = Calendar::Any::Chinese->new($adate+$month[$_]);
        $month[$_] = [$month[$_], $cdate];
        push @newmonth, $month[$_] if $cdate->day == 1;
    }
    $start = $month[$weekday]->[1];
    # mark up jieqi
    my $first_jieqi = Calendar::Any::Chinese::next_jieqi_date($adate, $tz);
    my $second_jieqi = Calendar::Any::Chinese::next_jieqi_date($first_jieqi+1, $tz);
    $adate++;
    $month[$first_jieqi-$adate+$weekday]->[1]->{jieqi}=2*($month-1);
    $month[$second_jieqi-$adate+$weekday]->[1]->{jieqi}=2*$month-1;
    return ([map { [ @month[$_*7..$_*7+6]] } 0..(@month/7-1)], \@newmonth);
}

sub center_han_string {
    require Encode;
    my ($str, $len) = @_;
    my $enc = $str;
    Encode::_utf8_on($enc);
    $enc = Encode::encode('cp936', $enc);
    my $pad = ($len-length($enc))/2;
    return ' ' x $pad . $str . ' ' x $pad;
}

1;

__END__

=head1 NAME

Calendar::Any::Util::Calendar - A collection of function for create calendars

=head1 VERSION

version 0.5

=head1 SYNOPSIS

     use Calendar::Any::Util::Calendar qw(calendar);
     print calendar(12, 2006), "\n";

=head1 DESCRIPTION

A very simple module that output various calendars.

=over

=item calendar(month, year, [package])

Output the calendar for the month in the year. If given package,
output the calendar of the package. For example: 

     print calendar(12, 2006, 'Julian'), "\n";

This will output calendar in Julian calendar.

=item  generic_calendar(month, year, [package])

Return an array of dates in the month break by weekday:

    ( [ undef, undef, undef, undef, undef, 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, undef, undef, undef, undef, undef, undef ] )

The default week start day is Sunday. If you want start from Monday,
set $week_start_day to 1.

=item chinese_calendar($month, $year)

The difference between the generic_calendar is the return array,
contain not only the day of the month, but also the Calendar::China
date. And to address the start date of the new chinese month, the
return value of the function contain two elements, one is the month
calendar array, which like:

    [ [ undef, undef, undef, undef, undef, [1, D], [2, D] ],
        ...
      [ [31, D], undef, undef, undef, undef, undef, undef ] ]

The D stands for Calendar::China date. The second element is an array
of new chinese month date. A month may contain two new chinese month
date. 

=back

=head1 AUTHOR

Ye Wenbin <wenbinye@gmail.com>

=head1 COPYRIGHT

Copyright (C) 2006 by ywb

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.7 or,
at your option, any later version of Perl 5 you may have available.

=cut