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 \d*) (?: \. (?P \d+) ) E (?P[\+\-]) (?P\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 >= 5.4.0)
* 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 json_encode, * 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 */ final class InvalidArgumentTypeException extends \InvalidArgumentException { /** * List of expected types. * * @var array */ 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 */ public function getExpectedTypes() { return $this->expected_types; } /** * Returns the given type. * * @return string */ public function getGivenType() { return $this->given_type; } }