From 57a17588ed29dcd194ec718fd95f9dc57601d781 Mon Sep 17 00:00:00 2001 From: SebastienDug <140097888+SebastienDug@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:03:03 -0500 Subject: [PATCH] BigNumber::of performance update (#77) --- src/BigNumber.php | 154 +++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/src/BigNumber.php b/src/BigNumber.php index 578283b..2f964bd 100644 --- a/src/BigNumber.php +++ b/src/BigNumber.php @@ -17,25 +17,27 @@ abstract class BigNumber implements \JsonSerializable { /** - * The regular expression used to parse integer, decimal and rational numbers. + * The regular expression used to parse integer or decimal numbers. */ - private const PARSE_REGEXP = + private const PARSE_REGEXP_NUMERICAL = '/^' . '(?[\-\+])?' . - '(?:' . - '(?:' . - '(?[0-9]+)?' . - '(?\.)?' . - '(?[0-9]+)?' . - '(?:[eE](?[\-\+]?[0-9]+))?' . - ')|(?:' . - '(?[0-9]+)' . - '\/?' . - '(?[0-9]+)' . - ')' . - ')' . + '(?[0-9]+)?' . + '(?\.)?' . + '(?[0-9]+)?' . + '(?:[eE](?[\-\+]?[0-9]+))?' . '$/'; + /** + * The regular expression used to parse rational numbers. + */ + private const PARSE_REGEXP_RATIONAL = + '/^' . + '(?[\-\+])?' . + '(?[0-9]+)' . + '\/?' . + '(?[0-9]+)' . + '$/'; /** * Creates a BigNumber of the given value. * @@ -84,32 +86,21 @@ private static function _of(BigNumber|int|float|string $value) : BigNumber $value = (string) $value; } - $throw = static function() use ($value) : never { - throw new NumberFormatException(\sprintf( - 'The given value "%s" does not represent a valid number.', - $value - )); - }; - - if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) { - $throw(); - } - - $getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null; + if (str_contains($value, '/')) { + // Rational number + if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + self::throwException($value); + } - $sign = $getMatch('sign'); - $numerator = $getMatch('numerator'); - $denominator = $getMatch('denominator'); + $sign = $matches['sign']; + $numerator = $matches['numerator']; + $denominator = $matches['denominator']; - if ($numerator !== null) { + assert($numerator !== null); assert($denominator !== null); - if ($sign !== null) { - $numerator = $sign . $numerator; - } - - $numerator = self::cleanUp($numerator); - $denominator = self::cleanUp($denominator); + $numerator = self::cleanUp($sign, $numerator); + $denominator = self::cleanUp(null, $denominator); if ($denominator === '0') { throw DivisionByZeroException::denominatorMustNotBeZero(); @@ -120,46 +111,63 @@ private static function _of(BigNumber|int|float|string $value) : BigNumber new BigInteger($denominator), false ); - } + } else { + // Integer or decimal number + if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + self::throwException($value); + } - $point = $getMatch('point'); - $integral = $getMatch('integral'); - $fractional = $getMatch('fractional'); - $exponent = $getMatch('exponent'); + $sign = $matches['sign']; + $point = $matches['point']; + $integral = $matches['integral']; + $fractional = $matches['fractional']; + $exponent = $matches['exponent']; - if ($integral === null && $fractional === null) { - $throw(); - } + if ($integral === null && $fractional === null) { + self::throwException($value); + } - if ($integral === null) { - $integral = '0'; - } + if ($integral === null) { + $integral = '0'; + } - if ($point !== null || $exponent !== null) { - $fractional = ($fractional ?? ''); - $exponent = ($exponent !== null) ? (int) $exponent : 0; + if ($point !== null || $exponent !== null) { + $fractional = ($fractional ?? ''); + $exponent = ($exponent !== null) ? (int)$exponent : 0; - if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { - throw new NumberFormatException('Exponent too large.'); - } + if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { + throw new NumberFormatException('Exponent too large.'); + } - $unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional); + $unscaledValue = self::cleanUp(($sign ?? ''), $integral . $fractional); - $scale = \strlen($fractional) - $exponent; + $scale = \strlen($fractional) - $exponent; - if ($scale < 0) { - if ($unscaledValue !== '0') { - $unscaledValue .= \str_repeat('0', - $scale); + if ($scale < 0) { + if ($unscaledValue !== '0') { + $unscaledValue .= \str_repeat('0', -$scale); + } + $scale = 0; } - $scale = 0; + + return new BigDecimal($unscaledValue, $scale); } - return new BigDecimal($unscaledValue, $scale); - } + $integral = self::cleanUp(($sign ?? ''), $integral); - $integral = self::cleanUp(($sign ?? '') . $integral); + return new BigInteger($integral); + } + } - return new BigInteger($integral); + /** + * Throws a NumberFormatException for the given value. + * @psalm-pure + */ + private static function throwException(string $value) : never { + throw new NumberFormatException(\sprintf( + 'The given value "%s" does not represent a valid number.', + $value + )); } /** @@ -327,31 +335,21 @@ private static function add(BigNumber $a, BigNumber $b) : BigNumber } /** - * Removes optional leading zeros and + sign from the given number. - * - * @param string $number The number, validated as a non-empty string of digits with optional leading sign. + * Removes optional leading zeros and applies sign if needed(- for negatives). * + * @param string|null $sign The sign, '+' or '-', optional. + * @param string $number The number, validated as a non-empty string of digits. * @psalm-pure */ - private static function cleanUp(string $number) : string + private static function cleanUp(string|null $sign, string $number) : string { - $firstChar = $number[0]; - - if ($firstChar === '+' || $firstChar === '-') { - $number = \substr($number, 1); - } - $number = \ltrim($number, '0'); if ($number === '') { return '0'; } - if ($firstChar === '-') { - return '-' . $number; - } - - return $number; + return $sign === '-' ? '-' . $number : $number; } /**