This commit is contained in:
Paolo A
2024-08-13 13:44:16 +00:00
parent 1bbb23088d
commit e796d76612
4001 changed files with 30101 additions and 40075 deletions

View File

@@ -14,26 +14,29 @@ use Brick\Math\Exception\RoundingNecessaryException;
*
* @psalm-immutable
*/
abstract class BigNumber implements \Serializable, \JsonSerializable
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]+)' .
'$/';
/**
@@ -53,7 +56,24 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigNumber
final public static function of(BigNumber|int|float|string $value) : static
{
$value = self::_of($value);
if (static::class === BigNumber::class) {
// https://github.com/vimeo/psalm/issues/10309
assert($value instanceof static);
return $value;
}
return static::from($value);
}
/**
* @psalm-pure
*/
private static function _of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
@@ -63,34 +83,25 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
return new BigInteger((string) $value);
}
$value = \is_float($value) ? self::floatToString($value) : $value;
$throw = static function() use ($value) : void {
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();
if (is_float($value)) {
$value = (string) $value;
}
$getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
if (str_contains($value, '/')) {
// Rational number
if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$numerator = self::cleanUp($numerator);
$denominator = self::cleanUp($denominator);
$sign = $matches['sign'];
$numerator = $matches['numerator'];
$denominator = $matches['denominator'];
assert($numerator !== null);
assert($denominator !== null);
$numerator = self::cleanUp($sign, $numerator);
$denominator = self::cleanUp(null, $denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
@@ -101,67 +112,62 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
new BigInteger($denominator),
false
);
}
$point = $getMatch('point');
$integral = $getMatch('integral');
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
if ($integral === null) {
$integral = '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.');
} else {
// Integer or decimal number
if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
$sign = $matches['sign'];
$point = $matches['point'];
$integral = $matches['integral'];
$fractional = $matches['fractional'];
$exponent = $matches['exponent'];
$scale = \strlen($fractional) - $exponent;
if ($integral === null && $fractional === null) {
throw NumberFormatException::invalidFormat($value);
}
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', - $scale);
if ($integral === null) {
$integral = '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.');
}
$scale = 0;
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
}
return new BigDecimal($unscaledValue, $scale);
$integral = self::cleanUp($sign, $integral);
return new BigInteger($integral);
}
$integral = self::cleanUp(($sign ?? '') . $integral);
return new BigInteger($integral);
}
/**
* Safely converts float to string, avoiding locale-dependent issues.
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
*
* @see https://github.com/brick/math/pull/20
* @throws MathException If the value cannot be converted.
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/
private static function floatToString(float $float) : string
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
abstract protected static function from(BigNumber $number): static;
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
@@ -169,7 +175,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @internal
* @psalm-pure
*/
protected function newBigInteger(string $value) : BigInteger
final protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
@@ -180,7 +186,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @internal
* @psalm-pure
*/
protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
@@ -191,7 +197,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @internal
* @psalm-pure
*/
protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
@@ -205,11 +211,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function min(BigNumber|int|float|string ...$values) : static
final public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
@@ -237,11 +241,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function max(BigNumber|int|float|string ...$values) : static
final public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
@@ -271,7 +273,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
*
* @psalm-pure
*/
public static function sum(BigNumber|int|float|string ...$values) : static
final public static function sum(BigNumber|int|float|string ...$values) : static
{
/** @var static|null $sum */
$sum = null;
@@ -323,37 +325,28 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
}
/**
* Removes optional leading zeros and + sign from the given number.
* Removes optional leading zeros and applies sign.
*
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
* @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'.
* @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;
}
/**
* Checks if this number is equal to the given one.
*/
public function isEqualTo(BigNumber|int|float|string $that) : bool
final public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
@@ -361,7 +354,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is strictly lower than the given one.
*/
public function isLessThan(BigNumber|int|float|string $that) : bool
final public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
@@ -369,7 +362,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is lower than or equal to the given one.
*/
public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) <= 0;
}
@@ -377,7 +370,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is strictly greater than the given one.
*/
public function isGreaterThan(BigNumber|int|float|string $that) : bool
final public function isGreaterThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) > 0;
}
@@ -385,7 +378,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is greater than or equal to the given one.
*/
public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
@@ -393,7 +386,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number equals zero.
*/
public function isZero() : bool
final public function isZero() : bool
{
return $this->getSign() === 0;
}
@@ -401,7 +394,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is strictly negative.
*/
public function isNegative() : bool
final public function isNegative() : bool
{
return $this->getSign() < 0;
}
@@ -409,7 +402,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is negative or zero.
*/
public function isNegativeOrZero() : bool
final public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
@@ -417,7 +410,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is strictly positive.
*/
public function isPositive() : bool
final public function isPositive() : bool
{
return $this->getSign() > 0;
}
@@ -425,7 +418,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Checks if this number is positive or zero.
*/
public function isPositiveOrZero() : bool
final public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
@@ -433,6 +426,8 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Returns the sign of this number.
*
* @psalm-return -1|0|1
*
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
*/
abstract public function getSign() : int;
@@ -440,7 +435,9 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Compares this number to the given one.
*
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
* @psalm-return -1|0|1
*
* @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
*
* @throws MathException If the number is not valid.
*/
@@ -468,13 +465,13 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param int $roundingMode A `RoundingMode` constant.
* @param int $scale The scale of the resulting `BigDecimal`.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*/
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
@@ -505,7 +502,7 @@ abstract class BigNumber implements \Serializable, \JsonSerializable
*/
abstract public function __toString() : string;
public function jsonSerialize() : string
final public function jsonSerialize() : string
{
return $this->__toString();
}