[\-\+])?' . '(?:' . '(?:' . '(?[0-9]+)?' . '(?\.)?' . '(?[0-9]+)?' . '(?:[eE](?[\-\+]?[0-9]+))?' . ')|(?:' . '(?[0-9]+)' . '\/?' . '(?[0-9]+)' . ')' . ')' . '$/'; /** * Creates a BigNumber of the given value. * * The concrete return type is dependent on the given value, with the following rules: * * - BigNumber instances are returned as is * - integer numbers are returned as BigInteger * - floating point numbers are converted to a string then parsed as such * - strings containing a `/` character are returned as BigRational * - strings containing a `.` character or using an exponential notation are returned as BigDecimal * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger * * @param BigNumber|int|float|string $value * * @return BigNumber * * @throws NumberFormatException If the format of the number is not valid. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. * * @psalm-pure */ public static function of($value) : BigNumber { if ($value instanceof BigNumber) { return $value; } if (\is_int($value)) { return new BigInteger((string) $value); } /** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */ $value = \is_float($value) ? self::floatToString($value) : (string) $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(); } $getMatch = static function(string $value) use ($matches) : ?string { return isset($matches[$value]) && $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; } $numerator = self::cleanUp($numerator); $denominator = self::cleanUp($denominator); if ($denominator === '0') { throw DivisionByZeroException::denominatorMustNotBeZero(); } return new BigRational( new BigInteger($numerator), 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.'); } $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); } $integral = self::cleanUp(($sign ?? '') . $integral); return new BigInteger($integral); } /** * Safely converts float to string, avoiding locale-dependent issues. * * @see https://github.com/brick/math/pull/20 * * @param float $float * * @return string * * @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; } /** * Proxy method to access protected constructors from sibling classes. * * @internal * * @param mixed ...$args The arguments to the constructor. * * @return static * * @psalm-pure * @psalm-suppress TooManyArguments * @psalm-suppress UnsafeInstantiation */ protected static function create(... $args) : BigNumber { return new static(... $args); } /** * Returns the minimum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The minimum value. * * @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(...$values) : BigNumber { $min = null; foreach ($values as $value) { $value = static::of($value); if ($min === null || $value->isLessThan($min)) { $min = $value; } } if ($min === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $min; } /** * Returns the maximum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The maximum value. * * @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(...$values) : BigNumber { $max = null; foreach ($values as $value) { $value = static::of($value); if ($max === null || $value->isGreaterThan($max)) { $max = $value; } } if ($max === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $max; } /** * Returns the sum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The sum. * * @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 sum(...$values) : BigNumber { /** @var BigNumber|null $sum */ $sum = null; foreach ($values as $value) { $value = static::of($value); $sum = $sum === null ? $value : self::add($sum, $value); } if ($sum === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $sum; } /** * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. * * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to * concrete classes the responsibility to perform the addition themselves or delegate it to the given number, * depending on their ability to perform the operation. This will also require a version bump because we're * potentially breaking custom BigNumber implementations (if any...) * * @param BigNumber $a * @param BigNumber $b * * @return BigNumber * * @psalm-pure */ private static function add(BigNumber $a, BigNumber $b) : BigNumber { if ($a instanceof BigRational) { return $a->plus($b); } if ($b instanceof BigRational) { return $b->plus($a); } if ($a instanceof BigDecimal) { return $a->plus($b); } if ($b instanceof BigDecimal) { return $b->plus($a); } /** @var BigInteger $a */ return $a->plus($b); } /** * 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. * * @return string * * @psalm-pure */ private static function cleanUp(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; } /** * Checks if this number is equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isEqualTo($that) : bool { return $this->compareTo($that) === 0; } /** * Checks if this number is strictly lower than the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isLessThan($that) : bool { return $this->compareTo($that) < 0; } /** * Checks if this number is lower than or equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isLessThanOrEqualTo($that) : bool { return $this->compareTo($that) <= 0; } /** * Checks if this number is strictly greater than the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isGreaterThan($that) : bool { return $this->compareTo($that) > 0; } /** * Checks if this number is greater than or equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isGreaterThanOrEqualTo($that) : bool { return $this->compareTo($that) >= 0; } /** * Checks if this number equals zero. * * @return bool */ public function isZero() : bool { return $this->getSign() === 0; } /** * Checks if this number is strictly negative. * * @return bool */ public function isNegative() : bool { return $this->getSign() < 0; } /** * Checks if this number is negative or zero. * * @return bool */ public function isNegativeOrZero() : bool { return $this->getSign() <= 0; } /** * Checks if this number is strictly positive. * * @return bool */ public function isPositive() : bool { return $this->getSign() > 0; } /** * Checks if this number is positive or zero. * * @return bool */ public function isPositiveOrZero() : bool { return $this->getSign() >= 0; } /** * Returns the sign of this number. * * @return int -1 if the number is negative, 0 if zero, 1 if positive. */ abstract public function getSign() : int; /** * Compares this number to the given one. * * @param BigNumber|int|float|string $that * * @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`. * * @throws MathException If the number is not valid. */ abstract public function compareTo($that) : int; /** * Converts this number to a BigInteger. * * @return BigInteger The converted number. * * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. */ abstract public function toBigInteger() : BigInteger; /** * Converts this number to a BigDecimal. * * @return BigDecimal The converted number. * * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. */ abstract public function toBigDecimal() : BigDecimal; /** * Converts this number to a BigRational. * * @return BigRational The converted number. */ abstract public function toBigRational() : BigRational; /** * 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. * * @return BigDecimal * * @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; /** * Returns the exact value of this number as a native integer. * * If this number cannot be converted to a native integer without losing precision, an exception is thrown. * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit. * * @return int The converted value. * * @throws MathException If this number cannot be exactly converted to a native integer. */ abstract public function toInt() : int; /** * Returns an approximation of this number as a floating-point value. * * Note that this method can discard information as the precision of a floating-point value * is inherently limited. * * If the number is greater than the largest representable floating point number, positive infinity is returned. * If the number is less than the smallest representable floating point number, negative infinity is returned. * * @return float The converted value. */ abstract public function toFloat() : float; /** * Returns a string representation of this number. * * The output of this method can be parsed by the `of()` factory method; * this will yield an object equal to this one, without any information loss. * * @return string */ abstract public function __toString() : string; /** * {@inheritdoc} */ public function jsonSerialize() : string { return $this->__toString(); } }