Cron-Toolkit

 view release on metacpan or  search on metacpan

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


sub next {
   my ( $self, $epoch_seconds ) = @_;
   $epoch_seconds //= time;

   my $clamped = max( $epoch_seconds, $self->{begin_epoch} );

   return if $clamped > $self->{end_epoch};

   my $tm = Time::Moment->from_epoch($clamped)->with_offset_same_instant( $self->{utc_offset} );
   $tm = $tm->plus_seconds(1);

   # shortcut for HMS
   NODE: foreach my $i ( 0 .. 2 ) {
      my $node    = $self->{nodes}[$i];
      my $curval  = $self->_field_value( $tm, $node->field_type );
      my $lowval  = $node->lowest($tm);
      my $highval = $node->highest($tm);

      if ($curval >= $highval) {
         $tm = $self->_set_date( $tm, $node->field_type, $lowval );
         $tm = $self->_plus_one( $tm, $self->{nodes}[ $i + 1 ]->field_type );
         next NODE;
      }

      for my $c ( $curval .. $highval ) {
         my $c_tm = $self->_set_date( $tm, $node->field_type, $c );
         if ( $self->_is_match($c_tm) ) {
            $tm = $c_tm;
            last NODE;
         }
      }

      # flip odometer if no match
      $tm = $self->_set_date( $tm, $node->field_type, $lowval );
      $tm = $self->_plus_one( $tm, $self->{nodes}[ $i + 1 ]->field_type );
   }

   # set year
   my $year_node   = $self->{nodes}[6];
   my $year_lowval = $year_node->lowest($tm); 
   my $tm_year_low = $self->_set_date( $tm, $year_node->field_type, $year_lowval );
   $tm_year_low = $self->_minus_one( $tm_year_low, $year_node->field_type );

   $tm = $tm_year_low if $tm->is_before($tm_year_low);

   my $max_tm = Time::Moment->new(
      year   => 2099,
      month  => 12,
      day    => 31,
      hour   => 23,
      minute => 59,
      second => 59,
   );

   my $max_iter = $tm->delta_days($max_tm);

   # the brute force approach for DMY is correct here because:
   # 1) the design is simple and easy to understand and debug
   # 2) solves all tricky end-of-month and leap year calculations
   # 3) 365 iterations per one-year time window is good enough

   for my $day ( 1 .. $max_iter ) {
      return $tm->epoch if $self->_is_match($tm);
      $tm = $tm->plus_days(1);
   }
   return;
}

sub previous {
   my ( $self, $epoch_seconds ) = @_;
   $epoch_seconds //= time;

   my $clamped = min( $epoch_seconds, $self->{end_epoch} );

   return if $clamped < $self->{begin_epoch};

   my $tm = Time::Moment->from_epoch($clamped)->with_offset_same_instant( $self->{utc_offset} );
   $tm = $tm->minus_seconds(1);

   NODE: foreach my $i ( 0 .. 2 ) {
      my $node = $self->{nodes}[$i];

      my $lowval  = $node->lowest($tm);
      my $highval = $node->highest($tm);
      my $curval  = $self->_field_value( $tm, $node->field_type );

      if ($curval <= $lowval) {
         $tm = $self->_set_date( $tm, $node->field_type, $highval );
         $tm = $self->_minus_one( $tm, $self->{nodes}[ $i + 1 ]->field_type );
         next NODE;
      }

      for ( my $c = $curval ; $c >= $lowval ; $c-- ) {
         my $c_tm = $self->_set_date( $tm, $node->field_type, $c );
         if ( $self->_is_match($c_tm) ) {
            $tm = $c_tm;
            last NODE;
         }
      }

      # flip odometer if no match
      $tm = $self->_set_date( $tm, $node->field_type, $highval );
      $tm = $self->_minus_one( $tm, $self->{nodes}[ $i + 1 ]->field_type );
   }

   # set year
   my $year_node    = $self->{nodes}[6];
   my $year_highval = $year_node->highest($tm);
   my $tm_year_high = $self->_set_date( $tm, $year_node->field_type, $year_highval );
   $tm_year_high    = $self->_plus_one( $tm_year_high, $year_node->field_type );
   $tm = $tm_year_high if $tm->is_after($tm_year_high);

   # calculate maximum iterations
   my $min_tm = Time::Moment->new(
      year   => 1970,
      month  => 1,
      day    => 1,
      hour   => 0,
      minute => 0,
      second => 0,
   );

   my $min_iter = $min_tm->delta_days($tm);

   for my $day ( 0 .. $min_iter ) {
      return $tm->epoch if $self->_is_match($tm);
      $tm = $tm->minus_days(1);
   }
   return;
}

sub _field_value {
   my ( $self, $tm, $field_type ) = @_;
   return $tm->second       if $field_type eq 'second';
   return $tm->minute       if $field_type eq 'minute';
   return $tm->hour         if $field_type eq 'hour';
   return $tm->day_of_month if $field_type eq 'dom';
   return $tm->month        if $field_type eq 'month';
   return $tm->day_of_week  if $field_type eq 'dow';
   return $tm->year         if $field_type eq 'year';
}

sub _set_date {
   my ( $self, $tm, $field_type, $value ) = @_;
   return $tm->with_second($value)       if $field_type eq 'second';
   return $tm->with_minute($value)       if $field_type eq 'minute';
   return $tm->with_hour($value)         if $field_type eq 'hour';
   return $tm->with_day_of_month($value) if $field_type eq 'dom';
   return $tm->with_month($value)        if $field_type eq 'month';
   if ( $field_type eq 'dow' ) {
      $value = 7 if $value == 0;
      return $tm->with_day_of_week($value);
   }
   return $tm->with_year($value) if $field_type eq 'year';
}

sub _plus_one {
   my ( $self, $tm, $field_type ) = @_;
   return $tm->plus_seconds(1) if $field_type eq 'second';
   return $tm->plus_minutes(1) if $field_type eq 'minute';
   return $tm->plus_hours(1)   if $field_type eq 'hour';
   return $tm->plus_days(1)    if $field_type eq 'dom';
   return $tm->plus_months(1)  if $field_type eq 'month';
   return $tm->plus_weeks(1)   if $field_type eq 'dow';
   return $tm->plus_years(1)   if $field_type eq 'year';
}

sub _minus_one {
   my ( $self, $tm, $field_type ) = @_;
   return $tm->minus_seconds(1) if $field_type eq 'second';
   return $tm->minus_minutes(1) if $field_type eq 'minute';
   return $tm->minus_hours(1)   if $field_type eq 'hour';
   return $tm->minus_days(1)    if $field_type eq 'dom';



( run in 0.941 second using v1.01-cache-2.11-cpan-96521ef73a4 )