The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Parse::Crontab::Schedule;
use 5.008_001;
use strict;
use warnings;
use Carp;
use Try::Tiny;

use Parse::Crontab::Schedule::Entity;

use Mouse;

my @SCHEDULES = qw/minute hour day month day_of_week/;

has $_ => (
    is => 'rw',
) for @SCHEDULES;

has user => (
    is => 'ro',
    isa => 'Maybe[Str]',
);

has definition => (
    is  => 'ro',
    isa => 'Str',
);

no Mouse;

my %DEFINITIONS = (
    yearly   => '0 0 1 1 *',
    annually => '0 0 1 1 *',
    monthly  => '0 0 1 * *',
    weekly   => '0 0 * * 0',
    daily    => '0 0 * * *',
    hourly   => '0 * * * *',
    reboot   => '@reboot',
);

my %ENTITY_PARAMS = (
    minute  => {
        range => [0,59],
    },
    hour    => {
        range => [0,23],
    },
    day     => {
        range => [1,31],
    },
    month   => {
        range   => [1,12],
        aliases => [qw/jan feb mar apr may jun jul aug sep oct nov dec/],
    },
    day_of_week => {
        range   => [0,7],
        aliases => [qw/sun mon tue wed thu fri sat/],
    },
);

sub BUILD {
    my $self = shift;

    my %s;
    if (my $def = $self->definition) {
        my $definition = $DEFINITIONS{$def};
        croak sprintf('bad time specifier: [%s]', $def) unless $definition;

        if ($def ne 'reboot') {
            @s{@SCHEDULES} = split /\s+/, $definition;
        }
    }
    else {
        $s{$_} = $self->$_ for @SCHEDULES;
    }

    if (exists $s{minute}) {
        for my $schedule (@SCHEDULES) {
            my $entity;
            try {
                $entity = Parse::Crontab::Schedule::Entity->new(
                    entity => $s{$schedule},
                    %{$ENTITY_PARAMS{$schedule}},
                    field  => $schedule,
                );
            }
            catch {
                croak "bad $schedule: $_";
            };
            $self->$schedule($entity);
        }
    }
}

sub parse {
    my ($cls, $str) = @_;

    my @s = split /\s+/, $str;
    my %args;
    for my $schedule (@SCHEDULES) {
        my $arg = shift @s;
        $args{$schedule} = $arg;
    }

    my $self = $cls->new(%args);

    if (my @warns = $self->_check_warnings) {
        croak join "\n", @warns;
    }
    $self;
}

sub _check_warnings {
    my $self = shift;

    my @warnings;
    if ($self->minute.'' eq '*') {
        push @warnings,
            q{Specifying '*' for minutes means EVERY MINUTES. You really want to do that and to remove this warning, specify '*/1' explicitly.}
    }
    if ($self->day_of_week.'' ne '*' && $self->day.'' ne '*') {
        push @warnings,
            q{Both specifying 'day_of_week' and 'day' field causes unexpected behavior. You should seperate job entries.}
    }
    @warnings;
}

sub match {
    my ($self, %args) = @_;

    for my $s (qw/minute hour month/) {
        return unless $self->$s->match($args{$s});
    }

    if ($self->day_of_week.'' ne '*') {
        croak q{args year is not specified. could detect day_of_week.} unless $args{year};

        require Time::Piece;
        my $str = sprintf '%04d-%02d-%02d', $args{year}, $args{month}, $args{day};
        my $day = Time::Piece->strptime($str, '%Y-%m-%d');

        return unless $self->day_of_week->match($day->day_of_week);
    }
    else {
        return unless $self->day->match($args{day});
    }

    1; # matched
}

__PACKAGE__->meta->make_immutable;
__END__
=for stopwords cron crontab

=head1 NAME

Parse::Crontab::Schedule - Perl extension to parse Vixie crontab schedule

=head1 SYNOPSIS

    use Parse::Crontab::Schedule;
    my $schedule = Parse::Crontab::Schedule->parse('*/1 12 10 10 *');
    if ($schedule->match(year => 2013, month => 10, day => 10, hour => 12, minute => 5) ) {
        ...
    }

=head1 DESCRIPTION

This software is for parsing and validating Vixie crontab files.

=head1 INTERFACE

=head2 Constructor

=head3 C<< $schedule = Parse::Crontab::Schedule->parse($str) >>

C<$str> is crontab schedule string like C<'*/1 12 10 10 *>.

=head2 Functions

=head3 C<< $bool = $schedule->match(%opt) >>

The schedule matches or not.
Keys of C<%opt> are C<minute>, C<hour>, C<day>, C<month>, C<year>.

=head1 DEPENDENCIES

Perl 5.8.1 or later.

=head1 BUGS

All complex software has bugs lurking in it, and this module is no
exception. If you find a bug please either email me, or add the bug
to cpan-RT.

=head1 AUTHOR

Masayuki Matsuki E<lt>y.songmu@gmail.comE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2013, Masayuki Matsuki. All rights reserved.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut