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   1508085 use strict;
  23         29  
  23         533  
4 23     23   73 use Carp;
  23         18  
  23         1220  
5 23     23   1382 use Params::Validate qw( validate SCALAR BOOLEAN OBJECT CODEREF ARRAYREF );
  23         17991  
  23         1164  
6 23     23   2271 use DateTime 0.12; # this is for version checking only
  23         194069  
  23         423  
7 23     23   76 use DateTime::Duration;
  23         23  
  23         361  
8 23     23   8057 use DateTime::Span;
  23         33  
  23         565  
9 23     23   12698 use Set::Infinite 0.59;
  23         456206  
  23         1038  
10 23     23   8985 use Set::Infinite::_recurrence;
  23         44  
  23         944  
11              
12 23     23   95 use vars qw( $VERSION );
  23         24  
  23         822  
13              
14 23     23   80 use constant INFINITY => 100 ** 100 ** 100 ;
  23         30  
  23         1247  
15 23     23   82 use constant NEG_INFINITY => -1 * (100 ** 100 ** 100);
  23         24  
  23         900  
16              
17             BEGIN {
18 23     23   57620 $VERSION = '0.3800';
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   16375 return $_[0]
30             unless defined $_[0]; # error
31 7940 100       21993 return $_[0]->clone
32             if ref( $_[0] ); # "immutable" datetime
33 89 100       341 return DateTime::Infinite::Future->new
34             if $_[0] == INFINITY; # Inf
35 10 50       55 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   11938 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       237 return unless $dt;
52 61 50       1684 return unless $dt_arg;
53 61 100 66     1268 if ( $dt_arg->can('time_zone_long_name') &&
54             !( $dt_arg->time_zone_long_name eq 'floating' ) )
55             {
56 2         14 $dt->set_time_zone( $dt_arg->time_zone );
57             }
58 61         740 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 289 my ( $self, $callback ) = @_;
77 1         2 my $class = ref( $self );
78 1 50       4 die "The callback parameter to map() must be a subroutine reference"
79             unless ref( $callback ) eq 'CODE';
80 1         10 my $return = $class->empty_set;
81             $return->{set} = $self->{set}->iterate(
82             sub {
83 2     2   49 local $_ = $_[0]->min;
84 2 50       27 next unless ref( $_ );
85 2         5 $_ = $_->clone;
86 2         17 my @list = $callback->();
87 2         1084 my $set = Set::Infinite::_recurrence->new();
88 2         26 $set = $set->union( $_ ) for @list;
89 2         140 return $set;
90             }
91 1         20 );
92 1         90 $return;
93             }
94              
95             sub grep {
96 1     1 1 399 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         2 my $return = $class->empty_set;
101             $return->{set} = $self->{set}->iterate(
102             sub {
103 2     2   45 local $_ = $_[0]->min;
104 2 50       26 next unless ref( $_ );
105 2         5 $_ = $_->clone;
106 2         15 my $result = $callback->();
107 2 100       387 return $_ if $result;
108 1         3 return;
109             }
110 1         17 );
111 1         75 $return;
112             }
113              
114 8     8 1 392 sub add { return shift->add_duration( DateTime::Duration->new(@_) ) }
115              
116 1     1 1 4 sub subtract { return shift->subtract_duration( DateTime::Duration->new(@_) ) }
117              
118 3     3 1 96 sub subtract_duration { return $_[0]->add_duration( $_[1]->inverse ) }
119              
120             sub add_duration {
121 18     18 1 603 my ( $self, $dur ) = @_;
122 18         45 $dur = $dur->clone; # $dur must be "immutable"
123              
124             $self->{set} = $self->{set}->iterate(
125             sub {
126 74     74   5228 my $min = $_[0]->min;
127 74 50       1100 $min->clone->add_duration( $dur ) if ref($min);
128             },
129             backtrack_callback => sub {
130 18     18   777 my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
131 18 100       396 if ( ref($min) )
132             {
133 14         34 $min = $min->clone;
134 14         119 $min->subtract_duration( $dur );
135             }
136 18 100       5586 if ( ref($max) )
137             {
138 8         23 $max = $max->clone;
139 8         82 $max->subtract_duration( $dur );
140             }
141 18         2131 return Set::Infinite::_recurrence->new( $min, $max );
142             },
143 18         200 );
144 18         1171 $self;
145             }
146              
147             sub set_time_zone {
148 4     4 1 197 my ( $self, $tz ) = @_;
149              
150             $self->{set} = $self->{set}->iterate(
151             sub {
152 43     43   2367 my $min = $_[0]->min;
153 43 50       670 $min->clone->set_time_zone( $tz ) if ref($min);
154             },
155             backtrack_callback => sub {
156 8     8   365 my ( $min, $max ) = ( $_[0]->min, $_[0]->max );
157 8 100       190 if ( ref($min) )
158             {
159 6         17 $min = $min->clone;
160 6         49 $min->set_time_zone( $tz );
161             }
162 8 100       979 if ( ref($max) )
163             {
164 2         4 $max = $max->clone;
165 2         14 $max->set_time_zone( $tz );
166             }
167 8         199 return Set::Infinite::_recurrence->new( $min, $max );
168             },
169 4         41 );
170 4         363 $self;
171             }
172              
173             sub set {
174 1     1 1 2 my $self = shift;
175 1         11 my %args = validate( @_,
176             { locale => { type => SCALAR | OBJECT,
177             default => undef },
178             }
179             );
180             $self->{set} = $self->{set}->iterate(
181             sub {
182 2     2   301 my $min = $_[0]->min;
183 2 50       32 $min->clone->set( %args ) if ref($min);
184             },
185 1         7 );
186 1         19 $self;
187             }
188              
189             sub from_recurrence {
190 37     37 1 11647 my $class = shift;
191              
192 37         103 my %args = @_;
193 37         322 my %param;
194            
195             # Parameter renaming, such that we can use either
196             # recurrence => xxx or next => xxx, previous => xxx
197 37   66     138 $param{next} = delete $args{recurrence} || delete $args{next};
198 37         56 $param{previous} = delete $args{previous};
199              
200 37         49 $param{span} = delete $args{span};
201             # they might be specifying a span using start / end
202 37 100       176 $param{span} = DateTime::Span->new( %args ) if keys %args;
203              
204 37         47 my $self = {};
205            
206             die "Not enough arguments in from_recurrence()"
207 37 50 66     93 unless $param{next} || $param{previous};
208              
209 37 100       85 if ( ! $param{previous} )
210             {
211 30         36 my $data = {};
212             $param{previous} =
213             sub {
214 1635     1635   7056 _callback_previous ( _fix_datetime( $_[0] ), $param{next}, $data );
215             }
216 30         120 }
217             else
218             {
219 7         9 my $previous = $param{previous};
220             $param{previous} =
221             sub {
222 28     28   183 $previous->( _fix_datetime( $_[0] ) );
223             }
224 7         20 }
225              
226 37 100       97 if ( ! $param{next} )
227             {
228 2         3 my $data = {};
229             $param{next} =
230             sub {
231 4     4   17 _callback_next ( _fix_datetime( $_[0] ), $param{previous}, $data );
232             }
233 2         6 }
234             else
235             {
236 35         47 my $next = $param{next};
237             $param{next} =
238             sub {
239 6055     6055   9692 $next->( _fix_datetime( $_[0] ) );
240             }
241 35         87 }
242              
243 37         42 my ( $min, $max );
244 37         180 $max = $param{previous}->( DateTime::Infinite::Future->new );
245 37         1789 $min = $param{next}->( DateTime::Infinite::Past->new );
246 37 100       4928 $max = INFINITY if $max->is_infinite;
247 37 100       192 $min = NEG_INFINITY if $min->is_infinite;
248            
249 37         308 my $base_set = Set::Infinite::_recurrence->new( $min, $max );
250             $base_set = $base_set->intersection( $param{span}->{set} )
251 37 100       1358 if $param{span};
252            
253             # warn "base set is $base_set\n";
254              
255 37         1402 my $data = {};
256             $self->{set} =
257             $base_set->_recurrence(
258             $param{next},
259             $param{previous},
260 37         291 $data,
261             );
262 37         262 bless $self, $class;
263            
264 37         120 return $self;
265             }
266              
267             sub from_datetimes {
268 83     83 1 16360 my $class = shift;
269 83         1221 my %args = validate( @_,
270             { dates =>
271             { type => ARRAYREF,
272             },
273             }
274             );
275 82         216 my $self = {};
276 82         355 $self->{set} = Set::Infinite::_recurrence->new;
277             # possible optimization: sort datetimes and use "push"
278 82         1069 for( @{ $args{dates} } )
  82         168  
279             {
280             # DateTime::Infinite objects are not welcome here,
281             # but this is not enforced (it does't hurt)
282              
283 113 50       3592 carp "The 'dates' argument to from_datetimes() must only contain ".
284             "datetime objects"
285             unless UNIVERSAL::can( $_, 'utc_rd_values' );
286              
287 113         291 $self->{set} = $self->{set}->union( $_->clone );
288             }
289              
290 82         9068 bless $self, $class;
291 82         250 return $self;
292             }
293              
294             sub empty_set {
295 79     79 1 375 my $class = shift;
296              
297 79         147 return bless { set => Set::Infinite::_recurrence->new }, $class;
298             }
299              
300             sub is_empty_set {
301 2     2 1 286 my $set = $_[0];
302 2         6 $set->{set}->is_null;
303             }
304              
305             sub clone {
306 54     54 1 4341 my $self = bless { %{ $_[0] } }, ref $_[0];
  54         189  
307 54         190 $self->{set} = $_[0]->{set}->copy;
308 54         1830 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   11852 my ($value, $callback_next, $callback_info) = @_;
319 1635         2626 my $previous = $value->clone;
320              
321 1635 100       10349 return $value if $value->is_infinite;
322              
323 1433         3986 my $freq = $callback_info->{freq};
324 1433 100       2099 unless (defined $freq)
325             {
326             # This is called just once, to setup the recurrence frequency
327 28         43 my $previous = $callback_next->( $value );
328 28         16183 my $next = $callback_next->( $previous );
329 28         15775 $freq = 2 * ( $previous - $next );
330             # save it for future use with this same recurrence
331 28         5296 $callback_info->{freq} = $freq;
332             }
333              
334 1433         2430 $previous->add_duration( $freq );
335 1433         455442 $previous = $callback_next->( $previous );
336 1433 50       733028 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         58851 my $previous1;
346 1433         1211 while (1)
347             {
348 1445         3290 $previous1 = $previous->clone;
349 1445         10448 $previous = $callback_next->( $previous );
350 1445 100       730434 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   68 my ($value, $callback_previous, $callback_info) = @_;
362 4         6 my $next = $value->clone;
363              
364 4 100       27 return $value if $value->is_infinite;
365              
366 2         6 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         526 my $previous = $callback_previous->( $next );
372 1         585 $freq = 2 * ( $next - $previous );
373             # save it for future use with this same recurrence
374 1         204 $callback_info->{freq} = $freq;
375             }
376              
377 2         4 $next->add_duration( $freq );
378 2         597 $next = $callback_previous->( $next );
379 2 50       1036 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         33 while (1)
390             {
391 2         5 $next1 = $next->clone;
392 2         14 $next = $callback_previous->( $next );
393 2 50       1078 return $next1 if $next >= $value;
394             }
395             }
396              
397             sub iterator {
398 40     40 1 5390 my $self = shift;
399              
400 40         72 my %args = @_;
401 40         29 my $span;
402 40         53 $span = delete $args{span};
403 40 100       117 $span = DateTime::Span->new( %args ) if %args;
404              
405 40 100       93 return $self->intersection( $span ) if $span;
406 25         50 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 65853 my $self = shift;
414 1367 100       2480 return undef unless ref( $self->{set} );
415              
416 1355 100       1930 if ( @_ )
417             {
418 39 100       90 if ( $self->{set}->_is_recurrence )
419             {
420             return _fix_return_datetime(
421 20         345 $self->{set}->{param}[0]->( $_[0] ), $_[0] );
422             }
423             else
424             {
425 19         65 my $span = DateTime::Span->from_datetimes( after => $_[0] );
426 19         45 return _fix_return_datetime(
427             $self->intersection( $span )->next, $_[0] );
428             }
429             }
430              
431 1316         2368 my ($head, $tail) = $self->{set}->first;
432 1316         34354 $self->{set} = $tail;
433 1316 100       4022 return $head->min if defined $head;
434 3         13 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 17921 my $self = shift;
441 126 100       321 return undef unless ref( $self->{set} );
442              
443 111 100       178 if ( @_ )
444             {
445 35 100       79 if ( $self->{set}->_is_recurrence )
446             {
447             return _fix_return_datetime(
448 13         243 $self->{set}->{param}[1]->( $_[0] ), $_[0] );
449             }
450             else
451             {
452 22         64 my $span = DateTime::Span->from_datetimes( before => $_[0] );
453 22         36 return _fix_return_datetime(
454             $self->intersection( $span )->previous, $_[0] );
455             }
456             }
457              
458 76         162 my ($head, $tail) = $self->{set}->last;
459 76         3393 $self->{set} = $tail;
460 76 100       292 return $head->max if defined $head;
461 10         22 return $head;
462             }
463              
464             # "current" means less-or-equal to a datetime
465             sub current {
466 29     29 1 109 my $self = shift;
467              
468 29 50       59 return undef unless ref( $self->{set} );
469              
470 29 100       70 if ( $self->{set}->_is_recurrence )
471             {
472 9         144 my $tmp = $self->next( $_[0] );
473 9         26 return $self->previous( $tmp );
474             }
475              
476 20 50       34 return $_[0] if $self->contains( $_[0] );
477 20         8580 $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 803 my $self = shift;
497 4 50       22 return undef unless ref( $self->{set} );
498              
499 4         5 my %args = @_;
500 4         5 my $span;
501 4         5 $span = delete $args{span};
502 4 100       12 $span = DateTime::Span->new( %args ) if %args;
503              
504 4         9 my $set = $self->clone;
505 4 100       8 $set = $set->intersection( $span ) if $span;
506              
507 4 100       24 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     32 if $set->max->is_infinite ||
514             $set->min->is_infinite;
515              
516 2         31 my @result;
517 2         6 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         8 my $last = $self->max;
524 2 50       26 if ( $span ) {
525 0         0 my $last1 = $span->max;
526 0 0 0     0 $last = $last1 if $last1 && $last1 < $last;
527             }
528 2   66     3 do {
529 3 50 33     74 push @result, $next if !$span || $span->contains($next);
530 3         14 $next = $self->next( $next );
531             }
532             while $next && $next <= $last;
533 2         11 return @result;
534             }
535              
536             sub intersection {
537 65     65 1 870 my ($set1, $set2) = ( shift, shift );
538 65         71 my $class = ref($set1);
539 65         106 my $tmp = $class->empty_set();
540 65 50       825 $set2 = $set2->as_set
541             if $set2->can( 'as_set' );
542 65 100       170 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
543             unless $set2->can( 'union' );
544 65         165 $tmp->{set} = $set1->{set}->intersection( $set2->{set} );
545 65         27395 return $tmp;
546             }
547              
548             sub intersects {
549 10     10 1 2016 my ($set1, $set2) = ( shift, shift );
550 10         17 my $class = ref($set1);
551 10 50       37 $set2 = $set2->as_set
552             if $set2->can( 'as_set' );
553 10 100       28 unless ( $set2->can( 'union' ) )
554             {
555 9 100       24 if ( $set1->{set}->_is_recurrence )
556             {
557 3         47 for ( $set2, @_ )
558             {
559 4 100       50 return 1 if $set1->current( $_ ) == $_;
560             }
561 1         44 return 0;
562             }
563 6         167 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
564             }
565 7         20 return $set1->{set}->intersects( $set2->{set} );
566             }
567              
568             sub contains {
569 30     30 1 1869 my ($set1, $set2) = ( shift, shift );
570 30         57 my $class = ref($set1);
571 30 50       154 $set2 = $set2->as_set
572             if $set2->can( 'as_set' );
573 30 100       88 unless ( $set2->can( 'union' ) )
574             {
575 29 100       61 if ( $set1->{set}->_is_recurrence )
576             {
577 3         57 for ( $set2, @_ )
578             {
579 3 100       6 return 0 unless $set1->current( $_ ) == $_;
580             }
581 1         44 return 1;
582             }
583 26         219 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
584             }
585 27         76 return $set1->{set}->contains( $set2->{set} );
586             }
587              
588             sub union {
589 4     4 1 1640 my ($set1, $set2) = ( shift, shift );
590 4         6 my $class = ref($set1);
591 4         12 my $tmp = $class->empty_set();
592 4 50       80 $set2 = $set2->as_set
593             if $set2->can( 'as_set' );
594 4 50       13 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
595             unless $set2->can( 'union' );
596 4         14 $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 17 my ($set1, $set2) = ( shift, shift );
604 2         3 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       10 $set2 = $class->from_datetimes( dates => [ $set2, @_ ] )
611             unless $set2->can( 'union' );
612             # TODO: "compose complement";
613 2         10 $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         63 return $tmp;
621             }
622              
623             sub start {
624 54     54 1 5671 return _fix_datetime( $_[0]->{set}->min );
625             }
626              
627             *min = \&start;
628              
629             sub end {
630 34     34 1 5522 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 3421 my ($self) = shift;
644 9 50       30 return undef unless ref( $self->{set} );
645              
646 9         15 my %args = @_;
647 9         7 my $span;
648 9         13 $span = delete $args{span};
649 9 100       49 $span = DateTime::Span->new( %args ) if %args;
650              
651 9         19 my $set = $self->clone;
652 9 100       23 $set = $set->intersection( $span ) if $span;
653              
654             return $set->{set}->count
655 9 100       32 unless $set->{set}->is_too_complex;
656              
657             return undef
658 4 100 100     19 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         3 $count++ while $iter->next;
664 1         1493 return $count;
665             }
666              
667             1;
668              
669             __END__