You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
18 KiB
421 lines
18 KiB
use strict;
|
|
|
|
use Test::More;
|
|
|
|
{
|
|
use DateTime;
|
|
use DateTime::TimeZone;
|
|
my $tz = DateTime::TimeZone->new(name => 'America/Chicago'); #'Europe/Dublin');
|
|
my $dt = DateTime->now();
|
|
my $offset = $tz->offset_for_datetime($dt);
|
|
diag($dt .' - offset: ' . $offset);
|
|
}
|
|
|
|
#goto SKIP;
|
|
### add
|
|
{
|
|
# single day, no roll-over hours:
|
|
is_deeply(_add([{wday => '7',hour => '2-3',},],3), [{wday => '7',hour => '5-6',
|
|
},], "single day add, hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '7',hour => '20-23',},],3), [{wday => '7',hour => '23-23',},{wday => '1',hour => '0-2',
|
|
}], "single day add, hour to shifted to next day");
|
|
is_deeply(_add([{wday => '7',hour => '22-23',},],3), [{wday => '1',hour => '1-2',
|
|
},], "single day add, hour from and hour to shifted to next day");
|
|
|
|
# single day, roll over hours:
|
|
is_deeply(_add([{wday => '7',hour => '3-2',},],3), [{wday => '7',hour => '6-5',
|
|
},], "single day add, roll-over hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '7',hour => '22-3',},],3), [{wday => '1',hour => '1-6',
|
|
},], "single day add, roll-over hour from shifted to next day");
|
|
is_deeply(_add([{wday => '7',hour => '23-22',},],3), [{wday => '1',hour => '2-1',
|
|
},], "single day add, roll-over hour from and hour to shifted to next day");
|
|
|
|
# range days, no roll-over hours:
|
|
is_deeply(_add([{wday => '3-7',hour => '2-3',},],3), [{wday => '3-7',hour => '5-6',
|
|
},], "range days add, hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '3-7',hour => '20-23',},],3), [{wday => '3-7',hour => '23-23',},{wday => '4-1',hour => '0-2',
|
|
}], "range days add, hour to shifted to next day");
|
|
is_deeply(_add([{wday => '3-7',hour => '22-23',},],3), [{wday => '4-1',hour => '1-2',
|
|
},], "range days add, hour from and hour to shifted to next day");
|
|
|
|
# range days, roll over hours:
|
|
is_deeply(_add([{wday => '3-7',hour => '3-2',},],3), [{wday => '3-7',hour => '6-5',
|
|
},], "range days add, roll-over hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '3-7',hour => '22-3',},],3), [{wday => '4-1',hour => '1-6',
|
|
},], "range days add, roll-over hour from shifted to next day");
|
|
is_deeply(_add([{wday => '3-7',hour => '23-22',},],3), [{wday => '4-1',hour => '2-1',
|
|
},], "range days add, roll-over hour from and hour to shifted to next day");
|
|
|
|
# all days, no roll-over hours:
|
|
is_deeply(_add([{wday => '7-1',hour => '2-3',},],3), [{wday => '7-1',hour => '5-6',
|
|
},], "any days add, hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '1-7',hour => '20-23',},],3), [{wday => '1-7',hour => '23-23',},{wday => '1-7',hour => '0-2',
|
|
}], "any days add, hour to shifted to next day");
|
|
is_deeply(_add([{wday => '',hour => '22-23',},],3), [{wday => '',hour => '1-2',
|
|
},], "any days add, hour from and hour to shifted to next day");
|
|
|
|
# all days, roll over hours:
|
|
is_deeply(_add([{wday => '7-1',hour => '3-2',},],3), [{wday => '7-1',hour => '6-5',
|
|
},], "any days add, roll-over hour from and hour to not shifted to next day");
|
|
is_deeply(_add([{wday => '',hour => '22-3',},],3), [{wday => '',hour => '1-6',
|
|
},], "any days add, roll-over hour from shifted to next day");
|
|
is_deeply(_add([{wday => '1-7',hour => '23-22',},],3), [{wday => '1-7',hour => '2-1',
|
|
},], "any days add, roll-over hour from and hour to shifted to next day");
|
|
|
|
}
|
|
|
|
#SKIP:
|
|
### subtract
|
|
{
|
|
# single day, no roll-over hours:
|
|
is_deeply(_subtract([{wday => '1',hour => '5-6',},],3), [{wday => '1',hour => '2-3',
|
|
},], "single day subtract, hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1',hour => '2-4',},],3), [{wday => '7',hour => '23-23',},{wday => '1',hour => '0-1',
|
|
}], "single day subtract, hour from shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1',hour => '0-2',},],3), [{wday => '7',hour => '21-23',
|
|
},], "single day subtract, hour from and hour to shifted to previous day");
|
|
|
|
# single day, roll over hours:
|
|
is_deeply(_subtract([{wday => '1',hour => '6-5',},],3), [{wday => '1',hour => '3-2',
|
|
},], "single day subtract, roll-over hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1',hour => '6-1',},],3), [{wday => '7',hour => '3-22',
|
|
},], "single day subtract, roll-over hour to shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1',hour => '2-1',},],3), [{wday => '7',hour => '23-22',
|
|
},], "single day subtract, roll-over hour from and hour to shifted to previous day");
|
|
|
|
# range days, no roll-over hours:
|
|
is_deeply(_subtract([{wday => '1-3',hour => '5-6',},],3), [{wday => '1-3',hour => '2-3',
|
|
},], "range days subtract, hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-3',hour => '2-4',},],3), [{wday => '7-2',hour => '23-23',},{wday => '1-3',hour => '0-1',
|
|
}], "range days subtract, hour from shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-3',hour => '0-2',},],3), [{wday => '7-2',hour => '21-23',
|
|
},], "range days subtract, hour from and hour to shifted to previous day");
|
|
|
|
# range days, roll over hours:
|
|
is_deeply(_subtract([{wday => '1-3',hour => '6-5',},],3), [{wday => '1-3',hour => '3-2',
|
|
},], "range days subtract, roll-over hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-3',hour => '6-1',},],3), [{wday => '7-2',hour => '3-22',
|
|
},], "range days subtract, roll-over hour to shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-3',hour => '2-1',},],3), [{wday => '7-2',hour => '23-22',
|
|
},], "range days subtract, roll-over hour from and hour to shifted to previous day");
|
|
|
|
# all days, no roll-over hours:
|
|
is_deeply(_subtract([{wday => '7-1',hour => '5-6',},],3), [{wday => '7-1',hour => '2-3',
|
|
},], "any day subtract, hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-7',hour => '2-4',},],3), [{wday => '1-7',hour => '23-23',},{wday => '1-7',hour => '0-1',
|
|
}], "any day subtract, hour from shifted to previous day");
|
|
is_deeply(_subtract([{wday => '',hour => '0-2',},],3), [{wday => '',hour => '21-23',
|
|
},], "any day subtract, hour from and hour to shifted to previous day");
|
|
|
|
# all days, roll over hours:
|
|
is_deeply(_subtract([{wday => '7-1',hour => '6-5',},],3), [{wday => '7-1',hour => '3-2',
|
|
},], "any day subtract, roll-over hour from and hour to not shifted to previous day");
|
|
is_deeply(_subtract([{wday => '1-7',hour => '6-1',},],3), [{wday => '1-7',hour => '3-22',
|
|
},], "any day subtract, roll-over hour to shifted to previous day");
|
|
is_deeply(_subtract([{wday => '',hour => '2-1',},],3), [{wday => '',hour => '23-22',
|
|
},], "any day subtract, roll-over hour from and hour to shifted to previous day");
|
|
}
|
|
|
|
{
|
|
is_deeply(_subtract(_add([{wday => '7',hour => '20-23',},],3),3,1), [{wday => '7',hour => '20-23',},],
|
|
"merge adjacent add->subtract");
|
|
is_deeply(_add(_subtract([{wday => '1',hour => '2-4',},],3),3,1), [{wday => '1',hour => '2-4',},],
|
|
"merge adjacent subtract->add");
|
|
}
|
|
|
|
done_testing;
|
|
|
|
sub _add {
|
|
my ($times,$offset_hrs,$merge) = @_;
|
|
|
|
my @result = ();
|
|
foreach my $time (@$times) {
|
|
my $p1 = { %$time };
|
|
unless (length($time->{hour}) > 0) {
|
|
#nothing to do if there is no hour defined:
|
|
push(@result,$p1);
|
|
next;
|
|
}
|
|
my ($hour_start,$hour_end) = split(/\-/, $time->{hour});
|
|
my $hour_range;
|
|
if (defined $hour_end) {
|
|
$hour_range = 1;
|
|
} else {
|
|
$hour_end = $hour_start;
|
|
$hour_range = 0;
|
|
}
|
|
$hour_start += abs($offset_hrs);
|
|
$hour_end += abs($offset_hrs);
|
|
if ($hour_start < 24 and $hour_end < 24) {
|
|
$p1->{hour} = ($hour_range ? $hour_start . '-' . $hour_end : $hour_start);
|
|
push(@result,$p1);
|
|
next;
|
|
}
|
|
my ($wday_start, $wday_end) = split(/\-/, $time->{wday} || '1-7');
|
|
my $wday_range;
|
|
if (defined $wday_end) {
|
|
$wday_range = 1;
|
|
} else {
|
|
$wday_end = $wday_start;
|
|
$wday_range = 0;
|
|
}
|
|
my @nums = ();
|
|
if ($wday_start <= $wday_end) {
|
|
push(@nums,$wday_start .. $wday_end);
|
|
} else {
|
|
push(@nums,$wday_start .. 7);
|
|
push(@nums,1 .. $wday_end);
|
|
}
|
|
my ($p2,$p_shift_wday);
|
|
if ($hour_start > 23 and $hour_end > 23) { #26-28
|
|
$p1->{hour} = ($hour_range ? ($hour_start % 24) . '-' . ($hour_end % 24) : ($hour_start % 24)); #2-4
|
|
$p_shift_wday = $p1;
|
|
} elsif ($hour_start < $hour_end) {
|
|
if ($hour_end > 23) { #17-23 +3-> 20-26
|
|
$p1->{hour} = $hour_start . '-23'; #20-0
|
|
$p2 = { %$time };
|
|
$p2->{hour} = '0-' . ($hour_end % 24); #0-2
|
|
$p_shift_wday = $p2;
|
|
}
|
|
} else {
|
|
if ($hour_start > 23) { #23-17 +3-> 26-20
|
|
$p1->{hour} = ($hour_start % 24) . '-' . $hour_end; #2-20
|
|
$p_shift_wday = $p1;
|
|
}
|
|
}
|
|
if ($p_shift_wday and (scalar @nums) < 7) {
|
|
$p_shift_wday->{wday} = ($wday_range ? (($wday_start) % 7 + 1) . '-' . (($wday_end) % 7 + 1) : (($wday_start) % 7 + 1));
|
|
}
|
|
push(@result,$p1);
|
|
push(@result,$p2) if $p2;
|
|
}
|
|
return ($merge ? merge_adjacent(\@result) : \@result);
|
|
|
|
}
|
|
|
|
sub _subtract {
|
|
my ($times,$offset_hrs,$merge) = @_;
|
|
|
|
my @result = ();
|
|
foreach my $time (@$times) {
|
|
my $p1 = { %$time };
|
|
unless (length($time->{hour}) > 0) {
|
|
#nothing to do if there is no hour defined:
|
|
push(@result,$p1);
|
|
next;
|
|
}
|
|
my ($hour_start,$hour_end) = split(/\-/, $time->{hour});
|
|
my $hour_range;
|
|
if (defined $hour_end) {
|
|
$hour_range = 1;
|
|
} else {
|
|
$hour_end = $hour_start;
|
|
$hour_range = 0;
|
|
}
|
|
$hour_start -= abs($offset_hrs);
|
|
$hour_end -= abs($offset_hrs);
|
|
if ($hour_start >= 0 and $hour_end >= 0) {
|
|
$p1->{hour} = ($hour_range ? $hour_start . '-' . $hour_end : $hour_start);
|
|
push(@result,$p1);
|
|
next;
|
|
}
|
|
my ($wday_start, $wday_end) = split(/\-/, $time->{wday} || '1-7');
|
|
my $wday_range;
|
|
if (defined $wday_end) {
|
|
$wday_range = 1;
|
|
} else {
|
|
$wday_end = $wday_start;
|
|
$wday_range = 0;
|
|
}
|
|
my @nums = ();
|
|
if ($wday_start <= $wday_end) {
|
|
push(@nums,$wday_start .. $wday_end);
|
|
} else {
|
|
push(@nums,$wday_start .. 7);
|
|
push(@nums,1 .. $wday_end);
|
|
}
|
|
my ($p2,$p_shift_wday);
|
|
if ($hour_start < 0 and $hour_end < 0) { #-4 - -2
|
|
$p1->{hour} = ($hour_range ? ($hour_start % 24) . '-' . ($hour_end % 24) : ($hour_start % 24)); #20-22
|
|
$p_shift_wday = $p1;
|
|
} elsif ($hour_start < $hour_end) { #-4 - 3
|
|
if ($hour_start < 0) { #0-7 -4-> -4 - 3
|
|
$p1->{hour} = ($hour_start % 24) . '-23'; #20-0
|
|
$p2 = { %$time };
|
|
$p2->{hour} = '0-' . $hour_end; #0-3
|
|
$p_shift_wday = $p1;
|
|
}
|
|
} else {
|
|
if ($hour_end < 0) { #22 - 2 -6-> 16 - -4
|
|
$p1->{hour} = $hour_start . '-' . ($hour_end % 24); #16-20
|
|
$p_shift_wday = $p1;
|
|
}
|
|
}
|
|
if ($p_shift_wday and (scalar @nums) < 7) {
|
|
$p_shift_wday->{wday} = ($wday_range ? (($wday_start - 2) % 7 + 1) . '-' . (($wday_end - 2) % 7 + 1) : (($wday_start - 2) % 7 + 1));
|
|
}
|
|
push(@result,$p1);
|
|
push(@result,$p2) if $p2;
|
|
}
|
|
return ($merge ? _merge_adjacent(\@result) : \@result);
|
|
|
|
}
|
|
|
|
sub _merge_adjacent {
|
|
my ($times) = @_;
|
|
|
|
my ($wday_map,$wdays) = array_to_map($times,sub { my $time = shift;
|
|
my $wday = $time->{wday} || '1-7';
|
|
$wday = '1-7' if $time->{wday} eq '7-1';
|
|
$wday .= '_' . (defined $time->{minute} ? $time->{minute} : '*');
|
|
return $wday;
|
|
},undef,'group');
|
|
|
|
my @result = ();
|
|
my $idx = 0;
|
|
foreach my $wday (@$wdays) {
|
|
my %hour_start_map = ();
|
|
my %hour_end_map = ();
|
|
my %skip_map = ();
|
|
my $old_idx = $idx;
|
|
foreach my $time (@{$wday_map->{$wday}}) {
|
|
if (length($time->{hour}) > 0) {
|
|
my ($hour_start,$hour_end) = split(/\-/, $time->{hour});
|
|
$hour_end //= $hour_start;
|
|
if ($hour_end >= $hour_start) { #we do not create any adjacent roll-over hours, so we also skip such when merging
|
|
if (not defined $hour_start_map{$hour_start}
|
|
or $hour_end > $hour_start_map{$hour_start}->{hour_end}) {
|
|
$hour_start_map{$hour_start} = { hour_end => $hour_end, idx => $idx, };
|
|
} else {
|
|
$skip_map{$idx} = 0;
|
|
}
|
|
if (not defined $hour_end_map{$hour_end}
|
|
or $hour_start < $hour_end_map{$hour_end}->{hour_start}) {
|
|
$hour_end_map{$hour_end} = { hour_start => $hour_start, }; #, idx => $idx,
|
|
} else {
|
|
$skip_map{$idx} = 0;
|
|
}
|
|
} else {
|
|
$skip_map{$idx} = 1;
|
|
}
|
|
} else {
|
|
$skip_map{$idx} = 1;
|
|
}
|
|
$idx++;
|
|
}
|
|
$idx = $old_idx;
|
|
foreach my $time (@{$wday_map->{$wday}}) {
|
|
my $p = { %$time };
|
|
if (exists $skip_map{$idx}) {
|
|
push(@result,$p) if $skip_map{$idx};
|
|
} else {
|
|
my ($hour_start,$hour_end) = split(/\-/, $time->{hour});
|
|
$hour_end //= $hour_start;
|
|
#if ($hour_end_map{$hour_end}->{idx} == $idx) {
|
|
my $adjacent_start = $hour_end + 1;
|
|
if (exists $hour_start_map{$adjacent_start}) {
|
|
$p->{hour} = $hour_start . '-' . $hour_start_map{$adjacent_start}->{hour_end};
|
|
$skip_map{$hour_start_map{$adjacent_start}->{idx}} = 0;
|
|
}
|
|
push(@result,$p);
|
|
#}
|
|
}
|
|
$idx++;
|
|
}
|
|
}
|
|
return \@result;
|
|
}
|
|
|
|
sub _merge {
|
|
my ($times) = @_;
|
|
|
|
my ($wday_map,$wdays,$wday_groups) = array_to_map($times,sub { my $time = shift;
|
|
my $wday = $time->{wday} || '1-7';
|
|
$wday = '1-7' if $time->{wday} eq '7-1';
|
|
$wday .= '_' . (defined $time->{minute} ? $time->{minute} : '*');
|
|
return $wday;
|
|
},undef,'group');
|
|
|
|
my @result = ();
|
|
my %ranges = ();
|
|
foreach my $wday (@$wdays) {
|
|
foreach my $time (@{$wday_map->{$wday}}) {
|
|
my $p = { %$time };
|
|
if (length($time->{hour}) > 0) {
|
|
my ($hour_start,$hour_end) = split(/\-/, $time->{hour});
|
|
$hour_end //= $hour_start;
|
|
if ($hour_end >= $hour_start) {
|
|
#https://stackoverflow.com/questions/42928964/finding-and-merging-down-intervalls-in-perl
|
|
my $in_range = 0;
|
|
foreach my $range (@{$ranges{$wday}} ) {
|
|
if (($hour_start >= $range->{start} and $hour_start <= $range->{end})
|
|
or ( $hour_end >= $range->{start} and $hour_end <= $range->{end})
|
|
) {
|
|
$range->{end} = $hour_end if $hour_end > $range->{end};
|
|
$range->{start} = $hour_start if $hour_start < $range->{start};
|
|
$in_range++;
|
|
}
|
|
}
|
|
if (not $in_range) {
|
|
push(@{$ranges{$wday}},{ start => $hour_start, end => $hour_end, });
|
|
$p->{hour} = $hour_start . '-' . $hour_end;
|
|
push(@result,$p);
|
|
}
|
|
} else { #splitting by the tz offset add/subtract never produces roll-overs, so we don't merge such
|
|
push(@result,$p);
|
|
}
|
|
} else {
|
|
push(@result,$p);
|
|
}
|
|
}
|
|
}
|
|
return \@result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub array_to_map {
|
|
|
|
my ($array_ptr,$get_key_code,$get_value_code,$mode) = @_;
|
|
my $map = {};
|
|
my @keys = ();
|
|
my @values = ();
|
|
if (defined $array_ptr and ref $array_ptr eq 'ARRAY') {
|
|
if (defined $get_key_code and ref $get_key_code eq 'CODE') {
|
|
if (not (defined $get_value_code and ref $get_value_code eq 'CODE')) {
|
|
$get_value_code = sub { return shift; };
|
|
}
|
|
$mode = lc($mode);
|
|
if (not ($mode eq 'group' or $mode eq 'first' or $mode eq 'last')) {
|
|
$mode = 'group';
|
|
}
|
|
foreach my $item (@$array_ptr) {
|
|
my $key = &$get_key_code($item);
|
|
if (defined $key) {
|
|
my $value = &$get_value_code($item);
|
|
if (defined $value) {
|
|
if (not exists $map->{$key}) {
|
|
if ($mode eq 'group') {
|
|
$map->{$key} = [ $value ];
|
|
} else {
|
|
$map->{$key} = $value;
|
|
}
|
|
push(@keys,$key);
|
|
} else {
|
|
if ($mode eq 'group') {
|
|
push(@{$map->{$key}}, $value);
|
|
} elsif ($mode eq 'last') {
|
|
$map->{$key} = $value;
|
|
}
|
|
}
|
|
push(@values,$value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ($map,\@keys,\@values);
|
|
|
|
} |