File Coverage

blib/lib/DateTime/Event/Recurrence.pm
Criterion Covered Total %
statement 295 302 97.6
branch 105 128 82.0
condition 31 44 70.4
subroutine 29 29 100.0
pod n/a
total 460 503 91.4


line stmt bran cond sub pod time code
1 15     15   1987347 use strict;
  15         34  
  15         724  
2              
3             package DateTime::Set::ICal;
4              
5 15     15   112 use vars qw(@ISA);
  15         31  
  15         9620  
6             # use Carp;
7              
8             # a "dt::set" with a symbolic string representation
9             @ISA = qw( DateTime::Set );
10              
11             sub set_ical { # include list, exclude list
12 107     107   175 my $self = shift;
13             # carp "set_ical $_[0] => @{$_[1]}" if @_;
14 107         361 $self->{as_ical} = [ @_ ];
15 107         271 $self;
16             }
17              
18             sub get_ical {
19 59     59   92 my $self = shift;
20 59 100       205 return unless $self->{as_ical};
21 38         57 return @{ $self->{as_ical} };
  38         173  
22             }
23              
24             sub clone {
25 31     31   12402 my $self = shift;
26 31         164 my $new = $self->SUPER::clone( @_ );
27 31         1956 $new->set_ical( $self->get_ical );
28 31         100 $new;
29             }
30              
31             sub union {
32 14     14   255 my $self = shift;
33 14         70 my $new = $self->SUPER::union( @_ );
34              
35             # RFC2445 - op1, op2 must have no 'exclude'
36 14         2196 my (%op1, %op2);
37 14 50       88 %op1 = ( $self->get_ical ) if ( UNIVERSAL::can( $self, 'get_ical' ) );
38 14 100       82 %op2 = ( $_[0]->get_ical ) if ( UNIVERSAL::can( $_[0], 'get_ical' ) );
39             return $new if ( ( exists $op1{exclude} ) ||
40 14 50 33     80 ( exists $op2{exclude} ) );
41              
42 14         28 bless $new, 'DateTime::Set::ICal';
43             # warn " -- 1 isa @{[%op1]} -- 2 isa @{[%op2]} -- ";
44 14         19 my @ical;
45             @ical = exists $op1{include} ?
46 14 100       76 @{$op1{include}} :
  7         18  
47             $self;
48              
49             # push @ical, @{$op2{include}}, @_;
50 14 50       37 if ( exists $op2{include} )
51             {
52 0         0 push @ical, @{$op2{include}};
  0         0  
53             }
54             else
55             {
56 14         63 push @ical, @_; # whatever...
57             }
58             # warn "union: @ical";
59 14         101 $new->set_ical( include => [ @ical ] );
60 14         87 $new;
61             }
62              
63             sub complement {
64 1     1   4336 my $self = shift;
65 1         8 my $new = $self->SUPER::complement( @_ );
66 1 50       841 return $new unless @_;
67              
68             # RFC2445 - op2 must have no 'exclude'
69 1         3 my (%op1, %op2);
70 1 50       8 %op1 = ( $self->get_ical ) if ( UNIVERSAL::can( $self, 'get_ical' ) );
71 1 50       7 %op2 = ( $_[0]->get_ical ) if ( UNIVERSAL::can( $_[0], 'get_ical' ) );
72 1 50       4 return $new if ( exists $op2{exclude} );
73              
74 1         3 bless $new, 'DateTime::Set::ICal';
75             # warn " -- 1 isa @{[%op1]} -- 2 isa @{[%op2]} -- ";
76 1         2 my ( @include, @exclude );
77             @include = exists $op1{include} ?
78 1 50       5 @{$op1{include}} :
  1         3  
79             $self;
80              
81             @exclude = exists $op1{exclude} ?
82 1 50       5 @{$op1{exclude}} :
  0         0  
83             ();
84              
85 1 50       2 if ( exists $op2{include} )
86             {
87 0         0 push @exclude, @{$op2{include}};
  0         0  
88             }
89             else
90             {
91 1         3 push @exclude, @_; # whatever...
92             }
93              
94             # warn "complement: include @include exclude @exclude";
95 1         5 $new->set_ical( include => [ @include ], exclude => [ @exclude ] );
96 1         4 $new;
97             }
98              
99             package DateTime::Event::Recurrence;
100              
101 15     15   120 use strict;
  15         28  
  15         363  
102 15     15   3849 use DateTime;
  15         247546  
  15         347  
103 15     15   11931 use DateTime::Set;
  15         643649  
  15         406  
104 15     15   140 use DateTime::Span;
  15         34  
  15         369  
105 15     15   81 use Params::Validate qw(:all);
  15         29  
  15         3566  
106 15     15   82 use vars qw( $VERSION );
  15         31  
  15         808  
107             $VERSION = '0.18';
108              
109 15     15   75 use constant INFINITY => 100 ** 100 ** 100 ;
  15         30  
  15         901  
110 15     15   213 use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
  15         30  
  15         977  
111              
112             # -------- BASE OPERATIONS
113              
114 15         4037 use vars qw(
115             %as_number
116              
117             %truncate
118             %next_unit
119             %previous_unit
120            
121             %truncate_interval
122             %next_unit_interval
123             %previous_unit_interval
124              
125             %weekdays
126             %weekdays_1
127             %weekdays_any
128            
129             %memoized_duration
130            
131             %ical_name
132             %ical_days
133             %limits
134             @units
135 15     15   80 );
  15         42  
136              
137             BEGIN {
138 15     15   136 %weekdays = qw( mo 1 tu 2 we 3 th 4 fr 5 sa 6 su 7 );
139 15         87 %weekdays_1 = qw( 1mo 1 1tu 2 1we 3 1th 4 1fr 5 1sa 6 1su 7 );
140 15         204 %weekdays_any = ( %weekdays, %weekdays_1 );
141            
142 15         91 %ical_name = qw(
143             months BYMONTH
144             weeks BYWEEKNO
145             days BYMONTHDAY
146             hours BYHOUR
147             minutes BYMINUTE
148             seconds BYSECOND
149             );
150            
151 15         173 %ical_days = qw(
152             1 MO 2 TU 3 WE 4 TH 5 FR 6 SA 7 SU
153             -7 MO -6 TU -5 WE -4 TH -3 FR -2 SA -1 SU
154             );
155            
156 15         45 @units = qw( years months weeks days hours minutes seconds nanoseconds );
157            
158 15         2919 %limits = qw(
159             nanoseconds 1000000000
160             seconds 61
161             minutes 60
162             hours 24
163             months 12
164             weeks 53
165             days 366
166             );
167              
168             } # BEGIN
169              
170              
171             # memoization reduces 'duration' creation from >10000 to about 30 per run,
172             # in DT::E::ICal
173              
174             sub _add {
175             # datetime, unit, value
176 17779     17779   46472 my $dur = \$memoized_duration{$_[1]}{$_[2]};
177 17779 100       43004 $$dur = new DateTime::Duration( $_[1] => $_[2] )
178             unless defined $$dur;
179 17779         72769 $_[0]->add_duration( $$dur );
180             }
181              
182             # TODO: %as_number should use the "subtract" routines from DateTime
183              
184             %as_number = (
185             years => sub {
186             $_[0]->year
187             },
188             months => sub {
189             12 * $_[0]->year + $_[0]->month - 1
190             },
191             days => sub {
192             ( $_[0]->local_rd_values() )[0]
193             },
194             weeks => sub {
195             # $_[1] is the "week start day", such as "1mo"
196 15     15   91 use integer;
  15         26  
  15         123  
197             return ( $as_number{days}->( $_[0] ) - $weekdays_any{ $_[1] } ) / 7;
198             },
199             hours => sub {
200             $as_number{days}->($_[0]) * 24 + $_[0]->hour
201             },
202             minutes => sub {
203             $as_number{hours}->($_[0]) * 60 + $_[0]->minute
204             },
205             seconds => sub {
206             $_[0]->local_rd_as_seconds
207             },
208             years_weekly => sub {
209             # get the internal year number, in 'week' mode
210             # $_[1] is the "week start day", such as "1mo"
211             my $base = $_[0]->clone;
212             $base = $truncate{years_weekly}->( $base, $_[1] )
213             if $base->month > 11 || $base->month < 2;
214             _add( $base, weeks => 1 );
215             return $as_number{years}->( $base );
216             },
217             months_weekly => sub {
218             # get the internal month number, in 'week' mode
219             # $_[1] is the "week start day", such as "1mo"
220             my $base = $_[0]->clone;
221             $base = $truncate{months_weekly}->( $base, $_[1] )
222             if $base->day > 20 || $base->day < 7;
223             _add( $base, weeks => 1 );
224             return $as_number{months}->( $base );
225             },
226             );
227              
228              
229             %truncate = (
230             # @_ = ( $datetime, $week_start_day )
231              
232             (
233             map {
234             my $name = $_;
235             $name =~ s/s$//;
236             $_ => sub {
237             my $tmp = $_[0]->clone;
238             $tmp->truncate( to => $name )
239             }
240             } qw( years months days hours minutes seconds )
241             ),
242              
243             weeks => sub {
244             my $base = $_[0]->clone->truncate( to => 'day' );
245             _add( $base, days => - $_[0]->day_of_week
246             + $weekdays_any{ $_[1] } );
247             while(1) {
248             return $base if $base <= $_[0];
249             _add( $base, weeks => -1 );
250             }
251             },
252              
253             months_weekly => sub {
254             my $tmp;
255             my $base = $_[0]->clone;
256             _add( $base, days => 7 );
257             $base->truncate( to => 'month' );
258             my $val;
259             my $diff;
260             while(1) {
261             $tmp = $base->clone;
262             $val = $weekdays_1{ $_[1] };
263             if ( $val )
264             {
265             $diff = $val - $base->day_of_week;
266             $diff += 7 if $diff < 0;
267             }
268             else
269             {
270             $diff = ( $weekdays{ $_[1] } -
271             $base->day_of_week ) % 7;
272             $diff -= 7 if $diff > 3;
273             }
274             _add( $tmp, days => $diff );
275             return $tmp if $tmp <= $_[0];
276             _add( $base, months => -1 );
277             }
278             },
279              
280             years_weekly => sub {
281             my $tmp;
282             my $base = $_[0]->clone;
283             _add( $base, months => 1 );
284             $base->truncate( to => 'year' );
285             my $val;
286             my $diff;
287             # warn "wsd $_[1]\n";
288             while(1) {
289             $tmp = $base->clone;
290             $val = $weekdays_1{ $_[1] };
291             if ( $val )
292             {
293             $diff = $val - $base->day_of_week;
294             $diff += 7 if $diff < 0;
295             }
296             else
297             {
298             $diff = ( $weekdays{ $_[1] } -
299             $base->day_of_week ) % 7;
300             $diff -= 7 if $diff > 3;
301             }
302             _add( $tmp, days => $diff );
303             return $tmp if $tmp <= $_[0];
304             _add( $base, years => -1 );
305             }
306             },
307             );
308              
309             %next_unit = (
310             # @_ = ( $datetime, $week_start_day )
311              
312             (
313             map {
314             my $names = $_;
315             $_ => sub {
316             _add( $_[0], $names => 1 )
317             }
318             } qw( years months weeks days hours minutes seconds )
319             ),
320              
321             months_weekly => sub {
322             my $base = $_[0]->clone;
323             my $return;
324             while(1) {
325             _add( $base, days => 21 );
326             $return = $truncate{months_weekly}->( $base, $_[1] );
327             return $_[0] = $return if $return > $_[0];
328             }
329             },
330              
331             years_weekly => sub {
332             my $base = $_[0]->clone;
333             my $return;
334             while(1) {
335             _add( $base, months => 11 );
336             $return = $truncate{years_weekly}->( $base, $_[1] );
337             return $_[0] = $return if $return > $_[0];
338             }
339             },
340             );
341              
342             %previous_unit = (
343             # @_ = ( $datetime, $week_start_day )
344              
345             months_weekly => sub {
346             my $base = $_[0]->clone;
347             my $return;
348             while(1) {
349             $return = $truncate{months_weekly}->( $base, $_[1] );
350             return $_[0] = $return if $return < $_[0];
351             _add( $base, days => -21 );
352             }
353             },
354              
355             years_weekly => sub {
356             my $base = $_[0]->clone;
357             my $return;
358             while(1) {
359             $return = $truncate{years_weekly}->( $base, $_[1] );
360             return $_[0] = $return if $return < $_[0];
361             _add( $base, months => -11 );
362             }
363             },
364             );
365              
366             # -------- "INTERVAL" OPERATIONS
367              
368             %truncate_interval = (
369             # @_ = ( $datetime, $args )
370              
371             (
372             map {
373             my $names = $_;
374             my $name = $_;
375             $name =~ s/s$//;
376             $_ => sub {
377             my $tmp = $_[0]->clone;
378             $tmp->truncate( to => $name );
379             _add( $tmp, $names =>
380             $_[1]{offset} -
381             ( $as_number{$names}->($_[0]) %
382             $_[1]{interval}
383             )
384             );
385             }
386             } qw( years months days hours minutes seconds )
387             ),
388              
389             weeks => sub {
390             my $tmp = $truncate{weeks}->( $_[0], $_[1]{week_start_day} );
391             while ( $_[1]{offset} !=
392             ( $as_number{weeks}->(
393             $tmp, $_[1]{week_start_day} ) %
394             $_[1]{interval}
395             )
396             )
397             {
398             _add( $tmp, weeks => -1 );
399             }
400             return $tmp;
401             },
402              
403             months_weekly => sub {
404             my $tmp = $truncate{months_weekly}->( $_[0], $_[1]{week_start_day} );
405             while ( $_[1]{offset} !=
406             ( $as_number{months_weekly}->(
407             $tmp, $_[1]{week_start_day} ) %
408             $_[1]{interval}
409             )
410             )
411             {
412             $previous_unit{months_weekly}->( $tmp, $_[1]{week_start_day} );
413             }
414             return $tmp;
415             },
416              
417             years_weekly => sub {
418             my $tmp = $truncate{years_weekly}->( $_[0], $_[1]{week_start_day} );
419             while ( $_[1]{offset} !=
420             ( $as_number{years_weekly}->( $tmp, $_[1]{week_start_day} ) %
421             $_[1]{interval}
422             )
423             )
424             {
425             $previous_unit{years_weekly}->( $tmp, $_[1]{week_start_day} );
426             }
427             return $tmp;
428             },
429             );
430              
431             %next_unit_interval = (
432             (
433             map {
434             my $names = $_;
435             $_ => sub {
436             _add( $_[0], $names => $_[1]{interval} )
437             }
438             } qw( years months weeks days hours minutes seconds )
439             ),
440              
441             months_weekly => sub {
442             for ( 1 .. $_[1]{interval} )
443             {
444             $next_unit{months_weekly}->( $_[0], $_[1]{week_start_day} );
445             }
446             },
447              
448             years_weekly => sub {
449             for ( 1 .. $_[1]{interval} )
450             {
451             $next_unit{years_weekly}->( $_[0], $_[1]{week_start_day} );
452             }
453             },
454             );
455              
456             %previous_unit_interval = (
457             (
458             map {
459             my $names = $_;
460             $_ => sub {
461             _add( $_[0], $names => - $_[1]{interval} )
462             }
463             } qw( years months weeks days hours minutes seconds )
464             ),
465              
466             months_weekly => sub {
467             for ( 1 .. $_[1]{interval} )
468             {
469             $previous_unit{months_weekly}->( $_[0], $_[1]{week_start_day} );
470             }
471             },
472              
473             years_weekly => sub {
474             for ( 1 .. $_[1]{interval} )
475             {
476             $previous_unit{years_weekly}->( $_[0], $_[1]{week_start_day} );
477             }
478             },
479             );
480              
481             # -------- CONSTRUCTORS
482              
483             BEGIN {
484             # setup all constructors: daily, ...
485              
486 15     15   101 for ( @units[ 0 .. $#units-1 ] )
487             {
488 105         167 my $name = $_;
489 105         138 my $namely = $_;
490 105         178 $namely =~ s/ys$/ily/;
491 105         307 $namely =~ s/s$/ly/;
492            
493 15     15   32377 no strict 'refs';
  15         33  
  15         882  
494 105         36793 *{__PACKAGE__ . "::$namely"} =
495             sub {
496 15     15   74 use strict 'refs';
  15         26  
  15         1138  
497 63     63   67469 my $class = shift;
498 63         242 return _create_recurrence( base => $name, @_ );
499 105         324 };
500             }
501             } # BEGIN
502              
503              
504             sub _create_recurrence {
505 63     63   290 my %args = @_;
506              
507             # print "ARGS: ";
508             # for(@_){ print (( ref($_) eq "ARRAY" ) ? "[ @$_ ] " : "$_ ") }
509             # print " \n";
510            
511             # --- FREQUENCY
512            
513 63         180 my $base = delete $args{base};
514 63         132 my $namely = $base;
515 63         194 $namely =~ s/ys$/ily/;
516 63         263 $namely =~ s/s$/ly/;
517 63         183 my $ical_string = uc( "RRULE:FREQ=$namely" );
518 63         117 my $base_unit = $base;
519             $base_unit = 'years_weekly'
520             if $base_unit eq 'years' &&
521 63 100 66     315 exists $args{weeks} ;
522             $base_unit = 'months_weekly'
523             if $base_unit eq 'months' &&
524 63 100 66     244 exists $args{weeks} ;
525              
526             # --- WEEK-START-DAY
527            
528 63         130 my $week_start_day = delete $args{week_start_day};
529 63 100       214 $ical_string .= ";WKST=". uc($week_start_day)
530             if $week_start_day;
531 63 100       257 $week_start_day = ( $base eq 'years' ) ? 'mo' : '1mo'
    100          
532             unless defined $week_start_day;
533             die "$base: invalid week start day ($week_start_day)"
534 63 50       235 unless $weekdays_any{ $week_start_day };
535            
536             # --- INTERVAL, START, and OFFSET
537            
538 63   50     398 my $interval = delete $args{interval} || 1;
539 63 50       187 die "invalid 'interval' specification ($interval)"
540             if $interval < 1;
541 63 50       174 $ical_string .= ";INTERVAL=$interval"
542             if $interval > 1;
543              
544 63         117 my $start = delete $args{start};
545 63 50 33     208 undef $start
546             if defined $start && $start->is_infinite;
547            
548 63         96 my $offset = 0;
549 63 50 33     206 $offset = $as_number{$base_unit}->( $start, $week_start_day ) % $interval
550             if $start && $interval > 1;
551              
552             # --- DURATION LIST
553            
554             # check for invalid "units" arguments, such as "daily( years=> )"
555 63         96 my @valid_units;
556 63         231 for ( 0 .. $#units )
557             {
558 136 100       416 if ( $base eq $units[$_] )
559             {
560 63         328 @valid_units = @units[ $_+1 .. $#units ];
561 63         155 last;
562             }
563             }
564             die "can't have both 'months' and 'weeks' arguments"
565             if exists $args{weeks} &&
566 63 50 66     244 exists $args{months};
567            
568 63         96 my $level = 1;
569 63         150 my @duration = ( [] );
570 63         155 my @level_unit = ( $base_unit );
571 63         143 for my $unit ( @valid_units )
572             {
573 364 100       919 next unless exists $args{$unit};
574              
575 100 100       278 if ( ref( $args{$unit} ) eq 'ARRAY' )
576             {
577 61         79 $args{$unit} = [ @{ $args{$unit} } ]
  61         190  
578             }
579             else
580             {
581 39         105 $args{$unit} = [ $args{$unit} ]
582             }
583            
584             # TODO: sort _after_ normalization
585              
586 100 100       281 if ( $unit eq 'days' )
587             {
588             # map rfc2445 weekdays to numbers
589 27         124 @{$args{$unit}} =
590             map {
591 149 100       462 $_ =~ /[a-z]/ ? $weekdays{$_} : $_
592 27         48 } @{$args{$unit}};
  27         68  
593             }
594              
595             # sort positive values first
596 100         306 @{$args{$unit}} =
597             sort {
598 381 50       671 ( $a < 0 ) <=> ( $b < 0 ) || $a <=> $b
599 100         160 } @{$args{$unit}};
  100         349  
600              
601              
602             # make the "ical" string
603 100 100 66     487 if ( $unit eq 'nanoseconds' )
    100          
604             {
605             # there are no nanoseconds in ICal
606             }
607             elsif ( $base eq 'weeks' &&
608             $unit eq 'days' )
609             {
610             # weekdays have names
611             $ical_string .= uc( ';' . 'BYDAY' . '=' .
612             join(",",
613             map {
614 6 50       44 exists( $ical_days{$_} ) ? $ical_days{$_} : $_
615 6         14 } @{$args{$unit}} )
  6         15  
616             )
617             }
618             else
619             {
620             $ical_string .= uc( ';' . $ical_name{$unit} . '=' .
621 93         261 join(",", @{$args{$unit}} ) )
  93         419  
622             }
623            
624 100 100 100     722 if ( $unit eq 'months' ||
      100        
625             $unit eq 'weeks' ||
626             $unit eq 'days' )
627             {
628             # these units start in '1'
629 58         91 for ( @{$args{$unit}} )
  58         168  
630             {
631 211 50       382 die $unit . ' cannot be zero'
632             unless $_;
633 211 100       510 $_-- if $_ > 0;
634             }
635             }
636            
637 100         382 @{$args{$unit}} =
638             grep {
639             $_ < $limits{ $unit } &&
640 431 100       1998 $_ >= - $limits{ $unit }
641 100         161 } @{$args{$unit}};
  100         211  
642            
643 100 100 100     443 if ( $unit eq 'days' &&
      66        
644             ( $base_unit eq 'months' ||
645             $level_unit[-1] eq 'months' ) )
646             { # month day
647 16         62 @{$args{$unit}} =
648             grep {
649 138 100       524 $_ < 31 && $_ >= -31
650 16         28 } @{$args{$unit}};
  16         41  
651             }
652              
653 100 100 100     377 if ( $unit eq 'days' &&
      66        
654             ( $base_unit eq 'weeks' ||
655             $level_unit[-1] eq 'weeks' ) )
656             { # week day
657            
658 10         25 @{$args{$unit}} =
659             grep {
660 10 50       66 $_ < 7 && $_ >= -7
661 10         18 } @{$args{$unit}};
  10         23  
662              
663 10         19 for ( @{$args{$unit}} )
  10         25  
664             {
665 10         24 $_ = $_ - $weekdays_any{ $week_start_day } + 1;
666 10         40 $_ += 7 while $_ < 0;
667             }
668              
669 10         18 @{$args{$unit}} = sort @{$args{$unit}};
  10         28  
  10         29  
670             }
671              
672             return DateTime::Set::ICal->empty_set
673 100 100       135 unless @{$args{$unit}}; # there are no args left
  100         316  
674              
675 99         183 push @duration, $args{$unit};
676 99         164 push @level_unit, $unit;
677              
678 99         159 delete $args{$unit};
679              
680 99         173 $level++;
681             }
682              
683             # TODO: use $span for selecting elements (using intersection)
684             # note - this may change the documented behaviour - check the pod first
685             # $span = delete $args{span};
686             # $span = DateTime::Span->new( %args ) if %args;
687              
688 62 100       226 die "invalid argument '@{[ keys %args ]}'"
  1         15  
689             if keys %args;
690            
691             # --- SPLIT NEGATIVE/POSITIVE DURATIONS
692              
693 61         100 my @args;
694 61         112 push @args, \@duration;
695            
696 61         263 for ( my $i = 0; $i < @args; $i++ )
697             {
698 74         127 my $dur1 = $args[$i];
699 74         113 for ( 1 .. $#{$dur1} )
  74         215  
700             {
701 121         168 my @negatives = grep { $_ < 0 } @{$dur1->[$_]};
  460         833  
  121         226  
702 121         168 my @positives = grep { $_ >= 0 } @{$dur1->[$_]};
  460         933  
  121         224  
703 121 100 100     787 if ( @positives && @negatives )
704             {
705             # split
706             # TODO: check if it really needs splitting
707 13         20 my $dur2 = [ @{$args[$i]} ];
  13         30  
708 13         26 $dur2->[$_] = \@negatives;
709 13         22 $dur1->[$_] = \@positives;
710 13         58 push @args, $dur2;
711             }
712             }
713             }
714              
715             # --- CREATE THE SET
716            
717 61         97 my $set;
718 61         168 for ( @args )
719             {
720 74         184 my @duration = @$_;
721 74         108 my $total_durations = 1;
722 74         105 my @total_level;
723 74         247 for ( my $i = $#duration; $i > 0; $i-- )
724             {
725 121 100       261 if ( $i == $#duration )
726             {
727 68         133 $total_level[$i] = 1;
728             }
729             else
730             {
731             $total_level[$i] = $total_level[$i + 1] *
732 53         85 ( 1 + $#{ $duration[$i + 1] } );
  53         101  
733             }
734 121         174 $total_durations *= 1 + $#{ $duration[$i] };
  121         402  
735             }
736              
737             my $args = {
738             truncate_interval => $truncate_interval{ $base_unit },
739             previous_unit_interval => $previous_unit_interval{ $base_unit },
740 74         789 next_unit_interval => $next_unit_interval{ $base_unit },
741            
742             duration => \@duration,
743             total_durations => $total_durations,
744             level_unit => \@level_unit,
745             total_level => \@total_level,
746            
747             interval => $interval,
748             offset => $offset,
749             week_start_day => $week_start_day,
750             };
751            
752             my $tmp = DateTime::Set::ICal->from_recurrence(
753             next => sub {
754 1084     1084   566461 _get_next( $_[0], $args );
755             },
756             previous => sub {
757 525     525   113810 _get_previous( $_[0], $args );
758             },
759 74         842 );
760            
761 74 100       11428 $set = defined $set ? $set->union( $tmp ) : $tmp;
762             }
763 61         264 $set->set_ical( include => [ $ical_string ] );
764             # warn "Creating set: ". $ical_string ." \n";
765            
766 61         306 return $set;
767            
768             } # _create_recurrence
769              
770              
771             sub _get_occurrence_by_index {
772 3063     3063   5129 my ( $base, $occurrence, $args ) = @_;
773             # TODO: memoize "occurrences" within an "INTERVAL" ???
774 3063         5859 RETRY_OVERFLOW: for ( 0 .. 5 )
775             {
776             return undef
777 3171 100       6779 if $occurrence < 0;
778 3137         8739 my $next = $base->clone;
779 3137         32914 my $previous = $base;
780 3137         5491 my @values = ( -1 );
781 3137         3962 for my $j ( 1 .. $#{$args->{duration}} )
  3137         8715  
782             {
783             # decode the occurrence-number into a parameter-index
784 7559         66220 my $i = int( $occurrence / $args->{total_level}[$j] );
785 7559         12306 $occurrence -= $i * $args->{total_level}[$j];
786 7559         10414 push @values, $i;
787            
788 7559 100       18913 if ( $args->{duration}[$j][$i] < 0 )
789             {
790             # warn "negative unit\n";
791             $next_unit{ $args->{level_unit}[$j - 1] }->(
792 437         1447 $next, $args->{week_start_day} );
793             }
794 7559         180984 _add( $next, $args->{level_unit}[$j], $args->{duration}[$j][$i] );
795            
796             # overflow check
797 7559 100       3141390 if ( $as_number{ $args->{level_unit}[$j - 1] }->(
798             $next, $args->{week_start_day} ) !=
799             $as_number{ $args->{level_unit}[$j - 1] }->(
800             $previous, $args->{week_start_day} )
801             )
802             {
803             # calculate the "previous" occurrence-number
804 108         886 $occurrence = -1;
805 108         254 for ( 1 .. $j )
806             {
807 203         431 $occurrence += $values[$_] * $args->{total_level}[$_];
808             }
809 108         864 next RETRY_OVERFLOW;
810             }
811 7451         67888 $previous = $next->clone;
812             }
813 3029         46219 return $next;
814             }
815 0         0 return undef;
816             }
817              
818              
819             sub _get_previous {
820 525     525   954 my ( $self, $args ) = @_;
821              
822 525 100       1580 return $self if $self->is_infinite;
823 318         1737 $self->set_time_zone( 'floating' );
824              
825 318         16598 my $base = $args->{truncate_interval}->( $self, $args );
826 318         7792 my ( $next, $i, $start, $end );
827 318         494 my $init = 0;
828 318         436 my $retry = 30;
829              
830 318         456 INTERVAL: while(1) {
831 498 100       1455 $args->{previous_unit_interval}->( $base, $args ) if $init;
832 498         68518 $init = 1;
833              
834             # binary search
835 498         651 $start = 0;
836 498         935 $end = $args->{total_durations} - 1;
837 498         1230 while ( $retry-- ) {
838 844 100       2097 if ( $end - $start < 3 )
839             {
840 498         1278 for ( $i = $end; $i >= $start; $i-- )
841             {
842 635         10398 $next = _get_occurrence_by_index ( $base, $i, $args );
843 635 50       2576 next INTERVAL unless defined $next;
844 635 100       2035 return $next if $next < $self;
845             }
846 180         12455 next INTERVAL;
847             }
848              
849 346         707 $i = int( $start + ( $end - $start ) / 2 );
850 346         696 $next = _get_occurrence_by_index ( $base, $i, $args );
851 346 50       1670 next INTERVAL unless defined $next;
852              
853 346 100       1116 if ( $next < $self )
854             {
855 189         12429 $start = $i;
856             }
857             else
858             {
859 157         10548 $end = $i - 1;
860             }
861             }
862 0         0 return undef;
863             }
864             }
865              
866              
867             sub _get_next {
868 1084     1084   1895 my ( $self, $args ) = @_;
869              
870 1084 100       3163 return $self if $self->is_infinite;
871 858         4774 $self->set_time_zone( 'floating' );
872              
873 858         59440 my $base = $args->{truncate_interval}->( $self, $args );
874 858         15664 my ( $next, $i, $start, $end );
875 858         1205 my $init = 0;
876 858         1098 my $retry = 30;
877            
878 858         1084 INTERVAL: while(1) {
879 1468 100       4039 $args->{next_unit_interval}->( $base, $args ) if $init;
880 1468         183405 $init = 1;
881              
882             # binary search
883 1468         1962 $start = 0;
884 1468         2458 $end = $args->{total_durations} - 1;
885 1468         3616 while ( $retry-- ) {
886 1893 100       4502 if ( $end - $start < 3 )
887             {
888 1467         2945 for $i ( $start .. $end )
889             {
890 1656         16257 $next = _get_occurrence_by_index ( $base, $i, $args );
891 1656 100       6529 next INTERVAL unless defined $next;
892 1622 100       5074 return $next if $next > $self;
893             }
894 576         39897 next INTERVAL;
895             }
896              
897 426         833 $i = int( $start + ( $end - $start ) / 2 );
898 426         896 $next = _get_occurrence_by_index ( $base, $i, $args );
899 426 50       1916 next INTERVAL unless defined $next;
900              
901 426 100       1291 if ( $next > $self )
902             {
903 212         14015 $end = $i;
904             }
905             else
906             {
907 214         14423 $start = $i + 1;
908             }
909             }
910 1         12 return undef;
911             }
912             }
913              
914             1;
915              
916             __END__