File Coverage

blib/lib/Date/Easy/Datetime.pm
Criterion Covered Total %
statement 157 160 98.1
branch 70 80 87.5
condition 34 39 87.1
subroutine 59 59 100.0
pod 34 34 100.0
total 354 372 95.1


line stmt bran cond sub pod time code
1             package Date::Easy::Datetime;
2              
3 18     18   384117 use strict;
  18         70  
  18         516  
4 18     18   91 use warnings;
  18         33  
  18         420  
5 18     18   1558 use autodie;
  18         44934  
  18         87  
6              
7             our $VERSION = '0.09'; # VERSION
8              
9 18     18   100501 use Exporter;
  18         46  
  18         770  
10 18     18   108 use parent 'Exporter';
  18         44  
  18         192  
11             our @EXPORT_OK = qw< datetime now >;
12             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
13              
14 18     18   2286 use Carp;
  18         44  
  18         1239  
15 18     18   10635 use Time::Piece;
  18         197126  
  18         93  
16 18     18   1906 use Scalar::Util 'blessed';
  18         41  
  18         1040  
17 18     18   117 use Time::Local 1.26, qw< timegm_modern timelocal_modern >;
  18         40  
  18         47334  
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   7825 my @args;
30 19 100       141 exists $ZONE_FLAG{$_} ? $DEFAULT_ZONE = $_ : push @args, $_ foreach @_;
31 19         59 @_ = @args;
32 19         6748 goto &Exporter::import;
33             }
34              
35              
36             ##############################
37             # FUNCTIONS (*NOT* METHODS!) #
38             ##############################
39              
40             sub datetime
41             {
42 2809 100   2809 1 1782892 my $zonespec = @_ % 2 == 0 ? shift : $DEFAULT_ZONE;
43 2809         5074 my $datetime = shift;
44 2809 100       12157 if ( $datetime =~ /^-?\d+$/ )
45             {
46 7         49 return Date::Easy::Datetime->new($zonespec, $datetime);
47             }
48             else
49             {
50 2802         5757 my $t = _str2time($datetime, $zonespec);
51 2802 100       5575 $t = _parsedate($datetime, $zonespec) unless defined $t;
52 2802 100       91825 croak("Illegal datetime: $datetime") unless defined $t;
53 2801         8292 return Date::Easy::Datetime->new( $zonespec, $t );
54             }
55 0         0 die("reached unreachable code");
56             }
57              
58 5     5 1 2236 sub now () { Date::Easy::Datetime->new }
59              
60              
61             sub _strptime
62             {
63 3047     3047   10501 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         14969 my ($str, $zonespec) = @_;
102              
103 3047 100       68206 my ($sec, $min, $hour, $day, $month, $year, $zone)
104             = Date::Parse::strptime($str, $zonespec eq 'local' ? () : $zonespec);
105 3047         267528 my $num_defined = defined($day) + defined($month) + defined($year);
106 3047 100       8560 return () if $num_defined == 0;
107 2710 100       5669 if ($num_defined < 3)
108             {
109 12         45 my @lt = localtime(time);
110              
111 12 50       751 $month = $lt[4] unless defined $month;
112 12 50       32 $day = $lt[3] unless defined $day;
113 12 100       53 $year = ($month > $lt[4]) ? ($lt[5] - 1) : $lt[5] unless defined $year;
    50          
114             }
115 2710   100     9725 $hour ||= 0; $min ||= 0; $sec ||= 0; # default time components to zero
  2710   100     7544  
  2710   100     7800  
116 2710         4609 my $subsec = $sec - int($sec); $sec = int($sec);# extract any fractional part (e.g. milliseconds)
  2710         3650  
117 2710 100       5136 $year += 1900 if $year < 1000; # undo timelocal funkiness and adjust for RT/53413 / RT/105031
118              
119 2710 100 100     23730 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         10399 return ($sec, $min, $hour, $day, $month, $year, $zone, $subsec);
123             }
124              
125             sub _str2time
126             {
127 2802     2802   15035 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         15602 my ($time, $zonespec) = @_;
133 2802         6155 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       6656 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         3312 my $result;
152 2484 100       4307 if (defined $zone)
153             {
154 563         850 $result = eval { timegm_modern($sec, $min, $hour, $day, $month, $year) };
  563         1657  
155 563 50       18288 return undef unless defined $result;
156 563         823 $result -= $zone;
157             }
158             else
159             {
160 1921         2861 $result = eval { timelocal_modern($sec, $min, $hour, $day, $month, $year) };
  1921         5588  
161 1921 50       138823 return undef unless defined $result;
162             }
163              
164 2484         5834 return $result + $subsec;
165             }
166              
167             sub _parsedate
168             {
169 318     318   1483 require Time::ParseDate;
170 318         620 my ($time, $zonespec) = @_;
171 318 100       1094 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 268002 my $class = shift;
182 8229 100 100     28673 my $zonespec = @_ == 2 || @_ == 7 ? shift : $DEFAULT_ZONE;
183 8229 100       19438 croak("Unrecognized timezone specifier") unless exists $ZONE_FLAG{$zonespec};
184              
185 8228         12096 my $t;
186 8228 100       20912 if (@_ == 0)
    100          
    100          
187             {
188 9         19 $t = time;
189             }
190             elsif (@_ == 6)
191             {
192 44         110 my ($y, $m, $d, $H, $M, $S) = @_;
193 44         87 --$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       73 $t = eval { $zonespec eq 'local'
  44         185  
196             ? timelocal_modern($S, $M, $H, $d, $m, $y)
197             : timegm_modern($S, $M, $H, $d, $m, $y)
198             };
199 44 100       2692 croak("Illegal datetime: $y/" . ($m + 1) . "/$d $H:$M:$S") unless defined $t;
200             }
201             elsif (@_ == 1)
202             {
203 8170         11663 $t = shift;
204 8170 100       25499 if ( my $conv_class = blessed $t )
205             {
206 3214 100       8832 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         22919 return bless { impl => $t }, $class;
211             }
212             else
213             {
214 2         24 croak("Don't know how to convert $conv_class to $class");
215             }
216             }
217             }
218             else
219             {
220 5         56 croak("Illegal number of arguments to datetime()");
221             }
222              
223 5008         15721 bless { impl => scalar Time::Piece->_mktime($t, $ZONE_FLAG{$zonespec}) }, $class;
224             }
225              
226              
227 7     7 1 895 sub is_local { shift->{impl}->[Time::Piece::c_islocal] }
228 11     11 1 1437 sub is_gmt { !shift->{impl}->[Time::Piece::c_islocal] }
229             *is_utc = \&is_gmt;
230              
231              
232             sub as
233             {
234 6     6 1 2623 my ($self, $conv_spec) = @_;
235              
236 6 100       41 if ( $conv_spec =~ /^(\W)(\w+)$/ )
237             {
238 3         14 my $fmt = join($1, map { "%$_" } split('', $2));
  9         26  
239 3         12 return $self->strftime($fmt);
240             }
241 3 100       14 if ( $conv_spec eq 'Time::Piece' )
242             {
243 2         100 return $self->{impl};
244             }
245             else
246             {
247 1         16 croak("Don't know how to convert " . ref( $self) . " to $conv_spec");
248             }
249             }
250              
251              
252             # ACCESSORS
253              
254 1903     1903 1 96147 sub year { shift->{impl}->year }
255 3     3 1 1977 sub month { shift->{impl}->mon }
256 3     3 1 1688 sub day { shift->{impl}->mday }
257 6     6 1 3403 sub hour { shift->{impl}->hour }
258 6     6 1 3362 sub minute { shift->{impl}->min }
259 6     6 1 3767 sub second { shift->{impl}->sec }
260 2608     2608 1 144453 sub epoch { shift->{impl}->epoch }
261 2     2 1 1055 sub time_zone { shift->{impl}->strftime('%Z') }
262 14 100   14 1 714 sub day_of_week { shift->{impl}->day_of_week || 7 } # change Sunday from 0 to 7
263 1462     1462 1 38017 sub day_of_year { shift->{impl}->yday + 1 } # change from 0-based to 1-based
264 24     24 1 1184 sub quarter { int(shift->{impl}->_mon / 3) + 1 } # calc quarter from (zero-based) month
265              
266             sub split
267             {
268 1     1 1 600 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 709858 sub strftime { shift->{impl}->strftime(@_) }
276 2     2 1 1660 sub iso8601 { shift->{impl}->datetime }
277             *iso = \&iso8601;
278              
279              
280             ########################
281             # OVERLOADED OPERATORS #
282             ########################
283              
284             sub _op_convert
285             {
286 14806     14806   21909 my $operand = shift;
287 14806 100       46935 return $operand unless blessed $operand;
288 9897 50       41958 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   7451 my $func = shift;
296 4909         10755 return ref($_[0])->new( scalar $func->(_op_convert($_[0]), _op_convert($_[1]), $_[2]) );
297             }
298              
299 3194     3194   8047 sub _add_seconds { _result_convert( \&Time::Piece::add => @_ ) }
300 1715     1715   4204 sub _subtract_seconds { _result_convert( \&Time::Piece::subtract => @_ ) }
301             # subclasses can override these to change what units an integer represents
302 1338     1338   2563 sub _add_integer { $_[0]->add_seconds($_[1]) }
303 607     607   1106 sub _subtract_integer { $_[0]->subtract_seconds($_[1]) }
304              
305             sub _dispatch_add
306             {
307 2333 100 66 2333   824023 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
308             {
309 17         52 $_[1]->_add_to($_[0]);
310             }
311             else
312             {
313             # this should DTRT for whichever class we are
314 2316         5808 $_[0]->_add_integer($_[1]);
315             }
316             }
317              
318             sub _dispatch_subtract
319             {
320 902 100 100 902   34178 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
    100 66        
321             {
322             # this shouldn't be possible ...
323 16 50       39 die("should have called overloaded - for ::Units") if $_[2];
324             # as the name implies, this method assumes reversed operands
325 16         49 $_[1]->_subtract_from($_[0]);
326             }
327             elsif ( blessed $_[1] && $_[1]->isa('Date::Easy::Datetime') )
328             {
329 33 50       99 my ($lhs, $rhs) = $_[2] ? @_[1,0] : @_[0,1];
330 33 100 66     138 my $divisor = $lhs->isa('Date::Easy::Date') && $rhs->isa('Date::Easy::Date') ? 86_400 : 1;
331 33         74 ($lhs->epoch - $rhs->epoch) / $divisor;
332             }
333             else
334             {
335             # this should DTRT for whichever class we are
336 853         1883 $_[0]->_subtract_integer($_[1]);
337             }
338             }
339              
340             use overload
341 1510     1510   11375 '""' => sub { Time::Piece::cdate (_op_convert($_[0]) ) },
342 10     10   3557 '<=>' => sub { Time::Piece::compare (_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
343 1729     1729   408513 'cmp' => sub { Time::Piece::str_compare(_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
344              
345 18         214 '+' => \&_dispatch_add,
346             '-' => \&_dispatch_subtract,
347 18     18   177 ;
  18         47  
348              
349              
350             # MATH METHODS
351              
352 1464     1464 1 52663 sub add_seconds { shift->_add_seconds (@_) }
353 125     125 1 415 sub add_minutes { shift->_add_seconds ($_[0] * 60) }
354 126     126 1 435 sub add_hours { shift->_add_seconds ($_[0] * 60 * 60) }
355 1479     1479 1 49286 sub add_days { shift->_add_seconds ($_[0] * 60 * 60 * 24) }
356 250     250 1 45491 sub add_weeks { shift->add_days ($_[0] * 7) }
357 19     19 1 3157 sub add_months { ref($_[0])->new( shift->{impl}->add_months(@_) ) }
358 18     18 1 2696 sub add_years { ref($_[0])->new( shift->{impl}->add_years (@_) ) }
359              
360 730     730 1 1561 sub subtract_seconds { shift->_subtract_seconds (@_) }
361 124     124 1 391 sub subtract_minutes { shift->_subtract_seconds ($_[0] * 60) }
362 123     123 1 396 sub subtract_hours { shift->_subtract_seconds ($_[0] * 60 * 60) }
363 738     738 1 47148 sub subtract_days { shift->_subtract_seconds ($_[0] * 60 * 60 * 24) }
364 246     246 1 45454 sub subtract_weeks { shift->subtract_days ($_[0] * 7) }
365 5     5 1 741 sub subtract_months { shift->add_months($_[0] * -1) }
366 6     6 1 720 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__