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   24211 use strict;
  1         2  
  1         38  
7              
8 1     1   6 no warnings;
  1         2  
  1         54  
9 1         79 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         2  
12 1     1   6 no warnings qw(exec newline unopened);
  1         2  
  1         35  
13              
14 1     1   6 use Carp;
  1         1  
  1         97  
15 1     1   6 use List::Util qw( first );
  1         2  
  1         121  
16              
17 1     1   6 use base qw(Range::Object);
  1         2  
  1         631  
18              
19             # Overload definitions
20              
21 1         6 use overload q{""} => 'stringify_collapsed',
22 1     1   1751 fallback => 1;
  1         1143  
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 15970 my ($class, $interval, @input_range) = @_;
32              
33 25 50 100     150 croak "Interval can be 15, 30 or 60 minutes not '$interval'"
      66        
34             if $interval != 15 && $interval != 30 && $interval != 60;
35              
36 25         170 my $self = bless { interval => $interval }, $class;
37              
38 25         125 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 29547 my ($self, $separator) = @_;
50              
51 36 100       181 return wantarray ? map { $self->_colonify($_) } $self->_full_range()
  242         445  
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 20302 my ($self, $separator) = @_;
69              
70 36 100       123 return $self->stringify_collapsed($separator) unless wantarray;
71              
72 72 100       244 return map {
73 18         91 !ref($_) ? $self->_colonify($_)
74             : {
75             start => $self->_colonify($_->{start}),
76             end => $self->_colonify($_->{end}),
77             count => $_->{count},
78             }
79             }
80 18         33 @{ $self->{range} };
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 54660 my ($self, $separator) = @_;
98              
99 18   33     100 $separator ||= $self->_list_separator;
100              
101 18 100       47 if ( wantarray ) {
102 36 100       206 return map {
103 9         30 !ref($_) ? 0 + $_
104             : {
105             start => 0 + $_->{start},
106             end => 0 + $_->{end},
107             count => $_->{count},
108             }
109             }
110 9         24 @{ $self->{range} };
111             }
112             else {
113             my @military_range
114 36 100       187 = map {
115 9         33 !ref($_) ? 0+$_
116             : $self->_stringify_range(0+$_->{start},
117             0+$_->{end})
118             }
119 9         17 @{ $self->{range} };
120              
121 9         53 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 180318 my ($self, @input_range) = @_;
136              
137 319 100       1725 return $self->SUPER::in(@input_range) unless wantarray;
138              
139 18         95 my @invalid_values = map { $self->_colonify($_) }
  180         312  
140             $self->SUPER::in(@input_range);
141              
142 18         213 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 28 my ($self, $separator) = @_;
152              
153 18   33     104 $separator ||= $self->_list_separator();
154 18         38 my $delimiter = $self->delimiter();
155              
156             my @full_range
157 242 50       422 = map { $self->_colonify($_) }
  242         466  
158             map {
159 18         57 ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
160             : "$_"
161             }
162             $self->_full_range();
163              
164 18         172 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 115 my ($self, $separator) = @_;
174              
175 36   33     119 $separator ||= $self->_list_separator();
176              
177             my @collapsed_range
178 144 100       283 = map { $self->_colonify($_) }
  144         563  
179             map {
180 36         88 ref($_) ? $self->_stringify_range( $_->{start}, $_->{end} )
181             : "$_"
182             }
183 36         54 @{ $self->{range} };
184              
185 36         206 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 1493 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 1040 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 2299 sub delimiter { '/' }
225              
226             ### PUBLIC INSTANCE METHOD ###
227             #
228             # Returns current object's interval length in minutes.
229             #
230              
231 717     717 0 4525 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   232 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   752 my ($self, @input_range) = @_;
250              
251 350         744 my $pattern = $self->pattern();
252 350         783 my $separator = $self->separator();
253 350         733 my $delimiter = $self->delimiter();
254 350         678 my $interval = $self->interval();
255              
256             # We use hash to remove duplicates
257 350         676 my %temp = ();
258              
259             # Validate and expand items in @input_range, add them if all OK
260             ITEM:
261 350         875 while ( @input_range ) {
262 774         1131 my $item = shift @input_range;
263              
264 774 100 66     5000 if ( $separator && $item =~ $separator ) {
265 11         156 unshift @input_range, split $separator, $item;
266 11         53 next ITEM;
267             };
268              
269 763 100 66     6031 croak "Invalid input: $item"
270             if $item && $item !~ $pattern;
271              
272             # Analogous to Range::Dates but not quite so
273 762         820 my @items;
274 762 100       5472 if ( $item =~ m{ ($pattern) / ($pattern) }xms ) {
    100          
275 25         74 my ($start, $end) = ($1, $2);
276              
277             # Remove colons
278 25         74 $start =~ s/://;
279 25         61 $end =~ s/://;
280              
281 25         94 my @full_list = $self->_explode_range($start.$delimiter.$end);
282 22         215 @temp{ @full_list } = (1) x @full_list;
283             }
284             elsif ( $item =~ / \A (\d{2}) : (\d{2}) \z /xms) {
285 736         1867 my ($hr, $mn) = ($1, $2);
286              
287 736 100 33     8546 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         2139 my $interval_value = sprintf "%02d%02d", $hr, $mn;
294 728         3555 @temp{ $interval_value } = 1;
295             }
296             else {
297 1         170 croak "Invalid input: '$item'";
298             };
299             };
300              
301             # Order of items is important, and we treat them as numbers
302 337         1411 my @validated_input = sort keys %temp;
303              
304 337         2019 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   242 my ($self, $item) = @_;
314              
315 143 50       639 croak "Invalid input: '$item'"
316             unless $item =~ m{ \A (\d{4}) / (\d{4}) \z }xms;
317              
318 143         336 my ($first, $last) = ($1, $2);
319 143         279 my $interval = $self->interval;
320              
321 143         602 my ($fhr, $fmn) = $first =~ /\A (\d{2}) (\d{2}) \z/xms;
322 143 50 33     996 croak "Invalid input: '$first'"
      33        
323             if $fhr < 0 || $fhr > 23 or ($fmn % $interval) != 0;
324              
325 143         521 my ($lhr, $lmn) = $last =~ /\A (\d{2}) (\d{2}) \z/xms;
326 143 100 33     1412 croak "Invalid input: '$last'"
      66        
327             if $lhr < 0 || $lhr > 23 or ($lmn % $interval) != 0;
328              
329 140 50 66     563 croak "Ending interval can't be less than starting interval"
      33        
330             if ($lhr < $fhr) || ($lhr == $fhr && $lmn < $fmn);
331              
332 140         151 my @result;
333 140         205 my ($hr, $mn) = ($fhr, $fmn);
334              
335 140         138 while (1) {
336 509         1064 push @result, sprintf "%02d%02d", $hr, $mn;
337              
338 509 100       1159 if ($mn < (60 - $interval)) { $mn += $interval }
  208 50       227  
339 301         267 elsif ($hr < 23) { $hr++; $mn = 0; }
  301         321  
340 0         0 else { $hr = $mn = 0; };
341              
342 509 100 50     1609 push @result, sprintf "%02d%02d", $hr, $mn and last
      100        
343             if ($hr == $lhr && $mn == $lmn);
344             };
345              
346 140         755 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   956 my ($self, $first, $last) = @_;
356              
357 656         2551 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   301 my ($self, $first, $last) = @_;
367              
368 224         351 my $interval = $self->interval;
369              
370 224         1505 my ($fhr, $fmn) = $first =~ /\A (\d{2}) (\d{2}) \z/xms;
371 224         580 my ($lhr, $lmn) = $last =~ /\A (\d{2}) (\d{2}) \z/xms;
372              
373             # Increment by interval
374 224         282 $fmn += $interval;
375              
376             # Check for overflown value
377 224 100       429 if ($fmn == 60) {
378 134         158 $fhr++;
379 134         154 $fmn = 0;
380             };
381              
382 224   100     1202 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   1240 my ($self, $value) = @_;
392              
393 930         1462 my $delim = $self->delimiter();
394              
395 930 50 66     3874 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       4976 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__