TT#48988 timezone conversions for CF timeset periods

when passing the ?tz=Europe/Vienna with POST/PUT/PATCH, the
callforward timeset period definition input will be converted from
Europe/Vienna timezone to system timezone before persising to DB.

when passing the ?tz parameter with GET requests, the
callforward timeset period definition from DB will be converted
to the given timezone.

the ?use_owner_tz parameter will take the subscriber's inherited
timezone.

disarmed in code for now.

Change-Id: If4e130b241c28821844e0700231d1cd6883bcbfb
changes/97/26097/8
Rene Krenn 6 years ago
parent 361563563e
commit 8ed0574a68

@ -155,12 +155,15 @@ sub POST :Allow {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'times'. Must be an array.");
last;
}
my $times = $resource->{times};
# enable tz and use_owner_tz params for POST:
#$times = $self->apply_owner_timezone($c,$b_subscriber,$resource->{times},'deflate');
try {
$tset = $schema->resultset('voip_cf_time_sets')->create({
name => $resource->{name},
subscriber_id => $subscriber->id,
});
for my $t ( @{$resource->{times}} ) {
for my $t ( @$times ) {
delete $t->{time_set_id};
$tset->create_related("voip_cf_periods", $t);
}
@ -169,7 +172,7 @@ sub POST :Allow {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cftimeset.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;

@ -15,6 +15,8 @@ use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Form;
use NGCP::Panel::Utils::DateTime qw();
sub get_form {
my ($self, $c) = @_;
if($c->user->roles eq "subscriber" || $c->user->roles eq "subscriberadmin") {
@ -24,6 +26,279 @@ sub get_form {
}
}
sub apply_owner_timezone {
my ($self, $c, $subscriber, $times, $mode) = @_;
my $tz_name;
if($c->req->param('tz')) {
if (DateTime::TimeZone->is_valid_name($c->req->param('tz'))) {
$tz_name = $c->req->param('tz');
} else {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Query parameter 'tz' value is not a valid time zone");
return;
}
} elsif ($subscriber and $c->req->param('use_owner_tz')) {
my $tz = $c->model('DB')->resultset('voip_subscriber_timezone')->search_rs({
subscriber_id => $subscriber->id
})->first;
$tz_name = NGCP::Panel::Utils::DateTime::normalize_db_tz_name($tz->name) if $tz;
}
$times //= [];
my $tz_local = DateTime::TimeZone->new(name => 'local');
my ($tz,$offset);
if ($tz_name
and ($tz = DateTime::TimeZone->new(name => $tz_name))
and abs($offset = $tz->offset_for_datetime(DateTime->now()) - $tz_local->offset_for_datetime(DateTime->now())) > 0) {
my $offset_hrs = int($offset / 3600.0);
if ($offset_hrs * 3600 != $offset) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "only full hours supported for timezone offset ($tz_name: $offset seconds = $offset_hrs hours)");
return;
}
#foreach my $time (@$times) {
# foreach $field (qw(year month mday) {
# if (exists $time->{$field} and $time->{$field}) {
# $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "cannot apply a timezone offset for time with '$field' field ($time->{$field})");
# return;
# }
# }
#}
my $merge = 0;
if ('deflate' eq $mode) { #for writing to db
$offset_hrs = $offset_hrs * -1;
$c->log->debug("cf timeset $mode for timezone $tz_name: offset $offset_hrs hours");
} elsif ('inflate' eq $mode) { #for reading from db
$merge = 1;
$c->log->debug("cf timeset $mode for timezone $tz_name: offset $offset_hrs hours");
} else {
die("invalid mode $mode");
}
my ($yearmonthmday_map,$yearmonthmdays) = array_to_map($times,sub { my $time = shift;
return (length($time->{year}) > 0 ? $time->{year} : '*') .
'_' . ($time->{month} || '*') . '_' . ($time->{mday} || '*');
},undef,'group');
$times = [];
foreach my $yearmonthmday (@$yearmonthmdays) {
if ($offset_hrs > 0) {
push(@$times,@{_add($yearmonthmday_map->{$yearmonthmday},$offset_hrs,$merge)});
} else {
push(@$times,@{_subtract($yearmonthmday_map->{$yearmonthmday},$offset_hrs,$merge)});
}
}
} else {
$c->log->debug("no timezone to convert to, or zero tz offset");
}
return $times;
}
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 hal_from_item {
my ($self, $c, $item, $type) = @_;
my $form;
@ -36,6 +311,8 @@ sub hal_from_item {
push @times, $timeelem;
}
$resource{times} = \@times;
# enable tz and use_owner_tz params for GET:
#$resource{times} = $self->apply_owner_timezone($c,$item->subscriber->voip_subscriber,\@times,'inflate');
my $b_subs_id = $item->subscriber->voip_subscriber->id;
$resource{subscriber_id} = $b_subs_id;
@ -135,13 +412,17 @@ sub update_item {
last;
}
my $times = $resource->{times};
# enable tz and use_owner_tz params for PUT/PATCH/POST:
#$times = $self->apply_owner_timezone($c,$b_subscriber,$resource->{times},'deflate');
try {
$item->update({
name => $resource->{name},
subscriber_id => $subscriber->id,
})->discard_changes;
$item->voip_cf_periods->delete;
for my $t ( @{$resource->{times}} ) {
for my $t ( @$times ) {
delete $t->{time_set_id};
$item->create_related("voip_cf_periods", $t);
}

@ -17,7 +17,7 @@ use Data::Compare qw//;
my $MIME_TYPES = {
#first extension is default, others are for extension 2 mime_type detection
'audio/x-wav' => ['wav'],
'audio/mpeg' => ['mp3'],
'audio/mpeg' => ['mp3'],
'audio/ogg' => ['ogg'],
};
@ -168,4 +168,48 @@ sub extension_to_mime_type {
return $mime_type;
}
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);
}
1;

@ -0,0 +1,421 @@
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);
}
Loading…
Cancel
Save