File Coverage

blib/lib/DateTime/Event/Recurrence.pm
Criterion Covered Total %
statement 293 300 97.6
branch 105 128 82.0
condition 33 44 75.0
subroutine 29 29 100.0
pod n/a
total 460 501 91.8


line stmt bran cond sub pod time code
1 14     14   3798964 use strict;
  14         34  
  14         872  
2              
3             package DateTime::Set::ICal;
4              
5 14     14   87 use vars qw(@ISA);
  14         28  
  14         8484  
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 106     106   180 my $self = shift;
13             # carp "set_ical $_[0] => @{$_[1]}" if @_;
14 106         410 $self->{as_ical} = [ @_ ];
15 106         259 $self;
16             }
17              
18             sub get_ical {
19 59     59   151 my $self = shift;
20 59 100       210 return unless $self->{as_ical};
21 38         64 return @{ $self->{as_ical} };
  38         230  
22             }
23              
24             sub clone {
25 31     31   13721 my $self = shift;
26 31         187 my $new = $self->SUPER::clone( @_ );
27 31         2089 $new->set_ical( $self->get_ical );
28 31         106 $new;
29             }
30              
31             sub union {
32 14     14   1328 my $self = shift;
33 14         83 my $new = $self->SUPER::union( @_ );
34              
35             # RFC2445 - op1, op2 must have no 'exclude'
36 14         2504 my (%op1, %op2);
37 14 50       90 %op1 = ( $self->get_ical ) if ( UNIVERSAL::can( $self, 'get_ical' ) );
38 14 100       83 %op2 = ( $_[0]->get_ical ) if ( UNIVERSAL::can( $_[0], 'get_ical' ) );
39 14 50 33     84 return $new if ( ( exists $op1{exclude} ) ||
40             ( exists $op2{exclude} ) );
41              
42 14         36 bless $new, 'DateTime::Set::ICal';
43             # warn " -- 1 isa @{[%op1]} -- 2 isa @{[%op2]} -- ";
44 14         21 my @ical;
45 7         19 @ical = exists $op1{include} ?
46 14 100       71 @{$op1{include}} :
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         50 push @ical, @_; # whatever...
57             }
58             # warn "union: @ical";
59 14         56 $new->set_ical( include => [ @ical ] );
60 14         84 $new;
61             }
62              
63             sub complement {
64 1     1   4988 my $self = shift;
65 1         14 my $new = $self->SUPER::complement( @_ );
66 1 50       948 return $new unless @_;
67              
68             # RFC2445 - op2 must have no 'exclude'
69 1         3 my (%op1, %op2);
70 1 50       12 %op1 = ( $self->get_ical ) if ( UNIVERSAL::can( $self, 'get_ical' ) );
71 1 50       8 %op2 = ( $_[0]->get_ical ) if ( UNIVERSAL::can( $_[0], 'get_ical' ) );
72 1 50       5 return $new if ( exists $op2{exclude} );
73              
74 1         4 bless $new, 'DateTime::Set::ICal';
75             # warn " -- 1 isa @{[%op1]} -- 2 isa @{[%op2]} -- ";
76 1         2 my ( @include, @exclude );
77 1         4 @include = exists $op1{include} ?
78 1 50       7 @{$op1{include}} :
79             $self;
80              
81 0         0 @exclude = exists $op1{exclude} ?
82 1 50       6 @{$op1{exclude}} :
83             ();
84              
85 1 50       4 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         9 $new->set_ical( include => [ @include ], exclude => [ @exclude ] );
96 1         4 $new;
97             }
98              
99             package DateTime::Event::Recurrence;
100              
101 14     14   103 use strict;
  14         43  
  14         458  
102 14     14   3993 use DateTime;
  14         2981677  
  14         306  
103 14     14   13639 use DateTime::Set;
  14         703235  
  14         437  
104 14     14   168 use DateTime::Span;
  14         31  
  14         348  
105 14     14   87 use Params::Validate qw(:all);
  14         32  
  14         3809  
106 14     14   86 use vars qw( $VERSION );
  14         29  
  14         1013  
107             $VERSION = '0.16';
108              
109 14     14   81 use constant INFINITY => 100 ** 100 ** 100 ;
  14         32  
  14         1063  
110 14     14   259 use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
  14         27  
  14         892  
111              
112             # -------- BASE OPERATIONS
113              
114 14         3986 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 14     14   64 );
  14         25  
136              
137             BEGIN {
138 14     14   191 %weekdays = qw( mo 1 tu 2 we 3 th 4 fr 5 sa 6 su 7 );
139 14         127 %weekdays_1 = qw( 1mo 1 1tu 2 1we 3 1th 4 1fr 5 1sa 6 1su 7 );
140 14         200 %weekdays_any = ( %weekdays, %weekdays_1 );
141            
142 14         93 %ical_name = qw(
143             months BYMONTH
144             weeks BYWEEKNO
145             days BYMONTHDAY
146             hours BYHOUR
147             minutes BYMINUTE
148             seconds BYSECOND
149             );
150            
151 14         199 %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 14         42 @units = qw( years months weeks days hours minutes seconds nanoseconds );
157            
158 14         3104 %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 17721     17721   47035 my $dur = \$memoized_duration{$_[1]}{$_[2]};
177 17721 100       56396 $$dur = new DateTime::Duration( $_[1] => $_[2] )
178             unless defined $$dur;
179 17721         72937 $_[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 14     14   88 use integer;
  14         29  
  14         127  
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 14     14   88 for ( @units[ 0 .. $#units-1 ] )
487             {
488 98         140 my $name = $_;
489 98         123 my $namely = $_;
490 98         156 $namely =~ s/ys$/ily/;
491 98         313 $namely =~ s/s$/ly/;
492            
493 14     14   37927 no strict 'refs';
  14         33  
  14         912  
494 98         42275 *{__PACKAGE__ . "::$namely"} =
495             sub {
496 14     14   89 use strict 'refs';
  14         40  
  14         1018  
497 62     62   72881 my $class = shift;
498 62         280 return _create_recurrence( base => $name, @_ );
499 98         315 };
500             }
501             } # BEGIN
502              
503              
504             sub _create_recurrence {
505 62     62   347 my %args = @_;
506              
507             # print "ARGS: ";
508             # for(@_){ print (( ref($_) eq "ARRAY" ) ? "[ @$_ ] " : "$_ ") }
509             # print " \n";
510            
511             # --- FREQUENCY
512            
513 62         203 my $base = delete $args{base};
514 62         143 my $namely = $base;
515 62         213 $namely =~ s/ys$/ily/;
516 62         292 $namely =~ s/s$/ly/;
517 62         216 my $ical_string = uc( "RRULE:FREQ=$namely" );
518 62         130 my $base_unit = $base;
519 62 100 100     435 $base_unit = 'years_weekly'
520             if $base_unit eq 'years' &&
521             exists $args{weeks} ;
522 62 100 100     351 $base_unit = 'months_weekly'
523             if $base_unit eq 'months' &&
524             exists $args{weeks} ;
525              
526             # --- WEEK-START-DAY
527            
528 62         141 my $week_start_day = delete $args{week_start_day};
529 62 100       198 $ical_string .= ";WKST=". uc($week_start_day)
530             if $week_start_day;
531 62 100       290 $week_start_day = ( $base eq 'years' ) ? 'mo' : '1mo'
    100          
532             unless defined $week_start_day;
533 62 50       465 die "$base: invalid week start day ($week_start_day)"
534             unless $weekdays_any{ $week_start_day };
535            
536             # --- INTERVAL, START, and OFFSET
537            
538 62   50     355 my $interval = delete $args{interval} || 1;
539 62 50       196 die "invalid 'interval' specification ($interval)"
540             if $interval < 1;
541 62 50       265 $ical_string .= ";INTERVAL=$interval"
542             if $interval > 1;
543              
544 62         126 my $start = delete $args{start};
545 62 50 33     216 undef $start
546             if defined $start && $start->is_infinite;
547            
548 62         119 my $offset = 0;
549 62 50 33     248 $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 62         114 my @valid_units;
556 62         246 for ( 0 .. $#units )
557             {
558 131 100       412 if ( $base eq $units[$_] )
559             {
560 62         369 @valid_units = @units[ $_+1 .. $#units ];
561 62         156 last;
562             }
563             }
564 62 50 66     319 die "can't have both 'months' and 'weeks' arguments"
565             if exists $args{weeks} &&
566             exists $args{months};
567            
568 62         115 my $level = 1;
569 62         155 my @duration = ( [] );
570 62         166 my @level_unit = ( $base_unit );
571 62         147 for my $unit ( @valid_units )
572             {
573 361 100       939 next unless exists $args{$unit};
574              
575 100 100       291 if ( ref( $args{$unit} ) eq 'ARRAY' )
576             {
577 61         78 $args{$unit} = [ @{ $args{$unit} } ]
  61         212  
578             }
579             else
580             {
581 39         114 $args{$unit} = [ $args{$unit} ]
582             }
583            
584             # TODO: sort _after_ normalization
585              
586 100 100       378 if ( $unit eq 'days' )
587             {
588             # map rfc2445 weekdays to numbers
589 27 100       149 @{$args{$unit}} =
  149         443  
590             map {
591 27         63 $_ =~ /[a-z]/ ? $weekdays{$_} : $_
592 27         54 } @{$args{$unit}};
593             }
594              
595             # sort positive values first
596 100 50       308 @{$args{$unit}} =
  381         693  
597             sort {
598 100         373 ( $a < 0 ) <=> ( $b < 0 ) || $a <=> $b
599 100         175 } @{$args{$unit}};
600              
601              
602             # make the "ical" string
603 100 100 66     497 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 6 50       48 $ical_string .= uc( ';' . 'BYDAY' . '=' .
612             join(",",
613             map {
614 6         18 exists( $ical_days{$_} ) ? $ical_days{$_} : $_
615 6         18 } @{$args{$unit}} )
616             )
617             }
618             else
619             {
620 93         407 $ical_string .= uc( ';' . $ical_name{$unit} . '=' .
621 93         271 join(",", @{$args{$unit}} ) )
622             }
623            
624 100 100 100     798 if ( $unit eq 'months' ||
      100        
625             $unit eq 'weeks' ||
626             $unit eq 'days' )
627             {
628             # these units start in '1'
629 58         92 for ( @{$args{$unit}} )
  58         153  
630             {
631 211 50       368 die $unit . ' cannot be zero'
632             unless $_;
633 211 100       506 $_-- if $_ > 0;
634             }
635             }
636            
637 100 100       405 @{$args{$unit}} =
  431         1949  
638             grep {
639 100         220 $_ < $limits{ $unit } &&
640             $_ >= - $limits{ $unit }
641 100         183 } @{$args{$unit}};
642            
643 100 100 100     472 if ( $unit eq 'days' &&
      66        
644             ( $base_unit eq 'months' ||
645             $level_unit[-1] eq 'months' ) )
646             { # month day
647 16 100       62 @{$args{$unit}} =
  138         504  
648             grep {
649 16         34 $_ < 31 && $_ >= -31
650 16         31 } @{$args{$unit}};
651             }
652              
653 100 100 100     463 if ( $unit eq 'days' &&
      66        
654             ( $base_unit eq 'weeks' ||
655             $level_unit[-1] eq 'weeks' ) )
656             { # week day
657            
658 10 50       29 @{$args{$unit}} =
  10         66  
659             grep {
660 10         21 $_ < 7 && $_ >= -7
661 10         17 } @{$args{$unit}};
662              
663 10         19 for ( @{$args{$unit}} )
  10         30  
664             {
665 10         29 $_ = $_ - $weekdays_any{ $week_start_day } + 1;
666 10         47 $_ += 7 while $_ < 0;
667             }
668              
669 10         20 @{$args{$unit}} = sort @{$args{$unit}};
  10         34  
  10         28  
670             }
671              
672 100         352 return DateTime::Set::ICal->empty_set
673 100 100       166 unless @{$args{$unit}}; # there are no args left
674              
675 99         189 push @duration, $args{$unit};
676 99         159 push @level_unit, $unit;
677              
678 99         202 delete $args{$unit};
679              
680 99         166 $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 61 100       250 die "invalid argument '@{[ keys %args ]}'"
  1         17  
689             if keys %args;
690            
691             # --- SPLIT NEGATIVE/POSITIVE DURATIONS
692              
693 60         98 my @args;
694 60         125 push @args, \@duration;
695            
696 60         236 for ( my $i = 0; $i < @args; $i++ )
697             {
698 73         168 my $dur1 = $args[$i];
699 73         131 for ( 1 .. $#{$dur1} )
  73         320  
700             {
701 121         187 my @negatives = grep { $_ < 0 } @{$dur1->[$_]};
  460         824  
  121         237  
702 121         187 my @positives = grep { $_ >= 0 } @{$dur1->[$_]};
  460         846  
  121         197  
703 121 100 100     827 if ( @positives && @negatives )
704             {
705             # split
706             # TODO: check if it really needs splitting
707 13         19 my $dur2 = [ @{$args[$i]} ];
  13         40  
708 13         27 $dur2->[$_] = \@negatives;
709 13         35 $dur1->[$_] = \@positives;
710 13         77 push @args, $dur2;
711             }
712             }
713             }
714              
715             # --- CREATE THE SET
716            
717 60         99 my $set;
718 60         140 for ( @args )
719             {
720 73         260 my @duration = @$_;
721 73         121 my $total_durations = 1;
722 73         97 my @total_level;
723 73         272 for ( my $i = $#duration; $i > 0; $i-- )
724             {
725 121 100       1252 if ( $i == $#duration )
726             {
727 68         1230 $total_level[$i] = 1;
728             }
729             else
730             {
731 53         109 $total_level[$i] = $total_level[$i + 1] *
732 53         86 ( 1 + $#{ $duration[$i + 1] } );
733             }
734 121         162 $total_durations *= 1 + $#{ $duration[$i] };
  121         394  
735             }
736              
737 73         1958 my $args = {
738             truncate_interval => $truncate_interval{ $base_unit },
739             previous_unit_interval => $previous_unit_interval{ $base_unit },
740             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 1056     1056   570819 _get_next( $_[0], $args );
755             },
756             previous => sub {
757 522     522   115885 _get_previous( $_[0], $args );
758             },
759 73         965 );
760            
761 73 100       14466 $set = defined $set ? $set->union( $tmp ) : $tmp;
762             }
763 60         1345 $set->set_ical( include => [ $ical_string ] );
764             # warn "Creating set: ". $ical_string ." \n";
765            
766 60         345 return $set;
767            
768             } # _create_recurrence
769              
770              
771             sub _get_occurrence_by_index {
772 3005     3005   4877 my ( $base, $occurrence, $args ) = @_;
773             # TODO: memoize "occurrences" within an "INTERVAL" ???
774 3005         5455 RETRY_OVERFLOW: for ( 0 .. 5 )
775             {
776             return undef
777 3113 100       6856 if $occurrence < 0;
778 3079         8008 my $next = $base->clone;
779 3079         34984 my $previous = $base;
780 3079         5413 my @values = ( -1 );
781 3079         3653 for my $j ( 1 .. $#{$args->{duration}} )
  3079         8463  
782             {
783             # decode the occurrence-number into a parameter-index
784 7559         69235 my $i = int( $occurrence / $args->{total_level}[$j] );
785 7559         12244 $occurrence -= $i * $args->{total_level}[$j];
786 7559         10660 push @values, $i;
787            
788 7559 100       18729 if ( $args->{duration}[$j][$i] < 0 )
789             {
790             # warn "negative unit\n";
791 437         1737 $next_unit{ $args->{level_unit}[$j - 1] }->(
792             $next, $args->{week_start_day} );
793             }
794 7559         177311 _add( $next, $args->{level_unit}[$j], $args->{duration}[$j][$i] );
795            
796             # overflow check
797 7559 100       2945364 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         909 $occurrence = -1;
805 108         253 for ( 1 .. $j )
806             {
807 203         846 $occurrence += $values[$_] * $args->{total_level}[$_];
808             }
809 108         921 next RETRY_OVERFLOW;
810             }
811 7451         68049 $previous = $next->clone;
812             }
813 2971         50717 return $next;
814             }
815 0         0 return undef;
816             }
817              
818              
819             sub _get_previous {
820 522     522   2047 my ( $self, $args ) = @_;
821              
822 522 100       1736 return $self if $self->is_infinite;
823              
824 316         1785 my $base = $args->{truncate_interval}->( $self, $args );
825 316         8040 my ( $next, $i, $start, $end );
826 316         469 my $init = 0;
827 316         477 my $retry = 30;
828              
829 316         1378 INTERVAL: while(1) {
830 494 100       1608 $args->{previous_unit_interval}->( $base, $args ) if $init;
831 494         62690 $init = 1;
832              
833             # binary search
834 494         645 $start = 0;
835 494         875 $end = $args->{total_durations} - 1;
836 494         1304 while ( $retry-- ) {
837 840 100       1977 if ( $end - $start < 3 )
838             {
839 494         1287 for ( $i = $end; $i >= $start; $i-- )
840             {
841 631         10146 $next = _get_occurrence_by_index ( $base, $i, $args );
842 631 50       7916 next INTERVAL unless defined $next;
843 631 100       9191 return $next if $next < $self;
844             }
845 178         12077 next INTERVAL;
846             }
847              
848 346         677 $i = int( $start + ( $end - $start ) / 2 );
849 346         688 $next = _get_occurrence_by_index ( $base, $i, $args );
850 346 50       1602 next INTERVAL unless defined $next;
851              
852 346 100       1205 if ( $next < $self )
853             {
854 189         11804 $start = $i;
855             }
856             else
857             {
858 157         10041 $end = $i - 1;
859             }
860             }
861 0         0 return undef;
862             }
863             }
864              
865              
866             sub _get_next {
867 1056     1056   2570 my ( $self, $args ) = @_;
868              
869 1056 100       5015 return $self if $self->is_infinite;
870              
871 831         6100 my $base = $args->{truncate_interval}->( $self, $args );
872 831         15660 my ( $next, $i, $start, $end );
873 831         1232 my $init = 0;
874 831         1324 my $retry = 30;
875            
876 831         1236 INTERVAL: while(1) {
877 1414 100       5287 $args->{next_unit_interval}->( $base, $args ) if $init;
878 1414         160001 $init = 1;
879              
880             # binary search
881 1414         2411 $start = 0;
882 1414         2951 $end = $args->{total_durations} - 1;
883 1414         3672 while ( $retry-- ) {
884 1839 100       4751 if ( $end - $start < 3 )
885             {
886 1413         2890 for $i ( $start .. $end )
887             {
888 1602         16336 $next = _get_occurrence_by_index ( $base, $i, $args );
889 1602 100       6794 next INTERVAL unless defined $next;
890 1568 100       5218 return $next if $next > $self;
891             }
892 549         37441 next INTERVAL;
893             }
894              
895 426         951 $i = int( $start + ( $end - $start ) / 2 );
896 426         927 $next = _get_occurrence_by_index ( $base, $i, $args );
897 426 50       1985 next INTERVAL unless defined $next;
898              
899 426 100       1417 if ( $next > $self )
900             {
901 212         13655 $end = $i;
902             }
903             else
904             {
905 214         14124 $start = $i + 1;
906             }
907             }
908 1         13 return undef;
909             }
910             }
911              
912             1;
913              
914             __END__