495 lines
14 KiB
PHP
495 lines
14 KiB
PHP
<?php
|
|
|
|
/**
|
|
* BCMath Emulation Class
|
|
*
|
|
* PHP version 5 and 7
|
|
*
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
* @copyright 2019 Jim Wigginton
|
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
*/
|
|
|
|
namespace bcmath_compat;
|
|
|
|
use phpseclib3\Math\BigInteger;
|
|
|
|
/**
|
|
* BCMath Emulation Class
|
|
*
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
* @access public
|
|
*/
|
|
abstract class BCMath
|
|
{
|
|
/**
|
|
* Default scale parameter for all bc math functions
|
|
*/
|
|
private static $scale;
|
|
|
|
/**
|
|
* Set or get default scale parameter for all bc math functions
|
|
*
|
|
* Uses the PHP 7.3+ behavior
|
|
*
|
|
* @var int $scale optional
|
|
*/
|
|
private static function scale($scale = null)
|
|
{
|
|
if (isset($scale)) {
|
|
self::$scale = (int) $scale;
|
|
}
|
|
return self::$scale;
|
|
}
|
|
|
|
/**
|
|
* Formats numbers
|
|
*
|
|
* Places the decimal place at the appropriate place, adds trailing 0's as appropriate, etc
|
|
*
|
|
* @var string $x
|
|
* @var int $scale
|
|
* @var int $pad
|
|
* @var boolean $trim
|
|
*/
|
|
private static function format($x, $scale, $pad)
|
|
{
|
|
$sign = self::isNegative($x) ? '-' : '';
|
|
$x = str_replace('-', '', $x);
|
|
|
|
if (strlen($x) != $pad) {
|
|
$x = str_pad($x, $pad, '0', STR_PAD_LEFT);
|
|
}
|
|
$temp = $pad ? substr_replace($x, '.', -$pad, 0) : $x;
|
|
$temp = explode('.', $temp);
|
|
if ($temp[0] == '') {
|
|
$temp[0] = '0';
|
|
}
|
|
if (isset($temp[1])) {
|
|
$temp[1] = substr($temp[1], 0, $scale);
|
|
$temp[1] = str_pad($temp[1], $scale, '0');
|
|
} elseif ($scale) {
|
|
$temp[1] = str_repeat('0', $scale);
|
|
}
|
|
$result = rtrim(implode('.', $temp), '.');
|
|
if ($sign == '-' && preg_match('#^0\.?0*$#', $result)) {
|
|
$sign = '';
|
|
}
|
|
return $sign . $result;
|
|
}
|
|
|
|
/**
|
|
* Negativity Test
|
|
*
|
|
* @var BigInteger $x
|
|
*/
|
|
private static function isNegative($x)
|
|
{
|
|
return $x->compare(new BigInteger()) < 0;
|
|
}
|
|
|
|
/**
|
|
* Add two arbitrary precision numbers
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function add($x, $y, $scale, $pad)
|
|
{
|
|
$z = $x->add($y);
|
|
|
|
return self::format($z, $scale, $pad);
|
|
}
|
|
|
|
/**
|
|
* Subtract one arbitrary precision number from another
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function sub($x, $y, $scale, $pad)
|
|
{
|
|
$z = $x->subtract($y);
|
|
|
|
return self::format($z, $scale, $pad);
|
|
}
|
|
|
|
/**
|
|
* Multiply two arbitrary precision numbers
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function mul($x, $y, $scale, $pad)
|
|
{
|
|
if ($x == '0' || $y == '0') {
|
|
$r = '0';
|
|
if ($scale) {
|
|
$r.= '.' . str_repeat('0', $scale);
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
$z = $x->abs()->multiply($y->abs());
|
|
$result = self::format($z, $scale, 2 * $pad);
|
|
|
|
$sign = (self::isNegative($x) ^ self::isNegative($y)) && !preg_match('#^0\.?0*$#', $result) ? '-' : '';
|
|
|
|
return $sign . $result;
|
|
}
|
|
|
|
/**
|
|
* Divide two arbitrary precision numbers
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function div($x, $y, $scale, $pad)
|
|
{
|
|
if ($y == '0') {
|
|
// < PHP 8.0 triggered a warning
|
|
// >= PHP 8.0 throws an exception
|
|
throw new \DivisionByZeroError('Division by zero');
|
|
}
|
|
|
|
$temp = '1' . str_repeat('0', $scale);
|
|
$temp = new BigInteger($temp);
|
|
list($q) = $x->multiply($temp)->divide($y);
|
|
|
|
return self::format($q, $scale, $scale);
|
|
}
|
|
|
|
/**
|
|
* Get modulus of an arbitrary precision number
|
|
*
|
|
* Uses the PHP 7.2+ behavior
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function mod($x, $y, $scale, $pad)
|
|
{
|
|
if ($y == '0') {
|
|
// < PHP 8.0 triggered a warning
|
|
// >= PHP 8.0 throws an exception
|
|
throw new \DivisionByZeroError('Division by zero');
|
|
}
|
|
|
|
list($q) = $x->divide($y);
|
|
$z = $y->multiply($q);
|
|
$z = $x->subtract($z);
|
|
|
|
return self::format($z, $scale, $pad);
|
|
}
|
|
|
|
/**
|
|
* Compare two arbitrary precision numbers
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function comp($x, $y, $scale, $pad)
|
|
{
|
|
$x = new BigInteger($x[0] . substr($x[1], 0, $scale));
|
|
$y = new BigInteger($y[0] . substr($y[1], 0, $scale));
|
|
|
|
return $x->compare($y);
|
|
}
|
|
|
|
/**
|
|
* Raise an arbitrary precision number to another
|
|
*
|
|
* Uses the PHP 7.2+ behavior
|
|
*
|
|
* @var string $x
|
|
* @var string $y
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function pow($x, $y, $scale, $pad)
|
|
{
|
|
if ($y == '0') {
|
|
$r = '1';
|
|
if ($scale) {
|
|
$r.= '.' . str_repeat('0', $scale);
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
$min = defined('PHP_INT_MIN') ? PHP_INT_MIN : ~PHP_INT_MAX;
|
|
if (bccomp($y, PHP_INT_MAX) > 0 || bccomp($y, $min) <= 0) {
|
|
throw new \ValueError('bcpow(): Argument #2 ($exponent) is too large');
|
|
}
|
|
|
|
$sign = self::isNegative($x) ? '-' : '';
|
|
$x = $x->abs();
|
|
|
|
$r = new BigInteger(1);
|
|
|
|
for ($i = 0; $i < abs($y); $i++) {
|
|
$r = $r->multiply($x);
|
|
}
|
|
|
|
if ($y < 0) {
|
|
$temp = '1' . str_repeat('0', $scale + $pad * abs($y));
|
|
$temp = new BigInteger($temp);
|
|
list($r) = $temp->divide($r);
|
|
$pad = $scale;
|
|
} else {
|
|
$pad*= abs($y);
|
|
}
|
|
|
|
return $sign . self::format($r, $scale, $pad);
|
|
}
|
|
|
|
/**
|
|
* Raise an arbitrary precision number to another, reduced by a specified modulus
|
|
*
|
|
* @var string $x
|
|
* @var string $e
|
|
* @var string $n
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function powmod($x, $e, $n, $scale, $pad)
|
|
{
|
|
if ($e[0] == '-' || $n == '0') {
|
|
// < PHP 8.0 returned false
|
|
// >= PHP 8.0 throws an exception
|
|
throw new \ValueError('bcpowmod(): Argument #2 ($exponent) must be greater than or equal to 0');
|
|
}
|
|
if ($n[0] == '-') {
|
|
$n = substr($n, 1);
|
|
}
|
|
if ($e == '0') {
|
|
return $scale ?
|
|
'1.' . str_repeat('0', $scale) :
|
|
'1';
|
|
}
|
|
|
|
$x = new BigInteger($x);
|
|
$e = new BigInteger($e);
|
|
$n = new BigInteger($n);
|
|
|
|
$z = $x->powMod($e, $n);
|
|
|
|
return $scale ?
|
|
"$z." . str_repeat('0', $scale) :
|
|
"$z";
|
|
}
|
|
|
|
/**
|
|
* Get the square root of an arbitrary precision number
|
|
*
|
|
* @var string $n
|
|
* @var int $scale
|
|
* @var int $pad
|
|
*/
|
|
private static function sqrt($n, $scale, $pad)
|
|
{
|
|
// the following is based off of the following URL:
|
|
// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Decimal_(base_10)
|
|
|
|
if (!is_numeric($n)) {
|
|
return '0';
|
|
}
|
|
$temp = explode('.', $n);
|
|
$decStart = ceil(strlen($temp[0]) / 2);
|
|
$n = implode('', $temp);
|
|
if (strlen($n) % 2) {
|
|
$n = "0$n";
|
|
}
|
|
$parts = str_split($n, 2);
|
|
$parts = array_map('intval', $parts);
|
|
$i = 0;
|
|
$p = 0; // for the first step, p = 0
|
|
$c = $parts[$i];
|
|
$result = '';
|
|
while (true) {
|
|
// determine the greatest digit x such that x(20p+x) <= c
|
|
for ($x = 1; $x <= 10; $x++) {
|
|
if ($x * (20 * $p + $x) > $c) {
|
|
$x--;
|
|
break;
|
|
}
|
|
}
|
|
$result.= $x;
|
|
$y = $x * (20 * $p + $x);
|
|
$p = 10 * $p + $x;
|
|
$c = 100 * ($c - $y);
|
|
if (isset($parts[++$i])) {
|
|
$c+= $parts[$i];
|
|
}
|
|
if ((!$c && $i >= $decStart) || $i - $decStart == $scale) {
|
|
break;
|
|
}
|
|
if ($decStart == $i) {
|
|
$result.= '.';
|
|
}
|
|
}
|
|
|
|
$result = explode('.', $result);
|
|
if (isset($result[1])) {
|
|
$result[1] = str_pad($result[1], $scale, '0');
|
|
} elseif ($scale) {
|
|
$result[1] = str_repeat('0', $scale);
|
|
}
|
|
return implode('.', $result);
|
|
}
|
|
|
|
/**
|
|
* __callStatic Magic Method
|
|
*
|
|
* @var string $name
|
|
* @var array $arguments
|
|
*/
|
|
public static function __callStatic($name, $arguments)
|
|
{
|
|
static $params = [
|
|
'add' => 3,
|
|
'comp' => 3,
|
|
'div' => 3,
|
|
'mod' => 3,
|
|
'mul' => 3,
|
|
'pow' => 3,
|
|
'powmod' => 4,
|
|
'scale' => 1,
|
|
'sqrt' => 2,
|
|
'sub' => 3
|
|
];
|
|
if (count($arguments) < $params[$name] - 1) {
|
|
$min = $params[$name] - 1;
|
|
throw new \ArgumentCountError("bc$name() expects at least $min parameters, " . func_num_args() . " given");
|
|
}
|
|
if (count($arguments) > $params[$name]) {
|
|
$str = "bc$name() expects at most {$params[$name]} parameters, " . func_num_args() . " given";
|
|
throw new \ArgumentCountError($str);
|
|
}
|
|
$numbers = array_slice($arguments, 0, $params[$name] - 1);
|
|
|
|
$ints = [];
|
|
switch ($name) {
|
|
case 'pow':
|
|
$ints = array_slice($numbers, count($numbers) - 1);
|
|
$numbers = array_slice($numbers, 0, count($numbers) - 1);
|
|
$names = ['exponent'];
|
|
break;
|
|
case 'powmod':
|
|
$ints = $numbers;
|
|
$numbers = [];
|
|
$names = ['base', 'exponent', 'modulus'];
|
|
break;
|
|
case 'sqrt':
|
|
$names = ['num'];
|
|
break;
|
|
default:
|
|
$names = ['num1', 'num2'];
|
|
}
|
|
foreach ($ints as $i => &$int) {
|
|
if (!is_numeric($int)) {
|
|
$int = '0';
|
|
}
|
|
$pos = strpos($int, '.');
|
|
if ($pos !== false) {
|
|
$int = substr($int, 0, $pos);
|
|
throw new \ValueError("bc$name(): Argument #2 (\$$names[$i]) cannot have a fractional part");
|
|
}
|
|
}
|
|
foreach ($numbers as $i => $arg) {
|
|
$num = $i + 1;
|
|
switch (true) {
|
|
case is_bool($arg):
|
|
case is_numeric($arg):
|
|
case is_string($arg):
|
|
case is_object($arg) && method_exists($arg, '__toString'):
|
|
if (!is_bool($arg) && !is_numeric("$arg")) {
|
|
throw new \ValueError("bc$name: bcmath function argument is not well-formed");
|
|
}
|
|
break;
|
|
// PHP >= 8.1 has deprecated the passing of nulls to string parameters
|
|
case is_null($arg):
|
|
$error = "bc$name(): Passing null to parameter #$num (\$$names[$i]) of type string is deprecated";
|
|
trigger_error($error, E_USER_DEPRECATED);
|
|
break;
|
|
default:
|
|
$type = is_object($arg) ? get_class($arg) : gettype($arg);
|
|
$error = "bc$name(): Argument #$num (\$$names[$i]) must be of type string, $type given";
|
|
throw new \TypeError($error);
|
|
}
|
|
}
|
|
if (!isset(self::$scale)) {
|
|
$scale = ini_get('bcmath.scale');
|
|
self::$scale = $scale !== false ? max(intval($scale), 0) : 0;
|
|
}
|
|
$scale = isset($arguments[$params[$name] - 1]) ? $arguments[$params[$name] - 1] : self::$scale;
|
|
switch (true) {
|
|
case is_bool($scale):
|
|
case is_numeric($scale):
|
|
case is_string($scale) && preg_match('#0-9\.#', $scale[0]):
|
|
break;
|
|
default:
|
|
$type = is_object($arg) ? get_class($arg) : gettype($arg);
|
|
$str = "bc$name(): Argument #$params[$name] (\$scale) must be of type ?int, string given";
|
|
throw new \TypeError($str);
|
|
}
|
|
$scale = (int) $scale;
|
|
if ($scale < 0) {
|
|
throw new \ValueError("bc$name(): Argument #$params[$name] (\$scale) must be between 0 and 2147483647");
|
|
}
|
|
|
|
$pad = 0;
|
|
foreach ($numbers as &$num) {
|
|
if (is_bool($num)) {
|
|
$num = $num ? '1' : '0';
|
|
} elseif (!is_numeric($num)) {
|
|
$num = '0';
|
|
}
|
|
$num = explode('.', $num);
|
|
if (isset($num[1])) {
|
|
$pad = max($pad, strlen($num[1]));
|
|
}
|
|
}
|
|
switch ($name) {
|
|
case 'add':
|
|
case 'sub':
|
|
case 'mul':
|
|
case 'div':
|
|
case 'mod':
|
|
case 'pow':
|
|
foreach ($numbers as &$num) {
|
|
if (!isset($num[1])) {
|
|
$num[1] = '';
|
|
}
|
|
$num[1] = str_pad($num[1], $pad, '0');
|
|
$num = new BigInteger($num[0] . $num[1]);
|
|
}
|
|
break;
|
|
case 'comp':
|
|
foreach ($numbers as &$num) {
|
|
if (!isset($num[1])) {
|
|
$num[1] = '';
|
|
}
|
|
$num[1] = str_pad($num[1], $pad, '0');
|
|
}
|
|
break;
|
|
case 'sqrt':
|
|
$numbers = [$arguments[0]];
|
|
}
|
|
|
|
$arguments = array_merge($numbers, $ints, [$scale, $pad]);
|
|
return call_user_func_array('self::' . $name, $arguments);
|
|
}
|
|
}
|