0). * * @param string $number The number. * @param int $precision The number of decimals to round to. * @param int $mode The rounding mode. One of the following constants: * PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, * PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD. * * @return string The rounded number. * * @throws \InvalidArgumentException */ public static function round($number, $precision = 0, $mode = PHP_ROUND_HALF_UP) { self::assertNumberFormat($number); if (!is_numeric($precision) || $precision < 0) { throw new \InvalidArgumentException('The provided precision should be a positive number'); } // Round the number in both directions (up/down) before choosing one. $rounding_increment = bcdiv('1', pow(10, $precision), $precision); if (self::compare($number, '0') == 1) { $rounded_up = bcadd($number, $rounding_increment, $precision); } else { $rounded_up = bcsub($number, $rounding_increment, $precision); } $rounded_down = bcsub($number, 0, $precision); // The rounding direction is based on the first decimal after $precision. $number_parts = explode('.', $number); $decimals = !empty($number_parts[1]) ? $number_parts[1] : '0'; $relevant_decimal = isset($decimals[$precision]) ? $decimals[$precision] : 0; if ($relevant_decimal < 5) { $number = $rounded_down; } elseif ($relevant_decimal == 5) { if ($mode == PHP_ROUND_HALF_UP) { $number = $rounded_up; } elseif ($mode == PHP_ROUND_HALF_DOWN) { $number = $rounded_down; } elseif ($mode == PHP_ROUND_HALF_EVEN) { $integer = bcmul($rounded_up, pow(10, $precision), 0); $number = bcmod($integer, '2') == 0 ? $rounded_up : $rounded_down; } elseif ($mode == PHP_ROUND_HALF_ODD) { $integer = bcmul($rounded_up, pow(10, $precision), 0); $number = bcmod($integer, '2') != 0 ? $rounded_up : $rounded_down; } } elseif ($relevant_decimal > 5) { $number = $rounded_up; } return $number; } /** * Compares the first number to the second number. * * @param string $first_number The first number. * @param string $second_number The second number. * @param int $scale The maximum number of digits after the * decimal place. Any digit after $scale will * be truncated. * * @return int 0 if both numbers are equal, 1 if the first one is greater, * -1 otherwise. */ public static function compare($first_number, $second_number, $scale = 6) { self::assertNumberFormat($first_number); self::assertNumberFormat($second_number); return bccomp($first_number, $second_number, $scale); } /** * Trims the given number. * * By default bcmath returns numbers with the number of digits according * to $scale. This means that bcadd('2', '2', 6) will return '4.00000'. * Trimming the number removes the excess zeroes. * * @param string $number The number to trim. * * @return string The trimmed number. */ public static function trim($number) { if (strpos($number, '.') != false) { // The number is decimal, strip trailing zeroes. // If no digits remain after the decimal point, strip it as well. $number = rtrim($number, '0'); $number = rtrim($number, '.'); } return $number; } /** * Assert that the given number is a numeric string value. * * @param string $number The number to check. * * @throws \InvalidArgumentException */ public static function assertNumberFormat($number) { if (is_float($number)) { throw new \InvalidArgumentException(sprintf('The provided value "%s" must be a string, not a float.', $number)); } if (!is_numeric($number)) { throw new \InvalidArgumentException(sprintf('The provided value "%s" is not a numeric value.', $number)); } } }