File Coverage

lib/Badger/Timestamp.pm
Criterion Covered Total %
statement 139 149 93.2
branch 59 68 86.7
condition 38 54 70.3
subroutine 26 27 96.3
pod 22 23 95.6
total 284 321 88.4


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         1126 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   2526 };
  70         120  
55              
56 70     70   31986 use Time::Local;
  70         137509  
  70         3956  
57 70     70   30511 use POSIX 'strftime';
  70         389875  
  70         395  
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   162 if (@_ > 1) {
93 6         10 $_[0]->{ $item } = $_[1];
94 6         10 $_[0]->join_timestamp;
95 6         22 return $_[0];
96             }
97 42         146 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 148 ? TS->new(@_)
113             : TS
114             }
115              
116             sub Now {
117 13     13 1 61 TS->now;
118             }
119              
120              
121             #-----------------------------------------------------------------------
122             # Methods
123             #-----------------------------------------------------------------------
124              
125             sub new {
126 60     60 1 77 my $class = shift;
127 60   66     96 my $self = bless { map { ($_, 0) } @YMDHMS }, ref $class || $class;
  360         632  
128 60         98 my ($config, $time);
129              
130 60 100 66     213 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       79 $config = ref $_[0] eq HASH ? shift : { time => shift };
138             }
139             # otherwise we default to now
140             else {
141 27         53 $config = { time => time() };
142             }
143              
144 60 100       119 if ($time = $config->{ time }) {
145 55 100 66     188 if (numlike $time) {
    100          
146             # $time is seconds since epoch
147 34         1086 (@$self{ @YMDHMS }) = reverse( ( localtime($time) )[0..5] );
148 34         122 $self->{ year }+= 1900;
149 34         53 $self->{ month }++;
150 34         60 $self->{ etime } = $time;
151             }
152             elsif (is_object(ref $class || $class, $time)) {
153 3         8 $self->{ timestamp } = $time->timestamp;
154 3         9 $self->{ etime } = $time->epoch_time;
155 3         67 $self->split_timestamp;
156             }
157             else {
158             # $time is a timestamp so split and rejoin into canonical form
159 18         157 $self->{ timestamp } = $time;
160 18         36 $self->split_timestamp;
161             }
162 54         145 $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     77 $self->{ $field } = $config->{ $field } || $config->{"${field}s"} || 0;
169             }
170             }
171 59         253 return $self;
172             }
173              
174             sub now {
175 23     23 1 51 shift->new;
176             }
177              
178             sub copy {
179 1     1 1 2 my $self = shift;
180 1         2 $self->new( $self->{ timestamp } );
181             }
182              
183             sub split_timestamp {
184 21     21 1 25 my $self = shift;
185 21 50       38 $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     250 (@$self{ @YMDHMS } = map { 0+($_||0) } $self->{ timestamp } =~ m/$STAMP_REGEX/o)
189 21 100       150 || return $self->error_msg( bad_timestamp => $self->{ timestamp } );
190             }
191              
192             sub join_timestamp {
193 84     84 1 93 my $self = shift;
194             return ($self->{ timestamp } = sprintf(
195             $STAMP_FORMAT,
196 504 50       864 map { defined $_ ? $_ : 0 }
197 84         178 @$self{ @YMDHMS }
198             ));
199             }
200              
201             sub epoch_time {
202 10     10 1 14 my $self = shift;
203             return $self->{ etime } ||= timelocal(
204             @$self{@SMHD},
205             $self->{ month } - 1,
206 10   66     41 $self->{ year } - 1900
207             );
208             }
209              
210             sub format {
211 11     11 1 18 my $self = shift;
212 11         15 my $fmt = shift;
213 11         371 return strftime($fmt, @$self{@SMHD}, $self->{ month } - 1, $self->{ year } - 1900);
214             }
215              
216             sub date {
217 17     17 1 34 my $self = shift;
218             return $self->{ date }
219 17   33     119 ||= sprintf( $DATE_FORMAT, @$self{ @YMD } );
220             }
221              
222             sub time {
223 13     13 1 19 my $self = shift;
224             return $self->{ time }
225 13   33     135 ||= sprintf( $TIME_FORMAT, @$self{ @HMS });
226             }
227              
228             sub adjust {
229 24     24 1 43 my $self = shift;
230 24         25 my ($args, $element, $dim);
231 24         21 my $fix_month = 0;
232              
233 24 100       35 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       16 $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 22         39 $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 24 100 66     129 if ((scalar(keys %$args) == 1) &&
      66        
250             (defined $args->{ month } || defined $args->{ months } ||
251             defined $args->{ year } || defined $args->{ years })) {
252 6         7 $fix_month = 1;
253             }
254              
255 24         23 $self->debug("adjust: ", $self->dump_data($args)) if DEBUG;
256              
257             # allow each element to be singular or plural: day/days, etc.
258 24         28 foreach $element (@YMDHMS) {
259             $args->{ $element } = $args->{ "${element}s" }
260 144 100       258 unless defined $args->{ $element };
261             }
262              
263             # adjust the time by the parameters specified
264 24         25 foreach $element (@YMDHMS) {
265             $self->{ $element } += $args->{ $element }
266 144 100       194 if defined $args->{ $element };
267             }
268              
269             # Handle negative seconds/minutes/hours
270 24         39 while ($self->{ second } < 0) {
271 5760         4312 $self->{ second } += 60;
272 5760         6205 $self->{ minute }--;
273             }
274 24         33 while ($self->{ minute } < 0) {
275 96         72 $self->{ minute } += 60;
276 96         103 $self->{ hour }--;
277             }
278 24         34 while ($self->{ hour } < 0) {
279 4         5 $self->{ hour } += 24;
280 4         6 $self->{ day }--;
281             }
282              
283             # now positive seconds/minutes/hours
284 24 100       35 if ($self->{ second } > 59) {
285 3         7 $self->{ minute } += int($self->{ second } / 60);
286 3         4 $self->{ second } %= 60;
287             }
288 24 100       33 if ($self->{ minute } > 59) {
289 2         6 $self->{ hour } += int($self->{ minute } / 60);
290 2         2 $self->{ minute } %= 60;
291             }
292 24 100       33 if ($self->{ hour } > 23) {
293 2         4 $self->{ day } += int($self->{ hour } / 24);
294 2         3 $self->{ hour } %= 24;
295             }
296              
297             # Handle negative days/months/years
298 24         31 while ($self->{ day } <= 0) {
299 1         2 $self->{ month }--;
300 1 50       3 unless ($self->{ month } > 0) {
301 0         0 $self->{ month } += 12;
302 0         0 $self->{ year }--;
303             }
304 1         3 $self->{ day } += $self->days_in_month;
305             }
306 24         35 while ($self->{ month } <= 0) {
307 0         0 $self->{ month } += 12;
308 0         0 $self->{ year } --;
309             }
310 24         34 while ($self->{ month } > 12) {
311 1         2 $self->{ month } -= 12;
312 1         2 $self->{ year } ++;
313             }
314              
315             # handle day wrap-around
316 24         34 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       5 if ($fix_month) {
323 0         0 $self->{ day } = $dim;
324             }
325             else {
326 2         3 $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 24         49 $self->uncache;
338 24         35 $self->join_timestamp;
339              
340 24         75 return $self;
341             }
342              
343             sub duration {
344 2     2 1 5 my ($self, $duration) = @_;
345              
346             # $duration can be a number, assumed to be seconds
347 2 50       8 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         5 $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       9 if ($duration =~ /^ ( -? (?: \d+ | \d*\.\d+ ) ) \s* ([smhdMy]?) /x) {
360 2   50     11 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 24     24 1 27 my $self = shift;
369 24         43 delete @$self{@CACHE};
370 24         23 return $self;
371             }
372              
373             sub compare {
374 57     57 1 75 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     179 && @_ == 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     177 my $comp = @_ && is_object(ref $self || $self, $_[0]) ? shift : $self->new(@_);
387              
388 52         86 foreach my $item (@YMDHMS) {
389 221 100       415 if ($self->{ $item } < $comp->{ $item }) {
    100          
390 21         93 return -1; # -1 - self earlier than comparison timestamp
391             }
392             elsif ($self->{ $item } > $comp->{ $item }) {
393 20         76 return 1; # 1 - self later than comparison timestamp
394             }
395             }
396 11         39 return 0; # 0 - same time
397             }
398              
399             sub equal {
400 12     12 1 35 shift->compare(@_) == 0;
401             }
402              
403             sub before {
404 8     8 1 104 shift->compare(@_) == -1;
405             }
406              
407             sub after {
408 8     8 1 63 shift->compare(@_) == 1;
409             }
410              
411             sub not_before {
412 3     3 1 7 shift->compare(@_) >= 0;
413             }
414              
415             sub not_after {
416 3     3 1 6 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 41     41 1 40 my $self = shift;
426 41   66     70 my $month = shift || $self->{ month };
427 41 100 100     151 if ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
    100 100        
      100        
428 14         35 return 30;
429             }
430             elsif ($month == 2) {
431 2 100       4 return $self->leap_year(@_) ? 29 : 28;
432             }
433             else {
434 25         76 return 31;
435             }
436             }
437              
438             sub leap_year {
439 10     10 1 20 my $self = shift;
440 10   33     14 my $year = shift || $self->{ year };
441 10 100       27 if ($year % 4) {
    100          
    100          
442 6         19 return 0;
443             }
444             elsif ($year % 400 == 0) {
445 1         3 return 1;
446             }
447             elsif ($year % 100 == 0) {
448 1         14 return 0;
449             }
450             else {
451 2         6 return 1;
452             }
453             }
454              
455              
456             1;
457             __END__