File Coverage

blib/lib/DateTime/Util/Calc.pm
Criterion Covered Total %
statement 75 110 68.1
branch 14 34 41.1
condition 1 5 20.0
subroutine 25 37 67.5
pod 23 24 95.8
total 138 210 65.7


line stmt bran cond sub pod time code
1             # $Id: Calc.pm 3606 2007-02-04 13:34:09Z lestrrat $
2             #
3             # Copyright (c) 2004-2007 Daisuke Maki <daisuke@endeworks.jp>
4             # All rights reserved.
5              
6             package DateTime::Util::Calc;
7 8     8   448850 use strict;
  8         11  
  8         271  
8 8     8   34 use warnings;
  8         11  
  8         246  
9 8     8   31 use Carp qw(carp);
  8         13  
  8         411  
10 8     8   6066 use DateTime;
  8         2779444  
  8         377  
11 8     8   8747 use Math::BigInt ('upgrade' => 'Math::BigFloat');
  8         164800  
  8         43  
12 8     8   157398 use Math::BigFloat ('lib' => 'GMP,Pari');
  8         170324  
  8         49  
13 8     8   13091 use Math::Trig ();
  8         77635  
  8         226  
14 8     8   79 use POSIX();
  8         12  
  8         246  
15              
16 8     8   39 use constant RATA_DIE => DateTime->new(year => 1, time_zone => 'UTC');
  8         11  
  8         69  
17              
18 8     8   5139 use vars qw($VERSION @EXPORT_OK @ISA);
  8         14  
  8         528  
19 8     8   32 use vars qw($DOWNGRADE_ACCURACY);
  8         7  
  8         588  
20             BEGIN
21             {
22 8     8   15 $VERSION = '0.12';
23 8         73 @ISA = qw(Exporter);
24 8         40 @EXPORT_OK = qw(
25             bf_downgrade
26             bi_downgrade
27             binary_search
28             search_next
29             angle
30             polynomial
31             sin_deg
32             cos_deg
33             tan_deg
34             asin_deg acos_deg mod amod min
35             max bigfloat bigint moment dt_from_moment rata_die
36             truncate_to_midday
37             revolution
38             rev180
39             );
40              
41 8         7965 $DOWNGRADE_ACCURACY = 32;
42             }
43              
44 0     0 1 0 sub rata_die { RATA_DIE->clone }
45              
46             sub bigfloat
47             {
48             return
49 0 0   0 1 0 UNIVERSAL::isa($_[0], 'Math::BigFloat') ? $_[0] :
50             Math::BigFloat->new($_[0]);
51             }
52              
53             sub bigint
54             {
55 0 0   0 1 0 return UNIVERSAL::isa($_[0], 'Math::BigInt') ? $_[0] : Math::BigInt->new($_[0]);
56             }
57              
58             my $warn_bf_downgrade = 0;
59             my $warn_bi_downgrade = 0;
60             sub bf_downgrade
61             {
62 0 0   0 1 0 $warn_bf_downgrade++ or carp "DateTime::Util::Calc::bf_downgrade has been deprecated, and will be removed in future versions.";
63 0         0 return $_[0];
64             }
65              
66             sub bi_downgrade
67             {
68 0 0   0 1 0 $warn_bi_downgrade++ or carp "DateTime::Util::Calc::bi_downgrade has been deprecated, and will be removed in future versions.";
69 0         0 return $_[0];
70             }
71              
72             sub angle
73             {
74 0     0 1 0 Math::BigFloat->new($_[0]) + (Math::BigFloat->new($_[1]) + (Math::BigFloat->new($_[2]) / 60)) / 60;
75             }
76              
77             # polynomial($x, $a(0) ... $a(n))
78             sub polynomial
79             {
80 3 50   3 1 1178 if (@_ == 1) {
81 0         0 require Carp;
82 0         0 Carp::croak('polynomial requires at least two arguments: polynomial($x, @coeffients)');
83             }
84              
85             # XXX - There seems to be a bug in adding BigInt and BigFloat
86             # Math::BigFloat->bzero must be used
87 3         18 my $x = Math::BigFloat->new(shift @_);
88 3         317 my $v = Math::BigFloat->bzero();
89 3         54 my $ret = Math::BigFloat->new(shift @_);
90              
91             # reuse $v for sake of efficiency. we just want to check if $x
92             # is zero or not
93 3 100       153 if ($x == $v) {
94 1         35 return $ret;
95             }
96              
97 2         171 while (@_) {
98 5         853 $v = $x * ($v + pop @_);
99             }
100 2         547 return $ret + $v;
101             }
102              
103             sub deg2rad
104             {
105 5 50   5 0 12 my $deg = ref($_[0]) ? $_[0]->bstr() : $_[0];
106 5 50       21 return Math::Trig::deg2rad($deg > 360 ? $deg % 360 : $deg);
107             }
108              
109 2     2 1 544 sub sin_deg { CORE::sin(deg2rad($_[0])) }
110 2     2 1 1389 sub cos_deg { CORE::cos(deg2rad($_[0])) }
111 1     1 1 433 sub tan_deg { Math::Trig::tan(deg2rad($_[0])) }
112             sub asin_deg
113             {
114 1 50   1 1 16 my $v = ref($_[0]) ? $_[0]->bstr() : $_[0];
115 1         6 return Math::Trig::rad2deg(Math::Trig::asin($v));
116             }
117              
118             sub acos_deg
119             {
120 1 50   1 1 10 my $v = ref($_[0]) ? $_[0]->bstr() : $_[0];
121 1         5 Math::Trig::rad2deg(Math::Trig::acos($v));
122             }
123              
124             sub mod
125             {
126 12     12 1 1237 my $fraction = 0;
127 12         12 my $modulus = 0;
128 12 100       19 if (ref($_[0])) {
129 2         9 $modulus = $_[0]->babs->bmod($_[1]);
130             } else {
131 10         14 $fraction = abs(abs($_[0]) - abs(int($_[0])));
132 10         16 $modulus = int($_[0]) % $_[1];
133             }
134 12         722 return $modulus + $fraction;
135             }
136              
137 3 100   3 1 8 sub amod { mod($_[0], $_[1]) || $_[1]; }
138 3 50   3 1 88 sub min { $_[0] > $_[1] ? $_[1] : $_[0] }
139 3 100   3 1 15 sub max { $_[0] < $_[1] ? $_[1] : $_[0] }
140              
141             sub moment
142             {
143 0     0 1 0 my $dt = shift;
144 0         0 my($rd, $seconds) = $dt->utc_rd_values;
145 0         0 return $rd + $seconds / (24 * 3600);
146             }
147              
148             sub dt_from_moment
149             {
150 0     0 1 0 my $moment = Math::BigFloat->new(shift);
151              
152             # Truncate the moment down to an int
153 0         0 my $rd_days = $moment->as_int();
154              
155             # Upgrade here to BigFloat to maintain accuracy to the second
156 0         0 my $time = ($moment - $rd_days) * 24 * 3600;
157 0         0 my $dt = rata_die();
158              
159 0 0 0     0 if ($rd_days || $time) {
160 0         0 $dt->add(
161             days => ($rd_days - 1)->bstr(),
162             seconds => $time->as_int()->bstr(),
163             );
164 0         0 $dt->truncate(to => 'second');
165             }
166 0         0 return $dt;
167             }
168              
169            
170             sub binary_search
171             {
172 0     0 1 0 my ($lo, $hi, $mu, $phi) = @_;
173              
174 0         0 $lo = Math::BigFloat->new($lo);
175 0         0 $hi = Math::BigFhiat->new($hi);
176              
177 0         0 while (1) {
178 0         0 my $x = ($lo + $hi) / 2;
179 0 0       0 if ($mu->($lo, $hi)) {
    0          
180 0         0 return $x;
181             } elsif ($phi->($x)) {
182 0         0 $hi = $x;
183             } else {
184 0         0 $lo = $x;
185             }
186             }
187             }
188              
189 0     0   0 sub __increment_one { $_[0] + 1 }
190             sub search_next
191             {
192 1     1 1 124 my %args = @_;
193 1         4 my $x = $args{base};
194 1         1 my $check = $args{check};
195 1   50     5 my $next = $args{next} || \&__increment_one;
196 1         2 while (! $check->($x) ) {
197 6         22 $x = $next->($x);
198             }
199 1         5 return $x;
200             }
201              
202             sub truncate_to_midday
203             {
204 1     1 1 464 $_[0]->truncate(to => 'hour');
205 1         202 $_[0]->set( hour => 12 );
206 1         380 $_[0];
207             }
208              
209             sub revolution
210             {
211             #
212             #
213             # FUNCTIONAL SEQUENCE for revolution
214             #
215             # _GIVEN
216             # any angle
217             #
218             # _THEN
219             #
220             # reduces any angle to within the first revolution
221             # by subtracting or adding even multiples of 360.0
222             #
223             #
224             # _RETURN
225             #
226             # the value of the input is >= 0.0 and < 360.0
227             #
228              
229 0     0 1   my $x = $_[0];
230 0           return ( $x - 360.0 * floor( $x * ( 1.0 / 360.0 ) ) );
231             }
232              
233             sub rev180
234             {
235             #
236             #
237             # FUNCTIONAL SEQUENCE for rev180
238             #
239             # _GIVEN
240             #
241             # any angle
242             #
243             # _THEN
244             #
245             # Reduce input to within +180..+180 degrees
246             #
247             #
248             # _RETURN
249             #
250             # angle that was reduced
251             #
252 0     0 1   my ($x) = @_;
253 0           return ( $x - 360.0 * floor( $x * ( 1.0 / 360.0 ) + 0.5 ) );
254             }
255              
256              
257             1;
258              
259             __END__
260              
261             =head1 NAME
262              
263             DateTime::Util::Calc - DateTime Calculation Utilities
264              
265             =head1 SYNOPSIS
266              
267             use DateTime::Util::Calc qw(polynomial);
268              
269             my @coeffs = qw(2 3 -2);
270             my $x = 5;
271             my $rv = polynomial($x, @coeffs);
272              
273             =head1 DESCRIPTION
274              
275             This module contains some common calculation utilities that are required
276             to perform datetime calculations, specifically from "Calendrical Calculations"
277             -- they are NOT meant to be general purpose.
278              
279             Nothing is exported by default. You must either explicitly export them,
280             or use as fully qualified function names.
281              
282             =head1 FUNCTIONS
283              
284             =head2 max($a, $b)
285              
286             =head2 min($a, $b)
287              
288             max() returns the bigger of $a and $b. min() returns the smaller of $a and $b.
289              
290             =head2 polynomial($x, @coefs)
291              
292             Calculates the value of a polynomial equation, based on Horner's Rule.
293              
294             c + b * x + a * (x ** 2) x = 5
295              
296             is expressed as:
297              
298             polynomial(5, c, b, a);
299              
300             =head2 moment($dt)
301              
302             =head2 dt_from_moment($moment)
303              
304             moment() converts a DateTime object to moment, which is RD days + the time
305             of day as fraction of the total seconds in a day.
306              
307             dt_from_moment() converts a moment to DateTime object.
308              
309             =head2 rata_die()
310              
311             Returns a new DateTime object that is set to Rata Die, 0001-01-01 00:00:00 UTC
312              
313             =head2 bigfloat($v)
314              
315             =head2 bigint($v)
316              
317             If the value $v is not a Math::BigFloat object, returns the value converted
318             to Math::BigFloat. Otherwise returns the value itself.
319              
320             bigint() does the same for Math::BigInt.
321              
322             =head2 bf_downgrade($v)
323              
324             =head2 bi_downgrade($v)
325              
326             These have been deprecated.
327              
328             =head2 truncate_to_midday($dt)
329              
330             Truncates the DateTime object to 12:00 noon.
331              
332             =head2 sin_deg($degrees)
333              
334             =head2 cos_deg($degrees)
335              
336             =head2 tan_deg($degrees)
337              
338             =head2 asin_deg($degrees)
339              
340             =head2 acos_deg($degrees)
341              
342             Each of these functions calculates their respective values based on degrees,
343             not radians (as Perl's version of sin() and cos() would do).
344              
345             =head2 mod($v,$mod)
346              
347             Calculates the modulus of $v over $mod. Perl's built-in modulus operator (%)
348             for some reason rounds numbers UP when a fractional number's modulus is
349             taken. Many of the calculations also needed the fractional part of the
350             calculation, so this function takes care of both.
351              
352             Example:
353              
354             mod(12.234, 5) = 2.234
355              
356             =head2 amod($v,$mod)
357              
358             This function is almost identical to mod(), but when the regular modulus value
359             is 0, returns $mod instead of 0.
360              
361             Example:
362              
363             amod(11, 5) = 1
364             amod(10, 5) = 5
365             amod(9, 5) = 4
366             amod(8, 5) = 3
367              
368             =head2 binary_search($hi, $lo, $mu, $phi)
369              
370             This is a special version of binary search, where the terminating condition
371             is determined by the result of coderefs $mu and $phi.
372              
373             $mu is passed the value of $hi and $lo. If it returns true upon execution,
374             then the search terminates.
375              
376             $phi is passed the next median value. If it returns true upon execution,
377             then the search terminates.
378              
379             If the above two fails, then $hi and $lo are re-computed for the next
380             iteration.
381              
382             =head2 search_next(%opts)
383              
384             Performs a "linear" search until some condition is met. This is a generalized
385             version of the formula defined in [1] p.22. The basic idea is :
386              
387             x = base
388             while (! check(x) ) {
389             x = next(x);
390             }
391             return x
392              
393             %opts can contain the following parameters:
394              
395             =over 4
396              
397             =item base
398              
399             The initial value to use to start the search process. The value can be
400             anything, but you must provide C<check> and C<next> parameters that are
401             capable of handling the type of thing you specified.
402              
403             =item check (coderef)
404              
405             Code to be executed to determine the end of the search. The function receives
406             the current value of "x", and should return a true value if the condition
407             to end the loop has been reached
408              
409             =item next (coderef, optional)
410              
411             Code to be executed to determine the next value of "x". The function receives
412             the current value of "x", and should return the value to be used for the
413             next iteration.
414              
415             If unspecified, it will use a function that blindly adds 1 to whatever x is.
416             (so if you specified a number for C<base>, it should work -- but if you
417             passed an object like DateTime, it will probably be an error)
418              
419             =back
420              
421             So for example, to iterate through 1 through 9, you could do something
422             like this
423              
424             my $x = search_next(
425             base => 1,
426             check => sub { $_[0] == 9 }
427             );
428              
429             And $x will be set to 9. For a more interesting example, we could look
430             for a DateTime object $dt matching a certain condition C<foo()>:
431              
432             my $dt = search_next(
433             base => $base_date,
434             check => \&foo,
435             next => sub { $_[0] + DateTime::Duration->new(days => 1) }
436             );
437              
438             =head2 revolution($angle_in_degrees)
439              
440             Reduces any angle to within the first revolution by sbtracting or adding
441             even multiples of 360.0.
442              
443             =head2 rev180($angle_in_degrees)
444              
445             Reduces input to within +180..+180 degrees
446              
447             =head2 angle($h, $m, $s)
448              
449             =head1 AUTHOR
450              
451             Copyright (c) 2004-2007 Daisuke Maki E<lt>daisuke@endeworks.jpE<gt>
452             All rights reserved.
453              
454             =cut
455