File Coverage

blib/lib/Date/Easy/Datetime.pm
Criterion Covered Total %
statement 157 160 98.1
branch 69 80 86.2
condition 34 39 87.1
subroutine 59 59 100.0
pod 34 34 100.0
total 353 372 94.8


line stmt bran cond sub pod time code
1             package Date::Easy::Datetime;
2              
3 18     18   304182 use strict;
  18         56  
  18         431  
4 18     18   72 use warnings;
  18         27  
  18         342  
5 18     18   1161 use autodie;
  18         35803  
  18         69  
6              
7             our $VERSION = '0.09_01'; # TRIAL VERSION
8              
9 18     18   81185 use Exporter;
  18         36  
  18         659  
10 18     18   87 use parent 'Exporter';
  18         30  
  18         121  
11             our @EXPORT_OK = qw< datetime now >;
12             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
13              
14 18     18   1750 use Carp;
  18         42  
  18         995  
15 18     18   8403 use Time::Piece;
  18         160479  
  18         73  
16 18     18   1488 use Scalar::Util 'blessed';
  18         34  
  18         786  
17 18     18   89 use Time::Local 1.26, qw< timegm_modern timelocal_modern >;
  18         32  
  18         38588  
18              
19              
20             # this can be modified (preferably using `local`) to use GMT/UTC as the default
21             # or you can pass a value to `import` via your `use` line
22             our $DEFAULT_ZONE = 'local';
23              
24             my %ZONE_FLAG = ( local => 1, UTC => 0, GMT => 0 );
25              
26              
27             sub import
28             {
29 19     19   6196 my @args;
30 19 100       107 exists $ZONE_FLAG{$_} ? $DEFAULT_ZONE = $_ : push @args, $_ foreach @_;
31 19         45 @_ = @args;
32 19         5272 goto &Exporter::import;
33             }
34              
35              
36             ##############################
37             # FUNCTIONS (*NOT* METHODS!) #
38             ##############################
39              
40             sub datetime
41             {
42 2809 100   2809 1 1426050 my $zonespec = @_ % 2 == 0 ? shift : $DEFAULT_ZONE;
43 2809         4113 my $datetime = shift;
44 2809 100       10242 if ( $datetime =~ /^-?\d+$/ )
45             {
46 7         36 return Date::Easy::Datetime->new($zonespec, $datetime);
47             }
48             else
49             {
50 2802         4748 my $t = _str2time($datetime, $zonespec);
51 2802 100       4929 $t = _parsedate($datetime, $zonespec) unless defined $t;
52 2802 100       73977 croak("Illegal datetime: $datetime") unless defined $t;
53 2801         6544 return Date::Easy::Datetime->new( $zonespec, $t );
54             }
55 0         0 die("reached unreachable code");
56             }
57              
58 5     5 1 1804 sub now () { Date::Easy::Datetime->new }
59              
60              
61             sub _strptime
62             {
63 3047     3047   8104 require Date::Parse;
64             # Most of this code is stolen from Date::Parse, by Graham Barr. It is used here (see _str2time,
65             # below), but its true raison d'etre is for use by Date::Easy::Date.
66             #
67             # In an ideal world, I would just use the code from Date::Parse and not repeat it here.
68             # However, the problem is that str2time() calls strptime() to generate the pieces of a datetime,
69             # then does some validation, then returns epoch seconds by calling timegm (from Time::Local) on
70             # it. For dates, I don't _want_ to call str2time because I'm just going to take the epoch
71             # seconds and turn them back into pieces, so it's inefficicent. But more importantly I _can't_
72             # call str2time because it converts to UTC, and I want the pieces as they are relative to
73             # whatever timezone the parsed date has.
74             #
75             # On the other hand, the problem with calling strptime directly is that str2time is doing two
76             # things there: the conversion to epoch seconds, which I don't want or need for dates, and the
77             # validation, which, it turns out, I *do* want, and need, even for dates. For instance,
78             # strptime will happily return a month of -1 if it hits a parsing hiccough. Which then str2time
79             # will turn into undef, as you would expect. But, if you're just calling strptime, that doesn't
80             # help you much. :-(
81             #
82             # Thus, for dates in particular, I'm left with 3 possibilities, none of them very palatable:
83             # # call strptime, then call str2time as well
84             # # repeat at least some of the code from str2time here
85             # # do Something Devious, like wrap/monkey-patch strptime
86             # #1 doesn't seem practical, because it means that every string that has to be parsed this way
87             # has to be parsed twice, meaning it will take twice as long. #3 seems too complex--since the
88             # call to strptime is out of my control, I can't add arguments to it, or get any extra data out
89             # of it, which means I have to store things in global variables, which means it wouldn't be
90             # reentrant ... it would be a big mess. So #2, unpalatable as it is, is what we're going with.
91             #
92             # Of course, this gives me the opportunity to tweak a few things. Primarily, we can tweak our
93             # code to fix RT/105031 et al (see comments below, in _str2time). There's a few minor
94             # efficiency gains we can get from not doing things the older code seemed to think was
95             # necessary. (Of course, maybe it really is, in which case I'll have to put it all back.)
96             #
97             # The code in _strptime is as much of Date::Parse::str2time as is necessary to handle all the
98             # validation and still return separate time values. This way it can be used by both dates and
99             # datetimes.
100              
101 3047         12198 my ($str, $zonespec) = @_;
102              
103 3047 100       54999 my ($sec, $min, $hour, $day, $month, $year, $zone)
104             = Date::Parse::strptime($str, $zonespec eq 'local' ? () : $zonespec);
105 3047         215545 my $num_defined = defined($day) + defined($month) + defined($year);
106 3047 100       6580 return () if $num_defined == 0;
107 2710 100       4508 if ($num_defined < 3)
108             {
109 12         35 my @lt = localtime(time);
110              
111 12 50       599 $month = $lt[4] unless defined $month;
112 12 50       24 $day = $lt[3] unless defined $day;
113 12 50       44 $year = ($month > $lt[4]) ? ($lt[5] - 1) : $lt[5] unless defined $year;
    50          
114             }
115 2710   100     8012 $hour ||= 0; $min ||= 0; $sec ||= 0; # default time components to zero
  2710   100     6864  
  2710   100     6369  
116 2710         3755 my $subsec = $sec - int($sec); $sec = int($sec);# extract any fractional part (e.g. milliseconds)
  2710         2912  
117 2710 100       4321 $year += 1900 if $year < 1000; # undo timelocal funkiness and adjust for RT/53413 / RT/105031
118              
119 2710 100 100     19540 return () unless $month >= 0 and $month <= 11 and $day >= 1 and $day <= 31
      66        
      100        
      100        
      100        
      66        
120             and $hour <= 23 and $min <= 59 and $sec <= 59;
121              
122 2637         8330 return ($sec, $min, $hour, $day, $month, $year, $zone, $subsec);
123             }
124              
125             sub _str2time
126             {
127 2802     2802   12550 require Date::Parse;
128             # Most of this code is also stolen from Date::Parse, by Graham Barr. This is the remainder of
129             # Date::Parse::str2time, which takes the separate values (from _strptime, above) and turns them
130             # into an epoch seconds value. See also the big comment block below.
131              
132 2802         12535 my ($time, $zonespec) = @_;
133 2802         4924 my ($sec, $min, $hour, $day, $month, $year, $zone, $subsec) = _strptime($time, $zonespec);
134             # doesn't really matter which one we check (other than $zone); either they're all defined, or none are
135 2802 100       5433 return undef unless defined $year;
136              
137             # This block is changed from the original in Date::Parse in the following ways:
138             # * We're using timegm_modern/timelocal_modern instead of timegm/timelocal. This fixes all
139             # sorts of gnarly issues, but most especially the heinous RT/53413 / RT/105031 bug. (Side
140             # note: perhaps Parse::Date could use these as well? If so, that would close that raft of
141             # bugs and then we wouldn't need to reimplement the guts of `str2time` at all.)
142             # * The original code set the __DIE__ sig handler to ignore in the `eval`s. But I'm not
143             # comfortable doing that, and I'm not convinced it's necessary.
144             # * The original code did a little dance to make sure that a -1 return from timegm/timelocal
145             # was a valid return and not an indication of an error. But I can't see any indication
146             # that they ever actually return -1 on error, either in the current Time::Local code, or
147             # in its Changes file (e.g. for older versions). And, since our version of `strptime`
148             # specifically adds 1900 to the year (sometimes) to avoid Time::Local's horrible
149             # "two-digit year" handling, it makes coming up with a value to compare -1 against more of
150             # a PITA. Plus it's inefficient for what appears to be no real gain.
151 2484         2918 my $result;
152 2484 100       3790 if (defined $zone)
153             {
154 563         713 $result = eval { timegm_modern($sec, $min, $hour, $day, $month, $year) };
  563         1398  
155 563 50       14879 return undef unless defined $result;
156 563         713 $result -= $zone;
157             }
158             else
159             {
160 1921         2389 $result = eval { timelocal_modern($sec, $min, $hour, $day, $month, $year) };
  1921         4606  
161 1921 50       111003 return undef unless defined $result;
162             }
163              
164 2484         4757 return $result + $subsec;
165             }
166              
167             sub _parsedate
168             {
169 318     318   1190 require Time::ParseDate;
170 318         526 my ($time, $zonespec) = @_;
171 318 100       915 return scalar Time::ParseDate::parsedate($time, $zonespec eq 'local' ? () : (GMT => 1));
172             }
173              
174              
175             #######################
176             # REGULAR CLASS STUFF #
177             #######################
178              
179             sub new
180             {
181 8229     8229 1 214805 my $class = shift;
182 8229 100 100     22863 my $zonespec = @_ == 2 || @_ == 7 ? shift : $DEFAULT_ZONE;
183 8229 100       15098 croak("Unrecognized timezone specifier") unless exists $ZONE_FLAG{$zonespec};
184              
185 8228         8938 my $t;
186 8228 100       17677 if (@_ == 0)
    100          
    100          
187             {
188 9         13 $t = time;
189             }
190             elsif (@_ == 6)
191             {
192 44         92 my ($y, $m, $d, $H, $M, $S) = @_;
193 44         74 --$m; # timelocal/timegm will expect month as 0..11
194             # but we'll use timelocal_modern/timegm_modern so we don't need to twiddle the year number
195 44 100       55 $t = eval { $zonespec eq 'local'
  44         148  
196             ? timelocal_modern($S, $M, $H, $d, $m, $y)
197             : timegm_modern($S, $M, $H, $d, $m, $y)
198             };
199 44 100       2119 croak("Illegal datetime: $y/" . ($m + 1) . "/$d $H:$M:$S") unless defined $t;
200             }
201             elsif (@_ == 1)
202             {
203 8170         9490 $t = shift;
204 8170 100       20015 if ( my $conv_class = blessed $t )
205             {
206 3214 100       6947 if ( $t->isa('Time::Piece') )
207             {
208             # it's already what we were going to construct anyway;
209             # just stick it in a hashref and call it a day
210 3212         18353 return bless { impl => $t }, $class;
211             }
212             else
213             {
214 2         20 croak("Don't know how to convert $conv_class to $class");
215             }
216             }
217             }
218             else
219             {
220 5         43 croak("Illegal number of arguments to datetime()");
221             }
222              
223 5008         12148 bless { impl => scalar Time::Piece->_mktime($t, $ZONE_FLAG{$zonespec}) }, $class;
224             }
225              
226              
227 7     7 1 755 sub is_local { shift->{impl}->[Time::Piece::c_islocal] }
228 11     11 1 1100 sub is_gmt { !shift->{impl}->[Time::Piece::c_islocal] }
229             *is_utc = \&is_gmt;
230              
231              
232             sub as
233             {
234 6     6 1 2262 my ($self, $conv_spec) = @_;
235              
236 6 100       34 if ( $conv_spec =~ /^(\W)(\w+)$/ )
237             {
238 3         13 my $fmt = join($1, map { "%$_" } split('', $2));
  9         22  
239 3         8 return $self->strftime($fmt);
240             }
241 3 100       11 if ( $conv_spec eq 'Time::Piece' )
242             {
243 2         78 return $self->{impl};
244             }
245             else
246             {
247 1         13 croak("Don't know how to convert " . ref( $self) . " to $conv_spec");
248             }
249             }
250              
251              
252             # ACCESSORS
253              
254 1903     1903 1 77015 sub year { shift->{impl}->year }
255 3     3 1 1665 sub month { shift->{impl}->mon }
256 3     3 1 1349 sub day { shift->{impl}->mday }
257 6     6 1 2746 sub hour { shift->{impl}->hour }
258 6     6 1 2679 sub minute { shift->{impl}->min }
259 6     6 1 3085 sub second { shift->{impl}->sec }
260 2608     2608 1 115091 sub epoch { shift->{impl}->epoch }
261 2     2 1 845 sub time_zone { shift->{impl}->strftime('%Z') }
262 14 100   14 1 592 sub day_of_week { shift->{impl}->day_of_week || 7 } # change Sunday from 0 to 7
263 1462     1462 1 30964 sub day_of_year { shift->{impl}->yday + 1 } # change from 0-based to 1-based
264 24     24 1 974 sub quarter { int(shift->{impl}->_mon / 3) + 1 } # calc quarter from (zero-based) month
265              
266             sub split
267             {
268 1     1 1 455 my $impl = shift->{impl};
269 1         4 ( $impl->year, $impl->mon, $impl->mday, $impl->hour, $impl->min, $impl->sec )
270             }
271              
272              
273             # FORMATTERS
274              
275 1348     1348 1 570982 sub strftime { shift->{impl}->strftime(@_) }
276 2     2 1 1329 sub iso8601 { shift->{impl}->datetime }
277             *iso = \&iso8601;
278              
279              
280             ########################
281             # OVERLOADED OPERATORS #
282             ########################
283              
284             sub _op_convert
285             {
286 14806     14806   18307 my $operand = shift;
287 14806 100       37297 return $operand unless blessed $operand;
288 9897 50       32546 return $operand->{impl} if $operand->isa('Date::Easy::Datetime');
289 0 0       0 return $operand if $operand->isa('Time::Piece');
290 0         0 croak ("don't know how to handle conversion of " . ref $operand);
291             }
292              
293             sub _result_convert
294             {
295 4909     4909   5749 my $func = shift;
296 4909         8672 return ref($_[0])->new( scalar $func->(_op_convert($_[0]), _op_convert($_[1]), $_[2]) );
297             }
298              
299 3194     3194   6421 sub _add_seconds { _result_convert( \&Time::Piece::add => @_ ) }
300 1715     1715   3219 sub _subtract_seconds { _result_convert( \&Time::Piece::subtract => @_ ) }
301             # subclasses can override these to change what units an integer represents
302 1338     1338   2011 sub _add_integer { $_[0]->add_seconds($_[1]) }
303 607     607   943 sub _subtract_integer { $_[0]->subtract_seconds($_[1]) }
304              
305             sub _dispatch_add
306             {
307 2333 100 66 2333   663013 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
308             {
309 17         48 $_[1]->_add_to($_[0]);
310             }
311             else
312             {
313             # this should DTRT for whichever class we are
314 2316         4669 $_[0]->_add_integer($_[1]);
315             }
316             }
317              
318             sub _dispatch_subtract
319             {
320 902 100 100 902   40380 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
    100 66        
321             {
322             # this shouldn't be possible ...
323 16 50       31 die("should have called overloaded - for ::Units") if $_[2];
324             # as the name implies, this method assumes reversed operands
325 16         40 $_[1]->_subtract_from($_[0]);
326             }
327             elsif ( blessed $_[1] && $_[1]->isa('Date::Easy::Datetime') )
328             {
329 33 50       81 my ($lhs, $rhs) = $_[2] ? @_[1,0] : @_[0,1];
330 33 100 66     114 my $divisor = $lhs->isa('Date::Easy::Date') && $rhs->isa('Date::Easy::Date') ? 86_400 : 1;
331 33         58 ($lhs->epoch - $rhs->epoch) / $divisor;
332             }
333             else
334             {
335             # this should DTRT for whichever class we are
336 853         1556 $_[0]->_subtract_integer($_[1]);
337             }
338             }
339              
340             use overload
341 1510     1510   9253 '""' => sub { Time::Piece::cdate (_op_convert($_[0]) ) },
342 10     10   2750 '<=>' => sub { Time::Piece::compare (_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
343 1729     1729   327192 'cmp' => sub { Time::Piece::str_compare(_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
344              
345 18         195 '+' => \&_dispatch_add,
346             '-' => \&_dispatch_subtract,
347 18     18   125 ;
  18         35  
348              
349              
350             # MATH METHODS
351              
352 1464     1464 1 42136 sub add_seconds { shift->_add_seconds (@_) }
353 125     125 1 321 sub add_minutes { shift->_add_seconds ($_[0] * 60) }
354 126     126 1 317 sub add_hours { shift->_add_seconds ($_[0] * 60 * 60) }
355 1479     1479 1 39571 sub add_days { shift->_add_seconds ($_[0] * 60 * 60 * 24) }
356 250     250 1 36531 sub add_weeks { shift->add_days ($_[0] * 7) }
357 19     19 1 2591 sub add_months { ref($_[0])->new( shift->{impl}->add_months(@_) ) }
358 18     18 1 2216 sub add_years { ref($_[0])->new( shift->{impl}->add_years (@_) ) }
359              
360 730     730 1 1277 sub subtract_seconds { shift->_subtract_seconds (@_) }
361 124     124 1 293 sub subtract_minutes { shift->_subtract_seconds ($_[0] * 60) }
362 123     123 1 305 sub subtract_hours { shift->_subtract_seconds ($_[0] * 60 * 60) }
363 738     738 1 37354 sub subtract_days { shift->_subtract_seconds ($_[0] * 60 * 60 * 24) }
364 246     246 1 36529 sub subtract_weeks { shift->subtract_days ($_[0] * 7) }
365 5     5 1 572 sub subtract_months { shift->add_months($_[0] * -1) }
366 6     6 1 582 sub subtract_years { shift->add_years ($_[0] * -1) }
367              
368              
369              
370             1;
371              
372              
373              
374             # ABSTRACT: easy datetime class
375             # COPYRIGHT
376              
377             __END__