File Coverage

lib/Badger/Period.pm
Criterion Covered Total %
statement 70 94 74.4
branch 23 48 47.9
condition 21 55 38.1
subroutine 14 19 73.6
pod 2 16 12.5
total 130 232 56.0


line stmt bran cond sub pod time code
1             package Badger::Period;
2              
3             use Badger::Class
4 2         19 version => 0.01,
5             debug => 0,
6             base => 'Badger::Comparable',
7             utils => 'numlike is_object',
8             accessors => 'uri',
9             as_text => 'uri',
10             is_true => 1,
11             constants => 'HASH ARRAY DELIMITER',
12             constant => {
13             FIELD_NAMES => undef,
14             TYPE_NAME => 'period',
15             },
16             messages => {
17             bad_type => 'Invalid %s: %s',
18             bad_duration => 'Invalid duration: %s',
19 2     2   13 };
  2         8  
20              
21 2     2   14 use Time::Local;
  2         4  
  2         101  
22 2     2   11 use POSIX 'strftime';
  2         4  
  2         13  
23              
24             our @YMD = qw( year month day );
25             our @HMS = qw( hour minute second );
26             our @SMHD = qw( second minute hour day );
27             our @YMDHMS = (@YMD, @HMS);
28             our $SECONDS = {
29             s => 1,
30             m => 60,
31             h => 60*60,
32             d => 60*60*24,
33             M => 60*60*24*30,
34             y => 60*60*24*365,
35             };
36              
37              
38             sub split_regex {
39 0     0 0 0 shift->not_implemented;
40             }
41              
42              
43             sub join_format {
44 0     0 0 0 shift->not_implemented;
45             }
46              
47              
48             sub text_format {
49 1     1 0 13 shift->join_format;
50             }
51              
52              
53             sub field_names {
54 28     28 0 37 my $class = shift;
55 28   50     71 my $names = $class->FIELD_NAMES
56             || return $class->not_implemented;
57              
58 28 50       245 $names = [ split(DELIMITER, $names) ]
59             unless ref $names eq ARRAY;
60              
61             return wantarray
62 28 50       123 ? @$names
63             : \@$names
64             }
65              
66              
67             #-----------------------------------------------------------------------
68             # Methods
69             #-----------------------------------------------------------------------
70              
71             sub new {
72 5     5 1 11 my $class = shift;
73 5         15 my @fields = $class->field_names;
74 5   66     13 my $self = bless { map { ($_, 0) } @fields }, ref $class || $class;
  15         58  
75 5         12 my ($config, $time);
76            
77 5 50 66     27 if (@_ > 1) {
    100          
78             # multiple arguments are named params
79 0         0 $config = { @_ };
80             }
81             elsif (@_ == 1 && defined $_[0]) {
82             # single argument is a hash of named params, a timestamp or time in
83             # seconds since the epoch
84 3 50       13 $config = ref $_[0] eq HASH ? shift : { time => shift };
85             }
86             # otherwise we default to now
87             else {
88 2         6 $config = { time => time() };
89             }
90              
91 5 50       17 if ($time = $config->{ time }) {
92 5 100 66     33 if (numlike $time) {
    50          
93             # $time is seconds since epoch
94 2         81 (@$config{ @YMDHMS }) = reverse( ( localtime($time) )[0..5] );
95 2         11 $config->{ year }+= 1900;
96 2         9 $config->{ month }++;
97 2         8 $config->{ etime } = $time;
98             }
99             elsif (is_object(ref $class || $class, $time)) {
100 0         0 $config->{ uri } = $time->uri;
101 0         0 $config->{ etime } = $time->epoch_time;
102 0         0 $self->split_uri($config->{ uri }, $config);
103             }
104             else {
105             # $time is a timestamp so split and rejoin into canonical form
106 3         7 $config->{ uri } = $time;
107 3         11 $self->split_uri($config);
108             }
109 4         28 $self->join_uri($config);
110             }
111              
112             # set any fields defined in config, allowing singular (second,month,
113             # etc) and plural (seconds, months, etc)
114 4         12 foreach my $field (@fields) {
115 12   50     108 $self->{ $field } = $config->{ $field } || $config->{"${field}s"} || 0;
116             }
117              
118 4         23 $self->join_uri;
119              
120 4         40 return $self;
121             }
122              
123              
124             sub copy {
125 1     1 0 5 my $self = shift;
126 1         4 $self->new( $self->{ uri } );
127             }
128              
129              
130             sub split_uri {
131 3     3 0 6 my $self = shift;
132 3   33     9 my $target = shift || $self;
133 3         9 my $regex = $self->split_regex;
134 3         7 my @fields = $self->field_names;
135              
136 3 50       12 $target->{ uri } = '' unless defined $target->{ uri };
137              
138 6         29 (@$target{ @fields } = map { 0+$_ } $target->{ uri } =~ m/$regex/o)
139 3 100       34 || return $self->error_msg( bad_type => $self->TYPE_NAME, $target->{ uri } );
140             }
141              
142              
143             sub join_uri {
144 9     9 0 15 my $self = shift;
145 9   66     34 my $target = shift || $self;
146 9         17 my @fields = $self->field_names;
147              
148             return ($target->{ uri } = sprintf(
149             $self->join_format,
150 27 50       78 map { defined $_ ? $_ : 0 }
151 9         35 @$target{ @fields }
152             ));
153             }
154              
155              
156             sub epoch_time {
157 0     0 0 0 my $self = shift;
158              
159 0   0     0 return $self->{ etime } ||= timelocal(
160             $self->posix_args
161             );
162             }
163              
164              
165             sub posix_args {
166 1     1 0 6 my $self = shift;
167             return (
168             (
169 4 100       203 map { $self->{ $_ } || 0 }
170             @SMHD
171             ),
172             ($self->{ month } || 1) - 1,
173 1   50     4 ($self->{ year } || 1900) - 1900
      50        
174             );
175             }
176              
177              
178             sub format {
179 1     1 0 6 my $self = shift;
180 1         3 my $fmt = shift;
181 1         12 return strftime(
182             $fmt,
183             $self->posix_args
184             );
185             }
186              
187              
188             sub text {
189 1     1 0 3 my $self = shift;
190 1         2 my @fields = $self->field_names;
191             return $self->{ text }
192 1   33     22 ||= sprintf( $self->text_format, @$self{ @fields } );
193             }
194              
195              
196              
197             sub duration {
198 0     0 0 0 my ($self, $duration) = @_;
199              
200             # $duration can be a number, assumed to be seconds
201 0 0       0 return $duration
202             if numlike($duration);
203              
204             # Otherwise the $duration should be of the form "3 minutes". We only
205             # look at the first character of the word (e.g. "3 m"), which creates a
206             # potential conflict between "m(inute) and m(onth)". So we use a capital
207             # 'M' for month. This is based on code by Mark Fisher in CGI.pm.
208              
209 0         0 $duration =~ s/month/Month/i;
210              
211             # TODO: make this parser a bit smarter so we can support multiple
212             # items (e.g. "2 hours 30 minutes") as per adjust()
213 0 0       0 if ($duration =~ /^ ( -? (?: \d+ | \d*\.\d+ ) ) \s* ([smhdMy]?) /x) {
214 0   0     0 return ($SECONDS->{ $2 } || 1) * $1;
215             }
216             else {
217 0         0 return $self->error_msg( bad_duration => $duration );
218             }
219             }
220              
221              
222             sub compare {
223 10     10 1 13 my $self = shift;
224 10         17 my @fields = $self->field_names;
225              
226             # optimisation: if the $self object has an epoch time and a single
227             # numerical argument is passed (also an epoch time) then we can do a
228             # simple comparison
229             return $self->{ etime } <=> $_[0]
230             if $self->{ etime }
231 10 0 33     25 && @_ == 1
      33        
232             && numlike $_[0];
233              
234             # otherwise we upgrade any argument(s) to another timestamp and comare
235             # them piecewise
236 10 50 33     43 my $comp = @_ && is_object(ref $self || $self, $_[0])
237             ? shift
238             : $self->new(@_);
239            
240 10         23 foreach my $item (@fields) {
241 30 100       70 if ($self->{ $item } < $comp->{ $item }) {
    100          
242 5         30 return -1; # -1 - self earlier than comparison timestamp
243             }
244             elsif ($self->{ $item } > $comp->{ $item }) {
245 5         47 return 1; # 1 - self later than comparison timestamp
246             }
247             }
248 0         0 return 0; # 0 - same time
249             }
250              
251              
252             sub days_in_month {
253 1     1 0 5 my $self = shift;
254 1   33     6 my $month = shift || $self->{ month };
255              
256 1 50 33     23 if ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
    50 33        
      33        
257 0         0 return 30;
258             }
259             elsif ($month == 2) {
260 0 0       0 return $self->leap_year(@_) ? 29 : 28;
261             }
262             else {
263 1         7 return 31;
264             }
265             }
266              
267              
268             sub leap_year {
269 0     0 0   my $self = shift;
270 0   0       my $year = shift || $self->{ year };
271              
272 0 0         if ($year % 4) {
    0          
    0          
273 0           return 0;
274             }
275             elsif ($year % 400 == 0) {
276 0           return 1;
277             }
278             elsif ($year % 100 == 0) {
279 0           return 0;
280             }
281             else {
282 0           return 1;
283             }
284             }
285              
286              
287             1;
288             __END__