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