SYN SYN
[p5sagit/p5-mst-13.2.git] / lib / Math / BigFloat.pm
1 package Math::BigFloat;
2
3 use Math::BigInt;
4
5 use Exporter;  # just for use to be happy
6 @ISA = (Exporter);
7 $VERSION = '0.01';      # never had version before
8
9 use overload
10 '+'     =>      sub {new Math::BigFloat &fadd},
11 '-'     =>      sub {new Math::BigFloat
12                        $_[2]? fsub($_[1],${$_[0]}) : fsub(${$_[0]},$_[1])},
13 '<=>'   =>      sub {$_[2]? fcmp($_[1],${$_[0]}) : fcmp(${$_[0]},$_[1])},
14 'cmp'   =>      sub {$_[2]? ($_[1] cmp ${$_[0]}) : (${$_[0]} cmp $_[1])},
15 '*'     =>      sub {new Math::BigFloat &fmul},
16 '/'     =>      sub {new Math::BigFloat 
17                        $_[2]? scalar fdiv($_[1],${$_[0]}) :
18                          scalar fdiv(${$_[0]},$_[1])},
19 'neg'   =>      sub {new Math::BigFloat &fneg},
20 'abs'   =>      sub {new Math::BigFloat &fabs},
21
22 qw(
23 ""      stringify
24 0+      numify)                 # Order of arguments unsignificant
25 ;
26
27 sub new {
28   my ($class) = shift;
29   my ($foo) = fnorm(shift);
30   bless \$foo, $class;
31 }
32
33 sub numify { 0 + "${$_[0]}" }   # Not needed, additional overhead
34                                 # comparing to direct compilation based on
35                                 # stringify
36 sub stringify {
37     my $n = ${$_[0]};
38
39     my $minus = ($n =~ s/^([+-])// && $1 eq '-');
40     $n =~ s/E//;
41
42     $n =~ s/([-+]\d+)$//;
43
44     my $e = $1;
45     my $ln = length($n);
46
47     if ($e > 0) {
48         $n .= "0" x $e . '.';
49     } elsif (abs($e) < $ln) {
50         substr($n, $ln + $e, 0) = '.';
51     } else {
52         $n = '.' . ("0" x (abs($e) - $ln)) . $n;
53     }
54     $n = "-$n" if $minus;
55
56     # 1 while $n =~ s/(.*\d)(\d\d\d)/$1,$2/;
57
58     return $n;
59 }
60
61 $div_scale = 40;
62
63 # Rounding modes one of 'even', 'odd', '+inf', '-inf', 'zero' or 'trunc'.
64
65 $rnd_mode = 'even';
66
67 sub fadd; sub fsub; sub fmul; sub fdiv;
68 sub fneg; sub fabs; sub fcmp;
69 sub fround; sub ffround;
70 sub fnorm; sub fsqrt;
71
72 # Convert a number to canonical string form.
73 #   Takes something that looks like a number and converts it to
74 #   the form /^[+-]\d+E[+-]\d+$/.
75 sub fnorm { #(string) return fnum_str
76     local($_) = @_;
77     s/\s+//g;                               # strip white space
78     no warnings;        # $4 and $5 below might legitimately be undefined
79     if (/^([+-]?)(\d*)(\.(\d*))?([Ee]([+-]?\d+))?$/ && "$2$4" ne '') {
80         &norm(($1 ? "$1$2$4" : "+$2$4"),(($4 ne '') ? $6-length($4) : $6));
81     } else {
82         'NaN';
83     }
84 }
85
86 # normalize number -- for internal use
87 sub norm { #(mantissa, exponent) return fnum_str
88     local($_, $exp) = @_;
89         $exp = 0 unless defined $exp;
90     if ($_ eq 'NaN') {
91         'NaN';
92     } else {
93         s/^([+-])0+/$1/;                        # strip leading zeros
94         if (length($_) == 1) {
95             '+0E+0';
96         } else {
97             $exp += length($1) if (s/(0+)$//);  # strip trailing zeros
98             sprintf("%sE%+ld", $_, $exp);
99         }
100     }
101 }
102
103 # negation
104 sub fneg { #(fnum_str) return fnum_str
105     local($_) = fnorm($_[$[]);
106     vec($_,0,8) ^= ord('+') ^ ord('-') unless $_ eq '+0E+0'; # flip sign
107     s/^H/N/;
108     $_;
109 }
110
111 # absolute value
112 sub fabs { #(fnum_str) return fnum_str
113     local($_) = fnorm($_[$[]);
114     s/^-/+/;                                   # mash sign
115     $_;
116 }
117
118 # multiplication
119 sub fmul { #(fnum_str, fnum_str) return fnum_str
120     local($x,$y) = (fnorm($_[$[]),fnorm($_[$[+1]));
121     if ($x eq 'NaN' || $y eq 'NaN') {
122         'NaN';
123     } else {
124         local($xm,$xe) = split('E',$x);
125         local($ym,$ye) = split('E',$y);
126         &norm(Math::BigInt::bmul($xm,$ym),$xe+$ye);
127     }
128 }
129
130 # addition
131 sub fadd { #(fnum_str, fnum_str) return fnum_str
132     local($x,$y) = (fnorm($_[$[]),fnorm($_[$[+1]));
133     if ($x eq 'NaN' || $y eq 'NaN') {
134         'NaN';
135     } else {
136         local($xm,$xe) = split('E',$x);
137         local($ym,$ye) = split('E',$y);
138         ($xm,$xe,$ym,$ye) = ($ym,$ye,$xm,$xe) if ($xe < $ye);
139         &norm(Math::BigInt::badd($ym,$xm.('0' x ($xe-$ye))),$ye);
140     }
141 }
142
143 # subtraction
144 sub fsub { #(fnum_str, fnum_str) return fnum_str
145     fadd($_[$[],fneg($_[$[+1]));    
146 }
147
148 # division
149 #   args are dividend, divisor, scale (optional)
150 #   result has at most max(scale, length(dividend), length(divisor)) digits
151 sub fdiv #(fnum_str, fnum_str[,scale]) return fnum_str
152 {
153     local($x,$y,$scale) = (fnorm($_[$[]),fnorm($_[$[+1]),$_[$[+2]);
154     if ($x eq 'NaN' || $y eq 'NaN' || $y eq '+0E+0') {
155         'NaN';
156     } else {
157         local($xm,$xe) = split('E',$x);
158         local($ym,$ye) = split('E',$y);
159         $scale = $div_scale if (!$scale);
160         $scale = length($xm)-1 if (length($xm)-1 > $scale);
161         $scale = length($ym)-1 if (length($ym)-1 > $scale);
162         $scale = $scale + length($ym) - length($xm);
163         &norm(&round(Math::BigInt::bdiv($xm.('0' x $scale),$ym),
164                     Math::BigInt::babs($ym)),
165             $xe-$ye-$scale);
166     }
167 }
168
169 # round int $q based on fraction $r/$base using $rnd_mode
170 sub round { #(int_str, int_str, int_str) return int_str
171     local($q,$r,$base) = @_;
172     if ($q eq 'NaN' || $r eq 'NaN') {
173         'NaN';
174     } elsif ($rnd_mode eq 'trunc') {
175         $q;                         # just truncate
176     } else {
177         local($cmp) = Math::BigInt::bcmp(Math::BigInt::bmul($r,'+2'),$base);
178         if ( $cmp < 0 ||
179                  ($cmp == 0 &&                                          (
180                    ($rnd_mode eq 'zero'                            ) ||
181                    ($rnd_mode eq '-inf' && (substr($q,$[,1) eq '+')) ||
182                    ($rnd_mode eq '+inf' && (substr($q,$[,1) eq '-')) ||
183                    ($rnd_mode eq 'even' && $q =~ /[13579]$/        ) ||
184                    ($rnd_mode eq 'odd'  && $q =~ /[24680]$/        )    )
185                   ) 
186                 ) {
187             $q;                     # round down
188         } else {
189             Math::BigInt::badd($q, ((substr($q,$[,1) eq '-') ? '-1' : '+1'));
190                                     # round up
191         }
192     }
193 }
194
195 # round the mantissa of $x to $scale digits
196 sub fround { #(fnum_str, scale) return fnum_str
197     local($x,$scale) = (fnorm($_[$[]),$_[$[+1]);
198     if ($x eq 'NaN' || $scale <= 0) {
199         $x;
200     } else {
201         local($xm,$xe) = split('E',$x);
202         if (length($xm)-1 <= $scale) {
203             $x;
204         } else {
205             &norm(&round(substr($xm,$[,$scale+1),
206                          "+0".substr($xm,$[+$scale+1,1),"+10"),
207                   $xe+length($xm)-$scale-1);
208         }
209     }
210 }
211
212 # round $x at the 10 to the $scale digit place
213 sub ffround { #(fnum_str, scale) return fnum_str
214     local($x,$scale) = (fnorm($_[$[]),$_[$[+1]);
215     if ($x eq 'NaN') {
216         'NaN';
217     } else {
218         local($xm,$xe) = split('E',$x);
219         if ($xe >= $scale) {
220             $x;
221         } else {
222             $xe = length($xm)+$xe-$scale;
223             if ($xe < 1) {
224                 '+0E+0';
225             } elsif ($xe == 1) {
226                 # The first substr preserves the sign, passing a non-
227                 # normalized "-0" to &round when rounding -0.006 (for
228                 # example), purely so &round won't lose the sign.
229                 &norm(&round(substr($xm,$[,1).'0',
230                       "+0".substr($xm,$[+1,1),"+10"), $scale);
231             } else {
232                 &norm(&round(substr($xm,$[,$xe),
233                       "+0".substr($xm,$[+$xe,1),"+10"), $scale);
234             }
235         }
236     }
237 }
238     
239 # compare 2 values returns one of undef, <0, =0, >0
240 #   returns undef if either or both input value are not numbers
241 sub fcmp #(fnum_str, fnum_str) return cond_code
242 {
243     local($x, $y) = (fnorm($_[$[]),fnorm($_[$[+1]));
244     if ($x eq "NaN" || $y eq "NaN") {
245         undef;
246     } else {
247         local($xm,$xe,$ym,$ye) = split('E', $x."E$y");
248         if ($xm eq '+0' || $ym eq '+0') {
249             return $xm <=> $ym;
250         }
251         if ( $xe < $ye )        # adjust the exponents to be equal
252         {
253                 $ym .= '0' x ($ye - $xe);
254                 $ye = $xe;
255         }
256         elsif ( $ye < $xe )     # same here
257         {
258                 $xm .= '0' x ($xe - $ye);
259                 $xe = $ye;
260         }
261         return Math::BigInt::cmp($xm,$ym);
262     }
263 }
264
265 # square root by Newtons method.
266 sub fsqrt { #(fnum_str[, scale]) return fnum_str
267     local($x, $scale) = (fnorm($_[$[]), $_[$[+1]);
268     if ($x eq 'NaN' || $x =~ /^-/) {
269         'NaN';
270     } elsif ($x eq '+0E+0') {
271         '+0E+0';
272     } else {
273         local($xm, $xe) = split('E',$x);
274         $scale = $div_scale if (!$scale);
275         $scale = length($xm)-1 if ($scale < length($xm)-1);
276         local($gs, $guess) = (1, sprintf("1E%+d", (length($xm)+$xe-1)/2));
277         while ($gs < 2*$scale) {
278             $guess = fmul(fadd($guess,fdiv($x,$guess,$gs*2)),".5");
279             $gs *= 2;
280         }
281         new Math::BigFloat &fround($guess, $scale);
282     }
283 }
284
285 1;
286 __END__
287
288 =head1 NAME
289
290 Math::BigFloat - Arbitrary length float math package
291
292 =head1 SYNOPSIS
293
294   use Math::BigFloat;
295   $f = Math::BigFloat->new($string);
296
297   $f->fadd(NSTR) return NSTR            addition
298   $f->fsub(NSTR) return NSTR            subtraction
299   $f->fmul(NSTR) return NSTR            multiplication
300   $f->fdiv(NSTR[,SCALE]) returns NSTR   division to SCALE places
301   $f->fneg() return NSTR                negation
302   $f->fabs() return NSTR                absolute value
303   $f->fcmp(NSTR) return CODE            compare undef,<0,=0,>0
304   $f->fround(SCALE) return NSTR         round to SCALE digits
305   $f->ffround(SCALE) return NSTR        round at SCALEth place
306   $f->fnorm() return (NSTR)             normalize
307   $f->fsqrt([SCALE]) return NSTR        sqrt to SCALE places
308
309 =head1 DESCRIPTION
310
311 All basic math operations are overloaded if you declare your big
312 floats as
313
314     $float = new Math::BigFloat "2.123123123123123123123123123123123";
315
316 =over 2
317
318 =item number format
319
320 canonical strings have the form /[+-]\d+E[+-]\d+/ .  Input values can
321 have embedded whitespace.
322
323 =item Error returns 'NaN'
324
325 An input parameter was "Not a Number" or divide by zero or sqrt of
326 negative number.
327
328 =item Division is computed to 
329
330 C<max($Math::BigFloat::div_scale,length(dividend)+length(divisor))>
331 digits by default.
332 Also used for default sqrt scale.
333
334 =item Rounding is performed
335
336 according to the value of
337 C<$Math::BigFloat::rnd_mode>:
338
339   trunc     truncate the value
340   zero      round towards 0
341   +inf      round towards +infinity (round up)
342   -inf      round towards -infinity (round down)
343   even      round to the nearest, .5 to the even digit
344   odd       round to the nearest, .5 to the odd digit
345
346 The default is C<even> rounding.
347
348 =back
349
350 =head1 BUGS
351
352 The current version of this module is a preliminary version of the
353 real thing that is currently (as of perl5.002) under development.
354
355 The printf subroutine does not use the value of
356 C<$Math::BigFloat::rnd_mode> when rounding values for printing.
357 Consequently, the way to print rounded values is
358 to specify the number of digits both as an
359 argument to C<ffround> and in the C<%f> printf string,
360 as follows:
361
362   printf "%.3f\n", $bigfloat->ffround(-3);
363
364 =head1 AUTHOR
365
366 Mark Biggar
367
368 =cut