* @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 * @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); } }