Perl 5.001
[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
8 %OVERLOAD = ( 
9                                 # Anonymous subroutines:
10 '+'     =>      sub {new BigFloat &fadd},
11 '-'     =>      sub {new BigFloat
12                        $_[2]? fsub($_[1],${$_[0]}) : fsub(${$_[0]},$_[1])},
13 '<=>'   =>      sub {new BigFloat
14                        $_[2]? fcmp($_[1],${$_[0]}) : fcmp(${$_[0]},$_[1])},
15 'cmp'   =>      sub {new BigFloat
16                        $_[2]? ($_[1] cmp ${$_[0]}) : (${$_[0]} cmp $_[1])},
17 '*'     =>      sub {new BigFloat &fmul},
18 '/'     =>      sub {new BigFloat 
19                        $_[2]? scalar fdiv($_[1],${$_[0]}) :
20                          scalar fdiv(${$_[0]},$_[1])},
21 'neg'   =>      sub {new BigFloat &fneg},
22 'abs'   =>      sub {new BigFloat &fabs},
23
24 qw(
25 ""      stringify
26 0+      numify)                 # Order of arguments unsignificant
27 );
28
29 sub new {
30   my $foo = fnorm($_[1]);
31   panic("Not a number initialized to BigFloat") if $foo eq "NaN";
32   bless \$foo;
33 }
34 sub numify { 0 + "${$_[0]}" }   # Not needed, additional overhead
35                                 # comparing to direct compilation based on
36                                 # stringify
37 sub stringify {
38     my $n = ${$_[0]};
39
40     $n =~ s/^\+//;
41     $n =~ s/E//;
42
43     $n =~ s/([-+]\d+)$//;
44
45     my $e = $1;
46     my $ln = length($n);
47
48     if ($e > 0) {
49         $n .= "0" x $e . '.';
50     } elsif (abs($e) < $ln) {
51         substr($n, $ln + $e, 0) = '.';
52     } else {
53         $n = '.' . ("0" x (abs($e) - $ln)) . $n;
54     }
55
56     # 1 while $n =~ s/(.*\d)(\d\d\d)/$1,$2/;
57
58     return $n;
59 }
60
61 # Arbitrary length float math package
62 #
63 # by Mark Biggar
64 #
65 # number format
66 #   canonical strings have the form /[+-]\d+E[+-]\d+/
67 #   Input values can have inbedded whitespace
68 # Error returns
69 #   'NaN'           An input parameter was "Not a Number" or 
70 #                       divide by zero or sqrt of negative number
71 # Division is computed to 
72 #   max($div_scale,length(dividend)+length(divisor)) 
73 #   digits by default.
74 # Also used for default sqrt scale
75
76 $div_scale = 40;
77
78 # Rounding modes one of 'even', 'odd', '+inf', '-inf', 'zero' or 'trunc'.
79
80 $rnd_mode = 'even';
81
82 sub fadd; sub fsub; sub fmul; sub fdiv;
83 sub fneg; sub fabs; sub fcmp;
84 sub fround; sub ffround;
85 sub fnorm; sub fsqrt;
86
87 #   bigfloat routines
88 #
89 #   fadd(NSTR, NSTR) return NSTR            addition
90 #   fsub(NSTR, NSTR) return NSTR            subtraction
91 #   fmul(NSTR, NSTR) return NSTR            multiplication
92 #   fdiv(NSTR, NSTR[,SCALE]) returns NSTR   division to SCALE places
93 #   fneg(NSTR) return NSTR                  negation
94 #   fabs(NSTR) return NSTR                  absolute value
95 #   fcmp(NSTR,NSTR) return CODE             compare undef,<0,=0,>0
96 #   fround(NSTR, SCALE) return NSTR         round to SCALE digits
97 #   ffround(NSTR, SCALE) return NSTR        round at SCALEth place
98 #   fnorm(NSTR) return (NSTR)               normalize
99 #   fsqrt(NSTR[, SCALE]) return NSTR        sqrt to SCALE places
100
101 \f
102 # Convert a number to canonical string form.
103 #   Takes something that looks like a number and converts it to
104 #   the form /^[+-]\d+E[+-]\d+$/.
105 sub fnorm { #(string) return fnum_str
106     local($_) = @_;
107     s/\s+//g;                               # strip white space
108     if (/^([+-]?)(\d*)(\.(\d*))?([Ee]([+-]?\d+))?$/ && "$2$4" ne '') {
109         &norm(($1 ? "$1$2$4" : "+$2$4"),(($4 ne '') ? $6-length($4) : $6));
110     } else {
111         'NaN';
112     }
113 }
114
115 # normalize number -- for internal use
116 sub norm { #(mantissa, exponent) return fnum_str
117     local($_, $exp) = @_;
118     if ($_ eq 'NaN') {
119         'NaN';
120     } else {
121         s/^([+-])0+/$1/;                        # strip leading zeros
122         if (length($_) == 1) {
123             '+0E+0';
124         } else {
125             $exp += length($1) if (s/(0+)$//);  # strip trailing zeros
126             sprintf("%sE%+ld", $_, $exp);
127         }
128     }
129 }
130
131 # negation
132 sub fneg { #(fnum_str) return fnum_str
133     local($_) = fnorm($_[$[]);
134     vec($_,0,8) ^= ord('+') ^ ord('-') unless $_ eq '+0E+0'; # flip sign
135     s/^H/N/;
136     $_;
137 }
138
139 # absolute value
140 sub fabs { #(fnum_str) return fnum_str
141     local($_) = fnorm($_[$[]);
142     s/^-/+/;                                   # mash sign
143     $_;
144 }
145
146 # multiplication
147 sub fmul { #(fnum_str, fnum_str) return fnum_str
148     local($x,$y) = (fnorm($_[$[]),fnorm($_[$[+1]));
149     if ($x eq 'NaN' || $y eq 'NaN') {
150         'NaN';
151     } else {
152         local($xm,$xe) = split('E',$x);
153         local($ym,$ye) = split('E',$y);
154         &norm(Math::BigInt::bmul($xm,$ym),$xe+$ye);
155     }
156 }
157 \f
158 # addition
159 sub fadd { #(fnum_str, fnum_str) return fnum_str
160     local($x,$y) = (fnorm($_[$[]),fnorm($_[$[+1]));
161     if ($x eq 'NaN' || $y eq 'NaN') {
162         'NaN';
163     } else {
164         local($xm,$xe) = split('E',$x);
165         local($ym,$ye) = split('E',$y);
166         ($xm,$xe,$ym,$ye) = ($ym,$ye,$xm,$xe) if ($xe < $ye);
167         &norm(Math::BigInt::badd($ym,$xm.('0' x ($xe-$ye))),$ye);
168     }
169 }
170
171 # subtraction
172 sub fsub { #(fnum_str, fnum_str) return fnum_str
173     fadd($_[$[],fneg($_[$[+1]));    
174 }
175
176 # division
177 #   args are dividend, divisor, scale (optional)
178 #   result has at most max(scale, length(dividend), length(divisor)) digits
179 sub fdiv #(fnum_str, fnum_str[,scale]) return fnum_str
180 {
181     local($x,$y,$scale) = (fnorm($_[$[]),fnorm($_[$[+1]),$_[$[+2]);
182     if ($x eq 'NaN' || $y eq 'NaN' || $y eq '+0E+0') {
183         'NaN';
184     } else {
185         local($xm,$xe) = split('E',$x);
186         local($ym,$ye) = split('E',$y);
187         $scale = $div_scale if (!$scale);
188         $scale = length($xm)-1 if (length($xm)-1 > $scale);
189         $scale = length($ym)-1 if (length($ym)-1 > $scale);
190         $scale = $scale + length($ym) - length($xm);
191         &norm(&round(Math::BigInt::bdiv($xm.('0' x $scale),$ym),$ym),
192             $xe-$ye-$scale);
193     }
194 }
195 \f
196 # round int $q based on fraction $r/$base using $rnd_mode
197 sub round { #(int_str, int_str, int_str) return int_str
198     local($q,$r,$base) = @_;
199     if ($q eq 'NaN' || $r eq 'NaN') {
200         'NaN';
201     } elsif ($rnd_mode eq 'trunc') {
202         $q;                         # just truncate
203     } else {
204         local($cmp) = Math::BigInt::bcmp(Math::BigInt::bmul($r,'+2'),$base);
205         if ( $cmp < 0 ||
206                  ($cmp == 0 &&
207                   ( $rnd_mode eq 'zero'                             ||
208                    ($rnd_mode eq '-inf' && (substr($q,$[,1) eq '+')) ||
209                    ($rnd_mode eq '+inf' && (substr($q,$[,1) eq '-')) ||
210                    ($rnd_mode eq 'even' && $q =~ /[24680]$/)        ||
211                    ($rnd_mode eq 'odd'  && $q =~ /[13579]$/)        )) ) {
212             $q;                     # round down
213         } else {
214             Math::BigInt::badd($q, ((substr($q,$[,1) eq '-') ? '-1' : '+1'));
215                                     # round up
216         }
217     }
218 }
219
220 # round the mantissa of $x to $scale digits
221 sub fround { #(fnum_str, scale) return fnum_str
222     local($x,$scale) = (fnorm($_[$[]),$_[$[+1]);
223     if ($x eq 'NaN' || $scale <= 0) {
224         $x;
225     } else {
226         local($xm,$xe) = split('E',$x);
227         if (length($xm)-1 <= $scale) {
228             $x;
229         } else {
230             &norm(&round(substr($xm,$[,$scale+1),
231                          "+0".substr($xm,$[+$scale+1,1),"+10"),
232                   $xe+length($xm)-$scale-1);
233         }
234     }
235 }
236 \f
237 # round $x at the 10 to the $scale digit place
238 sub ffround { #(fnum_str, scale) return fnum_str
239     local($x,$scale) = (fnorm($_[$[]),$_[$[+1]);
240     if ($x eq 'NaN') {
241         'NaN';
242     } else {
243         local($xm,$xe) = split('E',$x);
244         if ($xe >= $scale) {
245             $x;
246         } else {
247             $xe = length($xm)+$xe-$scale;
248             if ($xe < 1) {
249                 '+0E+0';
250             } elsif ($xe == 1) {
251                 &norm(&round('+0',"+0".substr($xm,$[+1,1),"+10"), $scale);
252             } else {
253                 &norm(&round(substr($xm,$[,$xe),
254                       "+0".substr($xm,$[+$xe,1),"+10"), $scale);
255             }
256         }
257     }
258 }
259     
260 # compare 2 values returns one of undef, <0, =0, >0
261 #   returns undef if either or both input value are not numbers
262 sub fcmp #(fnum_str, fnum_str) return cond_code
263 {
264     local($x, $y) = (fnorm($_[$[]),fnorm($_[$[+1]));
265     if ($x eq "NaN" || $y eq "NaN") {
266         undef;
267     } else {
268         ord($y) <=> ord($x)
269         ||
270         (  local($xm,$xe,$ym,$ye) = split('E', $x."E$y"),
271              (($xe <=> $ye) * (substr($x,$[,1).'1')
272              || Math::BigInt::cmp($xm,$ym))
273         );
274     }
275 }
276 \f
277 # square root by Newtons method.
278 sub fsqrt { #(fnum_str[, scale]) return fnum_str
279     local($x, $scale) = (fnorm($_[$[]), $_[$[+1]);
280     if ($x eq 'NaN' || $x =~ /^-/) {
281         'NaN';
282     } elsif ($x eq '+0E+0') {
283         '+0E+0';
284     } else {
285         local($xm, $xe) = split('E',$x);
286         $scale = $div_scale if (!$scale);
287         $scale = length($xm)-1 if ($scale < length($xm)-1);
288         local($gs, $guess) = (1, sprintf("1E%+d", (length($xm)+$xe-1)/2));
289         while ($gs < 2*$scale) {
290             $guess = fmul(fadd($guess,fdiv($x,$guess,$gs*2)),".5");
291             $gs *= 2;
292         }
293         new BigFloat &fround($guess, $scale);
294     }
295 }
296
297 1;