File Coverage

blib/lib/DateTimeX/Lite/Arithmetic.pm
Criterion Covered Total %
statement 149 152 98.0
branch 74 80 92.5
condition 37 45 82.2
subroutine 17 17 100.0
pod 0 9 0.0
total 277 303 91.4


line stmt bran cond sub pod time code
1              
2             package DateTimeX::Lite;
3 18     18   114 use strict;
  18         40  
  18         861  
4 18     18   109 use warnings;
  18         46  
  18         746  
5 18     18   134 use Carp ();
  18         51  
  18         519  
6 18     18   120 use Scalar::Util qw(blessed);
  18         43  
  18         1694  
7 18         268 use overload ( 'fallback' => 1,
8             '-' => '_subtract_overload',
9             '+' => '_add_overload',
10 18     18   212 );
  18         53  
11              
12              
13             sub subtract_datetime
14             {
15 51     51 0 5527 my $dt1 = shift;
16 51         87 my $dt2 = shift;
17              
18 51 100       198 $dt2 = $dt2->clone->set_time_zone( $dt1->time_zone )
19             unless $dt1->time_zone->name eq $dt2->time_zone->name;
20              
21             # We only want a negative duration if $dt2 > $dt1 ($self)
22 51 100       312 my ( $bigger, $smaller, $negative ) =
23             ( $dt1 >= $dt2 ?
24             ( $dt1, $dt2, 0 ) :
25             ( $dt2, $dt1, 1 )
26             );
27              
28 51   66     162 my $is_floating = $dt1->time_zone->is_floating &&
29             $dt2->time_zone->is_floating;
30              
31              
32 51         114 my $minute_length = 60;
33 51 100       125 unless ($is_floating)
34             {
35 39         115 my ( $utc_rd_days, $utc_rd_secs ) = $smaller->utc_rd_values;
36              
37 39 100 66     174 if ( $utc_rd_secs >= 86340 && ! $is_floating )
38             {
39             # If the smaller of the two datetimes occurs in the last
40             # UTC minute of the UTC day, then that minute may not be
41             # 60 seconds long. If we need to subtract a minute from
42             # the larger datetime's minutes count in order to adjust
43             # the seconds difference to be positive, we need to know
44             # how long that minute was. If one of the datetimes is
45             # floating, we just assume a minute is 60 seconds.
46              
47 5         27 $minute_length = DateTimeX::Lite::LeapSecond::day_length($utc_rd_days) - 86340;
48             }
49             }
50              
51             # This is a gross hack that basically figures out if the bigger of
52             # the two datetimes is the day of a DST change. If it's a 23 hour
53             # day (switching _to_ DST) then we subtract 60 minutes from the
54             # local time. If it's a 25 hour day then we add 60 minutes to the
55             # local time.
56             #
57             # This produces the most "intuitive" results, though there are
58             # still reversibility problems with the resultant duration.
59             #
60             # However, if the two objects are on the same (local) date, and we
61             # are not crossing a DST change, we don't want to invoke the hack
62             # - see 38local-subtract.t
63 51         197 my $bigger_min = $bigger->hour * 60 + $bigger->minute;
64 51 100 100     183 if ( $bigger->time_zone->has_dst_changes
      66        
65             && ( $bigger->ymd ne $smaller->ymd
66             || $bigger->is_dst != $smaller->is_dst )
67             )
68             {
69              
70             $bigger_min -= 60
71             # it's a 23 hour (local) day
72             if ( $bigger->is_dst
73             &&
74 13 100 66     47 do { local $@;
  7         13  
75 7         13 my $prev_day = eval { $bigger->clone->subtract( days => 1 ) };
  7         29  
76 7 100 100     62 $prev_day && ! $prev_day->is_dst ? 1 : 0 }
77             );
78              
79             $bigger_min += 60
80             # it's a 25 hour (local) day
81             if ( ! $bigger->is_dst
82             &&
83 13 100 66     50 do { local $@;
  6         11  
84 6         12 my $prev_day = eval { $bigger->clone->subtract( days => 1 ) };
  6         27  
85 6 100 66     59 $prev_day && $prev_day->is_dst ? 1 : 0 }
86             );
87             }
88              
89 51         235 my ( $months, $days, $minutes, $seconds, $nanoseconds ) =
90             $dt1->_adjust_for_positive_difference
91             ( $bigger->year * 12 + $bigger->month, $smaller->year * 12 + $smaller->month,
92              
93             $bigger->day, $smaller->day,
94              
95             $bigger_min, $smaller->hour * 60 + $smaller->minute,
96              
97             $bigger->second, $smaller->second,
98              
99             $bigger->nanosecond, $smaller->nanosecond,
100              
101             $minute_length,
102              
103             # XXX - using the smaller as the month length is
104             # somewhat arbitrary, we could also use the bigger -
105             # either way we have reversibility problems
106             DateTimeX::Lite::Util::month_length( $smaller->year, $smaller->month ),
107             );
108              
109 51 100       179 if ($negative)
110             {
111 16         46 for ( $months, $days, $minutes, $seconds, $nanoseconds )
112             {
113             # Some versions of Perl can end up with -0 if we do "0 * -1"!!
114 80 100       231 $_ *= -1 if $_;
115             }
116             }
117              
118             return
119 51         288 DateTimeX::Lite::Duration->new
120             ( months => $months,
121             days => $days,
122             minutes => $minutes,
123             seconds => $seconds,
124             nanoseconds => $nanoseconds,
125             );
126             }
127              
128             sub _adjust_for_positive_difference
129             {
130 55     55   160 my ( $self,
131             $month1, $month2,
132             $day1, $day2,
133             $min1, $min2,
134             $sec1, $sec2,
135             $nano1, $nano2,
136             $minute_length,
137             $month_length,
138             ) = @_;
139              
140 55 100       154 if ( $nano1 < $nano2 )
141             {
142 7         13 $sec1--;
143 7         29 $nano1 += &DateTimeX::Lite::MAX_NANOSECONDS;
144             }
145              
146 55 100       148 if ( $sec1 < $sec2 )
147             {
148 8         10 $min1--;
149 8         12 $sec1 += $minute_length;
150             }
151              
152             # A day always has 24 * 60 minutes, though the minutes may vary in
153             # length.
154 55 100       140 if ( $min1 < $min2 )
155             {
156 8         13 $day1--;
157 8         16 $min1 += 24 * 60;
158             }
159              
160 55 100       153 if ( $day1 < $day2 )
161             {
162 12         15 $month1--;
163 12         32 $day1 += $month_length;
164             }
165              
166 55         223 return ( $month1 - $month2,
167             $day1 - $day2,
168             $min1 - $min2,
169             $sec1 - $sec2,
170             $nano1 - $nano2,
171             );
172             }
173              
174             sub subtract_datetime_absolute
175             {
176 8     8 0 48 my $self = shift;
177 8         15 my $dt = shift;
178              
179 8         35 my $utc_rd_secs1 = $self->utc_rd_as_seconds;
180 8 100       30 $utc_rd_secs1 += DateTimeX::Lite::LeapSecond::leap_seconds( $self->{utc_rd_days} )
181             if ! $self->time_zone->is_floating;
182              
183 8         33 my $utc_rd_secs2 = $dt->utc_rd_as_seconds;
184 8 100       27 $utc_rd_secs2 += DateTimeX::Lite::LeapSecond::leap_seconds( $dt->{utc_rd_days} )
185             if ! $dt->time_zone->is_floating;
186              
187 8         18 my $seconds = $utc_rd_secs1 - $utc_rd_secs2;
188 8         29 my $nanoseconds = $self->nanosecond - $dt->nanosecond;
189              
190 8 100       31 if ( $nanoseconds < 0 )
191             {
192 1         2 $seconds--;
193 1         4 $nanoseconds += &DateTimeX::Lite::MAX_NANOSECONDS;
194             }
195              
196             return
197 8         40 DateTimeX::Lite::Duration->new
198             ( seconds => $seconds,
199             nanoseconds => $nanoseconds,
200             );
201             }
202              
203             sub delta_md
204             {
205 4     4 0 3445 my $self = shift;
206 4         9 my $dt = shift;
207              
208 4         37 my ( $smaller, $bigger ) = sort $self, $dt;
209              
210 4         23 my ( $months, $days, undef, undef, undef ) =
211             $dt->_adjust_for_positive_difference
212             ( $bigger->year * 12 + $bigger->month, $smaller->year * 12 + $smaller->month,
213              
214             $bigger->day, $smaller->day,
215              
216             0, 0,
217              
218             0, 0,
219              
220             0, 0,
221              
222             60,
223              
224             DateTimeX::Lite::Util::month_length( $smaller->year, $smaller->month ),
225             );
226              
227 4         33 return DateTimeX::Lite::Duration->new( months => $months,
228             days => $days );
229             }
230              
231             sub delta_days
232             {
233 5     5 0 2435 my $self = shift;
234 5         14 my $dt = shift;
235              
236 5         33 my ( $smaller, $bigger ) = sort( ($self->local_rd_values)[0], ($dt->local_rd_values)[0] );
237              
238 5         39 DateTimeX::Lite::Duration->new( days => $bigger - $smaller );
239             }
240              
241             sub delta_ms
242             {
243 3     3 0 11 my $self = shift;
244 3         6 my $dt = shift;
245              
246 3         27 my ( $smaller, $greater ) = sort $self, $dt;
247              
248 3         13 my $days = int( $greater->jd - $smaller->jd );
249              
250 3         16 my $dur = $greater->subtract_datetime($smaller);
251              
252 3         8 my %p;
253 3         11 $p{hours} = $dur->hours + ( $days * 24 );
254 3         12 $p{minutes} = $dur->minutes;
255 3         12 $p{seconds} = $dur->seconds;
256              
257 3         13 return DateTimeX::Lite::Duration->new(%p);
258             }
259              
260             sub _add_overload
261             {
262 4358     4358   8246 my ( $dt, $dur, $reversed ) = @_;
263              
264 4358 50       9867 if ($reversed)
265             {
266 0         0 ( $dur, $dt ) = ( $dt, $dur );
267             }
268              
269 4358 100 100     36549 unless ( blessed $dur && $dur->isa( 'DateTimeX::Lite::Duration' ) )
270             {
271 2         3 my $class = ref $dt;
272 2         8 my $dt_string = overload::StrVal($dt);
273              
274 2         297 Carp::croak( "Cannot add $dur to a $class object ($dt_string).\n"
275             . " Only a DateTimeX::Lite::Duration object can "
276             . " be added to a $class object." );
277             }
278              
279 4356         15007 return $dt->clone->add_duration($dur);
280             }
281              
282             sub _subtract_overload
283             {
284 1229     1229   3829 my ( $date1, $date2, $reversed ) = @_;
285              
286 1229 50       2732 if ($reversed)
287             {
288 0         0 ( $date2, $date1 ) = ( $date1, $date2 );
289             }
290              
291 1229 100 100     11576 if ( blessed $date2 && $date2->isa( 'DateTimeX::Lite::Duration' ) )
    100 100        
292             {
293 1208         4656 my $new = $date1->clone;
294 1208         6794 $new->add_duration( $date2->inverse );
295 1208         11503 return $new;
296             }
297             elsif ( blessed $date2 && $date2->isa( 'DateTimeX::Lite' ) )
298             {
299 19         71 return $date1->subtract_datetime($date2);
300             }
301             else
302             {
303 2         5 my $class = ref $date1;
304 2         6 my $dt_string = overload::StrVal($date1);
305              
306 2         275 Carp::croak( "Cannot subtract $date2 from a $class object ($dt_string).\n"
307             . " Only a DateTimeX::Lite::Duration or DateTimeX::Lite object can "
308             . " be subtracted from a $class object." );
309             }
310             }
311              
312 620     620 0 7721 sub add { return shift->add_duration( DateTimeX::Lite::Duration->new(@_) ) }
313              
314 54     54 0 387 sub subtract { return shift->subtract_duration( DateTimeX::Lite::Duration->new(@_) ) }
315              
316 71     71 0 367 sub subtract_duration { return $_[0]->add_duration( $_[1]->inverse ) }
317              
318             sub add_duration
319             {
320 6324     6324 0 10233 my ($self, $dur) = @_;
321 6324 50 33     42972 if (! blessed $dur || !$dur->isa('DateTimeX::Lite::Duration')) {
322 0         0 Carp::croak("Duration is not a DateTimeX::Lite::Duration object");
323             }
324              
325             # simple optimization
326 6324 50       23692 return $self if $dur->is_zero;
327              
328 6324         23986 my %deltas = $dur->deltas;
329              
330             # This bit isn't quite right since DateTimeX::Lite::Infinite::Future -
331             # infinite duration should NaN
332 6324         20519 foreach my $val ( values %deltas )
333             {
334 31612         31528 my $inf;
335 31612 100       142024 if ( $val == &DateTimeX::Lite::INFINITY )
    100          
336             {
337 1         9 $inf = DateTimeX::Lite::Infinite::Future->new;
338             }
339             elsif ( $val == &DateTimeX::Lite::NEG_INFINITY )
340             {
341 1         9 $inf = DateTimeX::Lite::Infinite::Past->new;
342             }
343              
344 31612 100       76475 if ($inf)
345             {
346 2         25 %$self = %$inf;
347 2         14 bless $self, blessed $inf;
348              
349 2         11 return $self;
350             }
351             }
352              
353 6322 100       22679 return $self if $self->is_infinite;
354              
355 6318 100       16608 if ( $deltas{days} )
356             {
357 3483         7121 $self->{local_rd_days} += $deltas{days};
358              
359 3483         8729 $self->{utc_year} += int( $deltas{days} / 365 ) + 1;
360             }
361              
362 6318 100       14632 if ( $deltas{months} )
363             {
364             # For preserve mode, if it is the last day of the month, make
365             # it the 0th day of the following month (which then will
366             # normalize back to the last day of the new month).
367 575 100       2323 my ($y, $m, $d) = ( $dur->is_preserve_mode ?
368             DateTimeX::Lite::Util::rd2ymd( $self->{local_rd_days} + 1 ) :
369             DateTimeX::Lite::Util::rd2ymd( $self->{local_rd_days} )
370             );
371              
372 575 100       1950 $d -= 1 if $dur->is_preserve_mode;
373              
374 575 100 100     2223 if ( ! $dur->is_wrap_mode && $d > 28 )
375             {
376             # find the rd for the last day of our target month
377 1         7 $self->{local_rd_days} = DateTimeX::Lite::Util::ymd2rd( $y, $m + $deltas{months} + 1, 0 );
378              
379             # what day of the month is it? (discard year and month)
380 1         6 my $last_day = (DateTimeX::Lite::Util::rd2ymd( $self->{local_rd_days} ))[2];
381              
382             # if our original day was less than the last day,
383             # use that instead
384 1 50       6 $self->{local_rd_days} -= $last_day - $d if $last_day > $d;
385             }
386             else
387             {
388 574         3824 $self->{local_rd_days} = DateTimeX::Lite::Util::ymd2rd( $y, $m + $deltas{months}, $d );
389             }
390              
391 575         2136 $self->{utc_year} += int( $deltas{months} / 12 ) + 1;
392             }
393              
394 6318 100 100     23617 if ( $deltas{days} || $deltas{months} )
395             {
396 4056         12009 $self->_calc_utc_rd;
397              
398 4053         12701 $self->_handle_offset_modifier( $self->second );
399             }
400              
401 6315 100       20426 if ( $deltas{minutes} )
402             {
403 85         220 $self->{utc_rd_secs} += $deltas{minutes} * 60;
404              
405             # This intentionally ignores leap seconds
406 85         332 DateTimeX::Lite::Util::normalize_tai_seconds( $self->{utc_rd_days}, $self->{utc_rd_secs} );
407             }
408              
409 6315 100 100     25386 if ( $deltas{seconds} || $deltas{nanoseconds} )
410             {
411 2200         4418 $self->{utc_rd_secs} += $deltas{seconds};
412              
413 2200 100       5385 if ( $deltas{nanoseconds} )
414             {
415 3         9 $self->{rd_nanosecs} += $deltas{nanoseconds};
416 3         15 DateTimeX::Lite::Util::normalize_nanoseconds( $self->{utc_rd_secs}, $self->{rd_nanosecs} );
417             }
418              
419 2200         7517 DateTimeX::Lite::Util::normalize_seconds($self);
420              
421             # This might be some big number much bigger than 60, but
422             # that's ok (there are tests in 19leap_second.t to confirm
423             # that)
424 2200         7213 $self->_handle_offset_modifier( $self->second + $deltas{seconds} );
425             }
426              
427 6315 50       37049 my $new =
428             (ref $self)->from_object
429             ( object => $self,
430             locale => $self->{locale},
431             ( $self->{formatter} ? ( formatter => $self->{formatter} ) : () ),
432             );
433              
434 6314         78765 %$self = %$new;
435              
436 6314         76319 return $self;
437             }
438              
439             1;