Skip to content

Commit

Permalink
BigNumber::of performance update (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienDug authored Nov 29, 2023
1 parent 21e0a1e commit 57a1758
Showing 1 changed file with 76 additions and 78 deletions.
154 changes: 76 additions & 78 deletions src/BigNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'/^' .
'(?<sign>[\-\+])?' .
'(?:' .
'(?:' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
')' .
')' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
'$/';

/**
* The regular expression used to parse rational numbers.
*/
private const PARSE_REGEXP_RATIONAL =
'/^' .
'(?<sign>[\-\+])?' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
'$/';
/**
* Creates a BigNumber of the given value.
*
Expand Down Expand Up @@ -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();
Expand All @@ -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
));
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down

0 comments on commit 57a1758

Please sign in to comment.