diff --git a/lib/NGCP/Panel/Controller/API/CFTimeSets.pm b/lib/NGCP/Panel/Controller/API/CFTimeSets.pm index cabb6533c5..4c8c9137ae 100644 --- a/lib/NGCP/Panel/Controller/API/CFTimeSets.pm +++ b/lib/NGCP/Panel/Controller/API/CFTimeSets.pm @@ -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) = @_; diff --git a/lib/NGCP/Panel/Role/API/CFTimeSets.pm b/lib/NGCP/Panel/Role/API/CFTimeSets.pm index f2ee5a7685..ae38f4588b 100644 --- a/lib/NGCP/Panel/Role/API/CFTimeSets.pm +++ b/lib/NGCP/Panel/Role/API/CFTimeSets.pm @@ -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); } diff --git a/lib/NGCP/Panel/Utils/Generic.pm b/lib/NGCP/Panel/Utils/Generic.pm index 10b0d2797e..45021aa984 100644 --- a/lib/NGCP/Panel/Utils/Generic.pm +++ b/lib/NGCP/Panel/Utils/Generic.pm @@ -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; diff --git a/sandbox/cftimeset_time_tz_conversion.t b/sandbox/cftimeset_time_tz_conversion.t new file mode 100644 index 0000000000..b43f068d0c --- /dev/null +++ b/sandbox/cftimeset_time_tz_conversion.t @@ -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); + +} \ No newline at end of file