File Coverage

blib/lib/Range/Object/Interval.pm
Criterion Covered Total %
statement 130 132 98.4
branch 43 50 86.0
condition 35 59 59.3
subroutine 25 25 100.0
pod 10 11 90.9
total 243 277 87.7


line stmt bran cond sub pod time code
1             package Range::Object::Interval;
2              
3             # This is basically what common::sense does, but without the pragma itself
4             # to remain compatible with Perls older than 5.8
5              
6 1     1   47735 use strict;
  1         11  
  1         28  
7              
8 1     1   4 no warnings;
  1         2  
  1         38  
9 1         60 use warnings qw(FATAL closed internal debugging pack malloc portable
10             prototype inplace io pipe unpack deprecated glob digit
11 1     1   5 printf reserved taint closure semicolon);
  1         1  
12 1     1   4 no warnings qw(exec newline unopened);
  1         2  
  1         26  
13              
14 1     1   5 use Carp;
  1         1  
  1         52  
15 1     1   5 use List::Util qw( first );
  1         2  
  1         75  
16              
17 1     1   5 use base qw(Range::Object);
  1         12  
  1         341  
18              
19             # Overload definitions
20              
21 1         6 use overload q{""} => 'stringify_collapsed',
22 1     1   756 fallback => 1;
  1         719  
23              
24             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
25             #
26             # Initializes new instance of Range::Object::Interval from @input_range using
27             # $interval length.
28             #
29              
30             sub new {
31 25     25 1 14990 my ($class, $interval, @input_range) = @_;
32              
33 25 50 100     143 croak "Interval can be 15, 30 or 60 minutes not '$interval'"
      66        
34             if $interval != 15 && $interval != 30 && $interval != 60;
35              
36 25         93 my $self = bless { interval => $interval }, $class;
37              
38 25         93 return $self->add(@input_range);
39             }
40              
41             ### PUBLIC INSTANCE METHOD ###
42             #
43             # Returns sorted array or string representation of internal storage.
44             # In scalar context it can use optional list separator instead of
45             # default one.
46             #
47              
48             sub range {
49 36     36 1 21361 my ($self, $separator) = @_;
50              
51 36 100       174 return wantarray ? map { $self->_colonify($_) } $self->_full_range()
  242         417  
52             : $self->stringify($separator)
53             ;
54             }
55              
56             ### PUBLIC INSTANCE METHOD ###
57             #
58             # Returns sorted and collapsed representation of internal storage.
59             # In list context, resulting list consists of separate values and/or
60             # range hashrefs with three elements: start, end and count.
61             # In scalar context, result is a string of separate values and/or
62             # ranges separated by value returned by delimiter() method.
63             # Optional list separator can be used instead of default one in
64             # scalar context.
65             #
66              
67             sub collapsed {
68 36     36 1 13928 my ($self, $separator) = @_;
69              
70 36 100       121 return $self->stringify_collapsed($separator) unless wantarray;
71              
72             return map {
73             !ref($_) ? $self->_colonify($_)
74             : {
75             start => $self->_colonify($_->{start}),
76             end => $self->_colonify($_->{end}),
77             count => $_->{count},
78             }
79 72 100       189 }
80 18         24 @{ $self->{range} };
  18         68  
81             }
82              
83             ### PUBLIC INSTANCE METHOD ###
84             #
85             # Returns the current range in military format. In scalar context it
86             # can use optional list $separator instead of default one.
87             #
88             # Note that this format cannot be fed back to add() or remove() since
89             # there is no way to distinguish certain time intervals in military
90             # format from any run of the mill integer numbers.
91             #
92             # military() always returns collapsed output in both list and scalar
93             # context.
94             #
95              
96             sub military {
97 18     18 1 39118 my ($self, $separator) = @_;
98              
99 18   33     90 $separator ||= $self->_list_separator;
100              
101 18 100       49 if ( wantarray ) {
102             return map {
103             !ref($_) ? 0 + $_
104             : {
105             start => 0 + $_->{start},
106             end => 0 + $_->{end},
107             count => $_->{count},
108             }
109 36 100       180 }
110 9         22 @{ $self->{range} };
  9         30  
111             }
112             else {
113             my @military_range
114             = map {
115             !ref($_) ? 0+$_
116             : $self->_stringify_range(0+$_->{start},
117             0+$_->{end})
118 36 100       121 }
119 9         19 @{ $self->{range} };
  9         33  
120              
121 9         48 return join $separator, @military_range;
122             };
123              
124 0         0 return; # Just in case, as usual
125             }
126              
127             ### PUBLIC INSTANCE METHOD ###
128             #
129             # Tests if items of @input_range are matching items in our internal storage.
130             # Returns true/false in scalar context, list of mismatching items in list
131             # context.
132             #
133              
134             sub in {
135 319     319 1 89936 my ($self, @input_range) = @_;
136              
137 319 100       1202 return $self->SUPER::in(@input_range) unless wantarray;
138              
139 18         63 my @invalid_values = map { $self->_colonify($_) }
  180         341  
140             $self->SUPER::in(@input_range);
141              
142 18         178 return @invalid_values;
143             }
144              
145             ### PUBLIC INSTANCE METHOD ###
146             #
147             # Returns string representation of all items in internal storage (sorted).
148             #
149              
150             sub stringify {
151 18     18 1 40 my ($self, $separator) = @_;
152              
153 18   33     92 $separator ||= $self->_list_separator();
154 18         38 my $delimiter = $self->delimiter();
155              
156             my @full_range
157 242         406 = map { $self->_colonify($_) }
158             map {
159 18         56 ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
160 242 50       402 : "$_"
161             }
162             $self->_full_range();
163              
164 18         133 return join $separator, @full_range;
165             }
166              
167             ### PUBLIC INSTANCE METHOD ###
168             #
169             # Returns string representation of collapsed current range.
170             #
171              
172             sub stringify_collapsed {
173 36     36 1 122 my ($self, $separator) = @_;
174              
175 36   33     125 $separator ||= $self->_list_separator();
176              
177             my @collapsed_range
178 144         260 = map { $self->_colonify($_) }
179             map {
180             ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
181 144 100       368 : "$_"
182             }
183 36         60 @{ $self->{range} };
  36         76  
184              
185 36         161 return join $separator, @collapsed_range;
186             }
187              
188             ### PUBLIC INSTANCE METHOD ###
189             #
190             # Returns regex that is used to validate range items. Regex should
191             # include patterns both for separate disjointed items and contiguous
192             # ranges.
193             #
194              
195             sub pattern {
196 350     350 1 1259 return qr/
197             (?
198             (?: # Group but don't collect
199             \d{2} # Two digits
200             : # Colon
201             \d{2} # Two digits
202             ) # ... end group
203             /xms
204             }
205              
206             ### PUBLIC INSTANCE METHOD ###
207             #
208             # Returns regex that is used to separate items in a range list.
209             #
210              
211             sub separator {
212 350     350 1 812 return qr/
213             [,;] # Comma or semicolon
214             \s* # Greedy whitespace
215             /xms
216             }
217              
218             ### PUBLIC INSTANCE METHOD ###
219             #
220             # Returns range delimiter, for Interval it is forward slash (/) in
221             # compliance with ISO 8601.
222             #
223              
224 1477     1477 1 2170 sub delimiter { '/' }
225              
226             ### PUBLIC INSTANCE METHOD ###
227             #
228             # Returns current object's interval length in minutes.
229             #
230              
231 717     717 0 1140 sub interval { $_[0]->{interval} }
232              
233             ############## PRIVATE METHODS BELOW ##############
234              
235             ### PRIVATE INSTANCE METHOD ###
236             #
237             # Returns default list separator for use with stringify() and
238             # stringify_collapsed()
239             #
240              
241 72     72   230 sub _list_separator { q{,} }
242              
243             ### PRIVATE INSTANCE METHOD ###
244             #
245             # Validates and unpacks input @input_range of items, returns full list.
246             #
247              
248             sub _validate_and_expand {
249 350     350   651 my ($self, @input_range) = @_;
250              
251 350         677 my $pattern = $self->pattern();
252 350         682 my $separator = $self->separator();
253 350         618 my $delimiter = $self->delimiter();
254 350         607 my $interval = $self->interval();
255              
256             # We use hash to remove duplicates
257 350         547 my %temp = ();
258              
259             # Validate and expand items in @input_range, add them if all OK
260             ITEM:
261 350         723 while ( @input_range ) {
262 774         1174 my $item = shift @input_range;
263              
264 774 100 66     3889 if ( $separator && $item =~ $separator ) {
265 11         122 unshift @input_range, split $separator, $item;
266 11         46 next ITEM;
267             };
268              
269 763 100 66     4786 croak "Invalid input: $item"
270             if $item && $item !~ $pattern;
271              
272             # Analogous to Range::Dates but not quite so
273 762         1137 my @items;
274 762 100       4270 if ( $item =~ m{ ($pattern) / ($pattern) }xms ) {
    100          
275 25         85 my ($start, $end) = ($1, $2);
276              
277             # Remove colons
278 25         74 $start =~ s/://;
279 25         63 $end =~ s/://;
280              
281 25         90 my @full_list = $self->_explode_range($start.$delimiter.$end);
282 22         191 @temp{ @full_list } = (1) x @full_list;
283             }
284             elsif ( $item =~ / \A (\d{2}) : (\d{2}) \z /xms) {
285 736         2157 my ($hr, $mn) = ($1, $2);
286              
287 736 100 33     5713 croak "Invalid input: '$item'"
      66        
      100        
288             if ( $hr !~ / \A \d{1,2} \z /xms
289             || $hr < 0 || $hr > 23
290             )
291             || ($mn % $interval) != 0;
292              
293 728         2106 my $interval_value = sprintf "%02d%02d", $hr, $mn;
294 728         2601 @temp{ $interval_value } = 1;
295             }
296             else {
297 1         97 croak "Invalid input: '$item'";
298             };
299             };
300              
301             # Order of items is important, and we treat them as numbers
302 337         1284 my @validated_input = sort keys %temp;
303              
304 337         1557 return @validated_input;
305             }
306              
307             ### PRIVATE INSTANCE METHOD ###
308             #
309             # Explodes stringified range of items using Perl range operator.
310             #
311              
312             sub _explode_range {
313 143     143   275 my ($self, $item) = @_;
314              
315 143 50       585 croak "Invalid input: '$item'"
316             unless $item =~ m{ \A (\d{4}) / (\d{4}) \z }xms;
317              
318 143         395 my ($first, $last) = ($1, $2);
319 143         270 my $interval = $self->interval;
320              
321 143         445 my ($fhr, $fmn) = $first =~ /\A (\d{2}) (\d{2}) \z/xms;
322 143 50 33     739 croak "Invalid input: '$first'"
      33        
323             if $fhr < 0 || $fhr > 23 or ($fmn % $interval) != 0;
324              
325 143         431 my ($lhr, $lmn) = $last =~ /\A (\d{2}) (\d{2}) \z/xms;
326 143 100 33     995 croak "Invalid input: '$last'"
      66        
327             if $lhr < 0 || $lhr > 23 or ($lmn % $interval) != 0;
328              
329 140 50 66     423 croak "Ending interval can't be less than starting interval"
      33        
330             if ($lhr < $fhr) || ($lhr == $fhr && $lmn < $fmn);
331              
332 140         170 my @result;
333 140         216 my ($hr, $mn) = ($fhr, $fmn);
334              
335 140         187 while (1) {
336 509         1096 push @result, sprintf "%02d%02d", $hr, $mn;
337              
338 509 100       879 if ($mn < (60 - $interval)) { $mn += $interval }
  208 50       250  
339 301         352 elsif ($hr < 23) { $hr++; $mn = 0; }
  301         361  
340 0         0 else { $hr = $mn = 0; };
341              
342 509 100 50     1286 push @result, sprintf "%02d%02d", $hr, $mn and last
      100        
343             if ($hr == $lhr && $mn == $lmn);
344             };
345              
346 140         613 return @result;
347             }
348              
349             ### PRIVATE INSTANCE METHOD ###
350             #
351             # Tests if two values are equal.
352             #
353              
354             sub _equal_value {
355 656     656   1049 my ($self, $first, $last) = @_;
356              
357 656         1846 return !!($first eq $last);
358             }
359              
360             ### PRIVATE INSTANCE METHOD ###
361             #
362             # Tests if two values are consequent.
363             #
364              
365             sub _next_in_range {
366 224     224   351 my ($self, $first, $last) = @_;
367              
368 224         302 my $interval = $self->interval;
369              
370 224         658 my ($fhr, $fmn) = $first =~ /\A (\d{2}) (\d{2}) \z/xms;
371 224         536 my ($lhr, $lmn) = $last =~ /\A (\d{2}) (\d{2}) \z/xms;
372              
373             # Increment by interval
374 224         347 $fmn += $interval;
375              
376             # Check for overflown value
377 224 100       363 if ($fmn == 60) {
378 134         166 $fhr++;
379 134         160 $fmn = 0;
380             };
381              
382 224   100     769 return !!($fhr == $lhr && $fmn == $lmn);
383             }
384              
385             ### INTERNAL INSTANCE METHOD ###
386             #
387             # Formats time from 4-digit internal representation to HH:MM
388             #
389              
390             sub _colonify {
391 930     930   1422 my ($self, $value) = @_;
392              
393 930         1238 my $delim = $self->delimiter();
394              
395 930 50 66     3031 croak "Can't colonify invalid value '$value'"
396             unless $value =~ /\A (\d{2}) (\d{2}) \z/xms
397             || $value =~ /\A (\d{2}) (\d{2}) ($delim) (\d{2}) (\d{2}) \z/xms;
398              
399 930 100       4117 return $3 ne $delim
400             ? sprintf "%02d:%02d", $1, $2
401             : sprintf "%02d:%02d".$delim."%02d:%02d", $1, $2, $4, $5
402             ;
403             }
404              
405             1;
406              
407             __END__