Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er)
- Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link
- Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest
- Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen)
- Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

975
vendor/brick/math/src/BigDecimal.php vendored Executable file
View File

@@ -0,0 +1,975 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\Internal\Calculator;
use Brick\Math\Internal\CalculatorRegistry;
use InvalidArgumentException;
use LogicException;
use Override;
use function func_num_args;
use function in_array;
use function intdiv;
use function max;
use function rtrim;
use function sprintf;
use function str_pad;
use function str_repeat;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
use const STR_PAD_LEFT;
/**
* An arbitrarily large decimal number.
*
* This class is immutable.
*
* The scale of the number is the number of digits after the decimal point. It is always positive or zero.
*/
final readonly class BigDecimal extends BigNumber
{
/**
* The unscaled value of this decimal number.
*
* This is a string of digits with an optional leading minus sign.
* No leading zero must be present.
* No leading minus sign must be present if the value is 0.
*/
private string $value;
/**
* The scale (number of digits after the decimal point) of this decimal number.
*
* This must be zero or more.
*/
private int $scale;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param string $value The unscaled value, validated.
* @param int $scale The scale, validated.
*
* @pure
*/
protected function __construct(string $value, int $scale = 0)
{
$this->value = $value;
$this->scale = $scale;
}
/**
* Creates a BigDecimal from an unscaled value and a scale.
*
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
*
* A negative scale is normalized to zero by appending zeros to the unscaled value.
*
* Example: `(12345, -3)` will result in the BigDecimal `12345000`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number. If negative, the scale will be set to zero
* and the unscaled value will be adjusted accordingly.
*
* @throws MathException If the value is not valid, or is not convertible to a BigInteger.
*
* @pure
*/
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0): BigDecimal
{
$value = BigInteger::of($value)->toString();
if ($scale < 0) {
if ($value !== '0') {
$value .= str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a BigDecimal representing zero, with a scale of zero.
*
* @pure
*/
public static function zero(): BigDecimal
{
/** @var BigDecimal|null $zero */
static $zero;
if ($zero === null) {
$zero = new BigDecimal('0');
}
return $zero;
}
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @pure
*/
public static function one(): BigDecimal
{
/** @var BigDecimal|null $one */
static $one;
if ($one === null) {
$one = new BigDecimal('1');
}
return $one;
}
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @pure
*/
public static function ten(): BigDecimal
{
/** @var BigDecimal|null $ten */
static $ten;
if ($ten === null) {
$ten = new BigDecimal('10');
}
return $ten;
}
/**
* Returns the sum of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/
public function plus(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
if ($this->value === '0' && $this->scale <= $that->scale) {
return $that;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = CalculatorRegistry::get()->add($a, $b);
$scale = max($this->scale, $that->scale);
return new BigDecimal($value, $scale);
}
/**
* Returns the difference of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/
public function minus(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = CalculatorRegistry::get()->sub($a, $b);
$scale = max($this->scale, $that->scale);
return new BigDecimal($value, $scale);
}
/**
* Returns the product of this number and the given one.
*
* The result has a scale of `$this->scale + $that->scale`.
*
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
*
* @throws MathException If the multiplier is not valid, or is not convertible to a BigDecimal.
*
* @pure
*/
public function multipliedBy(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '1' && $that->scale === 0) {
return $this;
}
if ($this->value === '1' && $this->scale === 0) {
return $that;
}
$value = CalculatorRegistry::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the result of the division of this number by the given one, at the given scale.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
* @param int|null $scale The desired scale. Omitting this parameter is deprecated; it will be required in 0.15.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
*
* @throws InvalidArgumentException If the scale is negative.
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
* exactly at the given scale.
*
* @pure
*/
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
if ($scale === null) {
// @phpstan-ignore-next-line
trigger_error(
'Not passing a $scale to BigDecimal::dividedBy() is deprecated. ' .
'Use $a->dividedBy($b, $a->getScale(), $roundingMode) to retain current behavior.',
E_USER_DEPRECATED,
);
$scale = $this->scale;
} elseif ($scale < 0) {
throw new InvalidArgumentException('Scale must not be negative.');
}
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
return $this;
}
$p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale);
$result = CalculatorRegistry::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @deprecated Will be removed in 0.15. Use dividedByExact() instead.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits.
*/
public function exactlyDividedBy(BigNumber|int|float|string $that): BigDecimal
{
trigger_error(
'BigDecimal::exactlyDividedBy() is deprecated and will be removed in 0.15. Use dividedByExact() instead.',
E_USER_DEPRECATED,
);
return $this->dividedByExact($that);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
* @throws RoundingNecessaryException If the result yields an infinite number of digits.
*
* @pure
*/
public function dividedByExact(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0') {
throw DivisionByZeroException::divisionByZero();
}
[, $b] = $this->scaleValues($this, $that);
$d = rtrim($b, '0');
$scale = strlen($b) - strlen($d);
$calculator = CalculatorRegistry::get();
foreach ([5, 2] as $prime) {
for (; ;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $this->dividedBy($that, $scale)->strippedOfTrailingZeros();
}
/**
* Returns this number exponentiated to the given value.
*
* The result has a scale of `$this->scale * $exponent`.
*
* @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/
public function power(int $exponent): BigDecimal
{
if ($exponent === 0) {
return BigDecimal::one();
}
if ($exponent === 1) {
return $this;
}
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new InvalidArgumentException(sprintf(
'The exponent %d is not in the range 0 to %d.',
$exponent,
Calculator::MAX_POWER,
));
}
return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
}
/**
* Returns the quotient of the division of this number by the given one.
*
* The quotient has a scale of `0`.
*
* Examples:
*
* - `7.5` quotient `3` returns `2`
* - `7.5` quotient `-3` returns `-2`
* - `-7.5` quotient `3` returns `-2`
* - `-7.5` quotient `-3` returns `2`
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/
public function quotient(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$quotient = CalculatorRegistry::get()->divQ($p, $q);
return new BigDecimal($quotient, 0);
}
/**
* Returns the remainder of the division of this number by the given one.
*
* The remainder has a scale of `max($this->scale, $that->scale)`.
* The remainder, when non-zero, has the same sign as the dividend.
*
* Examples:
*
* - `7.5` remainder `3` returns `1.5`
* - `7.5` remainder `-3` returns `1.5`
* - `-7.5` remainder `3` returns `-1.5`
* - `-7.5` remainder `-3` returns `-1.5`
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/
public function remainder(BigNumber|int|float|string $that): BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$remainder = CalculatorRegistry::get()->divR($p, $q);
$scale = max($this->scale, $that->scale);
return new BigDecimal($remainder, $scale);
}
/**
* Returns the quotient and remainder of the division of this number by the given one.
*
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
*
* Examples:
*
* - `7.5` quotientAndRemainder `3` returns [`2`, `1.5`]
* - `7.5` quotientAndRemainder `-3` returns [`-2`, `1.5`]
* - `-7.5` quotientAndRemainder `3` returns [`-2`, `-1.5`]
* - `-7.5` quotientAndRemainder `-3` returns [`2`, `-1.5`]
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
*
* @throws MathException If the divisor is not valid, or is not convertible to a BigDecimal.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/
public function quotientAndRemainder(BigNumber|int|float|string $that): array
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
$scale = max($this->scale, $that->scale);
$quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale);
return [$quotient, $remainder];
}
/**
* Returns the square root of this number, rounded to the given scale according to the given rounding mode.
*
* @param int $scale The target scale. Must be non-negative.
* @param RoundingMode $roundingMode The rounding mode to use, defaults to Down.
* ⚠️ WARNING: the default rounding mode was kept as Down for backward
* compatibility, but will change to Unnecessary in version 0.15. Pass a rounding
* mode explicitly to avoid this upcoming breaking change.
*
* @throws InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented
* exactly at the given scale.
*
* @pure
*/
public function sqrt(int $scale, RoundingMode $roundingMode = RoundingMode::Down): BigDecimal
{
if (func_num_args() === 1) {
// @phpstan-ignore-next-line
trigger_error(
'The default rounding mode of BigDecimal::sqrt() will change from Down to Unnecessary in version 0.15. ' .
'Pass a rounding mode explicitly to avoid this breaking change.',
E_USER_DEPRECATED,
);
}
if ($scale < 0) {
throw new InvalidArgumentException('Scale must not be negative.');
}
if ($this->value === '0') {
return new BigDecimal('0', $scale);
}
if ($this->value[0] === '-') {
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
}
$value = $this->value;
$inputScale = $this->scale;
if ($inputScale % 2 !== 0) {
$value .= '0';
$inputScale++;
}
$calculator = CalculatorRegistry::get();
// Keep one extra digit for rounding.
$intermediateScale = max($scale, intdiv($inputScale, 2)) + 1;
$value .= str_repeat('0', 2 * $intermediateScale - $inputScale);
$sqrt = $calculator->sqrt($value);
$isExact = $calculator->mul($sqrt, $sqrt) === $value;
if (! $isExact) {
if ($roundingMode === RoundingMode::Unnecessary) {
throw RoundingNecessaryException::roundingNecessary();
}
// Non-perfect-square sqrt is irrational, so the true value is strictly above this sqrt floor.
// Add one at the intermediate scale to guarantee Up/Ceiling round up at the target scale.
if (in_array($roundingMode, [RoundingMode::Up, RoundingMode::Ceiling], true)) {
$sqrt = $calculator->add($sqrt, '1');
}
// Irrational sqrt cannot land exactly on a midpoint; treat tie-to-down modes as HalfUp.
elseif (in_array($roundingMode, [RoundingMode::HalfDown, RoundingMode::HalfEven, RoundingMode::HalfFloor], true)) {
$roundingMode = RoundingMode::HalfUp;
}
}
return (new BigDecimal($sqrt, $intermediateScale))->toScale($scale, $roundingMode);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved to the left by the given number of places.
*
* @pure
*/
public function withPointMovedLeft(int $n): BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedRight(-$n);
}
return new BigDecimal($this->value, $this->scale + $n);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved to the right by the given number of places.
*
* @pure
*/
public function withPointMovedRight(int $n): BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedLeft(-$n);
}
$value = $this->value;
$scale = $this->scale - $n;
if ($scale < 0) {
if ($value !== '0') {
$value .= str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*
* @deprecated Use strippedOfTrailingZeros() instead.
*/
public function stripTrailingZeros(): BigDecimal
{
trigger_error(
'BigDecimal::stripTrailingZeros() is deprecated, use strippedOfTrailingZeros() instead.',
E_USER_DEPRECATED,
);
return $this->strippedOfTrailingZeros();
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*
* @pure
*/
public function strippedOfTrailingZeros(): BigDecimal
{
if ($this->scale === 0) {
return $this;
}
$trimmedValue = rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = strlen($this->value) - strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
}
if ($trimmableZeros > $this->scale) {
$trimmableZeros = $this->scale;
}
$value = substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
#[Override]
public function negated(): static
{
return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
}
#[Override]
public function compareTo(BigNumber|int|float|string $that): int
{
$that = BigNumber::of($that);
if ($that instanceof BigInteger) {
$that = $that->toBigDecimal();
}
if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that);
return CalculatorRegistry::get()->cmp($a, $b);
}
return -$that->compareTo($this);
}
#[Override]
public function getSign(): int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
/**
* @pure
*/
public function getUnscaledValue(): BigInteger
{
return self::newBigInteger($this->value);
}
/**
* @pure
*/
public function getScale(): int
{
return $this->scale;
}
/**
* Returns the number of significant digits in the number.
*
* This is the number of digits to both sides of the decimal point, stripped of leading zeros.
* The sign has no impact on the result.
*
* Examples:
* 0 => 0
* 0.0 => 0
* 123 => 3
* 123.456 => 6
* 0.00123 => 3
* 0.0012300 => 5
*
* @pure
*/
public function getPrecision(): int
{
$value = $this->value;
if ($value === '0') {
return 0;
}
$length = strlen($value);
return ($value[0] === '-') ? $length - 1 : $length;
}
/**
* Returns a string representing the integral part of this decimal number.
*
* Example: `-123.456` => `-123`.
*
* @deprecated Will be removed in 0.15 and re-introduced as returning BigInteger in 0.16.
*/
public function getIntegralPart(): string
{
trigger_error(
'BigDecimal::getIntegralPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigInteger in 0.16.',
E_USER_DEPRECATED,
);
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return substr($value, 0, -$this->scale);
}
/**
* Returns a string representing the fractional part of this decimal number.
*
* If the scale is zero, an empty string is returned.
*
* Examples: `-123.456` => '456', `123` => ''.
*
* @deprecated Will be removed in 0.15 and re-introduced as returning BigDecimal with a different meaning in 0.16.
*/
public function getFractionalPart(): string
{
trigger_error(
'BigDecimal::getFractionalPart() is deprecated and will be removed in 0.15. It will be re-introduced as returning BigDecimal with a different meaning in 0.16.',
E_USER_DEPRECATED,
);
if ($this->scale === 0) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return substr($value, -$this->scale);
}
/**
* Returns whether this decimal number has a non-zero fractional part.
*
* @pure
*/
public function hasNonZeroFractionalPart(): bool
{
if ($this->scale === 0) {
return false;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return substr($value, -$this->scale) !== str_repeat('0', $this->scale);
}
#[Override]
public function toBigInteger(): BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
#[Override]
public function toBigDecimal(): BigDecimal
{
return $this;
}
#[Override]
public function toBigRational(): BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
#[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
#[Override]
public function toInt(): int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat(): float
{
return (float) $this->toString();
}
/**
* @return numeric-string
*/
#[Override]
public function toString(): string
{
if ($this->scale === 0) {
/** @var numeric-string */
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
/** @phpstan-ignore return.type */
return substr($value, 0, -$this->scale) . '.' . substr($value, -$this->scale);
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{value: string, scale: int}
*/
public function __serialize(): array
{
return ['value' => $this->value, 'scale' => $this->scale];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
*
* @param array{value: string, scale: int} $data
*
* @throws LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->value)) {
throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
}
/** @phpstan-ignore deadCode.unreachable */
$this->value = $data['value'];
$this->scale = $data['scale'];
}
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigDecimal();
}
/**
* Puts the internal values of the given decimal numbers on the same scale.
*
* @return array{string, string} The scaled integer values of $x and $y.
*
* @pure
*/
private function scaleValues(BigDecimal $x, BigDecimal $y): array
{
$a = $x->value;
$b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) {
$b .= str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
/**
* @pure
*/
private function valueWithMinScale(int $scale): string
{
$value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) {
$value .= str_repeat('0', $scale - $this->scale);
}
return $value;
}
/**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
*
* @pure
*/
private function getUnscaledValueWithLeadingZeros(): string
{
$value = $this->value;
$targetLength = $this->scale + 1;
$negative = ($value[0] === '-');
$length = strlen($value);
if ($negative) {
$length--;
}
if ($length >= $targetLength) {
return $this->value;
}
if ($negative) {
$value = substr($value, 1);
}
$value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) {
$value = '-' . $value;
}
return $value;
}
}

1351
vendor/brick/math/src/BigInteger.php vendored Executable file

File diff suppressed because it is too large Load Diff

712
vendor/brick/math/src/BigNumber.php vendored Executable file
View File

@@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use InvalidArgumentException;
use JsonSerializable;
use Override;
use Stringable;
use function array_shift;
use function assert;
use function filter_var;
use function is_float;
use function is_int;
use function is_nan;
use function is_null;
use function ltrim;
use function preg_match;
use function str_contains;
use function str_repeat;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
use const FILTER_VALIDATE_INT;
use const PREG_UNMATCHED_AS_NULL;
/**
* Base class for arbitrary-precision numbers.
*
* This class is sealed: it is part of the public API but should not be subclassed in userland.
* Protected methods may change in any version.
*
* @phpstan-sealed BigInteger|BigDecimal|BigRational
*/
abstract readonly class BigNumber implements JsonSerializable, Stringable
{
/**
* The regular expression used to parse integer or decimal numbers.
*/
private const PARSE_REGEXP_NUMERICAL =
'/^' .
'(?<sign>[\-\+])?' .
'(?<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.
*
* When of() is called on BigNumber, 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 (deprecated, will be removed in 0.15)
* - 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
*
* When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
*
* @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.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
*
* @pure
*/
final public static function of(BigNumber|int|float|string $value): static
{
$value = self::_of($value);
if (static::class === BigNumber::class) {
assert($value instanceof static);
return $value;
}
return static::from($value);
}
/**
* Creates a BigNumber of the given value, or returns null if the input is null.
*
* Behaves like of() for non-null values.
*
* @see BigNumber::of()
*
* @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.
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
*
* @pure
*/
final public static function ofNullable(BigNumber|int|float|string|null $value): ?static
{
if (is_null($value)) {
return null;
}
return static::of($value);
}
/**
* Returns the minimum of the given values.
*
* If several values are equal and minimal, the first one is returned.
* This can affect the concrete return type when calling this method on BigNumber.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function min(BigNumber|int|float|string ...$values): static
{
$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.
*
* If several values are equal and maximal, the first one is returned.
* This can affect the concrete return type when calling this method on BigNumber.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function max(BigNumber|int|float|string ...$values): static
{
$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.
*
* When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
* the given values (BigInteger < BigDecimal < BigRational).
*
* When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
* specific subclass, and returns a result of the same type.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers must be convertible to an
* instance of the class this method is called on.
*
* @throws InvalidArgumentException If no values are given.
* @throws MathException If a number is not valid, or is not convertible to an instance of the class
* this method is called on.
*
* @pure
*/
final public static function sum(BigNumber|int|float|string ...$values): static
{
$first = array_shift($values);
if ($first === null) {
throw new InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
$sum = static::of($first);
foreach ($values as $value) {
$sum = self::add($sum, static::of($value));
}
assert($sum instanceof static);
return $sum;
}
/**
* Checks if this number is equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly less than the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isLessThan(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is less than or equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isLessThanOrEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isGreaterThan(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*
* @throws MathException If the given number is not valid.
*
* @pure
*/
final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that): bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*
* @pure
*/
final public function isZero(): bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*
* @pure
*/
final public function isNegative(): bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*
* @pure
*/
final public function isNegativeOrZero(): bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*
* @pure
*/
final public function isPositive(): bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*
* @pure
*/
final public function isPositiveOrZero(): bool
{
return $this->getSign() >= 0;
}
/**
* Returns the absolute value of this number.
*
* @pure
*/
final public function abs(): static
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*
* @pure
*/
abstract public function negated(): static;
/**
* Returns the sign of this number.
*
* Returns -1 if the number is negative, 0 if zero, 1 if positive.
*
* @return -1|0|1
*
* @pure
*/
abstract public function getSign(): int;
/**
* Compares this number to the given one.
*
* Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
*
* @return -1|0|1
*
* @throws MathException If the number is not valid.
*
* @pure
*/
abstract public function compareTo(BigNumber|int|float|string $that): int;
/**
* Limits (clamps) this number between the given minimum and maximum values.
*
* If the number is lower than $min, returns $min.
* If the number is greater than $max, returns $max.
* Otherwise, returns this number unchanged.
*
* @param BigNumber|int|float|string $min The minimum. Must be convertible to an instance of the class this method is called on.
* @param BigNumber|int|float|string $max The maximum. Must be convertible to an instance of the class this method is called on.
*
* @throws MathException If min/max are not convertible to an instance of the class this method is called on.
* @throws InvalidArgumentException If min is greater than max.
*
* @pure
*/
final public function clamp(BigNumber|int|float|string $min, BigNumber|int|float|string $max): static
{
$min = static::of($min);
$max = static::of($max);
if ($min->isGreaterThan($max)) {
throw new InvalidArgumentException('Minimum value must be less than or equal to maximum value.');
}
if ($this->isLessThan($min)) {
return $min;
}
if ($this->isGreaterThan($max)) {
return $max;
}
return $this;
}
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*
* @pure
*/
abstract public function toBigInteger(): BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*
* @pure
*/
abstract public function toBigDecimal(): BigDecimal;
/**
* Converts this number to a BigRational.
*
* @pure
*/
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`. Must be non-negative.
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
*
* @throws InvalidArgumentException If the scale is negative.
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used, and this number cannot be converted to
* the given scale without rounding.
*
* @pure
*/
abstract public function toScale(int $scale, RoundingMode $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.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*
* @pure
*/
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.
* This method never returns NaN.
*
* @pure
*/
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, but possibly of a different type if instantiated through `BigNumber::of()`.
*
* @pure
*/
abstract public function toString(): string;
#[Override]
final public function jsonSerialize(): string
{
return $this->toString();
}
/**
* @pure
*/
final public function __toString(): string
{
return $this->toString();
}
/**
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
*
* @throws RoundingNecessaryException If the value cannot be converted.
*
* @pure
*/
abstract protected static function from(BigNumber $number): static;
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigInteger(string $value): BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigDecimal(string $value, int $scale = 0): BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
*
* @pure
*/
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator): BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* @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.
*
* @pure
*/
private static function _of(BigNumber|int|float|string $value): BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (is_int($value)) {
return new BigInteger((string) $value);
}
if (is_float($value)) {
// @phpstan-ignore-next-line
trigger_error(
'Passing floats to BigNumber::of() and arithmetic methods is deprecated and will be removed in 0.15. ' .
'Cast the float to string explicitly to preserve the previous behaviour.',
E_USER_DEPRECATED,
);
if (is_nan($value)) {
$value = 'NAN';
} else {
$value = (string) $value;
}
}
if (str_contains($value, '/')) {
// Rational number
if (preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$sign = $matches['sign'];
$numerator = $matches['numerator'];
$denominator = $matches['denominator'];
$numerator = self::cleanUp($sign, $numerator);
$denominator = self::cleanUp(null, $denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false,
);
} else {
// Integer or decimal number
if (preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw NumberFormatException::invalidFormat($value);
}
$sign = $matches['sign'];
$point = $matches['point'];
$integral = $matches['integral'];
$fractional = $matches['fractional'];
$exponent = $matches['exponent'];
if ($integral === null && $fractional === null) {
throw NumberFormatException::invalidFormat($value);
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional ??= '';
if ($exponent !== null) {
if ($exponent[0] === '-') {
$exponent = ltrim(substr($exponent, 1), '0') ?: '0';
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
if ($exponent !== false) {
$exponent = -$exponent;
}
} else {
if ($exponent[0] === '+') {
$exponent = substr($exponent, 1);
}
$exponent = ltrim($exponent, '0') ?: '0';
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
}
} else {
$exponent = 0;
}
if ($exponent === false) {
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);
}
}
/**
* Removes optional leading zeros and applies 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 string of digits.
*
* @pure
*/
private static function cleanUp(string|null $sign, string $number): string
{
$number = ltrim($number, '0');
if ($number === '') {
return '0';
}
return $sign === '-' ? '-' . $number : $number;
}
/**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
*
* @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);
}
return $a->plus($b);
}
}

606
vendor/brick/math/src/BigRational.php vendored Executable file
View File

@@ -0,0 +1,606 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use InvalidArgumentException;
use LogicException;
use Override;
use function is_finite;
use function max;
use function min;
use function strlen;
use function substr;
use function trigger_error;
use const E_USER_DEPRECATED;
/**
* An arbitrarily large rational number.
*
* This class is immutable.
*
* Fractions are automatically simplified to lowest terms. For example, `2/4` becomes `1/2`.
* The denominator is always strictly positive; the sign is carried by the numerator.
*/
final readonly class BigRational extends BigNumber
{
/**
* The numerator.
*/
private BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private BigInteger $denominator;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param BigInteger $numerator The numerator.
* @param BigInteger $denominator The denominator.
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
*
* @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
if ($checkDenominator) {
if ($denominator->isZero()) {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->denominator = $denominator;
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @deprecated Use ofFraction() instead.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*/
public static function nd(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
): BigRational {
trigger_error(
'The BigRational::nd() method is deprecated, use BigRational::ofFraction() instead.',
E_USER_DEPRECATED,
);
return self::ofFraction($numerator, $denominator);
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws MathException If an argument is not valid, or is not convertible to a BigInteger.
* @throws DivisionByZeroException If the denominator is zero.
*
* @pure
*/
public static function ofFraction(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
): BigRational {
$numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns a BigRational representing zero.
*
* @pure
*/
public static function zero(): BigRational
{
/** @var BigRational|null $zero */
static $zero;
if ($zero === null) {
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
}
return $zero;
}
/**
* Returns a BigRational representing one.
*
* @pure
*/
public static function one(): BigRational
{
/** @var BigRational|null $one */
static $one;
if ($one === null) {
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
}
return $one;
}
/**
* Returns a BigRational representing ten.
*
* @pure
*/
public static function ten(): BigRational
{
/** @var BigRational|null $ten */
static $ten;
if ($ten === null) {
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
}
return $ten;
}
/**
* @pure
*/
public function getNumerator(): BigInteger
{
return $this->numerator;
}
/**
* @pure
*/
public function getDenominator(): BigInteger
{
return $this->denominator;
}
/**
* Returns the quotient of the division of the numerator by the denominator.
*
* @deprecated Will be removed in 0.15. Use getIntegralPart() instead.
*/
public function quotient(): BigInteger
{
trigger_error(
'BigRational::quotient() is deprecated and will be removed in 0.15. Use getIntegralPart() instead.',
E_USER_DEPRECATED,
);
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the remainder of the division of the numerator by the denominator.
*
* @deprecated Will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.
*/
public function remainder(): BigInteger
{
trigger_error(
'BigRational::remainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->remainder($number->getDenominator())` instead.',
E_USER_DEPRECATED,
);
return $this->numerator->remainder($this->denominator);
}
/**
* Returns the quotient and remainder of the division of the numerator by the denominator.
*
* @deprecated Will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.
*
* @return array{BigInteger, BigInteger}
*/
public function quotientAndRemainder(): array
{
trigger_error(
'BigRational::quotientAndRemainder() is deprecated and will be removed in 0.15. Use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead.',
E_USER_DEPRECATED,
);
return $this->numerator->quotientAndRemainder($this->denominator);
}
/**
* Returns the integral part of this rational number.
*
* Examples:
*
* - `7/3` returns `2` (since 7/3 = 2 + 1/3)
* - `-7/3` returns `-2` (since -7/3 = -2 + (-1/3))
*
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
*
* @pure
*/
public function getIntegralPart(): BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the fractional part of this rational number.
*
* Examples:
*
* - `7/3` returns `1/3` (since 7/3 = 2 + 1/3)
* - `-7/3` returns `-1/3` (since -7/3 = -2 + (-1/3))
*
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
*
* @pure
*/
public function getFractionalPart(): BigRational
{
return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false);
}
/**
* Returns the sum of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @throws MathException If the number is not valid.
*
* @pure
*/
public function plus(BigNumber|int|float|string $that): BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the difference of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to subtract.
*
* @throws MathException If the number is not valid.
*
* @pure
*/
public function minus(BigNumber|int|float|string $that): BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the product of this number and the given one.
*
* @param BigNumber|int|float|string $that The multiplier.
*
* @throws MathException If the multiplier is not valid.
*
* @pure
*/
public function multipliedBy(BigNumber|int|float|string $that): BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->numerator);
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the result of the division of this number by the given one.
*
* @param BigNumber|int|float|string $that The divisor.
*
* @throws MathException If the divisor is not valid.
* @throws DivisionByZeroException If the divisor is zero.
*
* @pure
*/
public function dividedBy(BigNumber|int|float|string $that): BigRational
{
$that = BigRational::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns this number exponentiated to the given value.
*
* @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*
* @pure
*/
public function power(int $exponent): BigRational
{
if ($exponent === 0) {
return BigRational::one();
}
if ($exponent === 1) {
return $this;
}
return new BigRational(
$this->numerator->power($exponent),
$this->denominator->power($exponent),
false,
);
}
/**
* Returns the reciprocal of this BigRational.
*
* The reciprocal has the numerator and denominator swapped.
*
* @throws DivisionByZeroException If the numerator is zero.
*
* @pure
*/
public function reciprocal(): BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
#[Override]
public function negated(): static
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*
* @pure
*/
public function simplified(): BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
$numerator = $this->numerator->quotient($gcd);
$denominator = $this->denominator->quotient($gcd);
return new BigRational($numerator, $denominator, false);
}
#[Override]
public function compareTo(BigNumber|int|float|string $that): int
{
$that = BigRational::of($that);
if ($this->denominator->isEqualTo($that->denominator)) {
return $this->numerator->compareTo($that->numerator);
}
return $this->numerator
->multipliedBy($that->denominator)
->compareTo($that->numerator->multipliedBy($this->denominator));
}
#[Override]
public function getSign(): int
{
return $this->numerator->getSign();
}
#[Override]
public function toBigInteger(): BigInteger
{
$simplified = $this->simplified();
if (! $simplified->denominator->isEqualTo(1)) {
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
}
return $simplified->numerator;
}
#[Override]
public function toBigDecimal(): BigDecimal
{
return $this->numerator->toBigDecimal()->dividedByExact($this->denominator);
}
#[Override]
public function toBigRational(): BigRational
{
return $this;
}
#[Override]
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
#[Override]
public function toInt(): int
{
return $this->toBigInteger()->toInt();
}
#[Override]
public function toFloat(): float
{
$simplified = $this->simplified();
$numeratorFloat = $simplified->numerator->toFloat();
$denominatorFloat = $simplified->denominator->toFloat();
if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
return $numeratorFloat / $denominatorFloat;
}
// At least one side overflows to INF; use a decimal approximation instead.
// We need ~17 significant digits for double precision (we use 20 for some margin). Since $scale controls
// decimal places (not significant digits), we subtract the estimated order of magnitude so that large results
// use fewer decimal places and small results use more (to look past leading zeros). Clamped to [0, 350] as
// doubles range from e-324 to e308 (350 ≈ 324 + 20 significant digits + margin).
$magnitude = strlen($simplified->numerator->abs()->toString()) - strlen($simplified->denominator->toString());
$scale = min(350, max(0, 20 - $magnitude));
return $simplified->numerator
->toBigDecimal()
->dividedBy($simplified->denominator, $scale, RoundingMode::HalfEven)
->toFloat();
}
#[Override]
public function toString(): string
{
$numerator = $this->numerator->toString();
$denominator = $this->denominator->toString();
if ($denominator === '1') {
return $numerator;
}
return $numerator . '/' . $denominator;
}
/**
* Returns the decimal representation of this rational number, with repeating decimals in parentheses.
*
* WARNING: This method is unbounded.
* The length of the repeating decimal period can be as large as `denominator - 1`.
* For fractions with large denominators, this method can use excessive memory and CPU time.
* For example, `1/100019` has a repeating period of 100,018 digits.
*
* Examples:
*
* - `10/3` returns `3.(3)`
* - `171/70` returns `2.4(428571)`
* - `1/2` returns `0.5`
*
* @pure
*/
public function toRepeatingDecimalString(): string
{
if ($this->numerator->isZero()) {
return '0';
}
$sign = $this->numerator->isNegative() ? '-' : '';
$numerator = $this->numerator->abs();
$denominator = $this->denominator;
$integral = $numerator->quotient($denominator);
$remainder = $numerator->remainder($denominator);
$integralString = $integral->toString();
if ($remainder->isZero()) {
return $sign . $integralString;
}
$digits = '';
$remainderPositions = [];
$index = 0;
while (! $remainder->isZero()) {
$remainderString = $remainder->toString();
if (isset($remainderPositions[$remainderString])) {
$repeatIndex = $remainderPositions[$remainderString];
$nonRepeating = substr($digits, 0, $repeatIndex);
$repeating = substr($digits, $repeatIndex);
return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
}
$remainderPositions[$remainderString] = $index;
$remainder = $remainder->multipliedBy(10);
$digits .= $remainder->quotient($denominator)->toString();
$remainder = $remainder->remainder($denominator);
$index++;
}
return $sign . $integralString . '.' . $digits;
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{numerator: BigInteger, denominator: BigInteger}
*/
public function __serialize(): array
{
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws LogicException
*/
public function __unserialize(array $data): void
{
/** @phpstan-ignore isset.initializedProperty */
if (isset($this->numerator)) {
throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
}
/** @phpstan-ignore deadCode.unreachable */
$this->numerator = $data['numerator'];
$this->denominator = $data['denominator'];
}
#[Override]
protected static function from(BigNumber $number): static
{
return $number->toBigRational();
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
final class DivisionByZeroException extends MathException
{
/**
* @pure
*/
public static function divisionByZero(): DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @pure
*/
public static function modulusMustNotBeZero(): DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @pure
*/
public static function denominatorMustNotBeZero(): DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
use function sprintf;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
/**
* Exception thrown when an integer overflow occurs.
*/
final class IntegerOverflowException extends MathException
{
/**
* @pure
*/
public static function toIntOverflow(BigInteger $value): IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(sprintf($message, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use RuntimeException;
/**
* Base class for all math exceptions.
*/
class MathException extends RuntimeException
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
*/
final class NegativeNumberException extends MathException
{
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use function dechex;
use function ord;
use function sprintf;
use function strtoupper;
/**
* Exception thrown when attempting to create a number from a string with an invalid format.
*/
final class NumberFormatException extends MathException
{
/**
* @pure
*/
public static function invalidFormat(string $value): self
{
return new self(sprintf(
'The given value "%s" does not represent a valid number.',
$value,
));
}
/**
* @param string $char The failing character.
*
* @pure
*/
public static function charNotInAlphabet(string $char): self
{
return new self(sprintf(
'Character %s is not valid in the given alphabet.',
self::charToString($char),
));
}
/**
* @pure
*/
private static function charToString(string $char): string
{
$ord = ord($char);
if ($ord < 32 || $ord > 126) {
$char = strtoupper(dechex($ord));
if ($ord < 16) {
$char = '0' . $char;
}
return '0x' . $char;
}
return '"' . $char . '"';
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a number cannot be represented at the requested scale without rounding.
*/
final class RoundingNecessaryException extends MathException
{
/**
* @pure
*/
public static function roundingNecessary(): RoundingNecessaryException
{
return new self('Rounding is necessary to represent the result of the operation at this scale.');
}
}

704
vendor/brick/math/src/Internal/Calculator.php vendored Executable file
View File

@@ -0,0 +1,704 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
use function chr;
use function ltrim;
use function ord;
use function str_repeat;
use function strlen;
use function strpos;
use function strrev;
use function strtolower;
use function substr;
/**
* Performs basic operations on arbitrary size integers.
*
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
* without leading zero, and with an optional leading minus sign if the number is not zero.
*
* Any other parameter format will lead to undefined behaviour.
* All methods must return strings respecting this format, unless specified otherwise.
*
* @internal
*/
abstract readonly class Calculator
{
/**
* The maximum exponent value allowed for the pow() method.
*/
public const MAX_POWER = 1_000_000;
/**
* The alphabet for converting from and to base 2 to 36, lowercase.
*/
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
/**
* Returns the absolute value of a number.
*
* @pure
*/
final public function abs(string $n): string
{
return ($n[0] === '-') ? substr($n, 1) : $n;
}
/**
* Negates a number.
*
* @pure
*/
final public function neg(string $n): string
{
if ($n === '0') {
return '0';
}
if ($n[0] === '-') {
return substr($n, 1);
}
return '-' . $n;
}
/**
* Compares two numbers.
*
* Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
*
* @return -1|0|1
*
* @pure
*/
final public function cmp(string $a, string $b): int
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
if ($aNeg && ! $bNeg) {
return -1;
}
if ($bNeg && ! $aNeg) {
return 1;
}
$aLen = strlen($aDig);
$bLen = strlen($bDig);
if ($aLen < $bLen) {
$result = -1;
} elseif ($aLen > $bLen) {
$result = 1;
} else {
$result = $aDig <=> $bDig;
}
return $aNeg ? -$result : $result;
}
/**
* Adds two numbers.
*
* @pure
*/
abstract public function add(string $a, string $b): string;
/**
* Subtracts two numbers.
*
* @pure
*/
abstract public function sub(string $a, string $b): string;
/**
* Multiplies two numbers.
*
* @pure
*/
abstract public function mul(string $a, string $b): string;
/**
* Returns the quotient of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The quotient.
*
* @pure
*/
abstract public function divQ(string $a, string $b): string;
/**
* Returns the remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return string The remainder.
*
* @pure
*/
abstract public function divR(string $a, string $b): string;
/**
* Returns the quotient and remainder of the division of two numbers.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
*
* @return array{string, string} An array containing the quotient and remainder.
*
* @pure
*/
abstract public function divQR(string $a, string $b): array;
/**
* Exponentiates a number.
*
* @param string $a The base number.
* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
*
* @return string The power.
*
* @pure
*/
abstract public function pow(string $a, int $e): string;
/**
* @param string $b The modulus; must not be zero.
*
* @pure
*/
public function mod(string $a, string $b): string
{
return $this->divR($this->add($this->divR($a, $b), $b), $b);
}
/**
* Returns the modular multiplicative inverse of $x modulo $m.
*
* If $x has no multiplicative inverse mod m, this method must return null.
*
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
*
* @param string $m The modulus; must not be negative or zero.
*
* @pure
*/
public function modInverse(string $x, string $m): ?string
{
if ($m === '1') {
return '0';
}
$modVal = $x;
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
$modVal = $this->mod($x, $m);
}
[$g, $x] = $this->gcdExtended($modVal, $m);
if ($g !== '1') {
return null;
}
return $this->mod($this->add($this->mod($x, $m), $m), $m);
}
/**
* Raises a number into power with modulo.
*
* @param string $base The base number.
* @param string $exp The exponent; must be positive or zero.
* @param string $mod The modulus; must be strictly positive.
*
* @pure
*/
abstract public function modPow(string $base, string $exp, string $mod): string;
/**
* Returns the greatest common divisor of the two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for GCD calculations.
*
* @return string The GCD, always positive, or zero if both arguments are zero.
*
* @pure
*/
public function gcd(string $a, string $b): string
{
if ($a === '0') {
return $this->abs($b);
}
if ($b === '0') {
return $this->abs($a);
}
return $this->gcd($b, $this->divR($a, $b));
}
/**
* Returns the least common multiple of the two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for LCM calculations.
*
* @return string The LCM, always positive, or zero if at least one argument is zero.
*
* @pure
*/
public function lcm(string $a, string $b): string
{
if ($a === '0' || $b === '0') {
return '0';
}
return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
}
/**
* Returns the square root of the given number, rounded down.
*
* The result is the largest x such that x² ≤ n.
* The input MUST NOT be negative.
*
* @pure
*/
abstract public function sqrt(string $n): string;
/**
* Converts a number from an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
* @param int $base The base of the number, validated from 2 to 36.
*
* @return string The converted number, following the Calculator conventions.
*
* @pure
*/
public function fromBase(string $number, int $base): string
{
return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
}
/**
* Converts a number to an arbitrary base.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for base conversion.
*
* @param string $number The number to convert, following the Calculator conventions.
* @param int $base The base to convert to, validated from 2 to 36.
*
* @return string The converted number, lowercase.
*
* @pure
*/
public function toBase(string $number, int $base): string
{
$negative = ($number[0] === '-');
if ($negative) {
$number = substr($number, 1);
}
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
if ($negative) {
return '-' . $number;
}
return $number;
}
/**
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
*
* @param string $number The number to convert, validated as a non-empty string,
* containing only chars in the given alphabet/base.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base of the number, validated from 2 to alphabet length.
*
* @return string The number in base 10, following the Calculator conventions.
*
* @pure
*/
final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
{
// remove leading "zeros"
$number = ltrim($number, $alphabet[0]);
if ($number === '') {
return '0';
}
// optimize for "one"
if ($number === $alphabet[1]) {
return '1';
}
$result = '0';
$power = '1';
$base = (string) $base;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$index = strpos($alphabet, $number[$i]);
if ($index !== 0) {
$result = $this->add(
$result,
($index === 1) ? $power : $this->mul($power, (string) $index),
);
}
if ($i !== 0) {
$power = $this->mul($power, $base);
}
}
return $result;
}
/**
* Converts a non-negative number to an arbitrary base using a custom alphabet.
*
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
* @param int $base The base to convert to, validated from 2 to alphabet length.
*
* @return string The converted number in the given alphabet.
*
* @pure
*/
final public function toArbitraryBase(string $number, string $alphabet, int $base): string
{
if ($number === '0') {
return $alphabet[0];
}
$base = (string) $base;
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, $base);
$remainder = (int) $remainder;
$result .= $alphabet[$remainder];
}
return strrev($result);
}
/**
* Performs a rounded division.
*
* Rounding is performed when the remainder of the division is not zero.
*
* @param string $a The dividend.
* @param string $b The divisor, must not be zero.
* @param RoundingMode $roundingMode The rounding mode.
*
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is provided but rounding is necessary.
*
* @pure
*/
final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
{
[$quotient, $remainder] = $this->divQR($a, $b);
$hasDiscardedFraction = ($remainder !== '0');
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
$discardedFractionSign = function () use ($remainder, $b): int {
$r = $this->abs($this->mul($remainder, '2'));
$b = $this->abs($b);
return $this->cmp($r, $b);
};
$increment = false;
switch ($roundingMode) {
case RoundingMode::Unnecessary:
if ($hasDiscardedFraction) {
throw RoundingNecessaryException::roundingNecessary();
}
break;
case RoundingMode::Up:
$increment = $hasDiscardedFraction;
break;
case RoundingMode::Down:
break;
case RoundingMode::Ceiling:
$increment = $hasDiscardedFraction && $isPositiveOrZero;
break;
case RoundingMode::Floor:
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
break;
case RoundingMode::HalfUp:
$increment = $discardedFractionSign() >= 0;
break;
case RoundingMode::HalfDown:
$increment = $discardedFractionSign() > 0;
break;
case RoundingMode::HalfCeiling:
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
break;
case RoundingMode::HalfFloor:
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
case RoundingMode::HalfEven:
$lastDigit = (int) $quotient[-1];
$lastDigitIsEven = ($lastDigit % 2 === 0);
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
break;
}
if ($increment) {
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
}
return $quotient;
}
/**
* Calculates bitwise AND of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function and(string $a, string $b): string
{
return $this->bitwise('and', $a, $b);
}
/**
* Calculates bitwise OR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function or(string $a, string $b): string
{
return $this->bitwise('or', $a, $b);
}
/**
* Calculates bitwise XOR of two numbers.
*
* This method can be overridden by the concrete implementation if the underlying library
* has built-in support for bitwise operations.
*
* @pure
*/
public function xor(string $a, string $b): string
{
return $this->bitwise('xor', $a, $b);
}
/**
* Extracts the sign & digits of the operands.
*
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
*
* @pure
*/
final protected function init(string $a, string $b): array
{
return [
$aNeg = ($a[0] === '-'),
$bNeg = ($b[0] === '-'),
$aNeg ? substr($a, 1) : $a,
$bNeg ? substr($b, 1) : $b,
];
}
/**
* @return array{string, string, string} GCD, X, Y
*
* @pure
*/
private function gcdExtended(string $a, string $b): array
{
if ($a === '0') {
return [$b, '0', '1'];
}
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
$y = $x1;
return [$gcd, $x, $y];
}
/**
* Performs a bitwise operation on a decimal number.
*
* @param 'and'|'or'|'xor' $operator The operator to use.
* @param string $a The left operand.
* @param string $b The right operand.
*
* @pure
*/
private function bitwise(string $operator, string $a, string $b): string
{
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$aBin = $this->toBinary($aDig);
$bBin = $this->toBinary($bDig);
$aLen = strlen($aBin);
$bLen = strlen($bBin);
if ($aLen > $bLen) {
$bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
} elseif ($bLen > $aLen) {
$aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
}
if ($aNeg) {
$aBin = $this->twosComplement($aBin);
}
if ($bNeg) {
$bBin = $this->twosComplement($bBin);
}
$value = match ($operator) {
'and' => $aBin & $bBin,
'or' => $aBin | $bBin,
'xor' => $aBin ^ $bBin,
};
$negative = match ($operator) {
'and' => $aNeg and $bNeg,
'or' => $aNeg or $bNeg,
'xor' => $aNeg xor $bNeg,
};
if ($negative) {
$value = $this->twosComplement($value);
}
$result = $this->toDecimal($value);
return $negative ? $this->neg($result) : $result;
}
/**
* @param string $number A positive, binary number.
*
* @pure
*/
private function twosComplement(string $number): string
{
$xor = str_repeat("\xff", strlen($number));
$number ^= $xor;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$byte = ord($number[$i]);
if (++$byte !== 256) {
$number[$i] = chr($byte);
break;
}
$number[$i] = "\x00";
if ($i === 0) {
$number = "\x01" . $number;
}
}
return $number;
}
/**
* Converts a decimal number to a binary string.
*
* @param string $number The number to convert, positive or zero, only digits.
*
* @pure
*/
private function toBinary(string $number): string
{
$result = '';
while ($number !== '0') {
[$number, $remainder] = $this->divQR($number, '256');
$result .= chr((int) $remainder);
}
return strrev($result);
}
/**
* Returns the positive decimal representation of a binary number.
*
* @param string $bytes The bytes representing the number.
*
* @pure
*/
private function toDecimal(string $bytes): string
{
$result = '0';
$power = '1';
for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
$index = ord($bytes[$i]);
if ($index !== 0) {
$result = $this->add(
$result,
($index === 1) ? $power : $this->mul($power, (string) $index),
);
}
if ($i !== 0) {
$power = $this->mul($power, '256');
}
}
return $result;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use Override;
use function bcadd;
use function bcdiv;
use function bcmod;
use function bcmul;
use function bcpow;
use function bcpowmod;
use function bcsqrt;
use function bcsub;
/**
* Calculator implementation built around the bcmath library.
*
* @internal
*/
final readonly class BcMathCalculator extends Calculator
{
#[Override]
public function add(string $a, string $b): string
{
return bcadd($a, $b, 0);
}
#[Override]
public function sub(string $a, string $b): string
{
return bcsub($a, $b, 0);
}
#[Override]
public function mul(string $a, string $b): string
{
return bcmul($a, $b, 0);
}
#[Override]
public function divQ(string $a, string $b): string
{
return bcdiv($a, $b, 0);
}
#[Override]
public function divR(string $a, string $b): string
{
return bcmod($a, $b, 0);
}
#[Override]
public function divQR(string $a, string $b): array
{
$q = bcdiv($a, $b, 0);
$r = bcmod($a, $b, 0);
return [$q, $r];
}
#[Override]
public function pow(string $a, int $e): string
{
return bcpow($a, (string) $e, 0);
}
#[Override]
public function modPow(string $base, string $exp, string $mod): string
{
// normalize to Euclidean representative so modPow() stays consistent with mod()
$base = $this->mod($base, $mod);
return bcpowmod($base, $exp, $mod, 0);
}
#[Override]
public function sqrt(string $n): string
{
return bcsqrt($n, 0);
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use GMP;
use Override;
use function gmp_add;
use function gmp_and;
use function gmp_div_q;
use function gmp_div_qr;
use function gmp_div_r;
use function gmp_gcd;
use function gmp_init;
use function gmp_invert;
use function gmp_lcm;
use function gmp_mul;
use function gmp_or;
use function gmp_pow;
use function gmp_powm;
use function gmp_sqrt;
use function gmp_strval;
use function gmp_sub;
use function gmp_xor;
/**
* Calculator implementation built around the GMP library.
*
* @internal
*/
final readonly class GmpCalculator extends Calculator
{
#[Override]
public function add(string $a, string $b): string
{
return gmp_strval(gmp_add($a, $b));
}
#[Override]
public function sub(string $a, string $b): string
{
return gmp_strval(gmp_sub($a, $b));
}
#[Override]
public function mul(string $a, string $b): string
{
return gmp_strval(gmp_mul($a, $b));
}
#[Override]
public function divQ(string $a, string $b): string
{
return gmp_strval(gmp_div_q($a, $b));
}
#[Override]
public function divR(string $a, string $b): string
{
return gmp_strval(gmp_div_r($a, $b));
}
#[Override]
public function divQR(string $a, string $b): array
{
[$q, $r] = gmp_div_qr($a, $b);
/**
* @var GMP $q
* @var GMP $r
*/
return [
gmp_strval($q),
gmp_strval($r),
];
}
#[Override]
public function pow(string $a, int $e): string
{
return gmp_strval(gmp_pow($a, $e));
}
#[Override]
public function modInverse(string $x, string $m): ?string
{
$result = gmp_invert($x, $m);
if ($result === false) {
return null;
}
return gmp_strval($result);
}
#[Override]
public function modPow(string $base, string $exp, string $mod): string
{
return gmp_strval(gmp_powm($base, $exp, $mod));
}
#[Override]
public function gcd(string $a, string $b): string
{
return gmp_strval(gmp_gcd($a, $b));
}
#[Override]
public function lcm(string $a, string $b): string
{
return gmp_strval(gmp_lcm($a, $b));
}
#[Override]
public function fromBase(string $number, int $base): string
{
return gmp_strval(gmp_init($number, $base));
}
#[Override]
public function toBase(string $number, int $base): string
{
return gmp_strval($number, $base);
}
#[Override]
public function and(string $a, string $b): string
{
return gmp_strval(gmp_and($a, $b));
}
#[Override]
public function or(string $a, string $b): string
{
return gmp_strval(gmp_or($a, $b));
}
#[Override]
public function xor(string $a, string $b): string
{
return gmp_strval(gmp_xor($a, $b));
}
#[Override]
public function sqrt(string $n): string
{
return gmp_strval(gmp_sqrt($n));
}
}

View File

@@ -0,0 +1,616 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
use Override;
use function assert;
use function in_array;
use function intdiv;
use function is_int;
use function ltrim;
use function str_pad;
use function str_repeat;
use function strcmp;
use function strlen;
use function substr;
use const PHP_INT_SIZE;
use const STR_PAD_LEFT;
/**
* Calculator implementation using only native PHP code.
*
* @internal
*/
final readonly class NativeCalculator extends Calculator
{
/**
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
* For multiplication, this represents the max sum of the lengths of both operands.
*
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
*/
private int $maxDigits;
/**
* @pure
*
* @codeCoverageIgnore
*/
public function __construct()
{
$this->maxDigits = match (PHP_INT_SIZE) {
4 => 9,
8 => 18,
};
}
#[Override]
public function add(string $a, string $b): string
{
/**
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a + $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0') {
return $b;
}
if ($b === '0') {
return $a;
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
if ($aNeg) {
$result = $this->neg($result);
}
return $result;
}
#[Override]
public function sub(string $a, string $b): string
{
return $this->add($a, $this->neg($b));
}
#[Override]
public function mul(string $a, string $b): string
{
/**
* @var numeric-string $a
* @var numeric-string $b
*/
$result = $a * $b;
if (is_int($result)) {
return (string) $result;
}
if ($a === '0' || $b === '0') {
return '0';
}
if ($a === '1') {
return $b;
}
if ($b === '1') {
return $a;
}
if ($a === '-1') {
return $this->neg($b);
}
if ($b === '-1') {
return $this->neg($a);
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $this->doMul($aDig, $bDig);
if ($aNeg !== $bNeg) {
$result = $this->neg($result);
}
return $result;
}
#[Override]
public function divQ(string $a, string $b): string
{
return $this->divQR($a, $b)[0];
}
#[Override]
public function divR(string $a, string $b): string
{
return $this->divQR($a, $b)[1];
}
#[Override]
public function divQR(string $a, string $b): array
{
if ($a === '0') {
return ['0', '0'];
}
if ($a === $b) {
return ['1', '0'];
}
if ($b === '1') {
return [$a, '0'];
}
if ($b === '-1') {
return [$this->neg($a), '0'];
}
/** @var numeric-string $a */
$na = $a * 1; // cast to number
if (is_int($na)) {
/** @var numeric-string $b */
$nb = $b * 1;
if (is_int($nb)) {
// the only division that may overflow is PHP_INT_MIN / -1,
// which cannot happen here as we've already handled a divisor of -1 above.
$q = intdiv($na, $nb);
$r = $na % $nb;
return [
(string) $q,
(string) $r,
];
}
}
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
[$q, $r] = $this->doDiv($aDig, $bDig);
if ($aNeg !== $bNeg) {
$q = $this->neg($q);
}
if ($aNeg) {
$r = $this->neg($r);
}
return [$q, $r];
}
#[Override]
public function pow(string $a, int $e): string
{
if ($e === 0) {
return '1';
}
if ($e === 1) {
return $a;
}
$odd = $e % 2;
$e -= $odd;
$aa = $this->mul($a, $a);
$result = $this->pow($aa, $e / 2);
if ($odd === 1) {
$result = $this->mul($result, $a);
}
return $result;
}
/**
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/.
*/
#[Override]
public function modPow(string $base, string $exp, string $mod): string
{
// normalize to Euclidean representative so modPow() stays consistent with mod()
$base = $this->mod($base, $mod);
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
if ($exp === '0' && $mod === '1') {
return '0';
}
$x = $base;
$res = '1';
// numbers are positive, so we can use remainder instead of modulo
$x = $this->divR($x, $mod);
while ($exp !== '0') {
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
$res = $this->divR($this->mul($res, $x), $mod);
}
$exp = $this->divQ($exp, '2');
$x = $this->divR($this->mul($x, $x), $mod);
}
return $res;
}
/**
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html.
*/
#[Override]
public function sqrt(string $n): string
{
if ($n === '0') {
return '0';
}
// initial approximation
$x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
$decreased = false;
for (; ;) {
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
break;
}
$decreased = $this->cmp($nx, $x) < 0;
$x = $nx;
}
return $x;
}
/**
* Performs the addition of two non-signed large integers.
*
* @pure
*/
private function doAdd(string $a, string $b): string
{
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
$i = 0;
}
/** @var numeric-string $blockA */
$blockA = substr($a, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry);
$sumLength = strlen($sum);
if ($sumLength > $blockLength) {
$sum = substr($sum, 1);
$carry = 1;
} else {
if ($sumLength < $blockLength) {
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
}
$carry = 0;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
if ($carry === 1) {
$result = '1' . $result;
}
return $result;
}
/**
* Performs the subtraction of two non-signed large integers.
*
* @pure
*/
private function doSub(string $a, string $b): string
{
if ($a === $b) {
return '0';
}
// Ensure that we always subtract to a positive result: biggest minus smallest.
$cmp = $this->doCmp($a, $b);
$invert = ($cmp === -1);
if ($invert) {
$c = $a;
$a = $b;
$b = $c;
}
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0;
$result = '';
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
$blockLength = $this->maxDigits;
if ($i < 0) {
$blockLength += $i;
$i = 0;
}
/** @var numeric-string $blockA */
$blockA = substr($a, $i, $blockLength);
/** @var numeric-string $blockB */
$blockB = substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
if ($sum < 0) {
$sum += $complement;
$carry = 1;
} else {
$carry = 0;
}
$sum = (string) $sum;
$sumLength = strlen($sum);
if ($sumLength < $blockLength) {
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
}
$result = $sum . $result;
if ($i === 0) {
break;
}
}
// Carry cannot be 1 when the loop ends, as a > b
assert($carry === 0);
$result = ltrim($result, '0');
if ($invert) {
$result = $this->neg($result);
}
return $result;
}
/**
* Performs the multiplication of two non-signed large integers.
*
* @pure
*/
private function doMul(string $a, string $b): string
{
$x = strlen($a);
$y = strlen($b);
$maxDigits = intdiv($this->maxDigits, 2);
$complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
$blockALength = $maxDigits;
if ($i < 0) {
$blockALength += $i;
$i = 0;
}
$blockA = (int) substr($a, $i, $blockALength);
$line = '';
$carry = 0;
for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
$blockBLength = $maxDigits;
if ($j < 0) {
$blockBLength += $j;
$j = 0;
}
$blockB = (int) substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry;
$value = $mul % $complement;
$carry = ($mul - $value) / $complement;
$value = (string) $value;
$value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
if ($j === 0) {
break;
}
}
if ($carry !== 0) {
$line = $carry . $line;
}
$line = ltrim($line, '0');
if ($line !== '') {
$line .= str_repeat('0', $x - $blockALength - $i);
$result = $this->add($result, $line);
}
if ($i === 0) {
break;
}
}
return $result;
}
/**
* Performs the division of two non-signed large integers.
*
* @return string[] The quotient and remainder.
*
* @pure
*/
private function doDiv(string $a, string $b): array
{
$cmp = $this->doCmp($a, $b);
if ($cmp === -1) {
return ['0', $a];
}
$x = strlen($a);
$y = strlen($b);
// we now know that a >= b && x >= y
$q = '0'; // quotient
$r = $a; // remainder
$z = $y; // focus length, always $y or $y+1
/** @var numeric-string $b */
$nb = $b * 1; // cast to number
// performance optimization in cases where the remainder will never cause int overflow
if (is_int(($nb - 1) * 10 + 9)) {
$r = (int) substr($a, 0, $z - 1);
for ($i = $z - 1; $i < $x; $i++) {
$n = $r * 10 + (int) $a[$i];
/** @var int $nb */
$q .= intdiv($n, $nb);
$r = $n % $nb;
}
return [ltrim($q, '0') ?: '0', (string) $r];
}
for (; ;) {
$focus = substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
if ($cmp === -1) {
if ($z === $x) { // remainder < dividend
break;
}
$z++;
}
$zeros = str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros);
$a = $this->sub($a, $b . $zeros);
$r = $a;
if ($r === '0') { // remainder == 0
break;
}
$x = strlen($a);
if ($x < $y) { // remainder < dividend
break;
}
$z = $y;
}
return [$q, $r];
}
/**
* Compares two non-signed large numbers.
*
* @return -1|0|1
*
* @pure
*/
private function doCmp(string $a, string $b): int
{
$x = strlen($a);
$y = strlen($b);
$cmp = $x <=> $y;
if ($cmp !== 0) {
return $cmp;
}
return strcmp($a, $b) <=> 0; // enforce -1|0|1
}
/**
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
*
* The numbers must only consist of digits, without leading minus sign.
*
* @return array{string, string, int}
*
* @pure
*/
private function pad(string $a, string $b): array
{
$x = strlen($a);
$y = strlen($b);
if ($x > $y) {
$b = str_repeat('0', $x - $y) . $b;
return [$a, $b, $x];
}
if ($x < $y) {
$a = str_repeat('0', $y - $x) . $a;
return [$a, $b, $y];
}
return [$a, $b, $x];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Internal;
use function extension_loaded;
/**
* Stores the current Calculator instance used by BigNumber classes.
*
* @internal
*/
final class CalculatorRegistry
{
/**
* The Calculator instance in use.
*/
private static ?Calculator $instance = null;
/**
* Sets the Calculator instance to use.
*
* An instance is typically set only in unit tests: autodetect is usually the best option.
*
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
*/
final public static function set(?Calculator $calculator): void
{
self::$instance = $calculator;
}
/**
* Returns the Calculator instance to use.
*
* If none has been explicitly set, the fastest available implementation will be returned.
*
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
* only relying on autodetect.
*
* @pure
*/
final public static function get(): Calculator
{
/** @phpstan-ignore impure.staticPropertyAccess */
if (self::$instance === null) {
/** @phpstan-ignore impure.propertyAssign */
self::$instance = self::detect();
}
/** @phpstan-ignore impure.staticPropertyAccess */
return self::$instance;
}
/**
* Returns the fastest available Calculator implementation.
*
* @pure
*
* @codeCoverageIgnore
*/
private static function detect(): Calculator
{
if (extension_loaded('gmp')) {
return new Calculator\GmpCalculator();
}
if (extension_loaded('bcmath')) {
return new Calculator\BcMathCalculator();
}
return new Calculator\NativeCalculator();
}
}

143
vendor/brick/math/src/RoundingMode.php vendored Executable file
View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
/**
* Specifies rounding behavior by defining how discarded digits affect the returned result when an exact value cannot
* be represented at the requested scale.
*/
enum RoundingMode
{
/**
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
*
* If this rounding mode is specified on an operation that yields a result that
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
*/
case Unnecessary;
/**
* Rounds away from zero.
*
* Always increments the digit prior to a nonzero discarded fraction.
* Note that this rounding mode never decreases the magnitude of the calculated value.
*/
case Up;
/**
* Rounds towards zero.
*
* Never increments the digit prior to a discarded fraction (i.e., truncates).
* Note that this rounding mode never increases the magnitude of the calculated value.
*/
case Down;
/**
* Rounds towards positive infinity.
*
* If the result is positive, behaves as for Up; if negative, behaves as for Down.
* Note that this rounding mode never decreases the calculated value.
*/
case Ceiling;
/**
* Rounds towards negative infinity.
*
* If the result is positive, behaves as for Down; if negative, behaves as for Up.
* Note that this rounding mode never increases the calculated value.
*/
case Floor;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
*
* Behaves as for Up if the discarded fraction is >= 0.5; otherwise, behaves as for Down.
* Note that this is the rounding mode commonly taught at school.
*/
case HalfUp;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
*
* Behaves as for Up if the discarded fraction is > 0.5; otherwise, behaves as for Down.
*/
case HalfDown;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
*
* If the result is positive, behaves as for HalfUp; if negative, behaves as for HalfDown.
*/
case HalfCeiling;
/**
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
*
* If the result is positive, behaves as for HalfDown; if negative, behaves as for HalfUp.
*/
case HalfFloor;
/**
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
*
* Behaves as for HalfUp if the digit to the left of the discarded fraction is odd;
* behaves as for HalfDown if it's even.
*
* Note that this is the rounding mode that statistically minimizes
* cumulative error when applied repeatedly over a sequence of calculations.
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
*/
case HalfEven;
/**
* @deprecated Use RoundingMode::Unnecessary instead.
*/
public const UNNECESSARY = self::Unnecessary;
/**
* @deprecated Use RoundingMode::Up instead.
*/
public const UP = self::Up;
/**
* @deprecated Use RoundingMode::Down instead.
*/
public const DOWN = self::Down;
/**
* @deprecated Use RoundingMode::Ceiling instead.
*/
public const CEILING = self::Ceiling;
/**
* @deprecated Use RoundingMode::Floor instead.
*/
public const FLOOR = self::Floor;
/**
* @deprecated Use RoundingMode::HalfUp instead.
*/
public const HALF_UP = self::HalfUp;
/**
* @deprecated Use RoundingMode::HalfDown instead.
*/
public const HALF_DOWN = self::HalfDown;
/**
* @deprecated Use RoundingMode::HalfCeiling instead.
*/
public const HALF_CEILING = self::HalfCeiling;
/**
* @deprecated Use RoundingMode::HalfFloor instead.
*/
public const HALF_FLOOR = self::HalfFloor;
/**
* @deprecated Use RoundingMode::HalfEven instead.
*/
public const HALF_EVEN = self::HalfEven;
}