File Coverage

lib/DateTimeX/Duration/SkipDays.pm
Criterion Covered Total %
statement 94 94 100.0
branch 26 30 86.6
condition 3 3 100.0
subroutine 18 18 100.0
pod 6 6 100.0
total 147 151 97.3


line stmt bran cond sub pod time code
1             package DateTimeX::Duration::SkipDays;
2              
3             # # no critic qw( Modules::RequireExplicitInclusion )
4              
5             # ABSTRACT: Given a starting date, a number of days and a list of days to be skipped, returns the date X number of days away.
6              
7 3     3   717013 use 5.006;
  3         14  
  3         172  
8 3     3   20 use strict;
  3         6  
  3         109  
9 3     3   17 use warnings;
  3         6  
  3         139  
10              
11 3     3   28 use Carp;
  3         6  
  3         421  
12 3     3   1737 use DateTime;
  3         273187  
  3         90  
13 3     3   3407 use DateTime::Event::Holiday::US;
  3         365422  
  3         117  
14 3     3   20008 use DateTime::Format::Flexible;
  3         325058  
  3         46  
15 3     3   368 use Try::Tiny;
  3         7  
  3         1572  
16 3     3   21 use List::MoreUtils 'any';
  3         5  
  3         1900  
17              
18             # The circular::require test is having problems with one or more of the above modules, so we'll skip it.
19              
20             ## efm skip circular
21              
22             our $VERSION = '0.002'; # VERSION
23              
24              
25             sub new {
26              
27 15     15 1 34465 my ( $class, $arg ) = @_;
28              
29 15 100 100     504 croak 'Must pass nothing or a reference to a hash to new'
30             if ref $arg && ref $arg ne 'HASH';
31              
32 12         55 my $self = bless {}, $class;
33              
34 12         50 $self->{ 'bad_format' } = {};
35 12         101 $self->{ 'days_to_skip' } = DateTime::Set->empty_set;
36              
37 12         824 for my $key ( keys %$arg ) { ## no critic qw( References::ProhibitDoubleSigils )
38              
39 11 100       46 next if $key eq 'add';
40              
41 10 100       76 if ( my $method = $self->can( $key ) ) {
42              
43             ## no critic qw( ValuesAndExpressions::ProhibitAccessOfPrivateData )
44 9         36 $self->$method( $arg->{ $key } );
45              
46             }
47             }
48              
49 12         48 return $self;
50              
51             } ## end sub new
52              
53              
54             sub start_date {
55              
56 6     6 1 483 my ( $self, $start_date ) = @_;
57              
58 6 100       132 croak 'Must pass a DateTime object to start'
59             if ref $start_date ne 'DateTime';
60              
61 5         26 $self->{ 'start_date' } = $start_date->clone->truncate( 'to' => 'day' );
62              
63 5         1619 return 1;
64              
65             }
66              
67              
68             sub days_to_skip {
69              
70 13     13 1 30 my ( $self, @days_to_skip ) = @_;
71              
72 13         79 $self->{ 'days_to_skip' } = $self->{ 'days_to_skip' }->union( $_ ) for @days_to_skip;
73              
74 13         2985 return 1;
75              
76             }
77              
78              
79             sub parse_dates {
80              
81 9     9 1 614 my ( $self, $skip_dates ) = @_;
82              
83 9 100       130 croak 'Expected scalar'
84             if ref $skip_dates;
85              
86 8         52 my @known_holidays = DateTime::Event::Holiday::US::known();
87              
88 8         480 for my $line ( split /\n/, $skip_dates ) {
89              
90 27 100       145 next if $line =~ /^\s*$/;
91 20         172 $line =~ s/^\s*(\S.*?)\s*$/$1/;
92 20         68 $line =~ s/\s+/ /g;
93              
94 20         37 my $dt;
95              
96 20 100       216 if ( $line =~ /^RRULE:/i ) {
    100          
97              
98 3         18 $dt = DateTime::Format::ICal->parse_recurrence( 'recurrence' => $line );
99              
100             ## no tidy
101 612     612   1119 } elsif ( any { /$line/ } @known_holidays ) {
102             ## use tidy
103              
104 5         52 $dt = DateTime::Event::Holiday::US::holiday( $line );
105              
106             } else {
107              
108             ## no critic qw( TestingAndDebugging::ProhibitNoWarnings ErrorHandling::RequireCheckingReturnValueOfEval )
109 3     3   53 eval { no warnings 'uninitialized'; $dt = DateTime::Format::Flexible->parse_datetime( $line ) };
  3         7  
  3         836  
  12         22  
  12         74  
110             ## use critic
111              
112 12 100       77300 if ( $@ ) {
113              
114 7         147 ( my $err = $@ ) =~ s/^(Invalid date format: $line).*$/$1/ms;
115              
116 7         35 $self->{ 'bad_format' }{ $line } = $err;
117 7         46 next;
118              
119             }
120              
121             # This fails with
122             # Can't call method "can" on an undefined value at /usr/local/share/perl/5.10.1/DateTime/Set.pm line 593.
123             # and I don't have time to figure out what's wrong. So, the above is going to have to do.
124             #
125             #try {
126             #
127             # $dt = DateTime::Format::Flexible->parse_datetime( $line );
128             #
129             #} catch {
130             #
131             # ( my $err = $_ ) =~ s/^(Invalid date format: $line).*$/$1/ms;
132             #
133             # $self->{ 'bad_format' }{ $line } = $err;
134             #
135             #}
136             } ## end else [ if ( $line =~ /^RRULE:/i)]
137              
138 13         13615 $self->days_to_skip( $dt );
139              
140             } ## end for my $line ( split...)
141              
142 8         78 return 1;
143              
144             } ## end sub parse_dates
145              
146              
147             sub bad_format { ## no critic qw( Subroutines::RequireArgUnpacking )
148 12 100   12 1 64611 return wantarray ? keys %{ $_[0]->{ 'bad_format' } } : $_[0]->{ 'bad_format' };
  4         30  
149             }
150              
151              
152             sub add {
153              
154 6     6 1 2913 my ( $self, $x ) = @_;
155              
156 3     3   17 { no warnings 'numeric'; $x += 0 } ## no critic qw( TestingAndDebugging::ProhibitNoWarnings )
  3         4  
  3         1045  
  6         14  
  6         13  
157              
158 6 50       23 croak 'Must provide integer larger than or equal to 0'
159             if $x < 0;
160              
161             # XXX: Need better error handling here
162 6 50       29 croak 'No start date provided'
163             unless exists $self->{ 'start_date' };
164              
165             # XXX: Need better error handling here
166 6 50       22 croak 'No days_to_skip provided'
167             unless exists $self->{ 'days_to_skip' };
168              
169 6         45 my $duration = DateTime::Duration->new( 'days' => $x );
170 6         593 my $span = DateTime::Span->from_datetime_and_duration( 'start' => $self->{ 'start_date' }, 'duration' => $duration );
171 6         10746 my $skipped = $span->intersection( $self->{ 'days_to_skip' } );
172              
173 6         282517 my $count = my $new_count = 0;
174              
175 6         46 my $iter = $skipped->iterator;
176 6         384 $count++ while $iter->next;
177              
178 6         2354 while ( $count != $new_count ) {
179              
180 8         65 $duration = DateTime::Duration->new( 'days' => $x + $count );
181 8         806 $span = DateTime::Span->from_datetime_and_duration( 'start' => $self->{ 'start_date' }, 'duration' => $duration );
182 8         16445 $skipped = $span->intersection( $self->{ 'days_to_skip' } );
183              
184 8         886519 $iter = $skipped->iterator;
185 8         478 my $new_count; $new_count++ while $iter->next;
  8         46  
186              
187 8 100       6664 last if $new_count == $count;
188 4         16 $count = $new_count;
189              
190             }
191              
192             ## no critic qw( ValuesAndExpressions::ProhibitCommaSeparatedStatements )
193 6 50       117 return wantarray ? ( $span, $skipped ) : { 'span' => $span, 'skipped' => $skipped };
194              
195             } ## end sub add
196              
197              
198             1;
199              
200             __END__
201             =pod
202              
203             =head1 NAME
204              
205             DateTimeX::Duration::SkipDays - Given a starting date, a number of days and a list of days to be skipped, returns the date X number of days away.
206              
207             =head1 VERSION
208              
209             version 0.002
210              
211             =head1 SYNOPSIS
212              
213             #!/usr/bin/perl
214              
215             use strict;
216             use warnings;
217              
218             use DateTime;
219             use DateTimeX::Duration::SkipDays;
220              
221             my $skip_days = q(
222              
223             Christmas
224             Christmas Eve
225             RRULE:FREQ=WEEKLY;BYDAY=SA,SU
226              
227             );
228              
229             my $skip_x_days = 30;
230             my $start_date = DateTime->new( 'year' => 2011, 'month' => 12, 'day' => 1 );
231              
232             my $s = DateTimeX::Duration::SkipDays->new({
233             'parse_dates' => $skip_days,
234             'start_date' => $start_date,
235             });
236              
237             my ( $span, $skipped ) = $s->add( $skip_x_days );
238              
239             printf "\nCalculated Start: %s\nCalculated End: %s\n", $span->start->ymd, $span->end->ymd;
240              
241             my $iter = $skipped->iterator;
242              
243             while ( my $dt = $iter->next ) {
244              
245             printf "\nSkipped: %s", $dt->min->ymd;
246              
247             }
248              
249             if ( @{ $s->bad_format } ) {
250              
251             print "\n\nUnrecognized formats:";
252             print "\n\t$_" for @{ $s->bad_format };
253              
254             }
255              
256             # should output
257              
258             # Calculated Start: 2011-12-01
259             # Calculated End: 2012-01-12
260              
261             # Skipped: 2011-12-03
262             # Skipped: 2011-12-04
263             # Skipped: 2011-12-10
264             # Skipped: 2011-12-11
265             # Skipped: 2011-12-17
266             # Skipped: 2011-12-18
267             # Skipped: 2011-12-24
268             # Skipped: 2011-12-25
269             # Skipped: 2011-12-31
270             # Skipped: 2012-01-01
271             # Skipped: 2012-01-07
272             # Skipped: 2012-01-08
273              
274             =head1 METHODS
275              
276             =head2 new( [\%HASH] )
277              
278             With no arguments an empty object is returned.
279              
280             This method will croak if a non-hash reference is passed to it.
281              
282             The possible keys for the constructor are any of the available methods below,
283             except for C<add>. The C<add> method must be called explicitly. Unknown keys
284             will be silently ignored.
285              
286             The values have the same requirement as the matching methods.
287              
288             Returns a C<DateTimeX::Duration::SkipDays> object.
289              
290             =head2 start_date( DateTime )
291              
292             C<start_date> is expecting a L<DateTime> object. This will be used as the
293             starting point for calculations.
294              
295             Returns true on success.
296              
297             =head2 days_to_skip
298              
299             C<days_to_skip> accepts any object, or array of objects that will be added to the
300             current list of days to be skipped.
301              
302             Currently, L<DateTime>, L<DateTime::Span>, L<DateTime::Set>,
303             L<DateTime::Set::ICal> and L<DateTime::SpanSet> are known to work. Anything
304             that can be used with L<DateTime::Set>'s union method should work.
305              
306             Returns true on success
307              
308             =head2 parse_dates( $SCALAR )
309              
310             C<parse_dates> is expecting a scalar that has a newline separated list of
311             dates. The text can contain any of the following:
312              
313             =over
314              
315             =item A holiday known to L<DateTime::Event::Holiday::US>
316              
317             =item A RRULE -- L<DateTime::Format::ICal> is being used to parse this input
318              
319             =item A formatted, or partially formatted, date string --
320             L<DateTime::Format::Flexible> is being used to parse this input.
321              
322             =back
323              
324             Returns true on success or false on failure.
325              
326             Any line that is not recognized is silently ignored. Check C<bad_format> for
327             a list of unknown formats.
328              
329             =head2 bad_format
330              
331             Returns a reference to an array of unrecognized formats.
332              
333             =head2 add
334              
335             C<add> expects a single integer greater than or equal to 0 (though 0 would be
336             kind of useless).
337              
338             This is the number of days into the future you are looking for.
339              
340             The C<start_date> and C<days_to_skip> values need to have been populated or
341             this method will croak.
342              
343             In array context a reference to a L<DateTime::Span> object and
344             a L<DateTime::SpanSet> object is returned, otherwise a reference to a hash with
345             those objects as values is returned.
346              
347             X<DateTime>
348             X<DateTime::Duration>
349              
350             =head1 INSTALLATION
351              
352             See perlmodinstall for information and options on installing Perl modules.
353              
354             =head1 AUTHOR
355              
356             Alan Young <harleypig@gmail.com>
357              
358             =head1 COPYRIGHT AND LICENSE
359              
360             This software is copyright (c) 2012 by Alan Young.
361              
362             This is free software; you can redistribute it and/or modify it under
363             the same terms as the Perl 5 programming language system itself.
364              
365             =cut
366