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
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;
my $tests = [
[
'*',
- { h => '*', m => '*', dow => $alldays },
+ undef,
[
[0, 60],
[30, 60],
],
[
'*/10',
- { h => '*', m => [0, 10, 20, 30, 40, 50], dow => $alldays },
+ undef,
[
[0, 600],
[599, 600],
],
[
'*/12:0' ,
- { h => [0, 12], m => [0], dow => $alldays },
+ undef,
[
[ 10, 43200],
[ 13*3600, 24*3600],
],
[
'1/12:0/15' ,
- { h => [1, 13], m => [0, 15, 30, 45], dow => $alldays },
+ undef,
[
[0, 3600],
[3600, 3600+15*60],
],
[
'1,4,6',
- { h => '*', m => [1, 4, 6], dow => $alldays},
+ undef,
[
[0, 60],
[60, 4*60],
],
[
'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],
],
[
'sat..sun',
- { h => [0], m => [0], dow => [0, 6] },
+ undef,
[
[0, 2*86400],
[2*86400, 3*86400],
],
[
'sun..sat',
- { h => [0], m => [0], dow => $alldays },
+ undef,
],
[
'Fri..Mon',
],
[
'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],
],
[
'*/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],
],
[
'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],
],
[
'0,1,3..5',
- { h => '*', m => [0,1,3,4,5], dow => $alldays },
+ undef,
[
[0, 60],
[60, 3*60],
],
[
'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],
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;