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   317911 use strict;
  18         67  
  18         443  
4 18     18   74 use warnings;
  18         28  
  18         361  
5 18     18   1309 use autodie;
  18         36658  
  18         88  
6              
7             our $VERSION = '0.10'; # VERSION
8              
9 18     18   82254 use Exporter;
  18         47  
  18         742  
10 18     18   107 use parent 'Exporter';
  18         34  
  18         127  
11             our @EXPORT_OK = qw< datetime now >;
12             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
13              
14 18     18   2006 use Carp;
  18         33  
  18         1050  
15 18     18   9299 use Time::Piece;
  18         165228  
  18         77  
16 18     18   1563 use Scalar::Util 'blessed';
  18         36  
  18         870  
17 18     18   91 use Time::Local 1.26, qw< timegm_modern timelocal_modern >;
  18         33  
  18         39105  
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   6393 my @args;
30 19 100       121 exists $ZONE_FLAG{$_} ? $DEFAULT_ZONE = $_ : push @args, $_ foreach @_;
31 19         54 @_ = @args;
32 19         5770 goto &Exporter::import;
33             }
34              
35              
36             ##############################
37             # FUNCTIONS (*NOT* METHODS!) #
38             ##############################
39              
40             sub datetime
41             {
42 2809 100   2809 1 1737048 my $zonespec = @_ % 2 == 0 ? shift : $DEFAULT_ZONE;
43 2809         4301 my $datetime = shift;
44 2809 100       10540 if ( $datetime =~ /^-?\d+$/ )
45             {
46 7         43 return Date::Easy::Datetime->new($zonespec, $datetime);
47             }
48             else
49             {
50 2802         4806 my $t = _str2time($datetime, $zonespec);
51 2802 100       5025 $t = _parsedate($datetime, $zonespec) unless defined $t;
52 2802 100       80518 croak("Illegal datetime: $datetime") unless defined $t;
53 2801         6787 return Date::Easy::Datetime->new( $zonespec, $t );
54             }
55 0         0 die("reached unreachable code");
56             }
57              
58 5     5 1 1885 sub now () { Date::Easy::Datetime->new }
59              
60              
61             sub _strptime
62             {
63 3047     3047   8501 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         17092 my ($str, $zonespec) = @_;
102              
103 3047 100       58405 my ($sec, $min, $hour, $day, $month, $year, $zone)
104             = Date::Parse::strptime($str, $zonespec eq 'local' ? () : $zonespec);
105 3047         241992 my $num_defined = defined($day) + defined($month) + defined($year);
106 3047 100       7021 return () if $num_defined == 0;
107 2710 100       4988 if ($num_defined < 3)
108             {
109 12         43 my @lt = localtime(time);
110              
111 12 50       16431 $month = $lt[4] unless defined $month;
112 12 50       28 $day = $lt[3] unless defined $day;
113 12 50       57 $year = ($month > $lt[4]) ? ($lt[5] - 1) : $lt[5] unless defined $year;
    50          
114             }
115 2710   100     8189 $hour ||= 0; $min ||= 0; $sec ||= 0; # default time components to zero
  2710   100     6913  
  2710   100     9666  
116 2710         3894 my $subsec = $sec - int($sec); $sec = int($sec);# extract any fractional part (e.g. milliseconds)
  2710         2992  
117 2710 100       4502 $year += 1900 if $year < 1000; # undo timelocal funkiness and adjust for RT/53413 / RT/105031
118              
119 2710 100 100     20543 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         9022 return ($sec, $min, $hour, $day, $month, $year, $zone, $subsec);
123             }
124              
125             sub _str2time
126             {
127 2802     2802   13104 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         12832 my ($time, $zonespec) = @_;
133 2802         4544 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       5158 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         2794 my $result;
152 2484 100       3539 if (defined $zone)
153             {
154 563         696 $result = eval { timegm_modern($sec, $min, $hour, $day, $month, $year) };
  563         1492  
155 563 50       15211 return undef unless defined $result;
156 563         740 $result -= $zone;
157             }
158             else
159             {
160 1921         2517 $result = eval { timelocal_modern($sec, $min, $hour, $day, $month, $year) };
  1921         4855  
161 1921 50       117286 return undef unless defined $result;
162             }
163              
164 2484         4770 return $result + $subsec;
165             }
166              
167             sub _parsedate
168             {
169 318     318   1340 require Time::ParseDate;
170 318         586 my ($time, $zonespec) = @_;
171 318 100       1026 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 242567 my $class = shift;
182 8229 100 100     22989 my $zonespec = @_ == 2 || @_ == 7 ? shift : $DEFAULT_ZONE;
183 8229 100       15430 croak("Unrecognized timezone specifier") unless exists $ZONE_FLAG{$zonespec};
184              
185 8228         14327 my $t;
186 8228 100       18413 if (@_ == 0)
    100          
    100          
187             {
188 9         62 $t = time;
189             }
190             elsif (@_ == 6)
191             {
192 44         94 my ($y, $m, $d, $H, $M, $S) = @_;
193 44         70 --$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       59 $t = eval { $zonespec eq 'local'
  44         158  
196             ? timelocal_modern($S, $M, $H, $d, $m, $y)
197             : timegm_modern($S, $M, $H, $d, $m, $y)
198             };
199 44 100       3699 croak("Illegal datetime: $y/" . ($m + 1) . "/$d $H:$M:$S") unless defined $t;
200             }
201             elsif (@_ == 1)
202             {
203 8170         9419 $t = shift;
204 8170 100       20179 if ( my $conv_class = blessed $t )
205             {
206 3214 100       7150 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         18233 return bless { impl => $t }, $class;
211             }
212             else
213             {
214 2         21 croak("Don't know how to convert $conv_class to $class");
215             }
216             }
217             }
218             else
219             {
220 5         54 croak("Illegal number of arguments to datetime()");
221             }
222              
223 5008         12912 bless { impl => scalar Time::Piece->_mktime($t, $ZONE_FLAG{$zonespec}) }, $class;
224             }
225              
226              
227 7     7 1 831 sub is_local { shift->{impl}->[Time::Piece::c_islocal] }
228 11     11 1 1174 sub is_gmt { !shift->{impl}->[Time::Piece::c_islocal] }
229             *is_utc = \&is_gmt;
230              
231              
232             sub as
233             {
234 6     6 1 2160 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         20  
239 3         9 return $self->strftime($fmt);
240             }
241 3 100       9 if ( $conv_spec eq 'Time::Piece' )
242             {
243 2         74 return $self->{impl};
244             }
245             else
246             {
247 1         14 croak("Don't know how to convert " . ref( $self) . " to $conv_spec");
248             }
249             }
250              
251              
252             # ACCESSORS
253              
254 1903     1903 1 81634 sub year { shift->{impl}->year }
255 3     3 1 1637 sub month { shift->{impl}->mon }
256 3     3 1 1360 sub day { shift->{impl}->mday }
257 6     6 1 2741 sub hour { shift->{impl}->hour }
258 6     6 1 2778 sub minute { shift->{impl}->min }
259 6     6 1 3028 sub second { shift->{impl}->sec }
260 2608     2608 1 118195 sub epoch { shift->{impl}->epoch }
261 2     2 1 987 sub time_zone { shift->{impl}->strftime('%Z') }
262 14 100   14 1 586 sub day_of_week { shift->{impl}->day_of_week || 7 } # change Sunday from 0 to 7
263 1462     1462 1 31187 sub day_of_year { shift->{impl}->yday + 1 } # change from 0-based to 1-based
264 24     24 1 985 sub quarter { int(shift->{impl}->_mon / 3) + 1 } # calc quarter from (zero-based) month
265              
266             sub split
267             {
268 1     1 1 460 my $impl = shift->{impl};
269 1         3 ( $impl->year, $impl->mon, $impl->mday, $impl->hour, $impl->min, $impl->sec )
270             }
271              
272              
273             # FORMATTERS
274              
275 1348     1348 1 739553 sub strftime { shift->{impl}->strftime(@_) }
276 2     2 1 1349 sub iso8601 { shift->{impl}->datetime }
277             *iso = \&iso8601;
278              
279              
280             ########################
281             # OVERLOADED OPERATORS #
282             ########################
283              
284             sub _op_convert
285             {
286 14806     14806   18563 my $operand = shift;
287 14806 100       38444 return $operand unless blessed $operand;
288 9897 50       32727 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   5846 my $func = shift;
296 4909         9180 return ref($_[0])->new( scalar $func->(_op_convert($_[0]), _op_convert($_[1]), $_[2]) );
297             }
298              
299 3194     3194   6403 sub _add_seconds { _result_convert( \&Time::Piece::add => @_ ) }
300 1715     1715   3214 sub _subtract_seconds { _result_convert( \&Time::Piece::subtract => @_ ) }
301             # subclasses can override these to change what units an integer represents
302 1338     1338   2081 sub _add_integer { $_[0]->add_seconds($_[1]) }
303 607     607   921 sub _subtract_integer { $_[0]->subtract_seconds($_[1]) }
304              
305             sub _dispatch_add
306             {
307 2333 100 66 2333   671292 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
308             {
309 17         44 $_[1]->_add_to($_[0]);
310             }
311             else
312             {
313             # this should DTRT for whichever class we are
314 2316         4995 $_[0]->_add_integer($_[1]);
315             }
316             }
317              
318             sub _dispatch_subtract
319             {
320 902 100 100 902   27585 if ( blessed $_[1] && $_[1]->isa('Date::Easy::Units') )
    100 66        
321             {
322             # this shouldn't be possible ...
323 16 50       33 die("should have called overloaded - for ::Units") if $_[2];
324             # as the name implies, this method assumes reversed operands
325 16         44 $_[1]->_subtract_from($_[0]);
326             }
327             elsif ( blessed $_[1] && $_[1]->isa('Date::Easy::Datetime') )
328             {
329 33 50       77 my ($lhs, $rhs) = $_[2] ? @_[1,0] : @_[0,1];
330 33 100 66     112 my $divisor = $lhs->isa('Date::Easy::Date') && $rhs->isa('Date::Easy::Date') ? 86_400 : 1;
331 33         61 ($lhs->epoch - $rhs->epoch) / $divisor;
332             }
333             else
334             {
335             # this should DTRT for whichever class we are
336 853         1549 $_[0]->_subtract_integer($_[1]);
337             }
338             }
339              
340             use overload
341 1510     1510   9083 '""' => sub { Time::Piece::cdate (_op_convert($_[0]) ) },
342 10     10   2927 '<=>' => sub { Time::Piece::compare (_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
343 1729     1729   333177 'cmp' => sub { Time::Piece::str_compare(_op_convert($_[0]), _op_convert($_[1]), $_[2]) },
344              
345 18         213 '+' => \&_dispatch_add,
346             '-' => \&_dispatch_subtract,
347 18     18   156 ;
  18         34  
348              
349              
350             # MATH METHODS
351              
352 1464     1464 1 43546 sub add_seconds { shift->_add_seconds (@_) }
353 125     125 1 305 sub add_minutes { shift->_add_seconds ($_[0] * 60) }
354 126     126 1 316 sub add_hours { shift->_add_seconds ($_[0] * 60 * 60) }
355 1479     1479 1 39499 sub add_days { shift->_add_seconds ($_[0] * 60 * 60 * 24) }
356 250     250 1 36920 sub add_weeks { shift->add_days ($_[0] * 7) }
357 19     19 1 2546 sub add_months { ref($_[0])->new( shift->{impl}->add_months(@_) ) }
358 18     18 1 2273 sub add_years { ref($_[0])->new( shift->{impl}->add_years (@_) ) }
359              
360 730     730 1 1302 sub subtract_seconds { shift->_subtract_seconds (@_) }
361 124     124 1 292 sub subtract_minutes { shift->_subtract_seconds ($_[0] * 60) }
362 123     123 1 303 sub subtract_hours { shift->_subtract_seconds ($_[0] * 60 * 60) }
363 738     738 1 37828 sub subtract_days { shift->_subtract_seconds ($_[0] * 60 * 60 * 24) }
364 246     246 1 37111 sub subtract_weeks { shift->subtract_days ($_[0] * 7) }
365 5     5 1 580 sub subtract_months { shift->add_months($_[0] * -1) }
366 6     6 1 585 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__