4

Here's what I've got

function bcln($n, $scale=10) {
    $iscale = $scale+3;
    $result = '0.0';
    $i = 0;

    do {
        $pow = (1 + (2 * $i++));
        $mul = bcdiv('1', $pow, $iscale);
        $fraction = bcmul($mul, bcpow(bcsub($n, '1', $iscale) / bcadd($n, '1.0', $iscale), $pow, $iscale), $iscale);
        $lastResult = $result;
        $result = bcadd($fraction, $result, $iscale);
    } while($result !== $lastResult);

    return bcmul('2', $result, $scale);
}

But this takes 5.7 seconds to run bcln(100) (natural log of 100, 10 decimal places). Furthermore, it's not always accurate for more decimal places. Is there a better algorithm?

For that specific run, it takes 573 iterations to settle on the result.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • I'm not sure if this is the answer you're looking for, but the native [`log()`](http://php.net/manual/en/function.log.php) function returns the same result in virtually no time... – scrowler Jul 24 '14 at 22:56
  • 1
    @scrowler You must have missed the part about "arbitrary precision". – mpen Jul 24 '14 at 23:18
  • Yep, I just compared results. You could look at how to shorten your loop's iterations perhaps... – scrowler Jul 24 '14 at 23:23
  • 1
    @scrowler The more I reduce the iterations the less accurate it becomes. I need a different algorithm that converges faster. – mpen Jul 24 '14 at 23:25

1 Answers1

1

Do you require an arbitrary-length string as an answer? Or do you require arbitrary precision, or arbitrary exponent size? Or… would a double-precision floating-point answer (return-value) suffice; given that we're "only" working with the logarithm of a number of "arbitrary" size?

Double precision floating point numbers have an 11-bit signed exponent: therefore, if your big number string has a length of ≤1022 bits ≈ 307 decimal digits (so string length of 306 characters including the decimal point), you are safe! More accurately, you should be safe if the absolute value of the resulting decimal exponent is ≤307. Do you need bigger exponents than that? (I suppose in other words: are you working with real-world numbers or theoretical/ pure mathematics?)

Why not just use some string processing, along with some simple floating-point log arithmetic? This should be very fast, for any real-world numbers…

function bclog10($n){
    //←Might need to implement some validation logic here!
    $pos=strpos($n,'.');
    if($pos===false){
        $dec_frac='.'.substr($n,0,15);$pos=strlen($n);
    }else{  $dec_frac='.'.substr(substr($n,0,$pos).substr($n,$pos+1),0,15);
    }
    return log10((float)$dec_frac)+(float)$pos;
}

You can convert the base using some well-known log arithmetic:

function bclogn($n,$base=M_E){//$base should be float: default is e
    return bclog10($n)*log(10)/log($base);
}

I have tested these functions and they work for me, for the examples I supplied; giving exactly the same answers as the Windows 10 calculator, up to the limits of double-precision arithmetic as used by PHP.

If you actually need more than 15 digits of precision and more than 307 for the decimal exponent, you might be able to implement your own "BigFloat" class object, and somehow build its methods out of the standard built-in floating-point functions using a divide-and-conquer approach! Then perhaps, we might use that as the basis of an arbitrary-precision floating-point logarithm algorithm, by combining this with the functions/techniques described above. You might want to consider consulting with the people at math.stackexchange.com, to find out more about whether this could be a feasible approach.

MAJOR EDIT: 2nd attempt…

function bclog10($n){//By Matthew Slyman @aaabit.com
    $m=array();// ↓ Validation, matching/processing regex…
    preg_match('/^(-)?0*([1-9][0-9]*)?(\.(0*))?([1-9][0-9]*)?([Ee](-)?0*([1-9][0-9]*))?$/',$n,$m);
    if(!isset($m[1])){throw new \Exception('Argument: not decimal number string!');}
    $sgn=$m[1];if($sgn==='-'){throw new \Exception('Cannot compute: log(<⁺0)!');}
    $abs=$m[2];$pos=strlen($abs);
    if(isset($m[4])){$fre=$m[4];}else{$fre='';}$neg=strlen($fre);
    if(isset($m[5])){$frc=$m[5];}else{$frc='';}
    if(isset($m[7])){$esgn=$m[7]==='-'?-1:1;}else{$esgn=1;}
    if(isset($m[8])){$eexp=$m[8];}else{$eexp=0;}
    if($pos===0){
        $dec_frac='.'.substr($frc,0,15);$pos=-1*$neg;
    }else{  $dec_frac='.'.substr($abs.$fre.$frc,0,15);
    }
    return log10((float)$dec_frac)+(float)$pos+($esgn*$eexp);
}
Community
  • 1
  • 1
Matthew Slyman
  • 346
  • 3
  • 9
  • Real-world is good. This is going into a general purpose PHP library, so the more numbers I can support the better, but not at a huge performance cost. I'm going to try out your implementation now. – mpen Oct 30 '15 at 23:43
  • Your implementation doesn't seem to work at all. `bclog10(100)` gives `6.0` instead of `2`. – mpen Oct 30 '15 at 23:52
  • I've noticed another serious bug: enter numbers like "0.000025" — you can easily see why this won't work. I'm working on fixing this right now, and will test your example as well. – Matthew Slyman Oct 31 '15 at 07:02
  • I mean, of course, numbers like "0.000000000000000000000000000025" … We need to do a little more on the string processing side of things, before we start leaning on the built-in floating-point functions. I'm fixing this now… With your example, bclog10(100), there was a simple bug on the $pos===false side… – Matthew Slyman Oct 31 '15 at 07:13
  • I've posted my second attempt now, and it appears to resolve the problems we've identified with the first version (as well as, including better validation logic as a by-product of the additional string processing required). I look forward to the results of your testing! What licenses are you releasing your PHP library under? Depending on your answer, or the terms & conditions or customs of StackOverflow; I might de-restrict my CC declaration, which I would like included if you don't mind. Just leaving the comment line intact in source code is enough for attribution anyway. – Matthew Slyman Oct 31 '15 at 08:01
  • p.s. The CC-SA clause doesn't mean you have to share/publish your entire source-code. Just any updated versions of this function. Looking forward to your feedback! – Matthew Slyman Oct 31 '15 at 08:07
  • [My library](https://bitbucket.org/mnpenner/ptilz) is released under MIT. I won't be adding any restrictions to it. – mpen Oct 31 '15 at 08:27
  • You function starts to break down for large numbers: `bclog10('43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875')` gives `208.63815524781` instead of `183.638155247810724`. It works for `280571172992510140037611932413038677189525` but even the built-in `log` can handle that. – mpen Oct 31 '15 at 10:03
  • I just chucked this long number into Microsoft Word and ran a "Word Count". It has 209 digits. So I think my function is giving you the right answer, rather than your other function which you are using for comparison! – Matthew Slyman Oct 31 '15 at 18:59
  • MIT License will be fine. I'm happy with anything reasonably open-source (actually, it doesn't get much more open than StackExchange itself), and I am delighted that you are releasing your other code under MIT License. – Matthew Slyman Oct 31 '15 at 19:00
  • I was using WolframAlpha to check, but I think you might be right -- my input must have got cut off. Tried [with Python](https://ideone.com/tQLsNP) instead -- looks like you were right. Sorry :D – mpen Nov 01 '15 at 23:53
  • 1
    [StackOverflow: arbitrary precision log() algorithm](http://stackoverflow.com/questions/27179674/examples-of-log-algorithm-using-arbitrary-precision-maths) — this looks interesting, although I believe that my approach takes advantage of built-in log functions supported by double-precision log tables (fast ROM lookups) built into the microprocessor! To do a perfect job of answering your original question (arbitrary precision, truly arbitrary size); we would need a BigFloat class with arbitrary-size member variables for significand/exponent, and methods implementing an algorithm like this one! – Matthew Slyman Nov 02 '15 at 12:32
  • 1
    I just updated the function to handle scientific notation as an alternative input format. – Matthew Slyman Nov 05 '15 at 05:52