0

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.

i336_
  • 1,813
  • 1
  • 20
  • 41

1 Answers1

1

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!

KIKO Software
  • 15,283
  • 3
  • 18
  • 33
  • Wow, thanks! This is great. After asking the question I had the idea to switch "float" to "fraction" in my searches, and soon found [some code](https://www.pgregg.com/projects/php/base_conversion/base_conversion.inc.phps) that is very similar. The high similarity of that code and your (from-scratch, I think!?) implementation is very encouraging that the implementation is correct. – i336_ Apr 15 '19 at 11:42
  • For reference, my searches also found [this](https://github.com/Riimu/Kit-BaseConversion/blob/master/src/BaseConverter.php) and [this](https://github.com/4thPlanet/PreciseBaseConvert/blob/master/PreciseBaseConverter.php) library, both high-precision floating-point. However there is *so much boilerplate* in the both of them that I actually gave up trying to tease the actual math out. Unsure if ADHD problem on my part or overengineering on library authors' parts. (If I'm running away from auditing the code, who else is as well?) – i336_ Apr 15 '19 at 11:45
  • Finally, regarding what I am trying to do - this may sound insane, but I am trying to figure how (...if?) I might find the shortest possible mathematical formula (eg, "123^456" would be 7 bytes) that will produce a certain large ~20-digit number whose leftmost 7 base-16 octets must correspond to a specific hexadecimal value, with the remainder being anything. So, I have to use brute force - and I need to know whether my number is correct in base 16! Hence this function (thanks!). – i336_ Apr 15 '19 at 11:53
  • (Oh, also - the CS.SE question reference was interesting.) – i336_ Apr 15 '19 at 11:56
  • Well, you're better at searching code than I am, those libraries look good. I don't think you need to use a brute force attack for the real problem you want to solve. Suppose the leftmost 7 octets, that's 14 digits, is `123456789ABCDE`, then the lowest value is `123456789ABCDE000000` and the highest `123456789ABCDEFFFFFF`. You now know your 'mathematical formula' must be between those two values. Then just choose one and you're done. But this would be material for another question... – KIKO Software Apr 15 '19 at 12:19
  • Memory Error: `PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 536870920 bytes)` – Max Base Feb 02 '21 at 03:20
  • @MaxBase Your comment is lacking context. Where does this error come from? I [tested the code](https://sandbox.onlinephpfunctions.com) in this answer, and it executes fine. – KIKO Software Feb 02 '21 at 07:39