Algorithm-Cron

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

        [BUGFIXES]
         * Fix t/90rt84352.t that fails in some timezones (RT103111)

0.09    2014/05/14 17:27:49
        [CHANGES]
         * Sanity-check time fields in the constructor and reject attempts to
           specify a list of no values for any field

        [BUGFIXES]
         * Space-trim crontab string before parsing, so leading space doesn't
           upset the seconds field (RT95454)

0.08    2013/11/06 18:07:40
        [BUGFIXES]
         * Declare dependency on perl >= 5.008 for 'use constant'
         * Perl 5.18 ignores 'wday' field to POSIX::strftime(); need to use
           mday adjustments for generating %WDAY hash (RT89947)

0.07    BUGFIXES:
         * Need to pass explicit -1, -1, -1 to mktime() on some platforms to
           override tm_isdst detection (RT84352)

README  view on Meta::CPAN


     wday => "fri-sun"
     wday => "5-7"
     # Both equivalent to: wday => "0,5,6"

    As per cron(8) behaviour, this algorithm looks for a match of the `min',
    `hour' and `mon' fields, and at least one of the `mday' or `mday'
    fields. If both `mday' and `wday' are specified, a match of either will
    be sufficient.

    As an extension, seconds may be provided either by passing six
    space-separated fields in the `crontab' string, or as an additional
    `sec' field. If not provided it will default to `0'. If six fields are
    provided, the first gives the seconds.

  Time Base
    `Algorithm::Cron' supports using either UTC or the local timezone when
    comparing against the given schedule.

CONSTRUCTOR
  $cron = Algorithm::Cron->new( %args )
    Constructs a new `Algorithm::Cron' object representing the given
    schedule relative to the given time base. Takes the following named
    arguments:

    base => STRING
            Gives the time base used for scheduling. Either `utc' or
            `local'.

    crontab => STRING
            Gives the crontab schedule in 5 or 6 space-separated fields.

    sec => STRING, min => STRING, ... mon => STRING
            Optional. Gives the schedule in a set of individual fields, if
            the `crontab' field is not specified.

METHODS
  @seconds = $cron->sec
  @minutes = $cron->min
  @hours = $cron->hour
  @mdays = $cron->mday
  @months = $cron->mon
  @wdays = $cron->wday
    Accessors that return a list of the accepted values for each scheduling
    field. These are returned in a plain list of numbers, regardless of the
    form they were specified to the constructor.

    Also note that the list of valid months will be 0-based (in the range 0

lib/Algorithm/Cron.pm  view on Meta::CPAN

#
#  (C) Paul Evans, 2012-2014 -- leonerd@leonerd.org.uk

package Algorithm::Cron;

use strict;
use warnings;

our $VERSION = '0.10';

my @FIELDS = qw( sec min hour mday mon year wday );
my @FIELDS_CTOR = grep { $_ ne "year" } @FIELDS;

use Carp;
use POSIX qw( mktime strftime setlocale LC_TIME );
use Time::timegm qw( timegm );

=head1 NAME

C<Algorithm::Cron> - abstract implementation of the F<cron(8)> scheduling
algorithm

lib/Algorithm/Cron.pm  view on Meta::CPAN


 wday => "fri-sun"
 wday => "5-7"
 # Both equivalent to: wday => "0,5,6"

As per F<cron(8)> behaviour, this algorithm looks for a match of the C<min>,
C<hour> and C<mon> fields, and at least one of the C<mday> or C<mday> fields.
If both C<mday> and C<wday> are specified, a match of either will be
sufficient.

As an extension, seconds may be provided either by passing six space-separated
fields in the C<crontab> string, or as an additional C<sec> field. If not
provided it will default to C<0>. If six fields are provided, the first gives
the seconds.

=head2 Time Base

C<Algorithm::Cron> supports using either UTC or the local timezone when
comparing against the given schedule.

=cut

# mday field starts at 1, others start at 0
my %MIN = (
   sec  => 0,
   min  => 0,
   hour => 0,
   mday => 1,
   mon  => 0
);

# These don't have to be real maxima, as the algorithm will cope. These are
# just the top end of the range expansions
my %MAX = (
   sec  => 59,
   min  => 59,
   hour => 23,
   mday => 31,
   mon  => 11,
   wday => 6,
);

my %MONTHS;
my %WDAYS;
# These always want to be in LC_TIME=C

lib/Algorithm/Cron.pm  view on Meta::CPAN

=over 8

=item base => STRING

Gives the time base used for scheduling. Either C<utc> or C<local>.

=item crontab => STRING

Gives the crontab schedule in 5 or 6 space-separated fields.

=item sec => STRING, min => STRING, ... mon => STRING

Optional. Gives the schedule in a set of individual fields, if the C<crontab>
field is not specified.

=back

=cut

sub new
{

lib/Algorithm/Cron.pm  view on Meta::CPAN

      s/^\s+//, s/\s+$// for $crontab;

      my @fields = split m/\s+/, $crontab;
      @fields >= 5 or croak "Expected at least 5 crontab fields";
      @fields <= 6 or croak "Expected no more than 6 crontab fields";

      @fields = ( "0", @fields ) if @fields < 6;
      @params{ @FIELDS_CTOR } = @fields;
   }

   $params{sec} = 0 unless exists $params{sec};

   my $self = bless {
      base => $base,
   }, $class;

   foreach ( @FIELDS_CTOR ) {
      next unless exists $params{$_};

      $self->{$_} = _expand_set( delete $params{$_}, $_ );
      !defined $self->{$_} or scalar @{ $self->{$_} } or
         croak "Require at least one value for '$_' field";
   }

   return $self;
}

=head1 METHODS

=cut

=head2 @seconds = $cron->sec

=head2 @minutes = $cron->min

=head2 @hours = $cron->hour

=head2 @mdays = $cron->mday

=head2 @months = $cron->mon

=head2 @wdays = $cron->wday

lib/Algorithm/Cron.pm  view on Meta::CPAN


=cut

sub next_time
{
   my $self = shift;
   my ( $time ) = @_;

   my $funcs = $time_funcs{$self->{base}};

   # Always need to add at least 1 second
   my @t = $funcs->[EXTRACT]->( $time + 1 );

RESTART:
   $self->next_time_field( \@t, TM_MON ) or goto RESTART;

   if( defined $self->{mday} and defined $self->{wday} ) {
      # Now it gets tricky because cron allows a match of -either- mday or wday
      # rather than requiring both. So we'll work out which of the two is sooner
      my $next_time_by_wday;
      my @wday_t = @t;

t/01expand.t  view on Meta::CPAN


use strict;
use warnings;

use Test::More;

use Algorithm::Cron;

local *expand = \&Algorithm::Cron::_expand_set;

is_deeply( expand( "*", "sec" ), undef, 'expand sec=*' );
is_deeply( expand( "0", "sec" ), [ 0 ], 'expand sec=0' );
is_deeply( expand( "*/10", "sec" ), [ 0, 10, 20, 30, 40, 50 ], 'expand sec=0/10' );
is_deeply( expand( "5-8", "sec" ), [ 5, 6, 7, 8 ], 'expand sec=5-8' );
is_deeply( expand( "3-17/4", "sec" ), [ 3, 7, 11, 15 ], 'expand sec=3-17/4' );

is_deeply( expand( "*/5", "mday" ), [ 1, 6, 11, 16, 21, 26, 31 ], 'expand mday=*/5' );

is_deeply( expand( "jan", "mon" ), [ 0 ], 'expand mon=jan' );
is_deeply( expand( "mar-sep", "mon" ), [ 2 .. 8 ], 'expand mon=mar-sep' );
is_deeply( expand( "5", "mon" ), [ 4 ], 'expand mon=5' );
is_deeply( expand( "*/3", "mon" ), [ 0, 3, 6, 9 ], 'expand mon=*/3' );

is_deeply( expand( "mon", "wday" ), [ 1 ], 'expand wday=mon' );
is_deeply( expand( "mon-fri", "wday" ), [ 1 .. 5 ], 'expand wday=mon-fri' );

t/02cron.t  view on Meta::CPAN

use Test::Fatal qw( dies_ok );

use Algorithm::Cron;

{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      crontab => '0 0 0 1 0',
   );

   is_deeply( [ $cron->sec  ], [ 0 ], '$cron->sec for 0 0 0 1 0' );
   is_deeply( [ $cron->min  ], [ 0 ], '$cron->min for 0 0 0 1 0' );
   is_deeply( [ $cron->hour ], [ 0 ], '$cron->hour for 0 0 0 1 0' );
   is_deeply( [ $cron->mday ], [ 0 ], '$cron->mday for 0 0 0 1 0' );
   is_deeply( [ $cron->mon  ], [ 0 ], '$cron->mon for 0 0 0 1 0' );
   is_deeply( [ $cron->wday ], [ 0 ], '$cron->wday for 0 0 0 1 0' );
}

{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      crontab => '1 3 5 7 9',
   );

   is_deeply( [ $cron->sec  ], [ 0 ], '$cron->sec for 1 3 5 7 9' );
   is_deeply( [ $cron->min  ], [ 1 ], '$cron->min for 1 3 5 7 9' );
   is_deeply( [ $cron->hour ], [ 3 ], '$cron->hour for 1 3 5 7 9' );
   is_deeply( [ $cron->mday ], [ 5 ], '$cron->mday for 1 3 5 7 9' );
   is_deeply( [ $cron->mon  ], [ 6 ], '$cron->mon for 1 3 5 7 9' );
   is_deeply( [ $cron->wday ], [ 9 ], '$cron->wday for 1 3 5 7 9' );
}

{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      crontab => '* * * * *',
   );

   is_deeply( [ $cron->sec  ], [ 0 ], '$cron->sec for * * * * *' );
   is_deeply( [ $cron->min  ], [], '$cron->min for * * * * *' );
   is_deeply( [ $cron->hour ], [], '$cron->hour for * * * * *' );
   is_deeply( [ $cron->mday ], [], '$cron->mday for * * * * *' );
   is_deeply( [ $cron->mon  ], [], '$cron->mon for * * * * *' );
   is_deeply( [ $cron->wday ], [], '$cron->wday for * * * * *' );
}

{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      crontab => '*/10 * * * * *',
   );

   is_deeply( [ $cron->sec  ], [ 0, 10, 20, 30, 40, 50 ], '$cron->sec for */10 * * * * *' );
}

{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      min  => 10,
      hour => 3,
      mday => 15,
      mon  => 2,
   );

   is_deeply( [ $cron->sec  ], [ 0  ], '$cron->sec for named' );
   is_deeply( [ $cron->min  ], [ 10 ], '$cron->min for named' );
   is_deeply( [ $cron->hour ], [ 3  ], '$cron->hour for named' );
   is_deeply( [ $cron->mday ], [ 15 ], '$cron->mday for named' );
   is_deeply( [ $cron->mon  ], [ 1  ], '$cron->mon for named' );
   is_deeply( [ $cron->wday ], [],     '$cron->wday for named' );
}

dies_ok { Algorithm::Cron->new( crontab => '@hourly', base => 'utc' ) }
   "crontab => '\@hourly' dies";

t/02cron.t  view on Meta::CPAN

   'Unrecognised number dies';

# RT95454
{
   my $cron = Algorithm::Cron->new(
      base => 'utc',
      crontab => ' 20 23 1 1 *'
   );

   is_deeply( [ $cron->min ], [ 20 ], '$cron->min for leading space' );
   is_deeply( [ $cron->sec ], [ 0 ], '$cron->sec for leading space' );

   my $next = $cron->next_time( 0 );

   is( $next, 84000, '->next_time for crontab with space' );
}

done_testing;

t/03next_time.t  view on Meta::CPAN

   }

   return @times;
}

is_deeply( [ list_times "* * * * * *", 5 ],
   [ "2012-01-01 00:00:01",
     "2012-01-01 00:00:02",
     "2012-01-01 00:00:03",
     "2012-01-01 00:00:04",
     "2012-01-01 00:00:05" ], 'per second' );

is_deeply( [ list_times "* * * * *", 5 ],
   [ "2012-01-01 00:01:00",
     "2012-01-01 00:02:00",
     "2012-01-01 00:03:00",
     "2012-01-01 00:04:00",
     "2012-01-01 00:05:00" ], 'per minute' );

is_deeply( [ list_times "*/10 * * * *", 7 ],
   [ "2012-01-01 00:10:00",

t/03next_time.t  view on Meta::CPAN

     "2012-01-10 02:15:00",
     "2012-01-15 02:15:00",
     "2012-01-17 02:15:00",
     "2012-01-24 02:15:00",
     "2012-01-31 02:15:00",
     "2012-02-07 02:15:00" ], '02:15 15th or Tuesday' );

is_deeply( [ list_times "59 59 23 31 01,03 *", 3 ],
   [ "2012-01-31 23:59:59",
     "2012-03-31 23:59:59",
     "2013-01-31 23:59:59" ], 'last second of the month');

is_deeply( [ list_times "00 00 00 31  * *", 7 ],
   [ "2012-01-31 00:00:00",
     "2012-03-31 00:00:00",
     "2012-05-31 00:00:00",
     "2012-07-31 00:00:00",
     "2012-08-31 00:00:00",
     "2012-10-31 00:00:00",
     "2012-12-31 00:00:00" ], '31st of each month');



( run in 0.645 second using v1.01-cache-2.11-cpan-39bf76dae61 )