File Coverage

blib/lib/DateTime/Set.pm
Criterion Covered Total %
statement 305 337 90.5
branch 124 166 74.7
condition 15 30 50.0
subroutine 53 57 92.9
pod 29 29 100.0
total 526 619 84.9


line stmt bran cond sub pod time code
1             package DateTime::Set;
2              
3 23     23   1544615 use strict;
  23         36  
  23         545  
4 23     23   77 use Carp;
  23         24  
  23         1312  
5 23     23   1441 use Params::Validate qw( validate SCALAR BOOLEAN OBJECT CODEREF ARRAYREF );
  23         20125  
  23         1176  
6 23     23   2607 use DateTime 0.12; # this is for version checking only
  23         218707  
  23         429  
7 23     23   81 use DateTime::Duration;
  23         25  
  23         338  
8 23     23   8180 use DateTime::Span;
  23         33  
  23         607  
9 23     23   13565 use Set::Infinite 0.59;
  23         475333  
  23         1200  
10 23     23   9780 use Set::Infinite::_recurrence;
  23         42  
  23         952  
11              
12 23     23   103 use vars qw( $VERSION );
  23         24  
  23         925  
13              
14 23     23   79 use constant INFINITY => 100 ** 100 ** 100 ;
  23         34  
  23         1127  
15 23     23   84 use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
  23         26  
  23         916  
16              
17             BEGIN {
18 23     23   58677 $VERSION = '0.3700';
19             }
20              
21              
22             sub _fix_datetime {
23             # internal function -
24             # (not a class method)
25             #
26             # checks that the parameter is an object, and
27             # also protects the object against mutation
28            
29 7952 100   7952   15874 return $_[0]
30             unless defined $_[0]; # error
31 7940 100       21074 return $_[0]->clone
32             if ref( $_[0] ); # "immutable" datetime
33 89 100       359 return DateTime::Infinite::Future->new
34             if $_[0] == INFINITY; # Inf
35 10 50       56 return DateTime::Infinite::Past->new
36             if $_[0] == NEG_INFINITY; # -Inf
37 0         0 return $_[0]; # error
38             }
39              
40             sub _fix_return_datetime {
41 74     74   12085 my ( $dt, $dt_arg ) = @_;
42              
43             # internal function -
44             # (not a class method)
45             #
46             # checks that the returned datetime has the same
47             # time zone as the parameter
48              
49             # TODO: set locale
50              
51 74 100       228 return unless $dt;
52 61 50       1701 return unless $dt_arg;
53 61 100 66     1310 if ( $dt_arg->can('time_zone_long_name') &&
54             !( $dt_arg->time_zone_long_name eq 'floating' ) )
55             {
56 2         17 $dt->set_time_zone( $dt_arg->time_zone );
57             }
58 61         894 return $dt;
59             }
60              
61             sub iterate {
62             # deprecated method - use map() or grep() instead
63 0     0 1 0 my ( $self, $callback ) = @_;
64 0         0 my $class = ref( $self );
65 0         0 my $return = $class->empty_set;
66             $return->{set} = $self->{set}->iterate(
67             sub {
68 0     0   0 my $min = $_[0]->min;
69 0 0       0 $callback->( $min->clone ) if ref($min);
70             }
71 0         0 );
72 0         0 $return;
73             }
74              
75             sub map {
76 1     1 1 449 my ( $self, $callback ) = @_;
77 1         2 my $class = ref( $self );
78 1 50       5 die "The callback parameter to map() must be a subroutine reference"
79             unless ref( $callback ) eq 'CODE';
80 1         8 my $return = $class->empty_set;
81             $return->{set} = $self->{set}->iterate(
82             sub {
83 2     2   50 local $_ = $_[0]->min;
84 2 50       28 next unless ref( $_ );
85 2         4 $_ = $_->clone;
86 2         17 my @list = $callback->();
87 2         1063 my $set = Set::Infinite::_recurrence->new();
88 2         25 $set = $set->union( $_ ) for @list;
89 2         143 return $set;
90             }
91 1         17 );
92 1         88 $return;
93             }
94              
95             sub grep {
96 1     1 1 516 my ( $self, $callback ) = @_;
97 1         2 my $class = ref( $self );
98 1 50       4 die "The callback parameter to grep() must be a subroutine reference"
99             unless ref( $callback ) eq 'CODE';
100 1         3 my $return = $class->empty_set;
101             $return->{set} = $self->{set}->iterate(
102             sub {
103 2     2   46 local $_ = $_[0]->min;
104 2 50       27 next unless ref( $_ );
105 2         5 $_ = $_->clone;
106 2         15 my $result = $callback->();
107 2 100       389 return $_ if $result;
108 1         4 return;
109             }
110 1         17 );
111 1         75 $return;
112             }
113              
114 8     8 1 452 sub add { return shift->add_duration( DateTime::Duration->new(@_) ) }
115              
116 1     1 1 6 sub subtract { return shift->subtract_duration( DateTime::Duration->new(@_) ) }
117              
118 3     3 1 138 sub subtract_duration { return $_[0]->add_duration( $_[1]->inverse ) }
119              
120             sub add_duration {
121 18     18 1 711 my ( $self, $dur ) = @_;
122 18         49 $dur = $dur->clone; # $dur must be "immutable"
123              
124             $self->{set} = $self->{set}->iterate(
125             sub {
126 74     74   5404 my $min = $_[0]->min;
127 74 50       1128 $min->clone->add_duration( $dur ) if ref($min);
128             },
129             backtrack_callback => sub {
130 18     18   883 my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
131 18 100       414 if ( ref($min) )
132             {
133 14         40 $min = $min->clone;
134 14         177 $min->subtract_duration( $dur );
135             }
136 18 100       5827 if ( ref($max) )
137             {
138 8         23 $max = $max->clone;
139 8         86 $max->subtract_duration( $dur );
140             }
141 18         2289 return Set::Infinite::_recurrence->new( $min, $max );
142             },
143 18         234 );
144 18         1146 $self;
145             }
146              
147             sub set_time_zone {
148 4     4 1 319 my ( $self, $tz ) = @_;
149              
150             $self->{set} = $self->{set}->iterate(
151             sub {
152 43     43   2865 my $min = $_[0]->min;
153 43 50       724 $min->clone->set_time_zone( $tz ) if ref($min);
154             },
155             backtrack_callback => sub {
156 8     8   459 my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
157 8 100       202 if ( ref($min) )
158             {
159 6         17 $min = $min->clone;
160 6         58 $min->set_time_zone( $tz );
161             }
162 8 100       1198 if ( ref($max) )
163             {
164 2         6 $max = $max->clone;
165 2         16 $max->set_time_zone( $tz );
166             }
167 8         235 return Set::Infinite::_recurrence->new( $min, $max );
168             },
169 4         47 );
170 4         413 $self;
171             }
172              
173             sub set {
174 1     1 1 2 my $self = shift;
175 1         12 my %args = validate( @_,
176             { locale => { type => SCALAR | OBJECT,
177             default => undef },
178             }
179             );
180             $self->{set} = $self->{set}->iterate(
181             sub {
182 2     2   306 my $min = $_[0]->min;
183 2 50       34 $min->clone->set( %args ) if ref($min);
184             },
185 1         7 );
186 1         21 $self;
187             }
188              
189             sub from_recurrence {
190 37     37 1 13126 my $class = shift;
191              
192 37         102 my %args = @_;
193 37         47 my %param;
194            
195             # Parameter renaming, such that we can use either
196             # recurrence => xxx or next => xxx, previous => xxx
197 37   66     163 $param{next} = delete $args{recurrence} || delete $args{next};
198 37         67 $param{previous} = delete $args{previous};
199              
200 37         60 $param{span} = delete $args{span};
201             # they might be specifying a span using start / end
202 37 100       161 $param{span} = DateTime::Span->new( %args ) if keys %args;
203              
204 37         61 my $self = {};
205            
206             die "Not enough arguments in from_recurrence()"
207 37 50 66     95 unless $param{next} || $param{previous};
208              
209 37 100       90 if ( ! $param{previous} )
210             {
211 30         36 my $data = {};
212             $param{previous} =
213             sub {
214 1635     1635   7219 _callback_previous ( _fix_datetime( $_[0] ), $param{next}, $data );
215             }
216 30         130 }
217             else
218             {
219 7         12 my $previous = $param{previous};
220             $param{previous} =
221             sub {
222 28     28   181 $previous->( _fix_datetime( $_[0] ) );
223             }
224 7         34 }
225              
226 37 100       92 if ( ! $param{next} )
227             {
228 2         4 my $data = {};
229             $param{next} =
230             sub {
231 4     4   12 _callback_next ( _fix_datetime( $_[0] ), $param{previous}, $data );
232             }
233 2         8 }
234             else
235             {
236 35         48 my $next = $param{next};
237             $param{next} =
238             sub {
239 6055     6055   9558 $next->( _fix_datetime( $_[0] ) );
240             }
241 35         94 }
242              
243 37         46 my ( $min, $max );
244 37         206 $max = $param{previous}->( DateTime::Infinite::Future->new );
245 37         2022 $min = $param{next}->( DateTime::Infinite::Past->new );
246 37 100       5199 $max = INFINITY if $max->is_infinite;
247 37 100       207 $min = NEG_INFINITY if $min->is_infinite;
248            
249 37         328 my $base_set = Set::Infinite::_recurrence->new( $min, $max );
250             $base_set = $base_set->intersection( $param{span}->{set} )
251 37 100       1502 if $param{span};
252            
253             # warn "base set is $base_set\n";
254              
255 37         1461 my $data = {};
256             $self->{set} =
257             $base_set->_recurrence(
258             $param{next},
259             $param{previous},
260 37         313 $data,
261             );
262 37         272 bless $self, $class;
263            
264 37         126 return $self;
265             }
266              
267             sub from_datetimes {
268 83     83 1 16163 my $class = shift;
269 83         1346 my %args = validate( @_,
270             { dates =>
271             { type => ARRAYREF,
272             },
273             }
274             );
275 82         238 my $self = {};
276 82         408 $self->{set} = Set::Infinite::_recurrence->new;
277             # possible optimization: sort datetimes and use "push"
278 82         1392 for( @{ $args{dates} } )
  82         185  
279             {
280             # DateTime::Infinite objects are not welcome here,
281             # but this is not enforced (it does't hurt)
282              
283 113 50       3825 carp "The 'dates' argument to from_datetimes() must only contain ".
284             "datetime objects"
285             unless UNIVERSAL::can( $_, 'utc_rd_values' );
286              
287 113         352 $self->{set} = $self->{set}->union( $_->clone );
288             }
289              
290 82         9700 bless $self, $class;
291 82         221 return $self;
292             }
293              
294             sub empty_set {
295 79     79 1 386 my $class = shift;
296              
297 79         163 return bless { set => Set::Infinite::_recurrence->new }, $class;
298             }
299              
300             sub is_empty_set {
301 2     2 1 292 my $set = $_[0];
302 2         7 $set->{set}->is_null;
303             }
304              
305             sub clone {
306 54     54 1 4757 my $self = bless { %{ $_[0] } }, ref $_[0];
  54         203  
307 54         205 $self->{set} = $_[0]->{set}->copy;
308 54         1846 return $self;
309             }
310              
311             # default callback that returns the
312             # "previous" value in a callback recurrence.
313             #
314             # This is used to simulate a 'previous' callback,
315             # when then 'previous' argument in 'from_recurrence' is missing.
316             #
317             sub _callback_previous {
318 1635     1635   12256 my ($value, $callback_next, $callback_info) = @_;
319 1635         2364 my $previous = $value->clone;
320              
321 1635 100       10447 return $value if $value->is_infinite;
322              
323 1433         3766 my $freq = $callback_info->{freq};
324 1433 100       1900 unless (defined $freq)
325             {
326             # This is called just once, to setup the recurrence frequency
327 28         36 my $previous = $callback_next->( $value );
328 28         16529 my $next = $callback_next->( $previous );
329 28         16261 $freq = 2 * ( $previous - $next );
330             # save it for future use with this same recurrence
331 28         5700 $callback_info->{freq} = $freq;
332             }
333              
334 1433         2306 $previous->add_duration( $freq );
335 1433         459516 $previous = $callback_next->( $previous );
336 1433 50       755240 if ($previous >= $value)
337             {
338             # This error happens if the event frequency oscillates widely
339             # (more than 100% of difference from one interval to next)
340 0         0 my @freq = $freq->deltas;
341 0         0 print STDERR "_callback_previous: Delta components are: @freq\n";
342 0         0 warn "_callback_previous: iterator can't find a previous value, got ".
343             $previous->ymd." after ".$value->ymd;
344             }
345 1433         59313 my $previous1;
346 1433         1214 while (1)
347             {
348 1445         3343 $previous1 = $previous->clone;
349 1445         11020 $previous = $callback_next->( $previous );
350 1445 100       739043 return $previous1 if $previous >= $value;
351             }
352             }
353              
354             # default callback that returns the
355             # "next" value in a callback recurrence.
356             #
357             # This is used to simulate a 'next' callback,
358             # when then 'next' argument in 'from_recurrence' is missing.
359             #
360             sub _callback_next {
361 4     4   71 my ($value, $callback_previous, $callback_info) = @_;
362 4         8 my $next = $value->clone;
363              
364 4 100       31 return $value if $value->is_infinite;
365              
366 2         5 my $freq = $callback_info->{freq};
367 2 100       5 unless (defined $freq)
368             {
369             # This is called just once, to setup the recurrence frequency
370 1         2 my $next = $callback_previous->( $value );
371 1         573 my $previous = $callback_previous->( $next );
372 1         526 $freq = 2 * ( $next - $previous );
373             # save it for future use with this same recurrence
374 1         210 $callback_info->{freq} = $freq;
375             }
376              
377 2         4 $next->add_duration( $freq );
378 2         608 $next = $callback_previous->( $next );
379 2 50       1044 if ($next <= $value)
380             {
381             # This error happens if the event frequency oscillates widely
382             # (more than 100% of difference from one interval to next)
383 0         0 my @freq = $freq->deltas;
384 0         0 print STDERR "_callback_next: Delta components are: @freq\n";
385 0         0 warn "_callback_next: iterator can't find a previous value, got ".
386             $next->ymd." before ".$value->ymd;
387             }
388 2         80 my $next1;
389 2         28 while (1)
390             {
391 2         5 $next1 = $next->clone;
392 2         16 $next = $callback_previous->( $next );
393 2 50       1072 return $next1 if $next >= $value;
394             }
395             }
396              
397             sub iterator {
398 40     40 1 7297 my $self = shift;
399              
400 40         77 my %args = @_;
401 40         38 my $span;
402 40         52 $span = delete $args{span};
403 40 100       162 $span = DateTime::Span->new( %args ) if %args;
404              
405 40 100       101 return $self->intersection( $span ) if $span;
406 25         46 return $self->clone;
407             }
408              
409              
410             # next() gets the next element from an iterator()
411             # next( $dt ) returns the next element after a datetime.
412             sub next {
413 1367     1367 1 67422 my $self = shift;
414 1367 100       2550 return undef unless ref( $self->{set} );
415              
416 1355 100       2033 if ( @_ )
417             {
418 39 100       99 if ( $self->{set}->_is_recurrence )
419             {
420             return _fix_return_datetime(
421 20         337 $self->{set}->{param}[0]->( $_[0] ), $_[0] );
422             }
423             else
424             {
425 19         82 my $span = DateTime::Span->from_datetimes( after => $_[0] );
426 19         54 return _fix_return_datetime(
427             $self->intersection( $span )->next, $_[0] );
428             }
429             }
430              
431 1316         2326 my ($head, $tail) = $self->{set}->first;
432 1316         34304 $self->{set} = $tail;
433 1316 100       4384 return $head->min if defined $head;
434 3         9 return $head;
435             }
436              
437             # previous() gets the last element from an iterator()
438             # previous( $dt ) returns the previous element before a datetime.
439             sub previous {
440 126     126 1 22151 my $self = shift;
441 126 100       334 return undef unless ref( $self->{set} );
442              
443 111 100       183 if ( @_ )
444             {
445 35 100       78 if ( $self->{set}->_is_recurrence )
446             {
447             return _fix_return_datetime(
448 13         262 $self->{set}->{param}[1]->( $_[0] ), $_[0] );
449             }
450             else
451             {
452 22         65 my $span = DateTime::Span->from_datetimes( before => $_[0] );
453 22         55 return _fix_return_datetime(
454             $self->intersection( $span )->previous, $_[0] );
455             }
456             }
457              
458 76         198 my ($head, $tail) = $self->{set}->last;
459 76         3380 $self->{set} = $tail;
460 76 100       291 return $head->max if defined $head;
461 10         27 return $head;
462             }
463              
464             # "current" means less-or-equal to a datetime
465             sub current {
466 29     29 1 118 my $self = shift;
467              
468 29 50       67 return undef unless ref( $self->{set} );
469              
470 29 100       69 if ( $self->{set}->_is_recurrence )
471             {
472 9         148 my $tmp = $self->next( $_[0] );
473 9         23 return $self->previous( $tmp );
474             }
475              
476 20 50       41 return $_[0] if $self->contains( $_[0] );
477 20         8545 $self->previous( $_[0] );
478             }
479              
480             sub closest {
481 0     0 1 0 my $self = shift;
482             # return $_[0] if $self->contains( $_[0] );
483 0         0 my $dt1 = $self->current( $_[0] );
484 0         0 my $dt2 = $self->next( $_[0] );
485              
486 0 0       0 return $dt2 unless defined $dt1;
487 0 0       0 return $dt1 unless defined $dt2;
488              
489 0         0 my $delta = $_[0] - $dt1;
490 0 0       0 return $dt1 if ( $dt2 - $delta ) >= $_[0];
491              
492 0         0 return $dt2;
493             }
494              
495             sub as_list {
496 4     4 1 821 my $self = shift;
497 4 50       17 return undef unless ref( $self->{set} );
498              
499 4         6 my %args = @_;
500 4         3 my $span;
501 4         5 $span = delete $args{span};
502 4 100       12 $span = DateTime::Span->new( %args ) if %args;
503              
504 4         8 my $set = $self->clone;
505 4 100       24 $set = $set->intersection( $span ) if $span;
506              
507 4 100       18 return if $set->{set}->is_null; # nothing = empty
508              
509             # Note: removing this line means we may end up in an infinite loop!
510             ## return undef if $set->{set}->is_too_complex; # undef = no start/end
511            
512             return undef
513 3 100 66     29 if $set->max->is_infinite ||
514             $set->min->is_infinite;
515              
516 2         35 my @result;
517 2         5 my $next = $self->min;
518 2 50       15 if ( $span ) {
519 0         0 my $next1 = $span->min;
520 0 0 0     0 $next = $next1 if $next1 && $next1 > $next;
521 0         0 $next = $self->current( $next );
522             }
523 2         4 my $last = $self->max;
524 2 50       40 if ( $span ) {
525 0         0 my $last1 = $span->max;
526 0 0 0     0 $last = $last1 if $last1 && $last1 < $last;
527             }
528 2   66     2 do {
529 3 50 33     69 push @result, $next if !$span || $span->contains($next);
530 3         15 $next = $self->next( $next );
531             }
532             while $next && $next <= $last;
533 2         23 return @result;
534             }
535              
536             sub intersection {
537 65     65 1 858 my ($set1, $set2) = ( shift, shift );
538 65         94 my $class = ref($set1);
539 65         131 my $tmp = $class->empty_set();
540 65 50       823 $set2 = $set2->as_set
541             if $set2->can( 'as_set' );
542 65 100       182 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
543             unless $set2->can( 'union' );
544 65         194 $tmp->{set} = $set1->{set}->intersection( $set2->{set} );
545 65         25776 return $tmp;
546             }
547              
548             sub intersects {
549 10     10 1 1698 my ($set1, $set2) = ( shift, shift );
550 10         14 my $class = ref($set1);
551 10 50       41 $set2 = $set2->as_set
552             if $set2->can( 'as_set' );
553 10 100       28 unless ( $set2->can( 'union' ) )
554             {
555 9 100       26 if ( $set1->{set}->_is_recurrence )
556             {
557 3         49 for ( $set2, @_ )
558             {
559 4 100       47 return 1 if $set1->current( $_ ) == $_;
560             }
561 1         44 return 0;
562             }
563 6         165 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
564             }
565 7         21 return $set1->{set}->intersects( $set2->{set} );
566             }
567              
568             sub contains {
569 30     30 1 2024 my ($set1, $set2) = ( shift, shift );
570 30         63 my $class = ref($set1);
571 30 50       163 $set2 = $set2->as_set
572             if $set2->can( 'as_set' );
573 30 100       96 unless ( $set2->can( 'union' ) )
574             {
575 29 100       63 if ( $set1->{set}->_is_recurrence )
576             {
577 3         64 for ( $set2, @_ )
578             {
579 3 100       8 return 0 unless $set1->current( $_ ) == $_;
580             }
581 1         45 return 1;
582             }
583 26         237 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
584             }
585 27         86 return $set1->{set}->contains( $set2->{set} );
586             }
587              
588             sub union {
589 4     4 1 1889 my ($set1, $set2) = ( shift, shift );
590 4         9 my $class = ref($set1);
591 4         15 my $tmp = $class->empty_set();
592 4 50       89 $set2 = $set2->as_set
593             if $set2->can( 'as_set' );
594 4 50       18 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
595             unless $set2->can( 'union' );
596 4         15 $tmp->{set} = $set1->{set}->union( $set2->{set} );
597 4 50 33     190 bless $tmp, 'DateTime::SpanSet'
598             if $set2->isa('DateTime::Span') or $set2->isa('DateTime::SpanSet');
599 4         9 return $tmp;
600             }
601              
602             sub complement {
603 2     2 1 22 my ($set1, $set2) = ( shift, shift );
604 2         4 my $class = ref($set1);
605 2         5 my $tmp = $class->empty_set();
606 2 50       25 if (defined $set2)
607             {
608 2 50       11 $set2 = $set2->as_set
609             if $set2->can( 'as_set' );
610 2 50       12 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
611             unless $set2->can( 'union' );
612             # TODO: "compose complement";
613 2         9 $tmp->{set} = $set1->{set}->complement( $set2->{set} );
614             }
615             else
616             {
617 0         0 $tmp->{set} = $set1->{set}->complement;
618 0         0 bless $tmp, 'DateTime::SpanSet';
619             }
620 2         70 return $tmp;
621             }
622              
623             sub start {
624 54     54 1 5792 return _fix_datetime( $_[0]->{set}->min );
625             }
626              
627             *min = \&start;
628              
629             sub end {
630 34     34 1 5024 return _fix_datetime( $_[0]->{set}->max );
631             }
632              
633             *max = \&end;
634              
635             # returns a DateTime::Span
636             sub span {
637 0     0 1 0 my $set = $_[0]->{set}->span;
638 0         0 my $self = bless { set => $set }, 'DateTime::Span';
639 0         0 return $self;
640             }
641              
642             sub count {
643 9     9 1 2887 my ($self) = shift;
644 9 50       32 return undef unless ref( $self->{set} );
645              
646 9         16 my %args = @_;
647 9         7 my $span;
648 9         14 $span = delete $args{span};
649 9 100       54 $span = DateTime::Span->new( %args ) if %args;
650              
651 9         23 my $set = $self->clone;
652 9 100       23 $set = $set->intersection( $span ) if $span;
653              
654             return $set->{set}->count
655 9 100       28 unless $set->{set}->is_too_complex;
656              
657             return undef
658 4 100 100     22 if $set->max->is_infinite ||
659             $set->min->is_infinite;
660              
661 1         14 my $count = 0;
662 1         3 my $iter = $set->iterator;
663 1         6 $count++ while $iter->next;
664 1         1605 return $count;
665             }
666              
667             1;
668              
669             __END__