File Coverage

lib/Badger/Timestamp.pm
Criterion Covered Total %
statement 139 149 93.2
branch 59 68 86.7
condition 39 54 72.2
subroutine 26 27 96.3
pod 22 23 95.6
total 285 321 88.7


line stmt bran cond sub pod time code
1             #========================================================================
2             #
3             # Badger::Timestamp
4             #
5             # DESCRIPTION
6             # Simple object representing a date/time and providing methods for
7             # accessing and manipulating various parts of it.
8             #
9             # AUTHOR
10             # Andy Wardley
11             #
12             # COPYRIGHT
13             # Copyright (C) 2001-2009 Andy Wardley. All Rights Reserved.
14             #
15             # This module is free software; you can redistribute it and/or
16             # modify it under the same terms as Perl itself.
17             #
18             #========================================================================
19              
20             package Badger::Timestamp;
21              
22             use Badger::Class
23 70         1307 version => 0.03,
24             debug => 0,
25             import => 'class CLASS',
26             base => 'Badger::Base',
27             utils => 'numlike self_params is_object',
28             accessors => 'timestamp',
29             as_text => 'timestamp',
30             is_true => 1,
31             methods => {
32             not_equal => \&compare,
33             },
34             overload => {
35             '!=' => \¬_equal,
36             '==' => \&equal,
37             '<' => \&before,
38             '>' => \&after,
39             '<=' => \¬_after,
40             '>=' => \¬_before,
41             fallback => 1,
42             },
43             constants => 'HASH',
44             constant => {
45             TS => __PACKAGE__,
46             TIMESTAMP => __PACKAGE__,
47             },
48             exports => {
49             any => 'TS TIMESTAMP Timestamp Now',
50             },
51             messages => {
52             bad_timestamp => 'Invalid timestamp: %s',
53             bad_duration => 'Invalid duration: %s',
54 70     70   3238 };
  70         148  
55              
56 70     70   37656 use Time::Local;
  70         170337  
  70         4297  
57 70     70   36569 use POSIX 'strftime';
  70         455448  
  70         404  
58              
59             # Example timestamp: 2006/12/31 23:59:59
60             our $DATE_REGEX = qr{ (\d{4})\D(\d{1,2})\D(\d{1,2}) }x;
61             our $TIME_REGEX = qr{ (\d{1,2})\D(\d{2})\D(\d{2}) }x;
62             our $STAMP_REGEX = qr{ ^\s* $DATE_REGEX (?:(?:T|\s) $TIME_REGEX)? }x;
63             our $DATE_FORMAT = '%04d-%02d-%02d';
64             our $LONGDATE_FORMAT = '%02d-%3s-%04d';
65             our $TIME_FORMAT = '%02d:%02d:%02d';
66             our $STAMP_FORMAT = "$DATE_FORMAT $TIME_FORMAT";
67             our @YMD = qw( year month day );
68             our @HMS = qw( hour minute second );
69             our @SMHD = qw( second minute hour day );
70             our @YMDHMS = (@YMD, @HMS);
71             our @MONTHS = qw( xxx Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
72             our @CACHE = qw( date time etime longmonth longdate );
73             our $SECONDS = {
74             s => 1,
75             m => 60,
76             h => 60*60,
77             d => 60*60*24,
78             M => 60*60*24*30,
79             y => 60*60*24*365,
80             };
81              
82              
83             #-----------------------------------------------------------------------
84             # Method generator: second()/seconds(), hour()/hours(), etc.
85             #-----------------------------------------------------------------------
86              
87             class->methods(
88             map {
89             my $item = $_; # lexical copy for closure
90             my $items = $_ . 's'; # provide singular and plural versions
91             my $code = sub {
92 48 100   48   165 if (@_ > 1) {
93 6         11 $_[0]->{ $item } = $_[1];
94 6         13 $_[0]->join_timestamp;
95 6         36 return $_[0];
96             }
97 42         165 return $_[0]->{ $item };
98             };
99             $item => $code,
100             $items => $code
101             }
102             @YMDHMS
103             );
104              
105              
106             #-----------------------------------------------------------------------
107             # Constructor subroutines
108             #-----------------------------------------------------------------------
109              
110             sub Timestamp {
111             return @_
112 26 100   26 1 169 ? TS->new(@_)
113             : TS
114             }
115              
116             sub Now {
117 24     24 1 157 TS->now;
118             }
119              
120              
121             #-----------------------------------------------------------------------
122             # Methods
123             #-----------------------------------------------------------------------
124              
125             sub new {
126 71     71 1 104 my $class = shift;
127 71   66     140 my $self = bless { map { ($_, 0) } @YMDHMS }, ref $class || $class;
  426         946  
128 71         154 my ($config, $time);
129              
130 71 100 66     260 if (@_ > 1) {
    100          
131             # multiple arguments are named params
132 5         9 $config = { @_ };
133             }
134             elsif (@_ == 1 && defined $_[0]) {
135             # single argument is a hash of named params, a timestamp or time in
136             # seconds since the epoch
137 28 50       106 $config = ref $_[0] eq HASH ? shift : { time => shift };
138             }
139             # otherwise we default to now
140             else {
141 38         96 $config = { time => time() };
142             }
143              
144 71 100       275 if ($time = $config->{ time }) {
145 66 100 66     251 if (numlike $time) {
    100          
146             # $time is seconds since epoch
147 45         1970 (@$self{ @YMDHMS }) = reverse( ( localtime($time) )[0..5] );
148 45         176 $self->{ year }+= 1900;
149 45         59 $self->{ month }++;
150 45         99 $self->{ etime } = $time;
151             }
152             elsif (is_object(ref $class || $class, $time)) {
153 3         12 $self->{ timestamp } = $time->timestamp;
154 3         13 $self->{ etime } = $time->epoch_time;
155 3         80 $self->split_timestamp;
156             }
157             else {
158             # $time is a timestamp so split and rejoin into canonical form
159 18         185 $self->{ timestamp } = $time;
160 18         43 $self->split_timestamp;
161             }
162 65         172 $self->join_timestamp;
163             }
164             else {
165             # set any fields defined in config, allowing singular (second,month,
166             # etc) and plural (seconds, months, etc)
167 5         8 foreach my $field (@YMDHMS) {
168 30   100     104 $self->{ $field } = $config->{ $field } || $config->{"${field}s"} || 0;
169             }
170             }
171 70         361 return $self;
172             }
173              
174             sub now {
175 34     34 1 91 shift->new;
176             }
177              
178             sub copy {
179 1     1 1 3 my $self = shift;
180 1         3 $self->new( $self->{ timestamp } );
181             }
182              
183             sub split_timestamp {
184 21     21 1 29 my $self = shift;
185 21 50       85 $self->{ timestamp } = '' unless defined $self->{ timestamp };
186              
187             # TODO: this regex should be tweaked to make time (and/or date parts) optional
188 120   50     303 (@$self{ @YMDHMS } = map { 0+($_||0) } $self->{ timestamp } =~ m/$STAMP_REGEX/o)
189 21 100       179 || return $self->error_msg( bad_timestamp => $self->{ timestamp } );
190             }
191              
192             sub join_timestamp {
193 96     96 1 130 my $self = shift;
194             return ($self->{ timestamp } = sprintf(
195             $STAMP_FORMAT,
196 576 50       1293 map { defined $_ ? $_ : 0 }
197 96         241 @$self{ @YMDHMS }
198             ));
199             }
200              
201             sub epoch_time {
202 10     10 1 21 my $self = shift;
203             return $self->{ etime } ||= timelocal(
204             @$self{@SMHD},
205             $self->{ month } - 1,
206 10   66     51 $self->{ year } - 1900
207             );
208             }
209              
210             sub format {
211 22     22 1 53 my $self = shift;
212 22         32 my $fmt = shift;
213 22         913 return strftime($fmt, @$self{@SMHD}, $self->{ month } - 1, $self->{ year } - 1900);
214             }
215              
216             sub date {
217 17     17 1 44 my $self = shift;
218             return $self->{ date }
219 17   33     143 ||= sprintf( $DATE_FORMAT, @$self{ @YMD } );
220             }
221              
222             sub time {
223 13     13 1 29 my $self = shift;
224             return $self->{ time }
225 13   33     149 ||= sprintf( $TIME_FORMAT, @$self{ @HMS });
226             }
227              
228             sub adjust {
229 25     25 1 69 my $self = shift;
230 25         29 my ($args, $element, $dim);
231 25         29 my $fix_month = 0;
232              
233 25 100       46 if (@_ == 1) {
234             # single argument can be a reference to a hash: { days => 3, etc }
235             # or a number/string representing a duration: "3 days", "1 year"
236 2 50       7 $args = ref $_[0] eq HASH
237             ? shift
238             : { seconds => $self->duration(shift) };
239             }
240             else {
241             # multiple arguments are named parameters: days => 3, etc.
242 23         62 $args = { @_ };
243             }
244              
245             # If we're only adjusting by a month or a year, then we fix the day
246             # within the range of the number of days in the new month. For example:
247             # 2007-01-31 + 1 month = 2007-02-28. We must handle this for a year
248             # adjustment for the case: 2008-02-29 + 1 year = 2009-02-28
249 25 100 66     177 if ((scalar(keys %$args) == 1) &&
      66        
250             (defined $args->{ month } || defined $args->{ months } ||
251             defined $args->{ year } || defined $args->{ years })) {
252 6         8 $fix_month = 1;
253             }
254              
255 25         31 $self->debug("adjust: ", $self->dump_data($args)) if DEBUG;
256              
257             # allow each element to be singular or plural: day/days, etc.
258 25         35 foreach $element (@YMDHMS) {
259             $args->{ $element } = $args->{ "${element}s" }
260 150 100       344 unless defined $args->{ $element };
261             }
262              
263             # adjust the time by the parameters specified
264 25         47 foreach $element (@YMDHMS) {
265             $self->{ $element } += $args->{ $element }
266 150 100       247 if defined $args->{ $element };
267             }
268              
269             # Handle negative seconds/minutes/hours
270 25         49 while ($self->{ second } < 0) {
271 5760         5446 $self->{ second } += 60;
272 5760         7445 $self->{ minute }--;
273             }
274 25         45 while ($self->{ minute } < 0) {
275 96         92 $self->{ minute } += 60;
276 96         123 $self->{ hour }--;
277             }
278 25         45 while ($self->{ hour } < 0) {
279 4         8 $self->{ hour } += 24;
280 4         6 $self->{ day }--;
281             }
282              
283             # now positive seconds/minutes/hours
284 25 100       66 if ($self->{ second } > 59) {
285 3         17 $self->{ minute } += int($self->{ second } / 60);
286 3         8 $self->{ second } %= 60;
287             }
288 25 100       51 if ($self->{ minute } > 59) {
289 2         5 $self->{ hour } += int($self->{ minute } / 60);
290 2         5 $self->{ minute } %= 60;
291             }
292 25 100       40 if ($self->{ hour } > 23) {
293 3         7 $self->{ day } += int($self->{ hour } / 24);
294 3         3 $self->{ hour } %= 24;
295             }
296              
297             # Handle negative days/months/years
298 25         52 while ($self->{ day } <= 0) {
299 1         6 $self->{ month }--;
300 1 50       6 unless ($self->{ month } > 0) {
301 0         0 $self->{ month } += 12;
302 0         0 $self->{ year }--;
303             }
304 1         14 $self->{ day } += $self->days_in_month;
305             }
306 25         45 while ($self->{ month } <= 0) {
307 0         0 $self->{ month } += 12;
308 0         0 $self->{ year } --;
309             }
310 25         47 while ($self->{ month } > 12) {
311 1         9 $self->{ month } -= 12;
312 1         3 $self->{ year } ++;
313             }
314              
315             # handle day wrap-around
316 25         59 while ($self->{ day } > ($dim = $self->days_in_month)) {
317             # If we're adjusting by a single month or year and the day is
318             # greater than the number days in the new month, then we adjust
319             # the new day to be the last day in the month. Otherwise we
320             # increment the month and remove the number of days in the current
321             # month.
322 2 50       4 if ($fix_month) {
323 0         0 $self->{ day } = $dim;
324             }
325             else {
326 2         7 $self->{ day } -= $dim;
327 2 50       4 if ($self->{ month } == 12) {
328 0         0 $self->{ month } = 1;
329 0         0 $self->{ year }++;
330             }
331             else {
332 2         4 $self->{ month }++;
333             }
334             }
335             }
336              
337 25         54 $self->uncache;
338 25         42 $self->join_timestamp;
339              
340 25         124 return $self;
341             }
342              
343             sub duration {
344 2     2 1 7 my ($self, $duration) = @_;
345              
346             # $duration can be a number, assumed to be seconds
347 2 50       7 return $duration
348             if numlike($duration);
349              
350             # Otherwise the $duration should be of the form "3 minutes". We only
351             # look at the first character of the word (e.g. "3 m"), which creates a
352             # potential conflict between "m(inute) and m(onth)". So we use a capital
353             # 'M' for month. This is based on code by Mark Fisher in CGI.pm.
354              
355 2         8 $duration =~ s/month/Month/i;
356              
357             # TODO: make this parser a bit smarter so we can support multiple
358             # items (e.g. "2 hours 30 minutes") as per adjust()
359 2 50       10 if ($duration =~ /^ ( -? (?: \d+ | \d*\.\d+ ) ) \s* ([smhdMy]?) /x) {
360 2   50     16 return ($SECONDS->{ $2 } || 1) * $1;
361             }
362             else {
363 0         0 return $self->error_msg( bad_duration => $duration );
364             }
365             }
366              
367             sub uncache {
368 25     25 1 28 my $self = shift;
369 25         67 delete @$self{@CACHE};
370 25         26 return $self;
371             }
372              
373             sub compare {
374 57     57 1 94 my $self = shift;
375              
376             # optimisation: if the $self object has an epoch time and a single
377             # numerical argument is passed (also an epoch time) then we can do a
378             # simple comparison
379             return $self->{ etime } <=> $_[0]
380             if $self->{ etime }
381 57 100 100     223 && @_ == 1
      100        
382             && numlike $_[0];
383              
384             # otherwise we upgrade any argument(s) to another timestamp and comare
385             # them piecewise
386 52 100 66     186 my $comp = @_ && is_object(ref $self || $self, $_[0]) ? shift : $self->new(@_);
387              
388 52         101 foreach my $item (@YMDHMS) {
389 221 100       509 if ($self->{ $item } < $comp->{ $item }) {
    100          
390 21         112 return -1; # -1 - self earlier than comparison timestamp
391             }
392             elsif ($self->{ $item } > $comp->{ $item }) {
393 20         119 return 1; # 1 - self later than comparison timestamp
394             }
395             }
396 11         144 return 0; # 0 - same time
397             }
398              
399             sub equal {
400 12     12 1 27 shift->compare(@_) == 0;
401             }
402              
403             sub before {
404 8     8 1 127 shift->compare(@_) == -1;
405             }
406              
407             sub after {
408 8     8 1 81 shift->compare(@_) == 1;
409             }
410              
411             sub not_before {
412 3     3 1 14 shift->compare(@_) >= 0;
413             }
414              
415             sub not_after {
416 3     3 1 20 shift->compare(@_) <= 0;
417             }
418              
419             sub tm_wday {
420 0     0 0 0 my $self = shift;
421 0         0 return (localtime($self->epoch_time))[6];
422             }
423              
424             sub days_in_month {
425 42     42 1 56 my $self = shift;
426 42   66     97 my $month = shift || $self->{ month };
427 42 100 100     202 if ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
    100 100        
      100        
428 14         87 return 30;
429             }
430             elsif ($month == 2) {
431 3 100       12 return $self->leap_year(@_) ? 29 : 28;
432             }
433             else {
434 25         78 return 31;
435             }
436             }
437              
438             sub leap_year {
439 11     11 1 31 my $self = shift;
440 11   66     32 my $year = shift || $self->{ year };
441 11 100       28 if ($year % 4) {
    100          
    100          
442 7         40 return 0;
443             }
444             elsif ($year % 400 == 0) {
445 1         2 return 1;
446             }
447             elsif ($year % 100 == 0) {
448 1         5 return 0;
449             }
450             else {
451 2         9 return 1;
452             }
453             }
454              
455              
456             1;
457             __END__