i336_
i336_

Reputation: 2011

How can I base-convert a large/high-precision floating-point number in PHP?

As generic of a question as this seems, I'm having a really hard time learning specifically about how to base-convert large high-precision float values in PHP using BCMath.

I'm trying to base-convert something like

1234.5678900000

to

4D2.91613D31B

How can I do this?

I just want base-10 → base-16, but a conversion for arbitrary-base floats would probably make the most useful answer for others as well.


The other results I've found are just talking about PHP's own float coercion, and don't relate to BC at all.

Upvotes: 0

Views: 644

Answers (1)

KIKO Software
KIKO Software

Reputation: 16716

Up to base 36 conversions with high precision

I think this question is just a bit too difficult for Stack Overflow. Not only do you want to base-convert floating-points, which is a bit unusual by itself, but it has to be done at high precision. This is certainly possible, but not many people will have a solution for this lying around and making one takes time. The math of base conversion is not very complex, and once you understand it you can work it out yourself.

Oh, well, to make a long story short, I couldn't resist this, and gave it a try.

<?php 

function splitNo($operant)
// get whole and fractional parts of operant
{
    if (strpos($operant, '.') !== false) {
      $sides = explode('.',$operant);
      return [$sides[0], '.' . $sides[1]];
    }
    return [$operant, ''];
}

function wholeNo($operant)
// get the whole part of an operant
{
    return explode('.', $operant)[0];
}

function toDigits($number, $base, $scale = 0)
// convert a positive number n to its digit representation in base b
{
    $symbols = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $digits = '';
    list($whole, $fraction) = splitNo($number);
    while (bccomp($whole, '0.0', $scale) > 0) {
        $digits = $symbols{(int)bcmod($whole, $base, $scale)} . $digits;
        $whole = wholeNo(bcdiv($whole, $base, $scale));
    }
    if ($scale > 0) {
        $digits .= '.';
        for ($i = 1; $i <= $scale; $i++) {
            $fraction = bcmul($fraction, $base, $scale);
            $whole = wholeNo($fraction);
            $fraction = bcsub($fraction, $whole, $scale);
            $digits .= $symbols{$whole};
        }
    }
    return $digits;
}

function toNumber($digits, $base, $scale = 0)
// compute the number given by digits in base b
{
    $symbols = str_split('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ');
    $number = '0';
    list($whole, $fraction) = splitNo($digits);
    foreach (str_split($whole) as $digit) {
        $shiftUp = bcmul($base, $number, $scale);
        $number = bcadd($shiftUp, array_search($digit, $symbols));
    }
    if ($fraction != '') {
      $shiftDown = bcdiv('1', $base, $scale);
      foreach (str_split(substr($fraction, 1)) as $symbol) {
          $index = array_search($symbol, $symbols);
          $number = bcadd($number, bcmul($index, $shiftDown, $scale), $scale);
          $shiftDown = bcdiv($shiftDown, $base, $scale);
      }
    }
    return $number;
}

function baseConv($operant, $fromBase, $toBase, $scale = 0)
// convert the digits representation of a number from base 1 to base 2
{
    return toDigits(toNumber($operant, $fromBase, $scale), $toBase, $scale);
}

echo '<pre>';
print_r(baseConv('1234.5678900000', 10, 16, 60));
echo '</pre>';

The output is:

4D2.91613D31B9B66F9335D249E44FA05143BF727136A400FBA8826AA8EB4634

It looks a bit complicated, but isn't really. It just takes time. I started with converting whole numbers, then added fractions, and when that all worked I put in all the BC Math functions.

The $scale argument represents the number of wanted decimal places.

It may look a bit strange that I use three function for the conversion: toDigits(), toNumber() and baseConv(). The reason is that the BC Math functions work with a base of 10. So, toDigits() converts away from 10 to another base and toNumber() does the opposite. To convert between two arbitrary-base operants we need both functions, and this results in the third: baseConv().

This could possible be further optimized, if needed, but you haven't told us what you need it for, so optimization wasn't a priority for me. I just tried to make it work.

You can get higher base conversions by simply adding more symbols. However, in the current implementation each symbol needs to be one character. With UTF8 that doesn't really limit you, but make sure everything is multibyte compatible (which it isn't at this moment).

NOTE: It seems to work, but I don't give any guarantees. Test thoroughly before use!

Upvotes: 1

Related Questions