diff --git a/src/PVE/CalendarEvent.pm b/src/PVE/CalendarEvent.pm index 56e9923..e2bf53a 100644 --- a/src/PVE/CalendarEvent.pm +++ b/src/PVE/CalendarEvent.pm @@ -6,6 +6,7 @@ use Data::Dumper; use Time::Local; use PVE::JSONSchema; use PVE::Tools qw(trim); +use PVE::RS::CalendarEvent; # Note: This class implements a parser/utils for systemd like calendar exents # Date specification is currently not implemented @@ -43,259 +44,13 @@ sub parse_calendar_event { die "unable to parse calendar event - event is empty\n"; } - my $parse_single_timespec = sub { - my ($p, $max, $matchall_ref, $res_hash) = @_; - - if ($p =~ m/^((?:\*|[0-9]+))(?:\/([1-9][0-9]*))?$/) { - my ($start, $repetition) = ($1, $2); - if (defined($repetition)) { - $repetition = int($repetition); - $start = $start eq '*' ? 0 : int($start); - die "value '$start' out of range\n" if $start >= $max; - die "repetition '$repetition' out of range\n" if $repetition >= $max; - while ($start < $max) { - $res_hash->{$start} = 1; - $start += $repetition; - } - } else { - if ($start eq '*') { - $$matchall_ref = 1; - } else { - $start = int($start); - die "value '$start' out of range\n" if $start >= $max; - $res_hash->{$start} = 1; - } - } - } elsif ($p =~ m/^([0-9]+)\.\.([1-9][0-9]*)$/) { - my ($start, $end) = (int($1), int($2)); - die "range start '$start' out of range\n" if $start >= $max; - die "range end '$end' out of range\n" if $end >= $max || $end < $start; - for (my $i = $start; $i <= $end; $i++) { - $res_hash->{$i} = 1; - } - } else { - die "unable to parse calendar event '$p'\n"; - } - }; - - my $h = undef; - my $m = undef; - - my $matchall_minutes = 0; - my $matchall_hours = 0; - my $minutes_hash = {}; - my $hours_hash = {}; - - my $dowsel = join('|', keys %$dow_names); - - my $dow_hash; - - my $parse_dowspec = sub { - my ($p) = @_; - - if ($p =~ m/^($dowsel)$/i) { - $dow_hash->{$dow_names->{lc($1)}} = 1; - } elsif ($p =~ m/^($dowsel)\.\.($dowsel)$/i) { - my $start = $dow_names->{lc($1)}; - my $end = $dow_names->{lc($2)} || 7; - die "wrong order in range '$p'\n" if $end < $start; - for (my $i = $start; $i <= $end; $i++) { - $dow_hash->{($i % 7)} = 1; - } - } else { - die "unable to parse weekday specification '$p'\n"; - } - }; - - my @parts = split(/\s+/, $event); - my $utc = (@parts && uc($parts[-1]) eq 'UTC'); - pop @parts if $utc; - - - if ($parts[0] =~ m/$dowsel/i) { - my $dow_spec = shift @parts; - foreach my $p (split(',', $dow_spec)) { - $parse_dowspec->($p); - } - } else { - $dow_hash = { 0 => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5=> 1, 6 => 1 }; - } - - if (scalar(@parts) && $parts[0] =~ m/\-/) { - my $date_spec = shift @parts; - die "date specification not implemented"; - } - - my $time_spec = shift(@parts) // "00:00"; - my $chars = '[0-9*/.,]'; - - if ($time_spec =~ m/^($chars+):($chars+)$/) { - my ($p1, $p2) = ($1, $2); - foreach my $p (split(',', $p1)) { - $parse_single_timespec->($p, 24, \$matchall_hours, $hours_hash); - } - foreach my $p (split(',', $p2)) { - $parse_single_timespec->($p, 60, \$matchall_minutes, $minutes_hash); - } - } elsif ($time_spec =~ m/^($chars)+$/) { # minutes only - $matchall_hours = 1; - foreach my $p (split(',', $time_spec)) { - $parse_single_timespec->($p, 60, \$matchall_minutes, $minutes_hash); - } - - } else { - die "unable to parse calendar event\n"; - } - - die "unable to parse calendar event - unused parts\n" if scalar(@parts); - - if ($matchall_hours) { - $h = '*'; - } else { - $h = [ sort { $a <=> $b } keys %$hours_hash ]; - } - - if ($matchall_minutes) { - $m = '*'; - } else { - $m = [ sort { $a <=> $b } keys %$minutes_hash ]; - } - - return { h => $h, m => $m, dow => [ sort keys %$dow_hash ], utc => $utc }; -} - -sub is_leap_year($) { - return 0 if $_[0] % 4; - return 1 if $_[0] % 100; - return 0 if $_[0] % 400; - return 1; -} - -# mon = 0.. (Jan = 0) -sub days_in_month($$) { - my ($mon, $year) = @_; - return 28 + is_leap_year($year) if $mon == 1; - return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$mon]; -} - -# day = 1.. -# mon = 0.. (Jan = 0) -sub wrap_time($) { - my ($time) = @_; - my ($sec, $min, $hour, $day, $mon, $year, $wday) = @$time; - - use integer; - if ($sec >= 60) { - $min += $sec / 60; - $sec %= 60; - } - - if ($min >= 60) { - $hour += $min / 60; - $min %= 60; - } - - if ($hour >= 24) { - $day += $hour / 24; - $wday += $hour / 24; - $hour %= 24; - } - - # Translate to 0..($days_in_mon-1) - --$day; - while (1) { - my $days_in_mon = days_in_month($mon % 12, $year); - last if $day < $days_in_mon; - # Wrap one month - $day -= $days_in_mon; - ++$mon; - } - # Translate back to 1..$days_in_mon - ++$day; - - if ($mon >= 12) { - $year += $mon / 12; - $mon %= 12; - } - - $wday %= 7; - return [$sec, $min, $hour, $day, $mon, $year, $wday]; -} - -# helper as we need to keep weekdays in sync -sub time_add_days($$) { - my ($time, $inc) = @_; - my ($sec, $min, $hour, $day, $mon, $year, $wday) = @$time; - return wrap_time([$sec, $min, $hour, $day + $inc, $mon, $year, $wday + $inc]); + return PVE::RS::CalendarEvent->new($event); } sub compute_next_event { my ($calspec, $last) = @_; - my $hspec = $calspec->{h}; - my $mspec = $calspec->{m}; - my $dowspec = $calspec->{dow}; - my $utc = $calspec->{utc}; - - $last += 60; # at least one minute later - - my $t = [$utc ? gmtime($last) : localtime($last)]; - $t->[0] = 0; # we're not interested in seconds, actually - $t->[5] += 1900; # real years for clarity - - outer: for (my $i = 0; $i < 1000; ++$i) { - my $wday = $t->[6]; - foreach my $d (@$dowspec) { - goto this_wday if $d == $wday; - if ($d > $wday) { - $t->[0] = $t->[1] = $t->[2] = 0; # sec = min = hour = 0 - $t = time_add_days($t, $d - $wday); - next outer; - } - } - # Test next week: - $t->[0] = $t->[1] = $t->[2] = 0; # sec = min = hour = 0 - $t = time_add_days($t, 7 - $wday); - next outer; - this_wday: - - goto this_hour if $hspec eq '*'; - my $hour = $t->[2]; - foreach my $h (@$hspec) { - goto this_hour if $h == $hour; - if ($h > $hour) { - $t->[0] = $t->[1] = 0; # sec = min = 0 - $t->[2] = $h; # hour = $h - next outer; - } - } - # Test next day: - $t->[0] = $t->[1] = $t->[2] = 0; # sec = min = hour = 0 - $t = time_add_days($t, 1); - next outer; - this_hour: - - goto this_min if $mspec eq '*'; - my $min = $t->[1]; - foreach my $m (@$mspec) { - goto this_min if $m == $min; - if ($m > $min) { - $t->[0] = 0; # sec = 0 - $t->[1] = $m; # min = $m - next outer; - } - } - # Test next hour: - $t->[0] = $t->[1] = 0; # sec = min = hour = 0 - $t->[2]++; - $t = wrap_time($t); - next outer; - this_min: - - return $utc ? timegm(@$t) : timelocal(@$t); - } - - die "unable to compute next calendar event\n"; + return $calspec->compute_next_event($last); } 1; diff --git a/test/calendar_event_test.pl b/test/calendar_event_test.pl index abbd74c..4572965 100755 --- a/test/calendar_event_test.pl +++ b/test/calendar_event_test.pl @@ -18,7 +18,7 @@ my $alldays = [0,1,2,3,4,5,6]; my $tests = [ [ '*', - { h => '*', m => '*', dow => $alldays }, + undef, [ [0, 60], [30, 60], @@ -28,7 +28,7 @@ my $tests = [ ], [ '*/10', - { h => '*', m => [0, 10, 20, 30, 40, 50], dow => $alldays }, + undef, [ [0, 600], [599, 600], @@ -38,7 +38,7 @@ my $tests = [ ], [ '*/12:0' , - { h => [0, 12], m => [0], dow => $alldays }, + undef, [ [ 10, 43200], [ 13*3600, 24*3600], @@ -46,7 +46,7 @@ my $tests = [ ], [ '1/12:0/15' , - { h => [1, 13], m => [0, 15, 30, 45], dow => $alldays }, + undef, [ [0, 3600], [3600, 3600+15*60], @@ -61,7 +61,7 @@ my $tests = [ ], [ '1,4,6', - { h => '*', m => [1, 4, 6], dow => $alldays}, + undef, [ [0, 60], [60, 4*60], @@ -71,15 +71,15 @@ my $tests = [ ], [ '0..3', - { h => '*', m => [ 0, 1, 2, 3 ], dow => $alldays }, + undef, ], [ '23..23:0..3', - { h => [ 23 ], m => [ 0, 1, 2, 3 ], dow => $alldays }, + undef, ], [ 'Mon', - { h => [0], m => [0], dow => [1] }, + undef, [ [0, 4*86400], # Note: Epoch 0 is Thursday, 1. January 1970 [4*86400, 11*86400], @@ -88,7 +88,7 @@ my $tests = [ ], [ 'sat..sun', - { h => [0], m => [0], dow => [0, 6] }, + undef, [ [0, 2*86400], [2*86400, 3*86400], @@ -97,7 +97,7 @@ my $tests = [ ], [ 'sun..sat', - { h => [0], m => [0], dow => $alldays }, + undef, ], [ 'Fri..Mon', @@ -105,15 +105,15 @@ my $tests = [ ], [ 'wed,mon..tue,fri', - { h => [0], m => [0], dow => [ 1, 2, 3, 5] }, + undef, ], [ 'mon */15', - { h => '*', m => [0, 15, 30, 45], dow => [1]}, + undef, ], [ '22/1:0', - { h => [22, 23], m => [0], dow => $alldays }, + undef, [ [0, 22*60*60], [22*60*60, 23*60*60], @@ -122,7 +122,7 @@ my $tests = [ ], [ '*/2:*', - { h => [0,2,4,6,8,10,12,14,16,18,20,22], m => '*', dow => $alldays }, + undef, [ [0, 60], [60*60, 2*60*60], @@ -131,7 +131,7 @@ my $tests = [ ], [ '20..22:*/30', - { h => [20,21,22], m => [0,30], dow => $alldays }, + undef, [ [0, 20*60*60], [20*60*60, 20*60*60 + 30*60], @@ -164,7 +164,7 @@ my $tests = [ ], [ '0,1,3..5', - { h => '*', m => [0,1,3,4,5], dow => $alldays }, + undef, [ [0, 60], [60, 3*60], @@ -173,7 +173,7 @@ my $tests = [ ], [ '2,4:0,1,3..5', - { h => [2,4], m => [0,1,3,4,5], dow => $alldays }, + undef, [ [0, 2*60*60], [2*60*60 + 60, 2*60*60 + 3*60], @@ -185,18 +185,16 @@ my $tests = [ foreach my $test (@$tests) { my ($t, $expect, $nextsync) = @$test; + $expect //= {}; + my $timespec; eval { $timespec = PVE::CalendarEvent::parse_calendar_event($t); }; my $err = $@; - delete $timespec->{utc}; if ($expect->{error}) { chomp $err if $err; - $timespec = { error => $err } if $err; - is_deeply($timespec, $expect, "expect parse error on '$t' - $expect->{error}"); + ok(defined($err) == defined($expect->{error}), "parsing '$t' failed expectedly"); die "unable to execute nextsync tests" if $nextsync; - } else { - is_deeply($timespec, $expect, "parse '$t'"); } next if !$nextsync;