]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/CalendarEvent.pm
Tools.pm: do not ignore "0" in split_list
[pve-common.git] / src / PVE / CalendarEvent.pm
index 2714841c1aab6039c4f6337e348d61d68231734d..56e992330003c445efe614c62c8154845225bed0 100644 (file)
@@ -5,8 +5,9 @@ use warnings;
 use Data::Dumper;
 use Time::Local;
 use PVE::JSONSchema;
+use PVE::Tools qw(trim);
 
-# Note: This class implements a parser/utils for systemd like calender exents
+# Note: This class implements a parser/utils for systemd like calendar exents
 # Date specification is currently not implemented
 
 my $dow_names = {
@@ -26,7 +27,7 @@ sub pve_verify_calendar_event {
     eval { parse_calendar_event($text); };
     if (my $err = $@) {
        return undef if $noerr;
-       die "invalid calendar event '$text'\n";
+       die "invalid calendar event '$text' - $err\n";
     }
     return $text;
 }
@@ -36,6 +37,12 @@ sub pve_verify_calendar_event {
 sub parse_calendar_event {
     my ($event) = @_;
 
+    $event = trim($event);
+
+    if ($event eq '') {
+       die "unable to parse calendar event - event is empty\n";
+    }
+
     my $parse_single_timespec = sub {
        my ($p, $max, $matchall_ref, $res_hash) = @_;
 
@@ -55,6 +62,7 @@ sub parse_calendar_event {
                    $$matchall_ref = 1;
                } else {
                    $start = int($start);
+                   die "value '$start' out of range\n" if $start >= $max;
                    $res_hash->{$start} = 1;
                }
            }
@@ -100,6 +108,9 @@ sub parse_calendar_event {
     };
 
     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;
@@ -120,8 +131,12 @@ sub parse_calendar_event {
 
     if ($time_spec =~ m/^($chars+):($chars+)$/) {
        my ($p1, $p2) = ($1, $2);
-       $parse_single_timespec->($p1, 24, \$matchall_hours, $hours_hash);
-       $parse_single_timespec->($p2, 60, \$matchall_minutes, $minutes_hash);
+       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)) {
@@ -146,81 +161,138 @@ sub parse_calendar_event {
        $m = [ sort { $a <=> $b } keys %$minutes_hash ];
     }
 
-    return { h => $h, m => $m, dow => [ sort keys %$dow_hash ]};
+    return { h => $h, m => $m, dow => [ sort keys %$dow_hash ], utc => $utc };
 }
 
-sub compute_next_event {
-    my ($calspec, $last, $utc) = @_;
+sub is_leap_year($) {
+    return 0 if $_[0] % 4;
+    return 1 if $_[0] % 100;
+    return 0 if $_[0] % 400;
+    return 1;
+}
 
-    my $hspec = $calspec->{h};
-    my $mspec = $calspec->{m};
-    my $dowspec = $calspec->{dow};
+# 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];
+}
 
-    $last += 60; # at least one minute later
+# 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;
 
-       my ($min, $hour, $mday, $mon, $year, $wday);
-       my $startofday;
+    if ($mon >= 12) {
+       $year += $mon / 12;
+       $mon %= 12;
+    }
 
-       if ($utc) {
-           (undef, $min, $hour, $mday, $mon, $year, $wday) = gmtime($last);
-           $startofday = timegm(0, 0, 0, $mday, $mon, $year);
-       } else {
-           (undef, $min, $hour, $mday, $mon, $year, $wday) = localtime($last);
-           $startofday = timelocal(0, 0, 0, $mday, $mon, $year);
-       }
+    $wday %= 7;
+    return [$sec, $min, $hour, $day, $mon, $year, $wday];
+}
 
-       $last = $startofday + $hour*3600 + $min*60;
+# 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]);
+}
 
-       my $check_dow = sub {
-           foreach my $d (@$dowspec) {
-               return $last if $d == $wday;
-               if ($d > $wday) {
-                   return $startofday + ($d-$wday)*86400;
-               }
-           }
-           return $startofday + (7-$wday)*86400; # start of next week
-       };
+sub compute_next_event {
+    my ($calspec, $last) = @_;
 
-       if ((my $next = $check_dow->()) != $last) {
-           $last = $next;
-           next; # repeat
-       }
+    my $hspec = $calspec->{h};
+    my $mspec = $calspec->{m};
+    my $dowspec = $calspec->{dow};
+    my $utc = $calspec->{utc};
 
-       my $check_hour = sub {
-           return $last if $hspec eq '*';
-           foreach my $h (@$hspec) {
-               return $last if $h == $hour;
-               if ($h > $hour) {
-                   return $startofday + $h*3600;
-               }
-           }
-           return $startofday + 24*3600; # test next day
-       };
+    $last += 60; # at least one minute later
 
-       if ((my $next = $check_hour->()) != $last) {
-           $last = $next;
-           next; # repeat
+    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;
+           }
        }
-
-       my $check_minute = sub {
-           return $last if $mspec eq '*';
-           foreach my $m (@$mspec) {
-               return $last if $m == $min;
-               if ($m > $min) {
-                   return $startofday +$hour*3600 + $m*60;
-               }
+       # 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;
            }
-           return $startofday + ($hour + 1)*3600; # test next hour
-       };
-
-       if ((my $next = $check_minute->()) != $last) {
-           $last = $next;
-           next; # repeat
-       } else {
-           return $last;
        }
+       # 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";