Reputation: 2011
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.
How to convert a huge integer to hex in php? involves BC, but only for integers.
https://www.exploringbinary.com/base-conversion-in-php-using-bcmath/ explores floats, but only in the context of decimal<->binary. (It says extending the code for other bases is easy, and it probably is (using the code in the previous point), but I have no idea how to reason through the correctness of the result I'd reach.)
Fast arbitrary-precision logarithms with bcmath is also float-based, but in the context of reimplementing high-precision log()
. (There is a mention of converting bases in there, though, along with notes about how BC dumbly uses PHP's own pow() and loses precision.)
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
Reputation: 16716
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