[\-\+]?[0-9]+)' . '(?:' . '(?:' . '(?:\.(?[0-9]+))?' . '(?:[eE](?[\-\+]?[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); } if (\is_float($value)) { $value = self::floatToString($value); } else { $value = (string) $value; } if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) { throw new NumberFormatException(\sprintf('The given value "%s" does not represent a valid number.', $value)); } if (isset($matches['denominator'])) { $numerator = self::cleanUp($matches['integral']); $denominator = \ltrim($matches['denominator'], '0'); if ($denominator === '') { throw DivisionByZeroException::denominatorMustNotBeZero(); } return new BigRational(new BigInteger($numerator), new BigInteger($denominator), false); } if (isset($matches['fractional']) || isset($matches['exponent'])) { $fractional = isset($matches['fractional']) ? $matches['fractional'] : ''; $exponent = isset($matches['exponent']) ? (int) $matches['exponent'] : 0; $unscaledValue = self::cleanUp($matches['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($matches['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 */ protected static function create(... $args) : BigNumber { /** @psalm-suppress TooManyArguments */ 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-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-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-pure */ public static function sum(...$values) : BigNumber { /** @var BigNumber|null $sum */ $sum = null; foreach ($values as $value) { $value = static::of($value); if ($sum === null) { $sum = $value; } else { $sum = 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 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(); } }