File Coverage

blib/lib/DateTimeX/Seinfeld.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package DateTimeX::Seinfeld;
3             #
4             # Copyright 2012 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen <perl@cjmweb.net>
7             # Created: 10 Mar 2012
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Calculate Seinfeld chain length
18             #---------------------------------------------------------------------
19              
20 3     3   139332 use 5.010;
  3         13  
  3         133  
21 3     3   4656 use Moose;
  0            
  0            
22             use namespace::autoclean;
23              
24             use MooseX::Types::Moose qw(CodeRef);
25             use MooseX::Types::DateTime (); # Just load coercions
26              
27             our $VERSION = '1.000';
28             # This file is part of DateTimeX-Seinfeld 1.000 (January 11, 2014)
29              
30             #=====================================================================
31              
32              
33             has start_date => (
34             is => 'ro',
35             isa => 'DateTime',
36             coerce => 1,
37             required => 1,
38             );
39              
40              
41             has increment => (
42             is => 'ro',
43             isa => 'DateTime::Duration',
44             coerce => 1,
45             required => 1,
46             );
47              
48              
49             has skip => (
50             is => 'ro',
51             isa => CodeRef,
52             );
53              
54             #=====================================================================
55              
56              
57             sub find_chains
58             {
59             my ($self, $dates, $info) = @_;
60              
61             # If we were passed $info, continue a previous search:
62             my $end;
63             if ($info and %$info) {
64             if ($info->{last} and $info->{longest} and
65             $info->{last} != $info->{longest} and
66             $info->{last}{start_period} == $info->{longest}{start_period}) {
67              
68             $info->{longest} = $info->{last};
69             } # end if last and longest are the same chain
70              
71             $end = $info->{last}{end_period} if $info->{last};
72             } else {
73             $info = {total_periods => 0, marked_periods => 0};
74             }
75              
76             $end ||= $self->start_date->clone;
77             my $inc = $self->increment;
78              
79             if (not $info->{last} and @$dates and $dates->[0] < $end) {
80             confess "start_date ($end) must be before first date ($dates->[0])";
81             }
82              
83             for my $d (@$dates) {
84             my $count = $self->_find_period($d, $end);
85              
86             undef $info->{last} if $count > 1; # the chain broke
87              
88             $info->{last} ||= {
89             start_event => $d,
90             start_period => $end->clone->subtract_duration( $inc ),
91             };
92              
93             ++$info->{last}{num_events};
94             if ($count) { # first event in period
95             ++$info->{last}{length};
96             ++$info->{marked_periods};
97             $info->{total_periods} += $count;
98             }
99             $info->{last}{end_event} = $d;
100             $info->{last}{end_period} = $end->clone;
101              
102             if (not $info->{longest}
103             or $info->{longest}{length} < $info->{last}{length}) {
104             $info->{longest} = $info->{last};
105             }
106             } # end for each $d in @$dates
107              
108             return $info;
109             } # end find_chains
110              
111             #---------------------------------------------------------------------
112             # Find the start of the first period *after* date:
113             #
114             # Returns the number of increments that had to be added to $end to
115             # make it greater than $date.
116              
117             sub _find_period
118             {
119             my ($self, $date, $end) = @_;
120              
121             my $count = 0;
122             my $inc = $self->increment;
123             my $skip = $self->skip;
124              
125             my $skip_this;
126             while ($date >= $end) {
127             $skip_this = $skip && $skip->($end);
128             $end->add_duration($inc);
129             redo if $skip_this;
130             ++$count;
131             }
132              
133             return $count;
134             } # end _find_period
135             #---------------------------------------------------------------------
136              
137              
138             sub period_containing
139             {
140             my ($self, $date) = @_;
141              
142             my $end = $self->start_date->clone;
143              
144             $self->_find_period($date, $end);
145              
146             $end->subtract_duration( $self->increment );
147             } # end period_containing
148              
149             #=====================================================================
150             # Package Return Value:
151              
152             __PACKAGE__->meta->make_immutable;
153             1;
154              
155             __END__
156              
157             =head1 NAME
158              
159             DateTimeX::Seinfeld - Calculate Seinfeld chain length
160              
161             =head1 VERSION
162              
163             This document describes version 1.000 of
164             DateTimeX::Seinfeld, released January 11, 2014.
165              
166             =head1 SYNOPSIS
167              
168             use DateTimeX::Seinfeld;
169              
170             my $seinfeld = DateTimeX::Seinfeld->new(
171             start_date => $starting_datetime,
172             increment => { weeks => 1 },
173             );
174              
175             my $chains = $seinfeld->find_chains( \@list_of_datetimes );
176              
177             say "Longest chain: $chains->{longest}{length}";
178             say "First event in longest chain: $chains->{longest}{start_event}";
179             say "The current chain may continue"
180             if $chains->{last}{end_period}
181             >= $seinfeld->period_containing( DateTime->now );
182              
183             =head1 DESCRIPTION
184              
185             DateTimeX::Seinfeld calculates the maximum Seinfeld chain length from
186             a sorted list of L<DateTime> objects.
187              
188             The term "Seinfeld chain" comes from advice attributed to comedian
189             Jerry Seinfeld. He got a large year-on-one-page calendar and marked a
190             big red X on every day he wrote something. The chain of continuous
191             X's gave him a sense of accomplishment and helped motivate him to
192             write every day.
193             (Source: L<http://lifehacker.com/281626/jerry-seinfelds-productivity-secret>)
194              
195             This module calculates the length of the longest such chain of
196             consecutive days. However, it generalizes the concept; instead of
197             having to do something every day, you can make it every week, or every
198             month, or any other period that can be defined by a
199             L<DateTime::Duration>.
200              
201             Some definitions: B<period> is the time period during which some
202             B<event> must occur in order to keep the chain from breaking. More
203             than one event may occur in a single period, but the period is only
204             counted once.
205              
206             =head1 ATTRIBUTES
207              
208             =head2 start_date
209              
210             This is the DateTime (or a hashref acceptable to C<< DateTime->new >>)
211             of the beginning of the first period. All events passed to
212             C<find_chains> must be greater than or equal to this value.
213             (required)
214              
215              
216             =head2 increment
217              
218             This is the DateTime::Duration (or a hashref acceptable to
219             C<< DateTime::Duration->new >>) giving the length of each period.
220             (required)
221              
222              
223             =head2 skip
224              
225             This is a CodeRef that allows you to skip specified periods. It is
226             called with one argument, the DateTime at which the period begins. If
227             the CodeRef returns a true value, any events taking place during this
228             period are instead considered to take place in the next period. (The
229             CodeRef must not modify the DateTime object it was given.) (optional)
230              
231             For example, to skip Sundays:
232              
233             skip => sub { shift->day_of_week == 7 }
234              
235             Using C<skip> does I<not> change the start time of the next period (as
236             reported by C<period_containing>, C<start_period>, or C<end_period>).
237             The idea is that events will not normally occur during skipped periods
238             (or you probably shouldn't be skipping them). This means that it is
239             possible for an event to be less than the start time of the period
240             containing it.
241              
242             =head1 METHODS
243              
244             =head2 find_chains
245              
246             $info = $seinfeld->find_chains( \@events );
247             $info = $seinfeld->find_chains( \@events, $info ); # continue search
248              
249             This calculates Seinfeld chains from the events in C<@events> (an
250             array of DateTime objects which must be sorted in ascending order).
251             Note that you must pass an array reference, not a list.
252              
253             The return value is a hashref describing the results.
254              
255             Two keys describe the number of periods. C<total_periods> is the
256             number of periods between the C<start_date> and
257             C<< $info->{last}{end_period} >>. C<marked_periods> is the number of
258             periods that contained at least one event. If C<marked_periods>
259             equals C<total_periods>, then the events form a single chain of the
260             same length.
261              
262             Two keys describe the chains: C<last> (the last chain of events found)
263             and C<longest> (the longest chain found). These may be the same chain
264             (in which case the values will be references to the same hash). If
265             there are multiple chains of the same length, C<longest> will be the
266             first such chain. The value of each key is a hashref describing that
267             chain with the following keys:
268              
269             =over
270              
271             =item C<start_period>
272              
273             The DateTime of the start of the period containg the first event of the chain.
274              
275             =item C<end_period>
276              
277             The DateTime of the start of the period where the chain broke
278             (i.e. the first period that didn't contain an event). If this is
279             greater than or equal to the period containing the current date (see
280             L</period_containing>), then the chain may still be extended.
281             Otherwise, the chain is already broken, and a future event would start
282             a new chain.
283              
284             =item C<start_event>
285              
286             The DateTime of the first event in the chain (this is the same object
287             that appeared in C<@events>, not a clone).
288              
289             =item C<end_event>
290              
291             The DateTime of the last event in the chain (again, the same object
292             that appeared in C<@events>).
293              
294             =item C<length>
295              
296             The number of periods in the chain.
297              
298             =item C<num_events>
299              
300             The number of events in the chain. This can never be less than
301             C<length>, but it can be more (if multiple events occurred in one period).
302              
303             =back
304              
305             Note: If C<@events> is empty, then C<last> and C<longest> will not
306             exist in the hash. Otherwise, there will always be at least one
307             chain, even if only of length 1.
308              
309             If you are monitoring an ongoing sequence of events, it would be
310             wasteful to have to start each search from the first event. Instead,
311             you can pass the hashref returned by the first search to
312             C<find_chains>, along with just the new events. The hashref you pass
313             will be modified (the same hashref will be returned). To simplify
314             this, it is not necessary that C<last> and C<longest> reference the
315             same hash if they are the same chain. If they have the same
316             C<start_period>, then C<find_chains> will link them automatically (by
317             setting S<C<< $info->{longest} = $info->{last} >>>).
318             When continuing a search, the C<start_date> is ignored. Instead, the
319             search resumes from C<< $info->{last}{end_period} >>.
320              
321             The only fields that you I<must> supply in order to continue a calculation
322             are C<start_period>, C<end_period>, & C<length> in C<< $info->{last} >>,
323             and C<start_period> & C<length> in C<< $info->{longest} >>.
324             However, any field that you don't supply can't be expected to hold
325             valid data afterwards.
326              
327             When continuing a calculation, C<@events> should not include any dates
328             before C<< $info->{last}{end_event} >>. If you disregard this rule,
329             any events less than C<< $info->{last}{end_period} >> are considered
330             to have occurred in the previous period (even if they actually
331             occurred in an even earlier period).
332              
333              
334             =head2 period_containing
335              
336             $start = $seinfeld->period_containing( $date );
337              
338             Returns the DateTime at which the period containing C<$date> (a
339             DateTime) begins.
340              
341             Note: If C<$date> occurs during a period that is skipped, then
342             C<$start> will be greater than C<$date>. Otherwise, C<$start> is
343             always less than or equal to C<$date>.
344              
345             =head1 DIAGNOSTICS
346              
347             =over
348              
349             =item C<start_date (%s) must be before first date (%s)>
350              
351             You must not pass an event to C<find_chains> that occurs before the
352             C<start_date> of the first period.
353              
354              
355             =back
356              
357             =head1 CONFIGURATION AND ENVIRONMENT
358              
359             DateTimeX::Seinfeld requires no configuration files or environment variables.
360              
361             =head1 DEPENDENCIES
362              
363             DateTimeX::Seinfeld requires
364             L<Moose>,
365             L<namespace::autoclean>,
366             L<MooseX::Types::DateTime>,
367             L<MooseX::Types::Moose>,
368             and Perl 5.10.0 or later.
369              
370             =head1 INCOMPATIBILITIES
371              
372             None reported.
373              
374             =head1 BUGS AND LIMITATIONS
375              
376             No bugs have been reported.
377              
378             =head1 AUTHOR
379              
380             Christopher J. Madsen S<C<< <perl AT cjmweb.net> >>>
381              
382             Please report any bugs or feature requests
383             to S<C<< <bug-DateTimeX-Seinfeld AT rt.cpan.org> >>>
384             or through the web interface at
385             L<< http://rt.cpan.org/Public/Bug/Report.html?Queue=DateTimeX-Seinfeld >>.
386              
387             You can follow or contribute to DateTimeX-Seinfeld's development at
388             L<< https://github.com/madsen/datetimex-seinfeld >>.
389              
390             =head1 COPYRIGHT AND LICENSE
391              
392             This software is copyright (c) 2014 by Christopher J. Madsen.
393              
394             This is free software; you can redistribute it and/or modify it under
395             the same terms as the Perl 5 programming language system itself.
396              
397             =head1 DISCLAIMER OF WARRANTY
398              
399             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
400             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
401             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
402             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
403             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
404             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
405             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
406             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
407             NECESSARY SERVICING, REPAIR, OR CORRECTION.
408              
409             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
410             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
411             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
412             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
413             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
414             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
415             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
416             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
417             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
418             SUCH DAMAGES.
419              
420             =cut