File Coverage

blib/lib/Date/MonthSet.pm
Criterion Covered Total %
statement 192 204 94.1
branch 46 66 69.7
condition 14 28 50.0
subroutine 41 43 95.3
pod 16 16 100.0
total 309 357 86.5


line stmt bran cond sub pod time code
1             package Date::MonthSet;
2              
3 1     1   1306 use strict;
  1         3  
  1         165  
4              
5             =head1 NAME
6              
7             Date::MonthSet - simple interface to a collection of months
8              
9             =head1 SYNOPSIS
10              
11             my $set = new Date::MonthSet;
12              
13             # accessors: capitalized and lowercase forms. long and short forms.
14              
15             $set->january(1);
16             $set->february(1);
17             $set->March(1);
18             $set->September(1);
19             $set->Nov(1);
20             $set->dec(1);
21              
22             my $s = "$set"; # JFM-----S-ND
23              
24             $set->add('June');
25             $set->mark('July');
26              
27             my $s = "$set"; # JFM--JJ-S-ND
28              
29             # configurable placeholder
30              
31             $set->placeholder('*');
32              
33             my $s = "$set"; # JFM**JJ*S*ND
34              
35             $set->remove(qw(jun jul november December));
36             $set->clear('march', 'sep');
37              
38             my $s = "$set"; # JFM*********
39              
40             # testing for members
41            
42             $set->contains(qw(jan feb)); # true
43             $set->contains(2, 3); # true
44             $set->contains(1, 2, 3, 'dec'); # false
45              
46             # extracting data
47            
48             $set->months; # (January February March);
49             $set->months_numeric; # (1, 2, 3);
50              
51             # numerification (january is the least significant bit)
52              
53             my $i = $set + 0; # 7
54              
55             $set->march(0);
56             my $i = $set + 0; # 3
57              
58             $set->february(0);
59             my $i = $set + 0; # 1
60              
61             $set->jan(0);
62             my $i = $set + 0; # 0
63              
64             ---
65            
66             my $a;
67             my $b;
68             my $c;
69             my $d;
70              
71             # initialization of Date::MonthSet objects
72              
73             $a = new Date::MonthSet integer => 4; # march
74             $a = new Date::MonthSet integer => 5; # january and march
75             $a = new Date::MonthSet integer => 4095; # twelve set bits: all months
76              
77             $b = new Date::MonthSet string => 'JFM---JAS---';
78             $b = new Date::MonthSet string => '000111000111'; # inversed
79             $b = new Date::MonthSet string => '###AMJ###OND', placeholder => '#'; # the same
80              
81             $c = new Date::MonthSet set => [ 1 .. 12 ]; # all months
82             $c = new Date::MonthSet set => [ qw(April sep) ]; # april and september
83             $c = new Date::MonthSet set => [ 'jan', 2 .. 3 ]; # the first quarter
84              
85             # comparison between Date::MonthSet objects
86              
87             $d = new Date::MonthSet set => [ qw(apr may jun oct nov dec) ];
88             $d == $a; # false (six months vs twelve)
89             $d == $b; # true (same six months)
90             $a == $c; # false (six months vs three)
91              
92             $d < $a; # true (six months vs twelve)
93             $d < $b; # false (equal)
94             $d < $c; # false (six months vs three)
95              
96             $d = new Date::MonthSet set => [ qw(oct nov dec) ];
97             $d == $a; # false (three months vs twelve)
98             $d == $b; # false (three months vs six)
99             $d == $c; # false (not the same three months)
100              
101             $d < $a; # true (three months vs twelve)
102             $d < $b; # true (three months vs six)
103             $d < $c; # false ($d is later in the year than $c)
104              
105             # addition and subtraction return new Date::MonthSet objects
106              
107             $a - $d; # JFMAMJJAS---
108             $a - $b; # JFM---JAS---
109              
110             $b + $c; # JFMAMJ---OND
111             $b + $c - $d; # JFMAMJ------
112              
113             =head1 DESCRIPTION
114              
115             =cut
116              
117             our $VERSION = 0.2;
118              
119 1     1   1216 use POSIX qw(isdigit isprint);
  1         8460  
  1         7  
120              
121 1     1   1238 use overload '""' => \&stringify;
  1         2  
  1         15  
122 1     1   80 use overload '0+' => \&numerify;
  1         2  
  1         5  
123 1     1   50 use overload '==' => \&equal;
  1         2  
  1         5  
124 1     1   65 use overload '!=' => sub { not equal @_ };
  1     2   2  
  1         7  
  2         93  
125 1     1   50 use overload '<=>' => \&compare;
  1         1  
  1         4  
126 1     1   51 use overload '+' => \&addition;
  1         1  
  1         4  
127 1     1   59 use overload '-' => \&subtraction;
  1         1  
  1         4  
128              
129 1     1   48 use constant COMPLEMENT => -1;
  1         9  
  1         69  
130 1     1   5 use constant CONJUNCTION => -2;
  1         2  
  1         57  
131 1     1   5 use constant JANUARY => 0;
  1         2  
  1         35  
132 1     1   5 use constant FEBRUARY => 1;
  1         2  
  1         40  
133 1     1   5 use constant MARCH => 2;
  1         2  
  1         40  
134 1     1   4 use constant APRIL => 3;
  1         2  
  1         41  
135 1     1   4 use constant MAY => 4;
  1         1  
  1         36  
136 1     1   4 use constant JUNE => 5;
  1         3  
  1         55  
137 1     1   5 use constant JULY => 6;
  1         2  
  1         43  
138 1     1   5 use constant AUGUST => 7;
  1         1  
  1         109  
139 1     1   5 use constant SEPTEMBER => 8;
  1         28  
  1         49  
140 1     1   5 use constant OCTOBER => 9;
  1         2  
  1         44  
141 1     1   5 use constant NOVEMEBER => 10;
  1         2  
  1         40  
142 1     1   4 use constant DECEMBER => 11;
  1         2  
  1         153  
143              
144             my @months = qw(January February March April May June July
145             August September October November December);
146              
147             # create four accessors for each month. for example,
148             # January will have all four of the following accessors:
149             #
150             # - January
151             # - january
152             # - Jan
153             # - jan
154              
155             for (my $i = 0; $i < scalar @months; $i++) {
156             my $j = $i;
157 16 100   16   97 my $sub = sub { scalar @_ > 1 ? $_[0]->[$j] = ($_[1]) ? 1 : 0 : $_[0]->[$j] };
    50          
158              
159 1     1   6 no strict 'refs';
  1         2  
  1         105  
160              
161             *{__PACKAGE__ . '::' . $_} = $sub
162             foreach map { $_, substr($_, 0, 3) } $months[$i], lc $months[$i];
163              
164 1     1   5 use strict 'refs';
  1         2  
  1         2552  
165             }
166              
167             =head1 METHODS
168              
169             =head2 new
170              
171             =over 2
172              
173             instantiate a new Date::MonthSet object. if no arguments
174             are supplied, an empty Date::MonthSet object will be created
175             with the placeholder set to a dash '-'.
176              
177             a Date::MonthSet object can be initialized in several ways.
178             the constructor accepts the following options, passed as a
179             hash:
180              
181             =over 2
182              
183             =over 2
184              
185             =item placeholder
186              
187             defines the placeholder value to be used during the parsing
188             of string values and the generation of flattened strings.
189             the default placeholder is a single dash ('-', 0x2d).
190              
191             =item integer
192              
193             initialize the Date::MonthSet object according to a single
194             12-bit integer value describing the months in the collection.
195             the least significant bit represents January while the most
196             significant bit represents December.
197              
198             =item string
199              
200             initializes the Date::MonthSet object according to a string
201             value describing the months in the collection. two formats
202             are accepted.
203              
204             the first format is a simple twelve character sequence of
205             zeroes and ones. the first byte in the sequence represents
206             January while the twelfth byte represents December. if more
207             that twelve bytes are specified, the constructor will die.
208              
209             the second format is identifical to the format produced by
210             stringification of a Date::MonthSet object. the value of
211             the placeholder is taken into account. if the month values
212             deviate from the standard JFMAMJJASOND, the constructor will
213             die. if more values are parsed out of the string than there
214             should be, the constructor will die.
215              
216             =item set
217              
218             initializes the Date::MonthSet object according to an array
219             of long month names, short month names, and/or numerical
220             indices. all three forms may be combined. duplicates are
221             ignored.
222              
223             =back
224              
225             =back
226              
227             =back
228              
229             =cut
230              
231             sub new
232             {
233 33     33 1 160303 my $proto = shift;
234 33   33     273 my $class = ref($proto) || $proto;
235              
236 33         166 my $aref = [ ((0) x 12), '%M', '-' ];
237              
238 33         113 my %opts = @_;
239              
240 33 100       112 warn 'placeholder option is deprecated' if exists $opts{placeholder};
241              
242 33   100     384 my $fmt_complement = $opts{format_complement} || $opts{placeholder} || '-';
243 33   100     433 my $fmt_conjunction = $opts{format_conjunction} || '%M';
244              
245 33         70 $aref->[COMPLEMENT] = $fmt_complement;
246 33         58 $aref->[CONJUNCTION] = $fmt_conjunction;
247              
248 33 100       98 if (my $val = $opts{integer}) {
249 6 50       131 die 'integer attribute specified to constructor, ' .
250             'but no integer value was specified!' if not isdigit $val;
251              
252 6 100       20 do { $aref->[$_] = (($val >> $_) % 2 == 1) ? 1 : $aref->[$_] } for 0 .. 11;
  72         167  
253             }
254              
255 33 100       89 if (my $val = $opts{string}) {
256 11 50       177 die 'string attribute specified to constructor, but no string value ' .
257             'was specified!' if not isprint $val;
258              
259 11         22 my @a;
260              
261 11 100       54 if ($val =~ /^[01]{12}$/) {
262 2         29 @a = split //, $val;
263             } else {
264 9         20 my @conjunctions = ();
265 9         23 my @complements = ();
266 9         19 my $re = '';
267              
268 9         55 foreach my $m (split //, 'JFMAMJJASOND') {
269 108         255 my $re_conj = $aref->[CONJUNCTION];
270 108         159 my $re_comp = $aref->[COMPLEMENT];
271              
272 108         344 $re_conj =~ s/%M/$m/g;
273 108         573 $re_comp =~ s/%M/$m/g;
274              
275 108         183 push @conjunctions, $re_conj;
276 108         122 push @complements, $re_comp;
277              
278 108         271 $re .= "(\Q$re_conj\E|\Q$re_comp\E)";
279             }
280              
281 9 100       430 @a = map { $_ eq shift @complements ? 0 : 1 } ($val =~ /^$re$/i);
  60         331  
282             }
283              
284 11 100       90 die 'unable to parse string attribute' if not scalar @a == 12;
285              
286 7         55 splice @$aref, 0, 11, @a;
287             }
288              
289 29 100       215 if (my $val = $opts{set}) {
290 15 50       60 die 'set attribute specified to constructor, but no set was ' .
291             'specified!' if not ref($val) eq 'ARRAY';
292              
293 15         33 my @numbers = grep { isdigit $_ } @$val;
  63         280  
294 15         29 my @terms = map { lc } grep { not isdigit $_ } @$val;
  14         37  
  63         488  
295              
296 49 50 33     1366 do { die "month number $_ is out of range" if $_ < 1 || $_ > 12 }
297 15         37 foreach @numbers;
298              
299 15         67 $aref->[$_-1] = 1 foreach @numbers;
300              
301 15         50 foreach my $term (@terms) {
302 14         39 for (my $i = 0; $i < scalar @months; $i++) {
303 168         534 my $month = lc $months[$i];
304 168 100 100     2029 $aref->[$i] = 1 if $term eq $month || $term eq substr $month, 0, 3;
305             }
306             }
307             }
308              
309 29 50       83 if (my $val = $opts{list}) {
310 0 0       0 die 'list attribute specified to constructor, but no list was ' .
311             'specified' if not ref($val) eq 'ARRAY';
312 0 0       0 die 'a list must have exactly twelve values!'
313             if 12 != scalar @$val;
314              
315 0         0 for (my $i = 0; $i < 11; $i++) {
316 0 0       0 $aref->[$i] = $val->[$i] ? 1 : 0;
317             }
318             }
319              
320 29         244 return bless $aref, $class;
321             }
322              
323             =head2 months
324              
325             =cut
326              
327             sub months
328             {
329 0     0 1 0 my $self = shift;
330 0         0 my $i = -1;
331              
332 0 0       0 return map { $i++; $_ == 1 ? ($months[$i]) : () } @$self[0..11];
  0         0  
  0         0  
333             }
334              
335             =head2 months_numeric
336              
337             =cut
338              
339             sub months_numeric
340             {
341 34     34 1 40 my $self = shift;
342 34         40 my $i = -1;
343              
344 34 100       72 return map { $i++; $_ == 1 ? $i+1 : () } @$self[0..11];
  408         357  
  408         813  
345             }
346              
347             =head2 mark/add
348              
349             =cut
350              
351             sub mark
352             {
353 8     8 1 10315 my $self = shift;
354              
355 8         43 return $self->$_(1) foreach @_;
356             }
357              
358 4     4 1 8462 sub add { return shift->mark(@_) }
359              
360             =head2 clear/remove
361              
362             =cut
363              
364             sub clear
365             {
366 8     8 1 8668 my $self = shift;
367              
368 8         46 return $self->$_(0) foreach @_;
369             }
370              
371 4     4 1 9155 sub remove { return shift->clear(@_) }
372              
373             =head2 contains
374              
375             =cut
376              
377             sub contains
378             {
379 22     22 1 7245 my $self = shift;
380              
381 22         40 my @numbers = grep { isdigit $_ } @_;
  48         281  
382 22         37 my @terms = map { lc } grep { not isdigit $_ } @_;
  34         86  
  48         183  
383 22         29 my $i = 0;
384              
385 14 50 33     73 do { die "month number $_ is out of range" if $_ < 1 || $_ > 12 }
386 22         46 foreach @numbers;
387              
388             # XXX: die if we are passed a term we don't recognize?
389             #
390             # do {
391             # my $term = $_;
392             # die "term $t does not describe a month"
393             # if not grep { $term eq lc($_) || $term eq substr(lc($_), 0, 3) };
394             # } foreach @terms;
395              
396 22         38 foreach my $term (@terms) {
397 102 100       491 $i += scalar grep { ($term eq lc($_)) || ($term eq substr(lc($_), 0, 3)) }
  102         154  
398 34         78 @months[map { $_ -1 } $self->months_numeric];
399             }
400              
401 22         60 $i += $self->[$_-1] foreach @numbers;
402              
403 22 100       152 return $i == scalar(@_) ? 1 : 0;
404             }
405              
406             =head2 placeholder
407              
408             =cut
409              
410             sub placeholder
411             {
412 0     0 1 0 my $self = shift;
413              
414 0         0 warn 'Date::MonthSet->placeholder is deprecated';
415              
416 0         0 return ($self->format(undef, @_))[1];
417             }
418              
419             =head2 format
420              
421             gets/sets the format used in stringification. when setting
422             the format, the first argument defines the format to be used
423             when the month is contained within the set while the second
424             argument defines the format to be used when the month is not
425             contained within the set. if undef is specified for either
426             of them, the current setting is unchanged.
427              
428             =cut
429              
430             sub format
431             {
432 3     3 1 7 my $self = shift;
433 3         5 my $fmt_conjunction = shift;
434 3         7 my $fmt_complement = shift;
435              
436 3 100       10 $self->[CONJUNCTION] = $fmt_conjunction if defined $fmt_conjunction;
437 3 100       9 $self->[COMPLEMENT] = $fmt_complement if defined $fmt_complement;
438              
439 3         11 return @$self[CONJUNCTION,COMPLEMENT];
440             }
441              
442             =head2 stringify
443              
444             =cut
445              
446             sub stringify
447             {
448 4     4 1 9 my $self = shift;
449              
450 48 100       96 return join '', map {
451 4         11 my $s = $self->[$_] ? $self->[CONJUNCTION] : $self->[COMPLEMENT];
452 48         76 $s =~ s/%M/substr($months[$_], 0, 1)/eg;
  21         51  
453 48         123 $s;
454             } JANUARY .. DECEMBER;
455             }
456              
457             =head2 numerify
458              
459             =cut
460              
461             sub numerify
462             {
463 18     18 1 29 my $self = shift;
464 18         29 my $val = 0;
465 18         26 my $i = 0;
466              
467 18         124 $val += $_ << $i++ foreach @$self[0..11];
468              
469 18         120 return $val;
470             }
471              
472             =head2 equal
473              
474             =cut
475              
476             sub equal
477             {
478 4     4 1 23 my $a = shift;
479 4         9 my $b = shift;
480              
481 4 50 33     115 die "can only test equality on another Date::MonthSet object"
482             if !UNIVERSAL::isa($a, 'Date::MonthSet')
483             || !UNIVERSAL::isa($b, 'Date::MonthSet');
484            
485 4 100       237 do { return 0 if $a->[$_] != $b->[$_] } for 0 .. 11;
  26         253  
486              
487 2         16 return 1;
488             }
489              
490             =head2 compare
491              
492             =cut
493              
494             sub compare
495             {
496 6     6 1 21 my $a = shift;
497 6         10 my $b = shift;
498              
499 6 50 33     48 die "can only compare to another Date::MonthSet object"
500             if !UNIVERSAL::isa($a, 'Date::MonthSet')
501             || !UNIVERSAL::isa($b, 'Date::MonthSet');
502              
503 6         16 my $amonths = scalar grep { $_ == 1 } @$a[0..11];
  72         113  
504 6         13 my $bmonths = scalar grep { $_ == 1 } @$a[0..11];
  72         774  
505              
506 6 50       20 return $amonths <=> $bmonths if $amonths != $bmonths;
507 6         18 return $a->numerify <=> $b->numerify;
508             }
509              
510             =head2 addition
511              
512             =cut
513              
514             sub addition
515             {
516 1     1 1 8 my $a = shift;
517 1         3 my $b = shift;
518              
519 1 50 33     23 die "can only add another Date::MonthSet object"
520             if !UNIVERSAL::isa($a, 'Date::MonthSet')
521             || !UNIVERSAL::isa($b, 'Date::MonthSet');
522              
523 1         5 return new Date::MonthSet integer => $a->numerify | $b->numerify;
524             }
525              
526             =head2 subtraction
527              
528             =cut
529              
530             sub subtraction
531             {
532 1     1 1 13 my $a = shift;
533 1         3 my $b = shift;
534              
535 1 50 33     13 die "can only subtract another Date::MonthSet object"
536             if !UNIVERSAL::isa($a, 'Date::MonthSet')
537             || !UNIVERSAL::isa($b, 'Date::MonthSet');
538              
539 1         7 return new Date::MonthSet integer => $a->numerify ^ ($a->numerify & $b->numerify);
540             }
541              
542             =head1 AUTHOR
543              
544             Mike Eldridge
545              
546             =head1 LICENSE
547              
548             this library is free software. you may distribute it
549             and/or modify it under the same terms as perl itself.
550              
551             =cut
552              
553             1;