2048 lines
54 KiB
PHP
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 >= 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;
|
|
}
|
|
}
|