* @license https://opensource.org/licenses/MIT MIT License */ namespace PrestaShop\Decimal; use InvalidArgumentException; use PrestaShop\Decimal\Operation\Rounding; /** * Decimal number. * * Allows for arbitrary precision math operations. */ class DecimalNumber { /** * Indicates if the number is negative * * @var bool */ private $isNegative = false; /** * Integer representation of this number * * @var string */ private $coefficient = ''; /** * Scientific notation exponent. For practical reasons, it's always stored as a positive value. * * @var int */ private $exponent = 0; /** * Number constructor. * * This constructor can be used in two ways: * * 1) With a number string: * * ```php * (string) new Number('0.123456'); // -> '0.123456' * ``` * * 2) With an integer string as coefficient and an exponent * * ```php * // 123456 * 10^(-6) * (string) new Number('123456', 6); // -> '0.123456' * ``` * * Note: decimal positions must always be a positive number. * * @param string $number Number or coefficient * @param int|null $exponent [default=null] If provided, the number can be considered as the negative * exponent of the scientific notation, or the number of fractional digits */ public function __construct($number, $exponent = null) { if (!is_string($number)) { throw new InvalidArgumentException(sprintf('Invalid type - expected string, but got (%s) "%s"', gettype($number), print_r($number, true))); } if (null === $exponent) { $decimalNumber = Builder::parseNumber($number); $number = $decimalNumber->getSign() . $decimalNumber->getCoefficient(); $exponent = $decimalNumber->getExponent(); } $this->initFromScientificNotation($number, $exponent); if ('0' === $this->coefficient) { // make sure the sign is always positive for zero $this->isNegative = false; } } /** * Returns the integer part of the number. * Note that this does NOT include the sign. * * @return string */ public function getIntegerPart() { if ('0' === $this->coefficient) { return $this->coefficient; } if (0 === $this->exponent) { return $this->coefficient; } if ($this->exponent >= strlen($this->coefficient)) { return '0'; } return substr($this->coefficient, 0, -$this->exponent); } /** * Returns the fractional part of the number. * Note that this does NOT include the sign. * * @return string */ public function getFractionalPart() { if (0 === $this->exponent || '0' === $this->coefficient) { return '0'; } if ($this->exponent > strlen($this->coefficient)) { return str_pad($this->coefficient, $this->exponent, '0', STR_PAD_LEFT); } return substr($this->coefficient, -$this->exponent); } /** * Returns the number of digits in the fractional part. * * @see self::getExponent() This method is an alias of getExponent(). * * @return int */ public function getPrecision() { return $this->getExponent(); } /** * Returns the number's sign. * Note that this method will return an empty string if the number is positive! * * @return string '-' if negative, empty string if positive */ public function getSign() { return $this->isNegative ? '-' : ''; } /** * Returns the exponent of this number. For practical reasons, this exponent is always >= 0. * * This value can also be interpreted as the number of significant digits on the fractional part. * * @return int */ public function getExponent() { return $this->exponent; } /** * Returns the raw number as stored internally. This coefficient is always an integer. * * It can be transformed to float by computing: * ``` * getCoefficient() * 10^(-getExponent()) * ``` * * @return string */ public function getCoefficient() { return $this->coefficient; } /** * Returns a string representation of this object * * @return string */ public function __toString() { $output = $this->getSign() . $this->getIntegerPart(); $fractionalPart = $this->getFractionalPart(); if ('0' !== $fractionalPart) { $output .= '.' . $fractionalPart; } return $output; } /** * Returns the number as a string, with exactly $precision decimals * * Example: * ``` * $n = new Number('123.4560'); * (string) $n->round(1); // '123.4' * (string) $n->round(2); // '123.45' * (string) $n->round(3); // '123.456' * (string) $n->round(4); // '123.4560' (trailing zeroes are added) * (string) $n->round(5); // '123.45600' (trailing zeroes are added) * ``` * * @param int $precision Exact number of desired decimals * @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm * * @return string */ public function toPrecision($precision, $roundingMode = Rounding::ROUND_TRUNCATE) { $currentPrecision = $this->getPrecision(); if ($precision === $currentPrecision) { return (string) $this; } $return = $this; if ($precision < $currentPrecision) { $return = (new Operation\Rounding())->compute($this, $precision, $roundingMode); } if ($precision > $return->getPrecision()) { return $return->getSign() . $return->getIntegerPart() . '.' . str_pad($return->getFractionalPart(), $precision, '0') ; } return (string) $return; } /** * Returns the number as a string, with up to $maxDecimals significant digits. * * Example: * ``` * $n = new Number('123.4560'); * (string) $n->round(1); // '123.4' * (string) $n->round(2); // '123.45' * (string) $n->round(3); // '123.456' * (string) $n->round(4); // '123.456' (does not add trailing zeroes) * (string) $n->round(5); // '123.456' (does not add trailing zeroes) * ``` * * @param int $maxDecimals Maximum number of decimals * @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm * * @return string */ public function round($maxDecimals, $roundingMode = Rounding::ROUND_TRUNCATE) { $currentPrecision = $this->getPrecision(); if ($maxDecimals < $currentPrecision) { return (string) (new Operation\Rounding())->compute($this, $maxDecimals, $roundingMode); } return (string) $this; } /** * Returns this number as a positive number * * @return self */ public function toPositive() { if (!$this->isNegative) { return $this; } return $this->invert(); } /** * Returns this number as a negative number * * @return self */ public function toNegative() { if ($this->isNegative) { return $this; } return $this->invert(); } /** * Returns the computed result of adding another number to this one * * @param self $addend Number to add * * @return self */ public function plus(self $addend) { return (new Operation\Addition())->compute($this, $addend); } /** * Returns the computed result of subtracting another number to this one * * @param self $subtrahend Number to subtract * * @return self */ public function minus(self $subtrahend) { return (new Operation\Subtraction())->compute($this, $subtrahend); } /** * Returns the computed result of multiplying this number with another one * * @param self $factor * * @return self */ public function times(self $factor) { return (new Operation\Multiplication())->compute($this, $factor); } /** * Returns the computed result of dividing this number by another one, with up to $precision number of decimals. * * A target maximum precision is required in order to handle potential infinite number of decimals * (e.g. 1/3 = 0.3333333...). * * If the division yields more decimal positions than the requested precision, * the remaining decimals are truncated, with **no rounding**. * * @param self $divisor * @param int $precision [optional] By default, up to Operation\Division::DEFAULT_PRECISION number of decimals * * @return self * * @throws Exception\DivisionByZeroException */ public function dividedBy(self $divisor, $precision = Operation\Division::DEFAULT_PRECISION) { return (new Operation\Division())->compute($this, $divisor, $precision); } /** * Indicates if this number equals zero * * @return bool */ public function equalsZero() { return '0' == $this->getCoefficient(); } /** * Indicates if this number is greater than the provided one * * @param self $number * * @return bool */ public function isGreaterThan(self $number) { return 1 === (new Operation\Comparison())->compare($this, $number); } /** * Indicates if this number is greater than zero * * @return bool */ public function isGreaterThanZero() { return $this->isPositive() && !$this->equalsZero(); } /** * Indicates if this number is greater or equal than zero * * @return bool */ public function isGreaterOrEqualThanZero() { return $this->isPositive(); } /** * Indicates if this number is greater or equal compared to the provided one * * @param self $number * * @return bool */ public function isGreaterOrEqualThan(self $number) { return 0 <= (new Operation\Comparison())->compare($this, $number); } /** * Indicates if this number is lower than zero * * @return bool */ public function isLowerThanZero() { return $this->isNegative() && !$this->equalsZero(); } /** * Indicates if this number is lower or equal than zero * * @return bool */ public function isLowerOrEqualThanZero() { return $this->isNegative() || $this->equalsZero(); } /** * Indicates if this number is greater than the provided one * * @param self $number * * @return bool */ public function isLowerThan(self $number) { return -1 === (new Operation\Comparison())->compare($this, $number); } /** * Indicates if this number is lower or equal compared to the provided one * * @param self $number * * @return bool */ public function isLowerOrEqualThan(self $number) { return 0 >= (new Operation\Comparison())->compare($this, $number); } /** * Indicates if this number is positive * * @return bool */ public function isPositive() { return !$this->isNegative; } /** * Indicates if this number is negative * * @return bool */ public function isNegative() { return $this->isNegative; } /** * Indicates if this number equals another one * * @param self $number * * @return bool */ public function equals(self $number) { return $this->isNegative === $number->isNegative && $this->coefficient === $number->getCoefficient() && $this->exponent === $number->getExponent() ; } /** * Returns the additive inverse of this number (that is, N * -1). * * @return static */ public function invert() { // invert sign $sign = $this->isNegative ? '' : '-'; return new static($sign . $this->getCoefficient(), $this->getExponent()); } /** * Creates a new copy of this number multiplied by 10^$exponent * * @param int $exponent * * @return static */ public function toMagnitude($exponent) { return (new Operation\MagnitudeChange())->compute($this, $exponent); } /** * Initializes the number using a coefficient and exponent * * @param string $coefficient * @param int $exponent */ private function initFromScientificNotation($coefficient, $exponent) { if ($exponent < 0) { throw new InvalidArgumentException(sprintf('Invalid value for exponent. Expected a positive integer or 0, but got "%s"', $coefficient)); } if (!preg_match("/^(?[-+])?(?\d+)$/", $coefficient, $parts)) { throw new InvalidArgumentException(sprintf('"%s" cannot be interpreted as a number', $coefficient)); } $this->isNegative = ('-' === $parts['sign']); $this->exponent = (int) $exponent; // trim leading zeroes $this->coefficient = ltrim($parts['integerPart'], '0'); // when coefficient is '0' or a sequence of '0' if ('' === $this->coefficient) { $this->exponent = 0; $this->coefficient = '0'; return; } $this->removeTrailingZeroesIfNeeded(); } /** * Removes trailing zeroes from the fractional part and adjusts the exponent accordingly */ private function removeTrailingZeroesIfNeeded() { $exponent = $this->getExponent(); $coefficient = $this->getCoefficient(); // trim trailing zeroes from the fractional part // for example 1000e-1 => 100.0 if (0 < $exponent && '0' === substr($coefficient, -1)) { $fractionalPart = $this->getFractionalPart(); $trailingZeroesToRemove = 0; for ($i = $exponent - 1; $i >= 0; --$i) { if ('0' !== $fractionalPart[$i]) { break; } ++$trailingZeroesToRemove; } if ($trailingZeroesToRemove > 0) { $this->coefficient = substr($coefficient, 0, -$trailingZeroesToRemove); $this->exponent = $exponent - $trailingZeroesToRemove; } } } }