File Coverage

blib/lib/Calendar/Slots.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             package Calendar::Slots;
2             {
3             $Calendar::Slots::VERSION = '0.15';
4             }
5 2     2   58706 use Moose;
  2         1138525  
  2         21  
6 2     2   20886 use MooseX::AttributeHelpers;
  0            
  0            
7             use Carp;
8             use Calendar::Slots::Slot;
9             use Calendar::Slots::Utils;
10              
11             has 'overlapping' => ( is => 'rw', isa => 'Bool', default=>0 );
12             has 'slots' => (
13             metaclass => 'Collection::Array',
14             is => 'ro',
15             isa => 'ArrayRef[Calendar::Slots::Slot]',
16             default => sub { [] },
17             provides => {
18             'push' => 'add_slots',
19             'pop' => 'remove_last_slot',
20             'shift' => 'remove_first_slot',
21             'unshift' => 'insert_slots',
22             'get' => 'get_slot_at',
23             'set' => 'set_slot_at',
24             'count' => 'num_slots',
25             'empty' => 'has_slots',
26             'clear' => 'clear_slots',
27             }
28             );
29              
30             sub slot {
31             my $self = shift;
32             my @ret;
33             for my $slot ( $self->_create_slots( @_ ) ) {
34             $self->_validate($slot) unless $self->overlapping;
35             my @slots = $self->_merge( {}, $slot, $self->all );
36             $self->clear_slots;
37             $self->add_slots( @slots );
38             push @ret, @slots;
39             }
40             return @ret;
41             }
42              
43             sub _create_slots {
44             my $self = shift;
45              
46             scalar(@_) == 1 and return $_[0];
47             my %args = format_args(@_);
48              
49             $args{start_date} and !$args{end_date} and croak 'Missing end_date';
50             $args{end_date} and !$args{start_date} and croak 'Missing start_date';
51              
52             if ( $args{start_date} && $args{end_date} ) {
53              
54             my $start_dt = parse_dt( '%Y%m%d', $args{start_date} )
55             or croak "Could not parse start_date '$args{start_date}'";
56             my $end_dt = parse_dt( '%Y%m%d', $args{end_date} )
57             or croak "Could not parse end_date '$args{end_date}'";
58             delete $args{start_date};
59             delete $args{end_date};
60             my @slots;
61             for( my $dt=$start_dt; $dt <= $end_dt; $dt->add( days=>1 ) ) {
62             push @slots, $self->_create_slots(date=>$dt->ymd, %args);
63             }
64             return @slots;
65             }
66             elsif ( $args{start} > $args{end} ) {
67             my $current = Calendar::Slots::Slot->new( %args, end => '2400' );
68             my $next = Calendar::Slots::Slot->new( %args, start => '0000' );
69             $next->reschedule( days=>1 );
70             return ($current, $next);
71             }
72             else {
73             return Calendar::Slots::Slot->new(%args);
74             }
75             }
76              
77             #check if different slots overlap
78             sub _validate {
79             my $self = shift;
80             my $slot = shift;
81             }
82              
83             #merge slots that are next to each other
84             sub _merge {
85             my $self = shift;
86             my $opts = shift;
87             my $new = shift;
88             my @slots = @_;
89             unless( scalar @slots ) {
90             return $new;
91             }
92             my $slot = shift @slots;
93             if( $opts->{materialize} ) {
94             if( ! $slot->same_weekday( $new ) ) {
95             return ( $slot, $self->_merge( $opts, $new, @slots ) );
96             }
97             }
98             elsif( !( $slot->same_type($new) && $slot->same_day($new) ) ) {
99             # skip this slot
100             return ( $slot, $self->_merge( $opts, $new, @slots ) );
101             }
102             my ( $s1, $s2, $n1, $n2 ) = (
103             $slot->start, $slot->end, $new->start, $new->end
104             );
105             #warn join ';', 'new', $new->name, ';', $n1, $n2, '***', $slot->name, $s1, $s2;
106              
107             my $same_name = $slot->name eq $new->name;
108             if ( ! $same_name && $self->overlapping) {
109             # overlapping ok
110             return ($slot, $self->_merge( $opts, $new, @slots) );
111             }
112             # invalid new, remove
113             elsif ( $n1 == $n2 ) {
114             return ( $slot, @slots );
115             }
116             # invalid slot, remove
117             elsif ( $s1 == $s2 ) {
118             return $self->_merge($opts, $new, @slots);
119             }
120             # equals => discard slot, keep new, discard old
121             elsif ( $s1 == $n1 and $s2 == $n2 ) {
122             return $self->_merge($opts, $new, @slots);
123             }
124             # s: 10-12, n: 09-13 => outsider, keep new, discard old
125             elsif ( $n1 <= $s1 and $s2 <= $n2 ) {
126             return $self->_merge( $opts, $new, @slots );
127             }
128             # s: 10-12, n: 11-12 => insider
129             elsif ( $n1 >= $s1 and $n2 <= $s2 ) {
130             if( $same_name ) { # discard old, expand new
131             $new->start( $slot->start );
132             $new->end( $slot->end );
133             return $self->_merge( $opts, $new, @slots );
134             } else {
135             my $third = new Calendar::Slots::Slot(
136             name => $slot->name,
137             data => $slot->data,
138             when => $slot->when,
139             start => $new->end,
140             end => $slot->end
141             );
142             #warn "THIRS=================" . join ',', $third->start, $third->end;
143             $slot->end( $new->start );
144             return $self->_merge( $opts, $new, $self->_merge( $opts, $third, $self->_merge( $opts, $slot, @slots ) ) );
145             }
146             }
147             # s: 10-12, n: 09-12 => merge start
148             elsif ( $n1 <= $s1 and ( $n2 >= $s1 and $n2 <= $s2 ) ) {
149             if( $same_name ) {
150             $new->end( $slot->end );
151             return $self->_merge( $opts, $new, @slots );
152             } else {
153             $slot->start( $new->end );
154             return ($slot, $self->_merge( $opts, $new, @slots ) );
155             }
156             }
157             # s: 10-12, n: 11-13 => merge end
158             elsif ( ( $n1 >= $s1 and $n1 <= $s2 ) and $n2 >= $s2 ) {
159             if( $same_name ) {
160             $new->start( $slot->start );
161             return $self->_merge( $opts, $new, @slots );
162             } else {
163             $slot->end( $new->start );
164             return ($slot, $self->_merge( $opts, $new, @slots ) );
165             }
166             }
167             # s: 10-12, n: 01-05 => add
168             else {
169             return ($slot, $self->_merge( $opts, $new, @slots) );
170             }
171             }
172              
173              
174             sub all {
175             my $self = shift;
176             @{ $self->slots };
177             }
178              
179             sub sorted {
180             my $self = shift;
181             sort {
182             $a->numeric <=> $b->numeric
183             } $self->all;
184             }
185              
186             sub clone {
187             my $self = shift;
188             my $new = __PACKAGE__->new( %$self, slots=>[] );
189             $new->clear_slots;
190             $new->slot( %$_ ) for $self->all;
191             return $new;
192             }
193              
194             sub week_of {
195             my ($self, $date ) = @_;
196             $date =~ s/\D//g;
197              
198             # clone
199             $self = $self->clone;
200              
201             # find a monday
202             my $dt = parse_dt( '%Y%m%d', $date );
203             my $wk = $dt->wday;
204             my $ep = $dt->epoch;
205             # sunday
206             my $sunday_ep = $ep + ( (7-$wk) * 86400 );
207             my $sunday = substr DateTime->from_epoch( epoch=>$sunday_ep ), 0, 10;
208             # monday
209             $wk = 7 if $wk == 0;
210             my $monday_ep = $ep - ( ( $wk - 1 ) * 86400 );
211             my $monday = substr DateTime->from_epoch( epoch=>$monday_ep ), 0, 10;
212             $sunday =~ s/\D//g;
213             $monday =~ s/\D//g;
214             # die "$monday - $sunday";
215             sub _dump { require YAML; YAML::Dump( \@_ ) };
216             return $self->materialize( $monday, $sunday );
217             }
218              
219             sub materialize {
220             my ($self, $start, $end) = @_;
221              
222             # get rid of dates outside this date range
223             my @s_wk ;
224             my @s_date;
225             for my $slot ( $self->all ) {
226             if( $slot->type eq 'weekday' ) {
227             push @s_wk, $slot;
228             }
229             elsif( $start <= $slot->when && $slot->when <= $end ) {
230             push @s_date, $slot;
231             }
232             }
233              
234             # merge materialized
235             $self->clear_slots;
236             my @slots = @s_wk;
237             for( @s_date ) {
238             @slots = $self->_merge( { materialize => 1 }, $_, @slots ); # put the dates first
239             }
240             $self->clear_slots;
241             $self->add_slots( @slots );
242             return $self;
243             }
244              
245             sub find {
246             my $self = shift;
247             my %args = @_;
248             for my $slot ( grep { $_->type eq 'date' } $self->all ) {
249             return $slot
250             if $slot->contains(%args);
251             }
252             for my $slot ( grep { $_->type eq 'weekday' } $self->all ) {
253             return $slot
254             if $slot->contains(%args);
255             }
256             }
257              
258             sub name {
259             my $slot;
260             return $slot->name if $slot = find( @_ );
261             }
262              
263             sub as_table {
264             my $self = shift;
265             require Data::Format::Pretty::Console;
266             return Data::Format::Pretty::Console::format_pretty(
267             [ map { { %$_ } } $self->sorted ],
268             { table_column_orders=>[ [qw/name start end when type _weekday/] ] }
269             );
270             }
271              
272             1;
273              
274             __END__
275              
276             =pod
277              
278             =head1 NAME
279              
280             Calendar::Slots - Manage time slots
281              
282             =head1 VERSION
283              
284             version 0.15
285              
286             =head1 SYNOPSIS
287              
288             use Calendar::Slots;
289             my $cal = new Calendar::Slots;
290             $cal->slot( date=>'2009-10-11', start=>'10:30', end=>'11:30', name=>'busy' );
291             my $slot = $cal->find( date=>'2009-10-11', time=>'11:00' );
292             print $slot->name; # 'busy'
293              
294             =head1 DESCRIPTION
295              
296             This is a simple module to manage a calendar of very generic time slots. Time slots are anything
297             with a start and end time on a given date or weekday. Time slots cannot overlap. If a new
298             time slot overlaps another pre-existing time slot, the calendar will acommodate the slot automatically.
299              
300             It handles two types of slots: fixed dates, or recurring on weekdays.
301             When looking for an event, it will search from most specific (date) to more
302             generic (recurring). That is, if a slot exist for both a date and a weekday,
303             it returns the date slot only.
304              
305             The calendar is able to compact itself and generate rows that can be easily
306             stored in a file or database.
307              
308             =head1 LIMITATIONS
309              
310             Some of it current limitations:
311              
312             =over
313              
314             =item * No overlapping of time slots.
315              
316             =item * If a time-slot spans over midnight, two slots will be created, one for the
317             selected date until midnight, and another for the next day from midnight until end-time.
318              
319             =item * It does not handle timezones.
320              
321             =item * It does not know of daylight-savings or any other DateTime features.
322              
323             =back
324              
325             =head1 METHODS
326              
327             =head2 slot ( name=>Str, { date=>'YYYY-MM-DD' | weekday=>1..7 | start_date/end_date }, start=>'HH:MM', end=>'HH:MM' )
328              
329             Add a time slot to the calendar.
330              
331             If the new time slot overlaps an existing slot with the same C<name>,
332             the slots are merged and become a single slot.
333              
334             If the new time slot overlaps an existing slot with a different C<name>,
335             it overwrites the previous slot, splitting it if necessary.
336              
337             my $cal = Calendar::Slots->new;
338            
339             # reserve that monday slot
340              
341             $cal->slot( date=>'2009-11-30', start=>'10:30', end=>'11:00', name=>'doctor appointment' );
342              
343             # create a time slot for a given date
344              
345             $cal->slot( date=>'2009-01-01', start=>'10:30', end=>'24:00' );
346              
347             # create a recurring time slot over 3 calendar days
348              
349             $cal->slot( start_date=>'2009-01-01', end_date=>'2009-02-01', start=>'10:30', end=>'24:00' );
350              
351             =head2 find ( { date=>'YYYY-MM-DD' | weekday=>1..7 }, time=>'HH:MM' )
352              
353             Returns a L<Calendar::Slots::Slot> object for a given .
354              
355             $cal->find( weekday=>1, time=>'11:30' ); # find what's on Monday at 11:30
356              
357             =head2 name
358              
359             Shortcut method to L<find|/find> a slot and return a name.
360              
361             =head2 sorted
362              
363             Returns a ARRAY of all slot objects in the calendar.
364              
365             =head2 materialize ( start_date, end_date )
366              
367             Returns an instance of L<Calendar::Slots> with
368             date slots converted into weekdays for a given
369             date range.
370              
371             my $new_cal = $cal->materialize( 2012_10_22, 2012_10_28 );
372              
373             =head2 week_of ( date )
374              
375             Returns a materialized instance of L<Calendar::Slots> with actual
376             dates merged for the week that comprises
377             the passed C<date>.
378              
379             my $week = $cal->week_of( 2012_10_22 );
380             $week->find( weekday=>2, time=>10_30 ); # ...
381              
382             =head2 all
383              
384             Returns an ARRAY of all slot objects in the calendar.
385              
386             =head2 as_table
387              
388             Returns a console string as a table for the calendar.
389              
390             Requires that L<Data::Format::Pretty::Console> be installed.
391              
392             print $cal->as_table;
393              
394             =head1 SEE ALSO
395              
396             L<DateTime::SpanSet>
397              
398             =head1 TODO
399              
400             There are many improvements planned for this module, as this is just
401             an ALPHA release that allows me to get somethings done at $work...
402              
403             =over
404              
405             =item * Other types of recurrence: first Monday, last Friday of September...
406              
407             =item * Merge several calendars into one.
408              
409             =item * Create subclasses of Calendar::Slots::Slot for each slot type.
410              
411             =item * Better input formatting based on DateTime objects and the such.
412              
413             =head1 AUTHOR
414              
415             Rodrigo de Oliveira C<rodrigolive@gmail.com>
416              
417             =head1 LICENSE
418              
419             This library is free software. You can redistribute it and/or modify it under
420             the same terms as Perl itself.
421              
422             =cut