Files
kupshop/class/class.Decimal.php
2025-08-02 16:30:27 +02:00

2048 lines
54 KiB
PHP

<?php
class Decimal implements JsonSerializable
{
public static $default_scale = 4;
/**
* Internal numeric value.
*
* @var string
*/
protected $value;
/**
* Number of digits behind the point.
*
* @var int
*/
private $scale;
/**
* Private constructor.
*
* @param int $scale
* @param string $value
*/
private function __construct($value, $scale)
{
$this->value = $value;
$this->scale = $scale;
}
/**
* Private clone method.
*/
private function __clone()
{
}
/**
* Returns a "Positive Infinite" object.
*
* @return Decimal
*/
public static function getPositiveInfinite()
{
return InfiniteDecimal::getPositiveInfinite();
}
/**
* Returns a "Negative Infinite" object.
*
* @return Decimal
*/
public static function getNegativeInfinite()
{
return InfiniteDecimal::getNegativeInfinite();
}
/**
* Decimal "constructor".
*
* @param int $scale
* @param bool $removeZeros If true then removes trailing zeros from the number representation
*
* @return Decimal
*/
public static function create($value, $scale = null, $removeZeros = false)
{
if (is_null($value)) {
return DecimalConstants::Zero();
} elseif (is_int($value)) {
return self::fromInteger($value);
} elseif (is_float($value)) {
return self::fromFloat($value, $scale, $removeZeros);
} elseif (is_string($value)) {
return self::fromString($value, $scale, $removeZeros);
} elseif ($value instanceof self) {
return self::fromDecimal($value, $scale);
} else {
throw new InvalidArgumentTypeException(
['int', 'float', 'string', 'Decimal'],
is_object($value) ? get_class($value) : gettype($value),
'Invalid argument type.'
);
}
}
/**
* @param int $intValue
*
* @return Decimal
*/
public static function fromInteger($intValue)
{
self::paramsValidation($intValue, null);
if (!is_int($intValue)) {
throw new InvalidArgumentTypeException(
['int'],
is_object($intValue) ? get_class($intValue) : gettype($intValue),
'$intValue must be of type int'
);
}
return new static((string) $intValue, 0);
}
/**
* @param float $fltValue
* @param int $scale
* @param bool $removeZeros If true then removes trailing zeros from the number representation
*
* @return Decimal
*/
public static function fromFloat($fltValue, $scale = null, $removeZeros = false)
{
self::paramsValidation($fltValue, $scale);
if (!is_float($fltValue)) {
throw new InvalidArgumentTypeException(
['float'],
is_object($fltValue) ?
get_class($fltValue) :
gettype($fltValue),
'$fltValue must be of type float'
);
} elseif ($fltValue === INF) {
return InfiniteDecimal::getPositiveInfinite();
} elseif ($fltValue === -INF) {
return InfiniteDecimal::getNegativeInfinite();
} elseif (is_nan($fltValue)) {
throw new \DomainException(
"To ensure consistency, this class doesn't handle NaN objects."
);
}
$defaultScale = 16;
$strValue = (string) $fltValue;
if (preg_match("/^ (?P<int> \d*) (?: \. (?P<dec> \d+) ) E (?P<sign>[\+\-]) (?P<exp>\d+) $/x", $strValue, $capture)) {
if ($scale === null) {
if ($capture['sign'] == '-') {
$scale = $capture['exp'] + strlen($capture['dec']);
} else {
$scale = $defaultScale;
}
}
$strValue = number_format($fltValue, $scale, '.', '');
}
if ($scale === null) {
$scale = $defaultScale;
}
if ($removeZeros) {
$strValue = self::removeTrailingZeros($strValue, $scale);
}
return new static($strValue, $scale);
}
/**
* @param string $strValue
* @param int $scale
* @param bool $removeZeros If true then removes trailing zeros from the number representation
*
* @return Decimal
*/
public static function fromString($strValue, $scale = null, $removeZeros = false)
{
if ($scale === null) {
$scale = self::$default_scale;
}
self::paramsValidation($strValue, $scale);
if ($strValue == '' || $strValue === null) {
$strValue = '0';
}
if (!is_string($strValue)) {
throw new InvalidArgumentTypeException(
['string'],
is_object($strValue) ? get_class($strValue) : gettype($strValue),
'$strVlue must be of type string.'
);
}
if (preg_match('/^([+\-]?)0*(([1-9][0-9]*|[0-9])([.,][0-9]+)?)$/', $strValue, $captures) === 1) {
// Now it's time to strip leading zeros in order to normalize inner values
$value = self::normalizeSign($captures[1]).strtr($captures[2], ',', '.');
$min_scale = isset($captures[4]) ? max(0, strlen($captures[4]) - 1) : 0;
} elseif (preg_match('/([+\-]?)0*([0-9](\.[0-9]+)?)[eE]([+\-]?)(\d+)/', $strValue, $captures) === 1) {
$mantissa_scale = max(strlen($captures[3]) - 1, 0);
$exp_val = (int) $captures[5];
if (self::normalizeSign($captures[4]) === '') {
$min_scale = max($mantissa_scale - $exp_val, 0);
$tmp_multiplier = bcpow(10, $exp_val);
} else {
$min_scale = $mantissa_scale + $exp_val;
$tmp_multiplier = bcpow(10, -$exp_val, $exp_val);
}
$value = self::normalizeSign($captures[1]).bcmul(
$captures[2],
$tmp_multiplier,
max($min_scale, $scale !== null ? $scale : 0)
);
} elseif (preg_match('/([+\-]?)(inf|Inf|INF)/', $strValue, $captures) === 1) {
if ($captures[1] === '-') {
return InfiniteDecimal::getNegativeInfinite();
} else {
return InfiniteDecimal::getPositiveInfinite();
}
} else {
throw new \InvalidArgumentException(
$strValue.' must be a string that represents uniquely a float point number.'
);
}
$scale = ($scale !== null) ? $scale : $min_scale;
if ($scale < $min_scale) {
$value = self::innerRound($value, $scale);
}
if ($removeZeros) {
$value = self::removeTrailingZeros($value, $scale);
}
return new static($value, $scale);
}
public static function detectStringScale($string)
{
if (($dotPosition = strpos($string, '.')) === false) {
return self::$default_scale;
}
return max(self::$default_scale, strlen($string) - $dotPosition - 1);
}
/**
* Constructs a new Decimal object based on a previous one,
* but changing it's $scale property.
*
* @param int $scale
*
* @return Decimal
*/
public static function fromDecimal(Decimal $decValue, $scale = null)
{
self::paramsValidation($decValue, $scale);
// This block protect us from unnecessary additional instances
if ($scale === null || $scale >= $decValue->scale || $decValue->isInfinite()) {
return $decValue;
}
return new static(
self::innerRound($decValue->value, $scale),
$scale
);
}
/**
* Returns larger of two decimals.
*
* @return Decimal
*/
public static function max(Decimal $a, Decimal $b)
{
return $a->lowerThan($b) ? $b : $a;
}
/**
* Returns lower of two decimals.
*
* @return Decimal
*/
public static function min(Decimal $a, Decimal $b)
{
return $a->lowerThan($b) ? $a : $b;
}
/**
* Adds two Decimal objects.
*
* @param int $scale
*
* @return Decimal
*/
public function add(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isInfinite()) {
return $b;
}
return self::fromString(
bcadd($this->value, $b->value, max($this->scale, $b->scale)),
$scale
);
}
/**
* Subtracts two BigNumber objects.
*
* @param int $scale
*
* @return Decimal
*/
public function sub(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isInfinite()) {
return $b->additiveInverse();
}
return self::fromString(
bcsub($this->value, $b->value, max($this->scale, $b->scale)),
$scale ?: $this->scale + $b->scale
);
}
/**
* Multiplies two BigNumber objects.
*
* @param int $scale
*
* @return Decimal
*/
public function mul(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isInfinite()) {
return $b->mul($this);
} elseif ($b->isZero()) {
return DecimalConstants::Zero();
}
return self::fromString(
bcmul($this->value, $b->value, $this->scale + $b->scale),
$scale ?: $this->scale + $b->scale
);
}
/**
* Divides the object by $b .
* Warning: div with $scale == 0 is not the same as
* integer division because it rounds the
* last digit in order to minimize the error.
*
* @param int $scale
*
* @return Decimal
*/
public function div(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isZero()) {
throw new \DomainException('Division by zero is not allowed.');
} elseif ($this->isZero() || $b->isInfinite()) {
return DecimalConstants::Zero();
} else {
if ($scale !== null) {
$divscale = $scale;
} else {
// $divscale is calculated in order to maintain a reasonable precision
$this_abs = $this->abs();
$b_abs = $b->abs();
// $log10_result =
// self::innerLog10($this_abs->value, $this_abs->scale, 1) -
// self::innerLog10($b_abs->value, $b_abs->scale, 1);
$divscale = (int) max(
$this->scale + $b->scale, 0
// max(
// self::countSignificativeDigits($this, $this_abs),
// self::countSignificativeDigits($b, $b_abs)
// ) - max(ceil($log10_result), 0),
// ceil(-$log10_result) + 1
);
}
return self::fromString(
bcdiv($this->value, $b->value, $divscale + 1), $divscale
);
}
}
/**
* Returns the square root of this object.
*
* @param int $scale
*
* @return Decimal
*/
public function sqrt($scale = null)
{
if ($this->isNegative()) {
throw new \DomainException(
"Decimal can't handle square roots of negative numbers (it's only for real numbers)."
);
} elseif ($this->isZero()) {
return DecimalConstants::Zero();
}
$sqrt_scale = ($scale !== null ? $scale : $this->scale);
return self::fromString(
bcsqrt($this->value, $sqrt_scale + 1),
$sqrt_scale
);
}
/**
* Powers this value to $b.
*
* @param Decimal $b exponent
* @param int $scale
*
* @return Decimal
*/
public function pow(Decimal $b, $scale = null)
{
if ($this->isZero()) {
if ($b->isPositive()) {
return self::fromDecimal($this, $scale);
} else {
throw new \DomainException(
"zero can't be powered to zero or negative numbers."
);
}
} elseif ($b->isZero()) {
return DecimalConstants::One();
} elseif ($b->isNegative()) {
return DecimalConstants::One()->div(
$this->pow($b->additiveInverse()), $scale
);
} elseif ($b->scale == 0) {
$pow_scale = $scale === null ?
max($this->scale, $b->scale) : max($this->scale, $b->scale, $scale);
return self::fromString(
bcpow($this->value, $b->value, $pow_scale + 1),
$pow_scale
);
} else {
if ($this->isPositive()) {
$pow_scale = $scale === null ?
max($this->scale, $b->scale) : max($this->scale, $b->scale, $scale);
$truncated_b = bcadd($b->value, '0', 0);
$remaining_b = bcsub($b->value, $truncated_b, $b->scale);
$first_pow_approx = bcpow($this->value, $truncated_b, $pow_scale + 1);
$intermediate_root = self::innerPowWithLittleExponent(
$this->value,
$remaining_b,
$b->scale,
$pow_scale + 1
);
return self::fromString(
bcmul($first_pow_approx, $intermediate_root, $pow_scale + 1),
$pow_scale
);
} else { // elseif ($this->isNegative())
if ($b->isInteger()) {
if (preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1) {
// $b is an even number
return $this->additiveInverse()->pow($b, $scale);
} else {
// $b is an odd number
return $this->additiveInverse()->pow($b, $scale)->additiveInverse();
}
}
throw new Exception(
"Usually negative numbers can't be powered to non integer numbers. ".
'The cases where is possible are not implemented.'
);
}
}
}
/**
* Returns the object's logarithm in base 10.
*
* @param int $scale
*
* @return Decimal
*/
public function log10($scale = null)
{
if ($this->isNegative()) {
throw new \DomainException(
"Decimal can't handle logarithms of negative numbers (it's only for real numbers)."
);
} elseif ($this->isZero()) {
return InfiniteDecimal::getNegativeInfinite();
}
return self::fromString(
self::innerLog10($this->value, $this->scale, $scale !== null ? $scale + 1 : $this->scale + 1),
$scale
);
}
/**
* @param int $scale
*
* @return bool
*/
public function isZero($scale = null)
{
$cmp_scale = $scale !== null ? $scale : $this->scale;
return bccomp(self::innerRound($this->value, $cmp_scale), '0', $cmp_scale) === 0;
}
/**
* @return bool
*/
public function isPositive()
{
return $this->value[0] !== '-' && !$this->isZero();
}
/**
* @return bool
*/
public function isNegative()
{
return $this->value[0] === '-';
}
/**
* @return bool
*/
public function isInteger()
{
return preg_match('/^[+\-]?[0-9]+(\.0+)?$/', $this->value, $captures) === 1;
}
/**
* @return bool
*/
public function isInfinite()
{
return false;
}
/**
* Equality comparison between this object and $b.
*
* @param int $scale
*
* @return bool
*/
public function equals(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($this === $b) {
return true;
} elseif ($b->isInfinite()) {
return false;
} else {
$cmp_scale = $scale !== null ? $scale : max($this->scale, $b->scale);
return
bccomp(
self::innerRound($this->value, $cmp_scale),
self::innerRound($b->value, $cmp_scale),
$cmp_scale
) == 0;
}
}
/**
* $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0.
*
* @param int $scale
*
* @return int
*/
public function comp(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($this === $b) {
return 0;
} elseif ($b->isInfinite()) {
return -$b->comp($this);
}
$cmp_scale = $scale !== null ? $scale : max($this->scale, $b->scale);
return bccomp(
self::innerRound($this->value, $cmp_scale),
self::innerRound($b->value, $cmp_scale),
$cmp_scale
);
}
public function lessThan(Decimal $b, $scale = null)
{
if ($this->comp($b, $scale) < 0) {
return true;
}
return false;
}
public function lowerThan(Decimal $b, $scale = null)
{
return static::lessThan($b, $scale);
}
public function lowerThanOrEqual(Decimal $b, $scale = null)
{
if ($this->comp($b, $scale) <= 0) {
return true;
}
return false;
}
/**
* Returns the element's additive inverse.
*
* @return Decimal
*/
public function additiveInverse()
{
if ($this->isZero()) {
return $this;
} elseif ($this->isNegative()) {
$value = substr($this->value, 1);
} else { // if ($this->isPositive()) {
$value = '-'.$this->value;
}
return new static($value, $this->scale);
}
/**
* "Rounds" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function round($scale = 0)
{
if ($scale >= $this->scale) {
return $this;
}
return self::fromString(self::innerRound($this->value, $scale));
}
/**
* "Rounds" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function value($scale = null)
{
if ($scale === null) {
$scale = self::$default_scale;
}
return $this->round($scale);
}
/**
* "Ceils" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function ceil($scale = 0)
{
if ($scale >= $this->scale) {
return $this;
}
if ($this->isNegative()) {
return self::fromString(bcadd($this->value, '0', $scale));
}
return $this->innerTruncate($scale);
}
private function innerTruncate($scale = 0, $ceil = true)
{
$rounded = bcadd($this->value, '0', $scale);
$rlen = strlen($rounded);
$tlen = strlen($this->value);
$mustTruncate = false;
for ($i = $tlen - 1; $i >= $rlen; $i--) {
if ((int) $this->value[$i] > 0) {
$mustTruncate = true;
break;
}
}
if ($mustTruncate) {
$rounded = $ceil ?
bcadd($rounded, bcpow('10', -$scale, $scale), $scale) :
bcsub($rounded, bcpow('10', -$scale, $scale), $scale);
}
return self::fromString($rounded, $scale);
}
/**
* "Floors" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function floor($scale = 0)
{
if ($scale >= $this->scale) {
return $this;
}
if ($this->isNegative()) {
return $this->innerTruncate($scale, false);
}
return self::fromString(bcadd($this->value, '0', $scale));
}
/**
* Returns the absolute value (always a positive number).
*
* @return Decimal
*/
public function abs()
{
if ($this->isZero() || $this->isPositive()) {
return $this;
}
return $this->additiveInverse();
}
/**
* Calculate modulo with a decimal.
*
* @param int $scale
*
* @return $this % $d
*/
public function mod(Decimal $d, $scale = null)
{
$div = $this->div($d, 1)->floor();
return $this->sub($div->mul($d), $scale);
}
/**
* Calculates the sine of this method with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal sin($this)
*/
public function sin($scale = null)
{
// First normalise the number in the [0, 2PI] domain
$x = $this->mod(DecimalConstants::PI()->mul(self::fromString('2')));
// PI has only 32 significant numbers
$scale = ($scale === null) ? 32 : $scale;
return self::factorialSerie(
$x,
DecimalConstants::zero(),
function ($i) {
return ($i % 2 === 1) ? (
($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne()
) : DecimalConstants::zero();
},
$scale
);
}
/**
* Calculates the cosecant of this with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal
*/
public function cosec($scale = null)
{
$sin = $this->sin($scale + 2);
if ($sin->isZero()) {
throw new \DomainException(
"The cosecant of this 'angle' is undefined."
);
}
return DecimalConstants::one()->div($sin)->round($scale);
}
/**
* Calculates the cosine of this method with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal cos($this)
*/
public function cos($scale = null)
{
// First normalise the number in the [0, 2PI] domain
$x = $this->mod(DecimalConstants::PI()->mul(self::fromString('2')));
// PI has only 32 significant numbers
$scale = ($scale === null) ? 32 : $scale;
return self::factorialSerie(
$x,
DecimalConstants::one(),
function ($i) {
return ($i % 2 === 0) ? (
($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne()
) : DecimalConstants::zero();
},
$scale
);
}
/**
* Calculates the secant of this with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal
*/
public function sec($scale = null)
{
$cos = $this->cos($scale + 2);
if ($cos->isZero()) {
throw new \DomainException(
"The secant of this 'angle' is undefined."
);
}
return DecimalConstants::one()->div($cos)->round($scale);
}
/**
* Calculates the arcsine of this with the highest possible accuracy.
*
* @param int $scale
*
* @return Decimal
*/
public function arcsin($scale = null)
{
if ($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
throw new \DomainException(
'The arcsin of this number is undefined.'
);
}
if ($this->round($scale)->isZero()) {
return DecimalConstants::zero();
}
if ($this->round($scale)->equals(DecimalConstants::one())) {
return DecimalConstants::pi()->div(self::fromInteger(2))->round($scale);
}
if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
return DecimalConstants::pi()->div(self::fromInteger(-2))->round($scale);
}
$scale = ($scale === null) ? 32 : $scale;
return self::powerSerie(
$this,
DecimalConstants::zero(),
$scale
);
}
/**
* Calculates the arccosine of this with the highest possible accuracy.
*
* @param int $scale
*
* @return Decimal
*/
public function arccos($scale = null)
{
if ($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
throw new \DomainException(
'The arccos of this number is undefined.'
);
}
$piOverTwo = DecimalConstants::pi()->div(self::fromInteger(2), $scale + 2)->round($scale);
if ($this->round($scale)->isZero()) {
return $piOverTwo;
}
if ($this->round($scale)->equals(DecimalConstants::one())) {
return DecimalConstants::zero();
}
if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
return DecimalConstants::pi()->round($scale);
}
$scale = ($scale === null) ? 32 : $scale;
return $piOverTwo->sub(
self::powerSerie(
$this,
DecimalConstants::zero(),
$scale
)
)->round($scale);
}
/**
* Calculates the arctangente of this with the highest possible accuracy.
*
* @param int $scale
*
* @return Decimal
*/
public function arctan($scale = null)
{
$piOverFour = DecimalConstants::pi()->div(self::fromInteger(4), $scale + 2)->round($scale);
if ($this->round($scale)->isZero()) {
return DecimalConstants::zero();
}
if ($this->round($scale)->equals(DecimalConstants::one())) {
return $piOverFour;
}
if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
return DecimalConstants::negativeOne()->mul($piOverFour);
}
$scale = ($scale === null) ? 32 : $scale;
return self::simplePowerSerie(
$this,
DecimalConstants::zero(),
$scale
)->round($scale);
}
/**
* Returns exp($this), said in other words: e^$this .
*
* @param int $scale
*
* @return Decimal
*/
public function exp($scale = null)
{
if ($this->isZero()) {
return DecimalConstants::one();
}
$scale = ($scale === null) ? max(
$this->scale,
(int) ($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : 16)
) : $scale;
return self::factorialSerie(
$this, DecimalConstants::one(), function ($i) {
return DecimalConstants::one();
}, $scale
);
}
/**
* Internal method used to compute sin, cos and exp.
*
* @return Decimal
*/
private static function factorialSerie(Decimal $x, Decimal $firstTerm, callable $generalTerm, $scale)
{
$approx = $firstTerm;
$change = InfiniteDecimal::getPositiveInfinite();
$faculty = DecimalConstants::One(); // Calculates the faculty under the sign
$xPowerN = DecimalConstants::One(); // Calculates x^n
for ($i = 1; !$change->floor($scale + 1)->isZero(); $i++) {
// update x^n and n! for this walkthrough
$xPowerN = $xPowerN->mul($x);
$faculty = $faculty->mul(self::fromInteger($i));
$multiplier = $generalTerm($i);
if (!$multiplier->isZero()) {
$change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
$approx = $approx->add($change, $scale + 2);
}
}
return $approx->round($scale);
}
/**
* Internal method used to compute arcsine and arcosine.
*
* @return Decimal
*/
private static function powerSerie(Decimal $x, Decimal $firstTerm, $scale)
{
$approx = $firstTerm;
$change = InfiniteDecimal::getPositiveInfinite();
$xPowerN = DecimalConstants::One(); // Calculates x^n
$factorN = DecimalConstants::One(); // Calculates a_n
$numerator = DecimalConstants::one();
$denominator = DecimalConstants::one();
for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
$xPowerN = $xPowerN->mul($x);
if ($i % 2 == 0) {
$factorN = DecimalConstants::zero();
} elseif ($i == 1) {
$factorN = DecimalConstants::one();
} else {
$incrementNum = self::fromInteger($i - 2);
$numerator = $numerator->mul($incrementNum, $scale + 2);
$incrementDen = self::fromInteger($i - 1);
$increment = self::fromInteger($i);
$denominator = $denominator
->div($incrementNum, $scale + 2)
->mul($incrementDen, $scale + 2)
->mul($increment, $scale + 2);
$factorN = $numerator->div($denominator, $scale + 2);
}
if (!$factorN->isZero()) {
$change = $factorN->mul($xPowerN, $scale + 2);
$approx = $approx->add($change, $scale + 2);
}
}
return $approx->round($scale);
}
/**
* Internal method used to compute arctan and arccotan.
*
* @return Decimal
*/
private static function simplePowerSerie(Decimal $x, Decimal $firstTerm, $scale)
{
$approx = $firstTerm;
$change = InfiniteDecimal::getPositiveInfinite();
$xPowerN = DecimalConstants::One(); // Calculates x^n
$sign = DecimalConstants::One(); // Calculates a_n
for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
$xPowerN = $xPowerN->mul($x);
if ($i % 2 === 0) {
$factorN = DecimalConstants::zero();
} else {
if ($i % 4 === 1) {
$factorN = DecimalConstants::one()->div(self::fromInteger($i), $scale + 2);
} else {
$factorN = DecimalConstants::negativeOne()->div(self::fromInteger($i), $scale + 2);
}
}
if (!$factorN->isZero()) {
$change = $factorN->mul($xPowerN, $scale + 2);
$approx = $approx->add($change, $scale + 2);
}
}
return $approx->round($scale);
}
/**
* Calculates the tangent of this method with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal tan($this)
*/
public function tan($scale = null)
{
$cos = $this->cos($scale + 2);
if ($cos->isZero()) {
throw new \DomainException(
"The tangent of this 'angle' is undefined."
);
}
return $this->sin($scale + 2)->div($cos)->round($scale);
}
/**
* Calculates the cotangent of this method with the highest possible accuracy
* Note that accuracy is limited by the accuracy of predefined PI;.
*
* @param int $scale
*
* @return Decimal cotan($this)
*/
public function cotan($scale = null)
{
$sin = $this->sin($scale + 2);
if ($sin->isZero()) {
throw new \DomainException(
"The cotangent of this 'angle' is undefined."
);
}
return $this->cos($scale + 2)->div($sin)->round($scale);
}
/**
* Indicates if the passed parameter has the same sign as the method's bound object.
*
* @return bool
*/
public function hasSameSign(Decimal $b)
{
return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
}
/**
* Return value as a float.
*
* @return float
*/
public function asFloat()
{
return floatval($this->value);
}
/**
* Return value as a integer.
*
* @return float
*/
public function asInteger()
{
return intval($this->value);
}
/**
* Return value as a string.
*
* @return string
*/
public function asString()
{
return $this->value;
}
/**
* Return the inner representation of the class
* use with caution.
*
* @return number
*/
public function innerValue()
{
return $this->value;
}
/**
* @return string
*/
public function __toString()
{
return self::innerRound($this->value, self::$default_scale);
}
/**
* @param int $scale
* @param string $decimal_mark
*
* @return string
*/
public function printValue($scale = 2, $decimal_mark = ',')
{
return number_format(self::innerRound($this->value, $scale), $scale, $decimal_mark, ' ');
}
/** Formats decimal to string
* Scale specifies how many decimal places to use - 1.12345 with scale 2 -> 1.12
* Negative scale means rounding to "nice" format - 1.1200 with scale -4 -> 1.12, trailing zeros are stripped.
*
* @param null $scale
*
* @return string
*/
public function printFloatValue($scale = null)
{
if (is_null($scale)) {
$scale = $this->scale;
}
$strValue = number_format(self::innerRound($this->value, abs($scale)), abs($scale), '.', '');
if ($scale < 0) {
$strValue = rtrim(rtrim($strValue, '0'), ',.');
}
return $strValue;
}
/**
* "Rounds" the decimal string to have at most $scale digits after the point.
*
* @param string $value
* @param int $scale
*
* @return string
*/
private static function innerRound($value, $scale = 0)
{
$rounded = bcadd($value, '0', $scale);
$diffDigit = bcsub($value, $rounded, $scale + 1);
$diffDigit = (int) $diffDigit[strlen($diffDigit) - 1];
if ($diffDigit >= 5) {
if ($value[0] !== '-') {
$rounded = bcadd($rounded, bcpow('10', -$scale, $scale), $scale);
} else {
$rounded = bcsub($rounded, bcpow('10', -$scale, $scale), $scale);
}
}
return $rounded;
}
/**
* Calculates the logarithm (in base 10) of $value.
*
* @param string $value The number we want to calculate its logarithm (only positive numbers)
* @param int $in_scale Expected scale used by $value (only positive numbers)
* @param int $out_scale Scale used by the return value (only positive numbers)
*
* @return string
*/
private static function innerLog10($value, $in_scale, $out_scale)
{
$value_len = strlen($value);
$cmp = bccomp($value, '1', $in_scale);
switch ($cmp) {
case 1:
$value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale + 2) : 1);
return bcadd(
$value_log10_approx,
log10(bcdiv(
$value,
bcpow('10', $value_log10_approx),
min($value_len, $out_scale)
)),
$out_scale
);
case -1:
preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
$value_log10_approx = -strlen($captures[1]) - 1;
return bcadd(
$value_log10_approx,
log10(bcmul(
$value,
bcpow('10', -$value_log10_approx),
$in_scale + $value_log10_approx
)),
$out_scale
);
default: // case 0:
return '0';
}
}
/**
* Returns $base^$exponent.
*
* @param string $base
* @param string $exponent 0 < $exponent < 1
* @param int $exp_scale Number of $exponent's significative digits
* @param int $out_scale Number of significative digits that we want to compute
*
* @return string
*/
private static function innerPowWithLittleExponent($base, $exponent, $exp_scale, $out_scale)
{
$inner_scale = ceil($exp_scale * log(10) / log(2)) + 1;
$result_a = '1';
$result_b = '0';
$actual_index = 0;
$exponent_remaining = $exponent;
while (bccomp($result_a, $result_b, $out_scale) !== 0 && bccomp($exponent_remaining, '0', $inner_scale) !== 0) {
$result_b = $result_a;
$index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
$exponent_remaining = $index_info[1];
$result_a = bcmul(
$result_a,
self::compute2NRoot($base, $index_info[0], 2 * ($out_scale + 1)),
2 * ($out_scale + 1)
);
}
return self::innerRound($result_a, $out_scale);
}
/**
* Auxiliar method. It helps us to decompose the exponent into many summands.
*
* @param string $exponent_remaining
* @param int $actual_index
* @param int $exp_scale Number of $exponent's significative digits
* @param int $inner_scale ceil($exp_scale*log(10)/log(2))+1;
*
* @return string
*/
private static function computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale)
{
$actual_rt = bcpow('0.5', $actual_index, $exp_scale);
$r = bcsub($exponent_remaining, $actual_rt, $inner_scale);
while (bccomp($r, 0, $exp_scale) === -1) {
++$actual_index;
$actual_rt = bcmul('0.5', $actual_rt, $inner_scale);
$r = bcsub($exponent_remaining, $actual_rt, $inner_scale);
}
return [$actual_index, $r];
}
/**
* Auxiliar method. Computes $base^((1/2)^$index).
*
* @param string $base
* @param int $index
* @param int $out_scale
*
* @return string
*/
private static function compute2NRoot($base, $index, $out_scale)
{
$result = $base;
for ($i = 0; $i < $index; $i++) {
$result = bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1);
}
return self::innerRound($result, $out_scale);
}
/**
* Validates basic constructor's arguments.
*
* @param int $scale
*/
protected static function paramsValidation($value, $scale)
{
if ($value === null) {
throw new \InvalidArgumentException('$value must be a non null number');
}
if ($scale !== null && (!is_int($scale) || $scale < 0)) {
throw new \InvalidArgumentException('$scale must be a positive integer');
}
}
/**
* @return string
*/
private static function normalizeSign($sign)
{
if ($sign === '+') {
return '';
}
return $sign;
}
private static function removeTrailingZeros($strValue, &$scale)
{
preg_match('/^[+\-]?[0-9]+(\.([0-9]*[1-9])?(0+)?)?$/', $strValue, $captures);
if (count($captures) === 4) {
$toRemove = strlen($captures[3]);
$scale = strlen($captures[2]);
$strValue = substr($strValue, 0, strlen($strValue) - $toRemove - ($scale === 0 ? 1 : 0));
}
return $strValue;
}
/**
* Counts the number of significative digits of $val.
* Assumes a consistent internal state (without zeros at the end or the start).
*
* @param Decimal $abs $val->abs()
*
* @return int
*/
private static function countSignificativeDigits(Decimal $val, Decimal $abs)
{
return strlen($val->value) - (
($abs->comp(DecimalConstants::One()) === -1) ? 2 : max($val->scale, 1)
) - ($val->isNegative() ? 1 : 0);
}
/**
* (PHP 5 &gt;= 5.4.0)<br/>
* Specify data which should be serialized to JSON.
*
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
*
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource
*/
public function jsonSerialize(): mixed
{
return $this->value;
}
/* KupsShop utility functions */
public function addVat($vat)
{
$vat = static::ensureDecimal($vat);
$vat_decimal = $vat->add(DecimalConstants::hundred())->div(DecimalConstants::hundred());
return $this->mul($vat_decimal);
}
public function removeVat($vat)
{
$vat = static::ensureDecimal($vat);
$vat_decimal = $vat->add(DecimalConstants::hundred())->div(DecimalConstants::hundred());
return $this->div($vat_decimal);
}
public function addDiscount($discount)
{
$discount = static::ensureDecimal($discount);
$discount_decimal = $this->mul($discount)->div(DecimalConstants::hundred());
return $this->sub($discount_decimal);
}
public function removeDiscount($discount)
{
$discount = static::ensureDecimal($discount);
if ($discount->equals(DecimalConstants::hundred())) {
return DecimalConstants::zero();
}
$discount_decimal = DecimalConstants::hundred()->sub($discount);
return $this->mul(DecimalConstants::hundred())->div($discount_decimal);
}
/**
* Calculates absolute difference between this and $b.
*/
public function diff(Decimal $b): Decimal
{
return $this->sub($b)->abs();
}
/**
* Is $this roughly equal to $b.
*/
public function equalsWithDelta(Decimal $b, Decimal $delta): bool
{
return $this->diff($b)->lowerThanOrEqual($delta);
}
/**
* @param int|null $scale
*
* @return Decimal
*/
public static function ensureDecimal($value, $scale = null)
{
if (!is_a($value, 'Decimal')) {
$value = self::create($value, $scale);
}
return $value;
}
}
class InfiniteDecimal extends Decimal
{
/**
* Single instance of "Positive Infinite".
*
* @var Decimal
*/
private static $pInf;
/**
* Single instance of "Negative Infinite".
*
* @var Decimal
*/
private static $nInf;
/**
* Private constructor.
*
* @param string $value
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Private clone method.
*/
private function __clone()
{
}
/**
* Returns a "Positive Infinite" object.
*
* @return Decimal
*/
public static function getPositiveInfinite()
{
if (self::$pInf === null) {
self::$pInf = new self('INF');
}
return self::$pInf;
}
/**
* Returns a "Negative Infinite" object.
*
* @return Decimal
*/
public static function getNegativeInfinite()
{
if (self::$nInf === null) {
self::$nInf = new self('-INF');
}
return self::$nInf;
}
/**
* Adds two Decimal objects.
*
* @param int $scale
*
* @return Decimal
*/
public function add(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if (!$b->isInfinite()) {
return $this;
} elseif ($this->hasSameSign($b)) {
return $this;
} else { // elseif ($this->isPositive() && $b->isNegative || $this->isNegative() && $b->isPositive()) {
throw new \DomainException("Infinite numbers with opposite signs can't be added.");
}
}
/**
* Subtracts two BigNumber objects.
*
* @param int $scale
*
* @return Decimal
*/
public function sub(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if (!$b->isInfinite()) {
return $this;
} elseif (!$this->hasSameSign($b)) {
return $this;
} else { // elseif () {
throw new \DomainException("Infinite numbers with the same sign can't be subtracted.");
}
}
/**
* Multiplies two BigNumber objects.
*
* @param int $scale
*
* @return Decimal
*/
public function mul(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isZero()) {
throw new \DomainException('Zero multiplied by infinite is not allowed.');
}
if ($this->hasSameSign($b)) {
return self::getPositiveInfinite();
} else { // elseif (!$this->hasSameSign($b)) {
return self::getNegativeInfinite();
}
}
/**
* Divides the object by $b .
* Warning: div with $scale == 0 is not the same as
* integer division because it rounds the
* last digit in order to minimize the error.
*
* @param int $scale
*
* @return Decimal
*/
public function div(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($b->isZero()) {
throw new \DomainException('Division by zero is not allowed.');
} elseif ($b->isInfinite()) {
throw new \DomainException('Infinite divided by Infinite is not allowed.');
} elseif ($b->isPositive()) {
return $this;
} else { // if ($b->isNegative()) {
return $this->additiveInverse();
}
}
/**
* Returns the square root of this object.
*
* @param int $scale
*
* @return Decimal
*/
public function sqrt($scale = null)
{
if ($this->isNegative()) {
throw new \DomainException(
"Decimal can't handle square roots of negative numbers (it's only for real numbers)."
);
}
return $this;
}
/**
* Powers this value to $b.
*
* @param Decimal $b exponent
* @param int $scale
*
* @return Decimal
*/
public function pow(Decimal $b, $scale = null)
{
if ($b->isPositive()) {
if ($this->isPositive()) {
return $this;
}
// if ($this->isNegative())
if ($b->isInfinite()) {
throw new \DomainException('Negative infinite elevated to infinite is undefined.');
}
if ($b->isInteger()) {
if (preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1) {
// $b is an even number
return self::$pInf;
} else {
// $b is an odd number
return $this; // Negative Infinite
}
}
throw new Exception('See issues #21, #22, #23 and #24 on Github.');
} elseif ($b->isNegative()) {
return DecimalConstants::Zero();
} elseif ($b->isZero()) {
throw new \DomainException('Infinite elevated to zero is undefined.');
}
}
/**
* Returns the object's logarithm in base 10.
*
* @param int $scale
*
* @return Decimal
*/
public function log10($scale = null)
{
if ($this->isNegative()) {
throw new \DomainException(
"Decimal can't handle logarithms of negative numbers (it's only for real numbers)."
);
}
return $this;
}
/**
* Equality comparison between this object and $b.
*
* @param int $scale
*
* @return bool
*/
public function equals(Decimal $b, $scale = null)
{
return $this === $b;
}
/**
* $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0.
*
* @param int $scale
*
* @return int
*/
public function comp(Decimal $b, $scale = null)
{
self::paramsValidation($b, $scale);
if ($this === $b) {
return 0;
} elseif ($this === self::getPositiveInfinite()) {
return 1;
} else { // if ($this === self::getNegativeInfinite()) {
return -1;
}
}
/**
* Returns the element's additive inverse.
*
* @return Decimal
*/
public function additiveInverse()
{
if ($this === self::getPositiveInfinite()) {
return self::$nInf;
} else { // if ($this === self::getNegativeInfinite()) {
return self::$pInf;
}
}
/**
* "Rounds" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function round($scale = 0)
{
return $this;
}
/**
* "Ceils" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function ceil($scale = 0)
{
return $this;
}
/**
* "Floors" the Decimal to have at most $scale digits after the point.
*
* @param int $scale
*
* @return Decimal
*/
public function floor($scale = 0)
{
return $this;
}
/**
* Throws exception because sine is undefined in the infinite.
*
* @param int $scale
*/
public function sin($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Sine function hasn't limit in the positive infinite." :
"Sine function hasn't limit in the negative infinite."
);
}
/**
* Throws exception because cosecant is undefined in the infinite.
*
* @param int $scale
*/
public function cosec($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Cosecant function hasn't limit in the positive infinite." :
"Cosecant function hasn't limit in the negative infinite."
);
}
/**
* Throws exception because cosine is undefined in the infinite.
*
* @param int $scale
*/
public function cos($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Cosine function hasn't limit in the positive infinite." :
"Cosine function hasn't limit in the negative infinite."
);
}
/**
* Throws exception because secant is undefined in the infinite.
*
* @param int $scale
*/
public function sec($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Secant function hasn't limit in the positive infinite." :
"Secant function hasn't limit in the negative infinite."
);
}
/**
* Returns exp($this), said in other words: e^$this .
*
* @param int $scale
*
* @return Decimal
*/
public function exp($scale = null)
{
if ($this == self::$pInf) {
return $this;
} else {
return DecimalConstants::zero();
}
}
public function tan($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Tangent function hasn't limit in the positive infinite." :
"Tangent function hasn't limit in the negative infinite."
);
}
public function cotan($scale = null)
{
throw new \DomainException(($this === self::$pInf) ?
"Cotangent function hasn't limit in the positive infinite." :
"Cotangent function hasn't limit in the negative infinite."
);
}
/**
* @param int $scale has no effect, exists only for compatibility
*
* @return bool
*/
public function isZero($scale = null)
{
return false;
}
/**
* @return bool
*/
public function isPositive()
{
return $this === self::$pInf;
}
/**
* @return bool
*/
public function isNegative()
{
return $this === self::$nInf;
}
/**
* @return bool
*/
public function isInteger()
{
return false;
}
/**
* @return bool
*/
public function isInfinite()
{
return true;
}
/**
* Return value as a float.
*
* @return float
*/
public function asFloat()
{
return ($this === self::$pInf) ? INF : -INF;
}
/**
* Return value as a integer.
*
* @return float
*/
public function asInteger()
{
throw new Exception('InfiniteDecimal', 'int', "PHP integers can't represent infinite values.");
}
}
/**
* @author Andreu Correa Casablanca <castarco@litipk.com>
*/
final class InvalidArgumentTypeException extends \InvalidArgumentException
{
/**
* List of expected types.
*
* @var array<string>
*/
private $expected_types;
/**
* Given type.
*
* @var string
*/
private $given_type;
/**
* @param array $expected_types The list of expected types for the problematic argument
* @param string $given_type The typpe of the problematic argument
* @param string $message See Exception definition
* @param int $code See Exception definition
* @param Exception $previous See Exception definition
*/
public function __construct(array $expected_types, $given_type, $message = '', $code = 0, ?Exception $previous = null)
{
parent::__construct($message, $code, $previous);
if ($expected_types === null || empty($expected_types) || $given_type === null || !is_string($given_type)) {
throw new \LogicException('InvalidArgumentTypeException requires valid $expected_types and $given_type parameters.');
}
if (in_array($given_type, $expected_types)) {
throw new \LogicException("It's a nonsense to raise an InvalidArgumentTypeException when \$given_type is in \$expected_types.");
}
$this->expected_types = $expected_types;
$this->given_type = $given_type;
}
/**
* Returns the list of expected types.
*
* @return array<string>
*/
public function getExpectedTypes()
{
return $this->expected_types;
}
/**
* Returns the given type.
*
* @return string
*/
public function getGivenType()
{
return $this->given_type;
}
}