This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | |||
4 | namespace Litipk\BigNumbers; |
||
5 | |||
6 | use Litipk\BigNumbers\DecimalConstants as DecimalConstants; |
||
7 | |||
8 | use Litipk\BigNumbers\Errors\InfiniteInputError; |
||
9 | use Litipk\BigNumbers\Errors\NaNInputError; |
||
10 | use Litipk\BigNumbers\Errors\NotImplementedError; |
||
11 | |||
12 | /** |
||
13 | * Immutable object that represents a rational number |
||
14 | * |
||
15 | * @author Andreu Correa Casablanca <[email protected]> |
||
16 | */ |
||
17 | class Decimal |
||
18 | { |
||
19 | const DEFAULT_SCALE = 16; |
||
20 | const CLASSIC_DECIMAL_NUMBER_REGEXP = '/^([+\-]?)0*(([1-9][0-9]*|[0-9])(\.[0-9]+)?)$/'; |
||
21 | const EXP_NOTATION_NUMBER_REGEXP = '/^ (?P<sign> [+\-]?) 0*(?P<mantissa> [0-9](?P<decimals> \.[0-9]+)?) [eE] (?P<expSign> [+\-]?)(?P<exp> \d+)$/x'; |
||
22 | const EXP_NUM_GROUPS_NUMBER_REGEXP = '/^ (?P<int> \d*) (?: \. (?P<dec> \d+) ) E (?P<sign>[\+\-]) (?P<exp>\d+) $/x'; |
||
23 | |||
24 | /** |
||
25 | * Internal numeric value |
||
26 | * @var string |
||
27 | */ |
||
28 | protected $value; |
||
29 | |||
30 | /** |
||
31 | * Number of digits behind the point |
||
32 | * @var integer |
||
33 | */ |
||
34 | private $scale; |
||
35 | |||
36 | 171 | private function __construct(string $value, int $scale) |
|
37 | { |
||
38 | 171 | $this->value = $value; |
|
39 | 171 | $this->scale = $scale; |
|
40 | 171 | } |
|
41 | |||
42 | private function __clone() |
||
43 | { |
||
44 | } |
||
45 | |||
46 | /** |
||
47 | * Decimal "constructor". |
||
48 | * |
||
49 | * @param mixed $value |
||
50 | * @param int $scale |
||
51 | * @return Decimal |
||
52 | */ |
||
53 | 9 | public static function create($value, int $scale = null): Decimal |
|
54 | { |
||
55 | 9 | if (\is_int($value)) { |
|
56 | 4 | return self::fromInteger($value); |
|
57 | 8 | } elseif (\is_float($value)) { |
|
58 | 4 | return self::fromFloat($value, $scale); |
|
59 | 4 | } elseif (\is_string($value)) { |
|
60 | 2 | return self::fromString($value, $scale); |
|
61 | 2 | } elseif ($value instanceof Decimal) { |
|
62 | 1 | return self::fromDecimal($value, $scale); |
|
63 | } else { |
||
64 | 1 | throw new \TypeError( |
|
65 | 'Expected (int, float, string, Decimal), but received ' . |
||
66 | 1 | (\is_object($value) ? \get_class($value) : \gettype($value)) |
|
67 | ); |
||
68 | } |
||
69 | } |
||
70 | |||
71 | 98 | public static function fromInteger(int $intValue): Decimal |
|
72 | { |
||
73 | 98 | self::paramsValidation($intValue, null); |
|
74 | |||
75 | 98 | return new static((string)$intValue, 0); |
|
76 | } |
||
77 | |||
78 | /** |
||
79 | * @param float $fltValue |
||
80 | * @param int $scale |
||
81 | * @return Decimal |
||
82 | */ |
||
83 | 49 | public static function fromFloat(float $fltValue, int $scale = null): Decimal |
|
84 | { |
||
85 | 49 | self::paramsValidation($fltValue, $scale); |
|
86 | |||
87 | 49 | if (\is_infinite($fltValue)) { |
|
88 | throw new InfiniteInputError('fltValue must be a finite number'); |
||
89 | 49 | } elseif (\is_nan($fltValue)) { |
|
90 | 1 | throw new NaNInputError("fltValue can't be NaN"); |
|
91 | } |
||
92 | |||
93 | 48 | $strValue = (string) $fltValue; |
|
94 | 48 | $hasPoint = (false !== \strpos($strValue, '.')); |
|
95 | |||
96 | 48 | if (\preg_match(self::EXP_NUM_GROUPS_NUMBER_REGEXP, $strValue, $capture)) { |
|
97 | 6 | if (null === $scale) { |
|
98 | 3 | $scale = ('-' === $capture['sign']) |
|
99 | 2 | ? $capture['exp'] + \strlen($capture['dec']) |
|
100 | 3 | : self::DEFAULT_SCALE; |
|
101 | } |
||
102 | 6 | $strValue = \number_format($fltValue, $scale, '.', ''); |
|
103 | } else { |
||
104 | $naturalScale = ( |
||
105 | 42 | \strlen((string)\fmod($fltValue, 1.0)) - 2 - (($fltValue < 0) ? 1 : 0) + (!$hasPoint ? 1 : 0) |
|
106 | ); |
||
107 | |||
108 | 42 | if (null === $scale) { |
|
109 | 40 | $scale = $naturalScale; |
|
110 | } else { |
||
111 | 2 | $strValue .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $naturalScale, '0'); |
|
112 | } |
||
113 | } |
||
114 | |||
115 | 48 | return new static($strValue, $scale); |
|
116 | } |
||
117 | |||
118 | /** |
||
119 | * @param string $strValue |
||
120 | * @param integer $scale |
||
121 | * @return Decimal |
||
122 | */ |
||
123 | 133 | public static function fromString(string $strValue, int $scale = null): Decimal |
|
124 | { |
||
125 | 133 | self::paramsValidation($strValue, $scale); |
|
126 | |||
127 | 132 | if (\preg_match(self::CLASSIC_DECIMAL_NUMBER_REGEXP, $strValue, $captures) === 1) { |
|
128 | |||
129 | // Now it's time to strip leading zeros in order to normalize inner values |
||
130 | 127 | $value = self::normalizeSign($captures[1]) . $captures[2]; |
|
131 | 127 | $min_scale = isset($captures[4]) ? \max(0, \strlen($captures[4]) - 1) : 0; |
|
132 | |||
133 | 8 | } elseif (\preg_match(self::EXP_NOTATION_NUMBER_REGEXP, $strValue, $captures) === 1) { |
|
134 | 7 | list($min_scale, $value) = self::fromExpNotationString( |
|
135 | 7 | $scale, |
|
136 | 7 | $captures['sign'], |
|
137 | 7 | $captures['mantissa'], |
|
138 | 7 | \strlen($captures['mantissa']) - 1, |
|
139 | 7 | $captures['expSign'], |
|
140 | 7 | (int)$captures['exp'] |
|
141 | ); |
||
142 | } else { |
||
143 | 1 | throw new NaNInputError('strValue must be a number'); |
|
144 | } |
||
145 | |||
146 | 131 | $scale = $scale ?? $min_scale; |
|
147 | 131 | if ($scale < $min_scale) { |
|
148 | 70 | $value = self::innerRound($value, $scale); |
|
149 | 128 | } elseif ($min_scale < $scale) { |
|
150 | 17 | $hasPoint = (false !== \strpos($value, '.')); |
|
151 | 17 | $value .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $min_scale, '0'); |
|
152 | } |
||
153 | |||
154 | 131 | return new static($value, $scale); |
|
155 | } |
||
156 | |||
157 | /** |
||
158 | * Constructs a new Decimal object based on a previous one, |
||
159 | * but changing it's $scale property. |
||
160 | * |
||
161 | * @param Decimal $decValue |
||
162 | * @param null|int $scale |
||
163 | * @return Decimal |
||
164 | */ |
||
165 | 3 | public static function fromDecimal(Decimal $decValue, int $scale = null): Decimal |
|
166 | { |
||
167 | 3 | self::paramsValidation($decValue, $scale); |
|
168 | |||
169 | // This block protect us from unnecessary additional instances |
||
170 | 3 | if ($scale === null || $scale >= $decValue->scale) { |
|
171 | 3 | return $decValue; |
|
172 | } |
||
173 | |||
174 | 2 | return new static( |
|
175 | 2 | self::innerRound($decValue->value, $scale), |
|
176 | 2 | $scale |
|
177 | ); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Adds two Decimal objects |
||
182 | * @param Decimal $b |
||
183 | * @param null|int $scale |
||
184 | * @return Decimal |
||
185 | */ |
||
186 | 40 | public function add(Decimal $b, int $scale = null): Decimal |
|
187 | { |
||
188 | 40 | self::paramsValidation($b, $scale); |
|
189 | |||
190 | 40 | return self::fromString( |
|
191 | 40 | \bcadd($this->value, $b->value, \max($this->scale, $b->scale)), |
|
192 | 40 | $scale |
|
193 | ); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Subtracts two BigNumber objects |
||
198 | * @param Decimal $b |
||
199 | * @param integer $scale |
||
200 | * @return Decimal |
||
201 | */ |
||
202 | 33 | public function sub(Decimal $b, int $scale = null): Decimal |
|
203 | { |
||
204 | 33 | self::paramsValidation($b, $scale); |
|
205 | |||
206 | 33 | return self::fromString( |
|
207 | 33 | \bcsub($this->value, $b->value, \max($this->scale, $b->scale)), |
|
208 | 33 | $scale |
|
209 | ); |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * Multiplies two BigNumber objects |
||
214 | * @param Decimal $b |
||
215 | * @param integer $scale |
||
216 | * @return Decimal |
||
217 | */ |
||
218 | 48 | public function mul(Decimal $b, int $scale = null): Decimal |
|
219 | { |
||
220 | 48 | self::paramsValidation($b, $scale); |
|
221 | |||
222 | 47 | if ($b->isZero()) { |
|
223 | 1 | return DecimalConstants::Zero(); |
|
224 | } |
||
225 | |||
226 | 47 | return self::fromString( |
|
227 | 47 | \bcmul($this->value, $b->value, $this->scale + $b->scale), |
|
228 | 47 | $scale |
|
229 | ); |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Divides the object by $b . |
||
234 | * Warning: div with $scale == 0 is not the same as |
||
235 | * integer division because it rounds the |
||
236 | * last digit in order to minimize the error. |
||
237 | * |
||
238 | * @param Decimal $b |
||
239 | * @param integer $scale |
||
240 | * @return Decimal |
||
241 | */ |
||
242 | 65 | public function div(Decimal $b, int $scale = null): Decimal |
|
243 | { |
||
244 | 65 | self::paramsValidation($b, $scale); |
|
245 | |||
246 | 65 | if ($b->isZero()) { |
|
247 | 1 | throw new \DomainException("Division by zero is not allowed."); |
|
248 | 65 | } elseif ($this->isZero()) { |
|
249 | 1 | return DecimalConstants::Zero(); |
|
250 | } else { |
||
251 | 64 | if (null !== $scale) { |
|
252 | 56 | $divscale = $scale; |
|
253 | } else { |
||
254 | // $divscale is calculated in order to maintain a reasonable precision |
||
255 | 22 | $this_abs = $this->abs(); |
|
256 | 22 | $b_abs = $b->abs(); |
|
257 | |||
258 | $log10_result = |
||
259 | 22 | self::innerLog10($this_abs->value, $this_abs->scale, 1) - |
|
260 | 22 | self::innerLog10($b_abs->value, $b_abs->scale, 1); |
|
261 | |||
262 | 22 | $divscale = (int)\max( |
|
263 | 22 | $this->scale + $b->scale, |
|
264 | 22 | \max( |
|
265 | 22 | self::countSignificativeDigits($this, $this_abs), |
|
266 | 22 | self::countSignificativeDigits($b, $b_abs) |
|
267 | 22 | ) - \max(\ceil($log10_result), 0), |
|
268 | 22 | \ceil(-$log10_result) + 1 |
|
269 | ); |
||
270 | } |
||
271 | |||
272 | 64 | return self::fromString( |
|
273 | 64 | \bcdiv($this->value, $b->value, $divscale+1), $divscale |
|
274 | ); |
||
275 | } |
||
276 | } |
||
277 | |||
278 | /** |
||
279 | * Returns the square root of this object |
||
280 | * @param integer $scale |
||
281 | * @return Decimal |
||
282 | */ |
||
283 | 4 | public function sqrt(int $scale = null): Decimal |
|
284 | { |
||
285 | 4 | if ($this->isNegative()) { |
|
286 | 1 | throw new \DomainException( |
|
287 | 1 | "Decimal can't handle square roots of negative numbers (it's only for real numbers)." |
|
288 | ); |
||
289 | 3 | } elseif ($this->isZero()) { |
|
290 | 1 | return DecimalConstants::Zero(); |
|
291 | } |
||
292 | |||
293 | 3 | $sqrt_scale = ($scale !== null ? $scale : $this->scale); |
|
294 | |||
295 | 3 | return self::fromString( |
|
296 | 3 | \bcsqrt($this->value, $sqrt_scale+1), |
|
297 | 3 | $sqrt_scale |
|
298 | ); |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * Powers this value to $b |
||
303 | * |
||
304 | * @param Decimal $b exponent |
||
305 | * @param integer $scale |
||
306 | * @return Decimal |
||
307 | */ |
||
308 | 10 | public function pow(Decimal $b, int $scale = null): Decimal |
|
309 | { |
||
310 | 10 | if ($this->isZero()) { |
|
311 | 2 | if ($b->isPositive()) { |
|
312 | 1 | return Decimal::fromDecimal($this, $scale); |
|
313 | } else { |
||
314 | 1 | throw new \DomainException("zero can't be powered to zero or negative numbers."); |
|
315 | } |
||
316 | 8 | } elseif ($b->isZero()) { |
|
317 | 1 | return DecimalConstants::One(); |
|
318 | 7 | } else if ($b->isNegative()) { |
|
319 | 2 | return DecimalConstants::One()->div( |
|
320 | 2 | $this->pow($b->additiveInverse(), max($scale, self::DEFAULT_SCALE)), |
|
321 | 2 | max($scale, self::DEFAULT_SCALE) |
|
322 | ); |
||
323 | 7 | } elseif (0 === $b->scale) { |
|
324 | 4 | $pow_scale = \max($this->scale, $b->scale, $scale ?? 0); |
|
325 | |||
326 | 4 | return self::fromString( |
|
327 | 4 | \bcpow($this->value, $b->value, $pow_scale+1), |
|
328 | 4 | $pow_scale |
|
329 | ); |
||
330 | } else { |
||
331 | 4 | if ($this->isPositive()) { |
|
332 | 3 | $pow_scale = \max($this->scale, $b->scale, $scale ?? 0); |
|
333 | |||
334 | 3 | $truncated_b = \bcadd($b->value, '0', 0); |
|
335 | 3 | $remaining_b = \bcsub($b->value, $truncated_b, $b->scale); |
|
336 | |||
337 | 3 | $first_pow_approx = \bcpow($this->value, $truncated_b, $pow_scale+1); |
|
338 | 3 | $intermediate_root = self::innerPowWithLittleExponent( |
|
339 | 3 | $this->value, |
|
340 | 3 | $remaining_b, |
|
341 | 3 | $b->scale, |
|
342 | 3 | $pow_scale+1 |
|
343 | ); |
||
344 | |||
345 | 3 | return Decimal::fromString( |
|
346 | 3 | \bcmul($first_pow_approx, $intermediate_root, $pow_scale+1), |
|
347 | 3 | $pow_scale |
|
348 | ); |
||
349 | } else { // elseif ($this->isNegative()) |
||
350 | 1 | if (!$b->isInteger()) { |
|
351 | 1 | throw new NotImplementedError( |
|
352 | "Usually negative numbers can't be powered to non integer numbers. " . |
||
353 | 1 | "The cases where is possible are not implemented." |
|
354 | ); |
||
355 | } |
||
356 | |||
357 | return (\preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1) |
||
358 | ? $this->additiveInverse()->pow($b, $scale) // $b is an even number |
||
359 | : $this->additiveInverse()->pow($b, $scale)->additiveInverse(); // $b is an odd number |
||
360 | } |
||
361 | } |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Returns the object's logarithm in base 10 |
||
366 | * @param integer $scale |
||
367 | * @return Decimal |
||
368 | */ |
||
369 | 7 | public function log10(int $scale = null): Decimal |
|
370 | { |
||
371 | 7 | if ($this->isNegative()) { |
|
372 | 1 | throw new \DomainException( |
|
373 | 1 | "Decimal can't handle logarithms of negative numbers (it's only for real numbers)." |
|
374 | ); |
||
375 | 6 | } elseif ($this->isZero()) { |
|
376 | 1 | throw new \DomainException( |
|
377 | 1 | "Decimal can't represent infinite numbers." |
|
378 | ); |
||
379 | } |
||
380 | |||
381 | 5 | return self::fromString( |
|
382 | 5 | self::innerLog10($this->value, $this->scale, $scale !== null ? $scale+1 : $this->scale+1), |
|
383 | 5 | $scale |
|
384 | ); |
||
385 | } |
||
386 | |||
387 | 96 | public function isZero(int $scale = null): bool |
|
388 | { |
||
389 | 96 | $cmp_scale = $scale !== null ? $scale : $this->scale; |
|
390 | |||
391 | 96 | return (\bccomp(self::innerRound($this->value, $cmp_scale), '0', $cmp_scale) === 0); |
|
392 | } |
||
393 | |||
394 | 31 | public function isPositive(): bool |
|
395 | { |
||
396 | 31 | return ($this->value[0] !== '-' && !$this->isZero()); |
|
397 | } |
||
398 | |||
399 | 71 | public function isNegative(): bool |
|
400 | { |
||
401 | 71 | return ($this->value[0] === '-'); |
|
402 | } |
||
403 | |||
404 | 3 | public function isInteger(): bool |
|
405 | { |
||
406 | 3 | return (\preg_match('/^[+\-]?[0-9]+(\.0+)?$/', $this->value, $captures) === 1); |
|
407 | } |
||
408 | |||
409 | /** |
||
410 | * Equality comparison between this object and $b |
||
411 | * @param Decimal $b |
||
412 | * @param integer $scale |
||
413 | * @return boolean |
||
414 | */ |
||
415 | 115 | public function equals(Decimal $b, int $scale = null): bool |
|
416 | { |
||
417 | 115 | self::paramsValidation($b, $scale); |
|
418 | |||
419 | 115 | if ($this === $b) { |
|
420 | 2 | return true; |
|
421 | } else { |
||
422 | 114 | $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale); |
|
423 | |||
424 | return ( |
||
425 | 114 | \bccomp( |
|
426 | 114 | self::innerRound($this->value, $cmp_scale), |
|
427 | 114 | self::innerRound($b->value, $cmp_scale), |
|
428 | 114 | $cmp_scale |
|
429 | 114 | ) === 0 |
|
430 | ); |
||
431 | } |
||
432 | } |
||
433 | |||
434 | /** |
||
435 | * $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0 |
||
436 | * |
||
437 | * @param Decimal $b |
||
438 | * @param integer $scale |
||
439 | * @return integer |
||
440 | */ |
||
441 | 56 | public function comp(Decimal $b, int $scale = null): int |
|
442 | { |
||
443 | 56 | self::paramsValidation($b, $scale); |
|
444 | |||
445 | 56 | if ($this === $b) { |
|
446 | 7 | return 0; |
|
447 | } |
||
448 | |||
449 | 55 | $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale); |
|
450 | |||
451 | 55 | return \bccomp( |
|
452 | 55 | self::innerRound($this->value, $cmp_scale), |
|
453 | 55 | self::innerRound($b->value, $cmp_scale), |
|
454 | 55 | $cmp_scale |
|
455 | ); |
||
456 | } |
||
457 | |||
458 | |||
459 | /** |
||
460 | * Returns true if $this > $b, otherwise false |
||
461 | * |
||
462 | * @param Decimal $b |
||
463 | * @param integer $scale |
||
464 | * @return bool |
||
465 | */ |
||
466 | 3 | public function isGreaterThan(Decimal $b, int $scale = null): bool |
|
467 | { |
||
468 | 3 | return $this->comp($b, $scale) === 1; |
|
469 | } |
||
470 | |||
471 | /** |
||
472 | * Returns true if $this >= $b |
||
473 | * |
||
474 | * @param Decimal $b |
||
475 | * @param integer $scale |
||
476 | * @return bool |
||
477 | */ |
||
478 | 3 | public function isGreaterOrEqualTo(Decimal $b, int $scale = null): bool |
|
479 | { |
||
480 | 3 | $comparisonResult = $this->comp($b, $scale); |
|
481 | |||
482 | 3 | return $comparisonResult === 1 || $comparisonResult === 0; |
|
483 | } |
||
484 | |||
485 | /** |
||
486 | * Returns true if $this < $b, otherwise false |
||
487 | * |
||
488 | * @param Decimal $b |
||
489 | * @param integer $scale |
||
490 | * @return bool |
||
491 | */ |
||
492 | 3 | public function isLessThan(Decimal $b, int $scale = null): bool |
|
493 | { |
||
494 | 3 | return $this->comp($b, $scale) === -1; |
|
495 | } |
||
496 | |||
497 | /** |
||
498 | * Returns true if $this <= $b, otherwise false |
||
499 | * |
||
500 | * @param Decimal $b |
||
501 | * @param integer $scale |
||
502 | * @return bool |
||
503 | */ |
||
504 | 3 | public function isLessOrEqualTo(Decimal $b, int $scale = null): bool |
|
505 | { |
||
506 | 3 | $comparisonResult = $this->comp($b, $scale); |
|
507 | |||
508 | 3 | return $comparisonResult === -1 || $comparisonResult === 0; |
|
509 | } |
||
510 | |||
511 | /** |
||
512 | * Returns the element's additive inverse. |
||
513 | * @return Decimal |
||
514 | */ |
||
515 | 15 | public function additiveInverse(): Decimal |
|
516 | { |
||
517 | 15 | if ($this->isZero()) { |
|
518 | 1 | return $this; |
|
519 | 14 | } elseif ($this->isNegative()) { |
|
520 | 12 | $value = \substr($this->value, 1); |
|
521 | } else { // if ($this->isPositive()) { |
||
522 | 2 | $value = '-' . $this->value; |
|
523 | } |
||
524 | |||
525 | 14 | return new static($value, $this->scale); |
|
526 | } |
||
527 | |||
528 | |||
529 | /** |
||
530 | * "Rounds" the Decimal to have at most $scale digits after the point |
||
531 | * @param integer $scale |
||
532 | * @return Decimal |
||
533 | */ |
||
534 | 52 | public function round(int $scale = 0): Decimal |
|
535 | { |
||
536 | 52 | if ($scale >= $this->scale) { |
|
537 | 21 | return $this; |
|
538 | } |
||
539 | |||
540 | 51 | return self::fromString(self::innerRound($this->value, $scale)); |
|
541 | } |
||
542 | |||
543 | /** |
||
544 | * "Ceils" the Decimal to have at most $scale digits after the point |
||
545 | * @param integer $scale |
||
546 | * @return Decimal |
||
547 | */ |
||
548 | 4 | public function ceil($scale = 0): Decimal |
|
549 | { |
||
550 | 4 | if ($scale >= $this->scale) { |
|
551 | 2 | return $this; |
|
552 | } |
||
553 | |||
554 | 3 | if ($this->isNegative()) { |
|
555 | 1 | return self::fromString(\bcadd($this->value, '0', $scale)); |
|
556 | } |
||
557 | |||
558 | 2 | return $this->innerTruncate($scale); |
|
559 | } |
||
560 | |||
561 | 28 | private function innerTruncate(int $scale = 0, bool $ceil = true): Decimal |
|
562 | { |
||
563 | 28 | $rounded = \bcadd($this->value, '0', $scale); |
|
564 | |||
565 | 28 | $rlen = \strlen($rounded); |
|
566 | 28 | $tlen = \strlen($this->value); |
|
567 | |||
568 | 28 | $mustTruncate = false; |
|
569 | 28 | for ($i=$tlen-1; $i >= $rlen; $i--) { |
|
570 | 28 | if ((int)$this->value[$i] > 0) { |
|
571 | 28 | $mustTruncate = true; |
|
572 | 28 | break; |
|
573 | } |
||
574 | } |
||
575 | |||
576 | 28 | if ($mustTruncate) { |
|
577 | 28 | $rounded = $ceil |
|
578 | 2 | ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale) |
|
579 | 28 | : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale); |
|
580 | } |
||
581 | |||
582 | 28 | return self::fromString($rounded, $scale); |
|
583 | } |
||
584 | |||
585 | /** |
||
586 | * "Floors" the Decimal to have at most $scale digits after the point |
||
587 | * @param integer $scale |
||
588 | * @return Decimal |
||
589 | */ |
||
590 | 47 | public function floor(int $scale = 0): Decimal |
|
591 | { |
||
592 | 47 | if ($scale >= $this->scale) { |
|
593 | 38 | return $this; |
|
594 | } |
||
595 | |||
596 | 38 | if ($this->isNegative()) { |
|
597 | 26 | return $this->innerTruncate($scale, false); |
|
598 | } |
||
599 | |||
600 | 35 | return self::fromString(\bcadd($this->value, '0', $scale)); |
|
601 | } |
||
602 | |||
603 | /** |
||
604 | * Returns the absolute value (always a positive number) |
||
605 | * @return Decimal |
||
606 | */ |
||
607 | 23 | public function abs(): Decimal |
|
608 | { |
||
609 | 23 | return ($this->isZero() || $this->isPositive()) |
|
610 | 21 | ? $this |
|
611 | 23 | : $this->additiveInverse(); |
|
612 | } |
||
613 | |||
614 | /** |
||
615 | * Calculate modulo with a decimal |
||
616 | * @param Decimal $d |
||
617 | * @param integer $scale |
||
618 | * @return $this % $d |
||
619 | */ |
||
620 | 27 | public function mod(Decimal $d, int $scale = null): Decimal |
|
621 | { |
||
622 | 27 | $div = $this->div($d, 1)->floor(); |
|
623 | 27 | return $this->sub($div->mul($d), $scale); |
|
624 | } |
||
625 | |||
626 | /** |
||
627 | * Calculates the sine of this method with the highest possible accuracy |
||
628 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
629 | * |
||
630 | * @param integer $scale |
||
631 | * @return Decimal sin($this) |
||
632 | */ |
||
633 | 13 | public function sin(int $scale = null): Decimal |
|
634 | { |
||
635 | // First normalise the number in the [0, 2PI] domain |
||
636 | 13 | $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2"))); |
|
637 | |||
638 | // PI has only 32 significant numbers |
||
639 | 13 | $scale = (null === $scale) ? 32 : $scale; |
|
640 | |||
641 | 13 | return self::factorialSerie( |
|
642 | 13 | $x, |
|
643 | 13 | DecimalConstants::zero(), |
|
644 | 13 | function ($i) { |
|
645 | 13 | return ($i % 2 === 1) ? ( |
|
646 | 13 | ($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne() |
|
647 | 13 | ) : DecimalConstants::zero(); |
|
648 | 13 | }, |
|
649 | 13 | $scale |
|
650 | ); |
||
651 | } |
||
652 | |||
653 | /** |
||
654 | * Calculates the cosecant of this with the highest possible accuracy |
||
655 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
656 | * |
||
657 | * @param integer $scale |
||
658 | * @return Decimal |
||
659 | */ |
||
660 | 3 | public function cosec(int $scale = null): Decimal |
|
661 | { |
||
662 | 3 | $sin = $this->sin($scale + 2); |
|
663 | 3 | if ($sin->isZero()) { |
|
664 | throw new \DomainException( |
||
665 | "The cosecant of this 'angle' is undefined." |
||
666 | ); |
||
667 | } |
||
668 | |||
669 | 3 | return DecimalConstants::one()->div($sin)->round($scale); |
|
670 | } |
||
671 | |||
672 | /** |
||
673 | * Calculates the cosine of this method with the highest possible accuracy |
||
674 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
675 | * |
||
676 | * @param integer $scale |
||
677 | * @return Decimal cos($this) |
||
678 | */ |
||
679 | 13 | public function cos(int $scale = null): Decimal |
|
680 | { |
||
681 | // First normalise the number in the [0, 2PI] domain |
||
682 | 13 | $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2"))); |
|
683 | |||
684 | // PI has only 32 significant numbers |
||
685 | 13 | $scale = ($scale === null) ? 32 : $scale; |
|
686 | |||
687 | 13 | return self::factorialSerie( |
|
688 | 13 | $x, |
|
689 | 13 | DecimalConstants::one(), |
|
690 | 13 | function ($i) { |
|
691 | 13 | return ($i % 2 === 0) ? ( |
|
692 | 13 | ($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne() |
|
693 | 13 | ) : DecimalConstants::zero(); |
|
694 | 13 | }, |
|
695 | 13 | $scale |
|
696 | ); |
||
697 | } |
||
698 | |||
699 | /** |
||
700 | * Calculates the secant of this with the highest possible accuracy |
||
701 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
702 | * |
||
703 | * @param integer $scale |
||
704 | * @return Decimal |
||
705 | */ |
||
706 | 3 | public function sec(int $scale = null): Decimal |
|
707 | { |
||
708 | 3 | $cos = $this->cos($scale + 2); |
|
709 | 3 | if ($cos->isZero()) { |
|
710 | throw new \DomainException( |
||
711 | "The secant of this 'angle' is undefined." |
||
712 | ); |
||
713 | } |
||
714 | |||
715 | 3 | return DecimalConstants::one()->div($cos)->round($scale); |
|
716 | } |
||
717 | |||
718 | /** |
||
719 | * Calculates the arcsine of this with the highest possible accuracy |
||
720 | * |
||
721 | * @param integer $scale |
||
722 | * @return Decimal |
||
723 | */ |
||
724 | 5 | public function arcsin(int $scale = null): Decimal |
|
725 | { |
||
726 | 5 | if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) { |
|
727 | 2 | throw new \DomainException( |
|
728 | 2 | "The arcsin of this number is undefined." |
|
729 | ); |
||
730 | } |
||
731 | |||
732 | 3 | if ($this->round($scale)->isZero()) { |
|
733 | return DecimalConstants::zero(); |
||
734 | } |
||
735 | 3 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
736 | 1 | return DecimalConstants::pi()->div(Decimal::fromInteger(2))->round($scale); |
|
737 | } |
||
738 | 2 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
739 | 1 | return DecimalConstants::pi()->div(Decimal::fromInteger(-2))->round($scale); |
|
740 | } |
||
741 | |||
742 | 1 | $scale = ($scale === null) ? 32 : $scale; |
|
743 | |||
744 | 1 | return self::powerSerie( |
|
745 | 1 | $this, |
|
746 | 1 | DecimalConstants::zero(), |
|
747 | 1 | $scale |
|
748 | ); |
||
749 | } |
||
750 | |||
751 | /** |
||
752 | * Calculates the arccosine of this with the highest possible accuracy |
||
753 | * |
||
754 | * @param integer $scale |
||
755 | * @return Decimal |
||
756 | */ |
||
757 | 5 | public function arccos(int $scale = null): Decimal |
|
758 | { |
||
759 | 5 | if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) { |
|
760 | 2 | throw new \DomainException( |
|
761 | 2 | "The arccos of this number is undefined." |
|
762 | ); |
||
763 | } |
||
764 | |||
765 | 3 | $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale); |
|
766 | |||
767 | 3 | if ($this->round($scale)->isZero()) { |
|
768 | return $piOverTwo; |
||
769 | } |
||
770 | 3 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
771 | 1 | return DecimalConstants::zero(); |
|
772 | } |
||
773 | 2 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
774 | 1 | return DecimalConstants::pi()->round($scale); |
|
775 | } |
||
776 | |||
777 | 1 | $scale = ($scale === null) ? 32 : $scale; |
|
778 | |||
779 | 1 | return $piOverTwo->sub( |
|
780 | 1 | self::powerSerie( |
|
781 | 1 | $this, |
|
782 | 1 | DecimalConstants::zero(), |
|
783 | 1 | $scale |
|
784 | ) |
||
785 | 1 | )->round($scale); |
|
786 | } |
||
787 | |||
788 | /** |
||
789 | * Calculates the arctangente of this with the highest possible accuracy |
||
790 | * |
||
791 | * @param integer $scale |
||
792 | * @return Decimal |
||
793 | */ |
||
794 | 3 | public function arctan(int $scale = null): Decimal |
|
795 | { |
||
796 | 3 | $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2)->round($scale); |
|
797 | |||
798 | 3 | if ($this->round($scale)->isZero()) { |
|
799 | 1 | return DecimalConstants::zero(); |
|
800 | } |
||
801 | 2 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
802 | return $piOverFour; |
||
803 | } |
||
804 | 2 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
805 | 1 | return DecimalConstants::negativeOne()->mul($piOverFour); |
|
806 | } |
||
807 | |||
808 | 1 | $scale = ($scale === null) ? 32 : $scale; |
|
809 | |||
810 | 1 | return self::simplePowerSerie( |
|
811 | 1 | $this, |
|
812 | 1 | DecimalConstants::zero(), |
|
813 | 1 | $scale + 2 |
|
814 | 1 | )->round($scale); |
|
815 | } |
||
816 | |||
817 | /** |
||
818 | * Calculates the arccotangente of this with the highest possible accuracy |
||
819 | * |
||
820 | * @param integer $scale |
||
821 | * @return Decimal |
||
822 | */ |
||
823 | 3 | public function arccot(int $scale = null): Decimal |
|
824 | { |
||
825 | 3 | $scale = ($scale === null) ? 32 : $scale; |
|
826 | |||
827 | 3 | $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2); |
|
828 | 3 | if ($this->round($scale)->isZero()) { |
|
829 | 1 | return $piOverTwo->round($scale); |
|
830 | } |
||
831 | |||
832 | 2 | $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2); |
|
833 | 2 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
834 | return $piOverFour->round($scale); |
||
835 | } |
||
836 | 2 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
837 | 1 | return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale); |
|
838 | } |
||
839 | |||
840 | 1 | return $piOverTwo->sub( |
|
841 | 1 | self::simplePowerSerie( |
|
842 | 1 | $this, |
|
843 | 1 | DecimalConstants::zero(), |
|
844 | 1 | $scale + 2 |
|
845 | ) |
||
846 | 1 | )->round($scale); |
|
847 | } |
||
848 | |||
849 | /** |
||
850 | * Calculates the arcsecant of this with the highest possible accuracy |
||
851 | * |
||
852 | * @param integer $scale |
||
853 | * @return Decimal |
||
854 | */ |
||
855 | 5 | public function arcsec(int $scale = null): Decimal |
|
856 | { |
||
857 | 5 | if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) { |
|
858 | 1 | throw new \DomainException( |
|
859 | 1 | "The arcsecant of this number is undefined." |
|
860 | ); |
||
861 | } |
||
862 | |||
863 | 4 | $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale); |
|
864 | |||
865 | 4 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
866 | 1 | return DecimalConstants::zero(); |
|
867 | } |
||
868 | 3 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
869 | 1 | return DecimalConstants::pi()->round($scale); |
|
870 | } |
||
871 | |||
872 | 2 | $scale = ($scale === null) ? 32 : $scale; |
|
873 | |||
874 | 2 | return $piOverTwo->sub( |
|
875 | 2 | self::powerSerie( |
|
876 | 2 | DecimalConstants::one()->div($this, $scale + 2), |
|
877 | 2 | DecimalConstants::zero(), |
|
878 | 2 | $scale + 2 |
|
879 | ) |
||
880 | 2 | )->round($scale); |
|
881 | } |
||
882 | |||
883 | /** |
||
884 | * Calculates the arccosecant of this with the highest possible accuracy |
||
885 | * |
||
886 | * @param integer $scale |
||
887 | * @return Decimal |
||
888 | */ |
||
889 | 5 | public function arccsc(int $scale = null): Decimal |
|
890 | { |
||
891 | 5 | if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) { |
|
892 | 1 | throw new \DomainException( |
|
893 | 1 | "The arccosecant of this number is undefined." |
|
894 | ); |
||
895 | } |
||
896 | |||
897 | 4 | $scale = ($scale === null) ? 32 : $scale; |
|
898 | |||
899 | 4 | if ($this->round($scale)->equals(DecimalConstants::one())) { |
|
900 | 1 | return DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale); |
|
901 | } |
||
902 | 3 | if ($this->round($scale)->equals(DecimalConstants::negativeOne())) { |
|
903 | 1 | return DecimalConstants::pi()->div(Decimal::fromInteger(-2), $scale + 2)->round($scale); |
|
904 | } |
||
905 | |||
906 | 2 | return self::powerSerie( |
|
907 | 2 | DecimalConstants::one()->div($this, $scale + 2), |
|
908 | 2 | DecimalConstants::zero(), |
|
909 | 2 | $scale + 2 |
|
910 | 2 | )->round($scale); |
|
911 | } |
||
912 | |||
913 | /** |
||
914 | * Returns exp($this), said in other words: e^$this . |
||
915 | * |
||
916 | * @param integer $scale |
||
917 | * @return Decimal |
||
918 | */ |
||
919 | 11 | public function exp(int $scale = null): Decimal |
|
920 | { |
||
921 | 11 | if ($this->isZero()) { |
|
922 | 3 | return DecimalConstants::one(); |
|
923 | } |
||
924 | |||
925 | 8 | $scale = $scale ?? \max( |
|
926 | $this->scale, |
||
927 | 8 | (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : self::DEFAULT_SCALE) |
|
928 | ); |
||
929 | |||
930 | 8 | return self::factorialSerie( |
|
931 | $this, DecimalConstants::one(), function ($i) { return DecimalConstants::one(); }, $scale |
||
0 ignored issues
–
show
|
|||
932 | ); |
||
933 | } |
||
934 | |||
935 | /** |
||
936 | * Internal method used to compute sin, cos and exp |
||
937 | * |
||
938 | * @param Decimal $x |
||
939 | * @param Decimal $firstTerm |
||
940 | * @param callable $generalTerm |
||
941 | * @param $scale |
||
942 | * @return Decimal |
||
943 | */ |
||
944 | 28 | private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal |
|
945 | { |
||
946 | 28 | $approx = $firstTerm; |
|
947 | 28 | $change = DecimalConstants::One(); |
|
948 | |||
949 | 28 | $faculty = DecimalConstants::One(); // Calculates the faculty under the sign |
|
950 | 28 | $xPowerN = DecimalConstants::One(); // Calculates x^n |
|
951 | |||
952 | 28 | for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) { |
|
953 | // update x^n and n! for this walkthrough |
||
954 | 28 | $xPowerN = $xPowerN->mul($x); |
|
955 | 28 | $faculty = $faculty->mul(Decimal::fromInteger($i)); |
|
956 | |||
957 | /** @var Decimal $multiplier */ |
||
958 | 28 | $multiplier = $generalTerm($i); |
|
959 | |||
960 | 28 | if (!$multiplier->isZero()) { |
|
961 | 28 | $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2); |
|
962 | 28 | $approx = $approx->add($change, $scale + 2); |
|
963 | } |
||
964 | } |
||
965 | |||
966 | 28 | return $approx->round($scale); |
|
967 | } |
||
968 | |||
969 | |||
970 | /** |
||
971 | * Internal method used to compute arcsine and arcosine |
||
972 | * |
||
973 | * @param Decimal $x |
||
974 | * @param Decimal $firstTerm |
||
975 | * @param $scale |
||
976 | * @return Decimal |
||
977 | */ |
||
978 | 6 | private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal |
|
979 | { |
||
980 | 6 | $approx = $firstTerm; |
|
981 | 6 | $change = DecimalConstants::One(); |
|
982 | |||
983 | 6 | $xPowerN = DecimalConstants::One(); // Calculates x^n |
|
984 | 6 | $factorN = DecimalConstants::One(); // Calculates a_n |
|
985 | |||
986 | 6 | $numerator = DecimalConstants::one(); |
|
987 | 6 | $denominator = DecimalConstants::one(); |
|
988 | |||
989 | 6 | for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) { |
|
990 | 6 | $xPowerN = $xPowerN->mul($x); |
|
991 | |||
992 | 6 | if ($i % 2 === 0) { |
|
993 | 6 | $factorN = DecimalConstants::zero(); |
|
994 | 6 | } elseif ($i === 1) { |
|
995 | 6 | $factorN = DecimalConstants::one(); |
|
996 | } else { |
||
997 | 6 | $incrementNum = Decimal::fromInteger($i - 2); |
|
998 | 6 | $numerator = $numerator->mul($incrementNum, $scale +2); |
|
999 | |||
1000 | 6 | $incrementDen = Decimal::fromInteger($i - 1); |
|
1001 | 6 | $increment = Decimal::fromInteger($i); |
|
1002 | $denominator = $denominator |
||
1003 | 6 | ->div($incrementNum, $scale +2) |
|
1004 | 6 | ->mul($incrementDen, $scale +2) |
|
1005 | 6 | ->mul($increment, $scale +2); |
|
1006 | |||
1007 | 6 | $factorN = $numerator->div($denominator, $scale + 2); |
|
1008 | } |
||
1009 | |||
1010 | 6 | if (!$factorN->isZero()) { |
|
1011 | 6 | $change = $factorN->mul($xPowerN, $scale + 2); |
|
1012 | 6 | $approx = $approx->add($change, $scale + 2); |
|
1013 | } |
||
1014 | } |
||
1015 | |||
1016 | 6 | return $approx->round($scale); |
|
1017 | } |
||
1018 | |||
1019 | /** |
||
1020 | * Internal method used to compute arctan and arccotan |
||
1021 | * |
||
1022 | * @param Decimal $x |
||
1023 | * @param Decimal $firstTerm |
||
1024 | * @param $scale |
||
1025 | * @return Decimal |
||
1026 | */ |
||
1027 | 2 | private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal |
|
1028 | { |
||
1029 | 2 | $approx = $firstTerm; |
|
1030 | 2 | $change = DecimalConstants::One(); |
|
1031 | |||
1032 | 2 | $xPowerN = DecimalConstants::One(); // Calculates x^n |
|
1033 | 2 | $sign = DecimalConstants::One(); // Calculates a_n |
|
1034 | |||
1035 | 2 | for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) { |
|
1036 | 2 | $xPowerN = $xPowerN->mul($x); |
|
1037 | |||
1038 | 2 | if ($i % 2 === 0) { |
|
1039 | 2 | $factorN = DecimalConstants::zero(); |
|
1040 | } else { |
||
1041 | 2 | if ($i % 4 === 1) { |
|
1042 | 2 | $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2); |
|
1043 | } else { |
||
1044 | 2 | $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2); |
|
1045 | } |
||
1046 | } |
||
1047 | |||
1048 | 2 | if (!$factorN->isZero()) { |
|
1049 | 2 | $change = $factorN->mul($xPowerN, $scale + 2); |
|
1050 | 2 | $approx = $approx->add($change, $scale + 2); |
|
1051 | } |
||
1052 | } |
||
1053 | |||
1054 | 2 | return $approx->round($scale); |
|
1055 | } |
||
1056 | |||
1057 | /** |
||
1058 | * Calculates the tangent of this method with the highest possible accuracy |
||
1059 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
1060 | * |
||
1061 | * @param integer $scale |
||
1062 | * @return Decimal tan($this) |
||
1063 | */ |
||
1064 | 4 | public function tan(int $scale = null): Decimal |
|
1065 | { |
||
1066 | 4 | $cos = $this->cos($scale + 2); |
|
1067 | 4 | if ($cos->isZero()) { |
|
1068 | 1 | throw new \DomainException( |
|
1069 | 1 | "The tangent of this 'angle' is undefined." |
|
1070 | ); |
||
1071 | } |
||
1072 | |||
1073 | 3 | return $this->sin($scale + 2)->div($cos)->round($scale); |
|
1074 | } |
||
1075 | |||
1076 | /** |
||
1077 | * Calculates the cotangent of this method with the highest possible accuracy |
||
1078 | * Note that accuracy is limited by the accuracy of predefined PI; |
||
1079 | * |
||
1080 | * @param integer $scale |
||
1081 | * @return Decimal cotan($this) |
||
1082 | */ |
||
1083 | 4 | public function cotan(int $scale = null): Decimal |
|
1084 | { |
||
1085 | 4 | $sin = $this->sin($scale + 2); |
|
1086 | 4 | if ($sin->isZero()) { |
|
1087 | 1 | throw new \DomainException( |
|
1088 | 1 | "The cotangent of this 'angle' is undefined." |
|
1089 | ); |
||
1090 | } |
||
1091 | |||
1092 | 3 | return $this->cos($scale + 2)->div($sin)->round($scale); |
|
1093 | } |
||
1094 | |||
1095 | /** |
||
1096 | * Indicates if the passed parameter has the same sign as the method's bound object. |
||
1097 | * |
||
1098 | * @param Decimal $b |
||
1099 | * @return bool |
||
1100 | */ |
||
1101 | public function hasSameSign(Decimal $b): bool |
||
1102 | { |
||
1103 | return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative(); |
||
1104 | } |
||
1105 | |||
1106 | 5 | public function asFloat(): float |
|
1107 | { |
||
1108 | 5 | return \floatval($this->value); |
|
1109 | } |
||
1110 | |||
1111 | 1 | public function asInteger(): int |
|
1112 | { |
||
1113 | 1 | return \intval($this->value); |
|
1114 | } |
||
1115 | |||
1116 | /** |
||
1117 | * WARNING: use with caution! Return the inner representation of the class. |
||
1118 | */ |
||
1119 | 11 | public function innerValue(): string |
|
1120 | { |
||
1121 | 11 | return $this->value; |
|
1122 | } |
||
1123 | |||
1124 | /** |
||
1125 | * @return string |
||
1126 | */ |
||
1127 | 58 | public function __toString(): string |
|
1128 | { |
||
1129 | 58 | return $this->value; |
|
1130 | } |
||
1131 | |||
1132 | /* |
||
1133 | * |
||
1134 | */ |
||
1135 | 7 | private static function fromExpNotationString( |
|
1136 | int $scale = null, |
||
1137 | string $sign, |
||
1138 | string $mantissa, |
||
1139 | int $nDecimals, |
||
1140 | string $expSign, |
||
1141 | int $expVal |
||
1142 | ): array |
||
1143 | { |
||
1144 | 7 | $mantissaScale = \max($nDecimals, 0); |
|
1145 | |||
1146 | 7 | if (self::normalizeSign($expSign) === '') { |
|
1147 | 5 | $minScale = \max($mantissaScale - $expVal, 0); |
|
1148 | 5 | $tmp_multiplier = \bcpow('10', (string)$expVal); |
|
1149 | } else { |
||
1150 | 2 | $minScale = $mantissaScale + $expVal; |
|
1151 | 2 | $tmp_multiplier = \bcpow('10', (string)-$expVal, $expVal); |
|
1152 | } |
||
1153 | |||
1154 | $value = ( |
||
1155 | 7 | self::normalizeSign($sign) . |
|
1156 | 7 | \bcmul( |
|
1157 | 7 | $mantissa, |
|
1158 | 7 | $tmp_multiplier, |
|
1159 | 7 | \max($minScale, $scale ?? 0) |
|
1160 | ) |
||
1161 | ); |
||
1162 | |||
1163 | 7 | return [$minScale, $value]; |
|
1164 | } |
||
1165 | |||
1166 | /** |
||
1167 | * "Rounds" the decimal string to have at most $scale digits after the point |
||
1168 | * |
||
1169 | * @param string $value |
||
1170 | * @param int $scale |
||
1171 | * @return string |
||
1172 | */ |
||
1173 | 150 | private static function innerRound(string $value, int $scale = 0): string |
|
1174 | { |
||
1175 | 150 | $rounded = \bcadd($value, '0', $scale); |
|
1176 | |||
1177 | 150 | $diffDigit = \bcsub($value, $rounded, $scale+1); |
|
1178 | 150 | $diffDigit = (int)$diffDigit[\strlen($diffDigit)-1]; |
|
1179 | |||
1180 | 150 | if ($diffDigit >= 5) { |
|
1181 | 68 | $rounded = ($diffDigit >= 5 && $value[0] !== '-') |
|
1182 | 64 | ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale) |
|
1183 | 68 | : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale); |
|
1184 | } |
||
1185 | |||
1186 | 150 | return $rounded; |
|
1187 | } |
||
1188 | |||
1189 | /** |
||
1190 | * Calculates the logarithm (in base 10) of $value |
||
1191 | * |
||
1192 | * @param string $value The number we want to calculate its logarithm (only positive numbers) |
||
1193 | * @param int $in_scale Expected scale used by $value (only positive numbers) |
||
1194 | * @param int $out_scale Scale used by the return value (only positive numbers) |
||
1195 | * @return string |
||
1196 | */ |
||
1197 | 25 | private static function innerLog10(string $value, int $in_scale, int $out_scale): string |
|
1198 | { |
||
1199 | 25 | $value_len = \strlen($value); |
|
1200 | |||
1201 | 25 | $cmp = \bccomp($value, '1', $in_scale); |
|
1202 | |||
1203 | switch ($cmp) { |
||
1204 | 25 | case 1: |
|
1205 | 12 | $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1); |
|
1206 | 12 | $value_log10_approx = max(0, $value_log10_approx); |
|
1207 | |||
1208 | 12 | return \bcadd( |
|
1209 | 12 | (string)$value_log10_approx, |
|
1210 | 12 | (string)\log10((float)\bcdiv( |
|
1211 | 12 | $value, |
|
1212 | 12 | \bcpow('10', (string)$value_log10_approx), |
|
1213 | 12 | \min($value_len, $out_scale) |
|
1214 | )), |
||
1215 | 12 | $out_scale |
|
1216 | ); |
||
1217 | 15 | case -1: |
|
1218 | 14 | \preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures); |
|
1219 | 14 | $value_log10_approx = -\strlen($captures[1])-1; |
|
1220 | |||
1221 | 14 | return \bcadd( |
|
1222 | 14 | (string)$value_log10_approx, |
|
1223 | 14 | (string)\log10((float)\bcmul( |
|
1224 | 14 | $value, |
|
1225 | 14 | \bcpow('10', (string)-$value_log10_approx), |
|
1226 | 14 | $in_scale + $value_log10_approx |
|
1227 | )), |
||
1228 | 14 | $out_scale |
|
1229 | ); |
||
1230 | default: // case 0: |
||
1231 | 7 | return '0'; |
|
1232 | } |
||
1233 | } |
||
1234 | |||
1235 | /** |
||
1236 | * Returns $base^$exponent |
||
1237 | * |
||
1238 | * @param string $base |
||
1239 | * @param string $exponent 0 < $exponent < 1 |
||
1240 | * @param int $exp_scale Number of $exponent's significative digits |
||
1241 | * @param int $out_scale Number of significative digits that we want to compute |
||
1242 | * @return string |
||
1243 | */ |
||
1244 | 3 | private static function innerPowWithLittleExponent( |
|
1245 | string $base, |
||
1246 | string $exponent, |
||
1247 | int $exp_scale, |
||
1248 | int $out_scale |
||
1249 | ): string |
||
1250 | { |
||
1251 | 3 | $inner_scale = (int)\ceil($exp_scale * \log(10) / \log(2)) + 1; |
|
1252 | |||
1253 | 3 | $result_a = '1'; |
|
1254 | 3 | $result_b = '0'; |
|
1255 | |||
1256 | 3 | $actual_index = 0; |
|
1257 | 3 | $exponent_remaining = $exponent; |
|
1258 | |||
1259 | 3 | while (\bccomp($result_a, $result_b, $out_scale) !== 0 && \bccomp($exponent_remaining, '0', $inner_scale) !== 0) { |
|
1260 | 3 | $result_b = $result_a; |
|
1261 | 3 | $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale); |
|
1262 | 3 | $exponent_remaining = $index_info[1]; |
|
1263 | 3 | $result_a = \bcmul( |
|
1264 | 3 | $result_a, |
|
1265 | 3 | self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)), |
|
1266 | 3 | 2*($out_scale+1) |
|
1267 | ); |
||
1268 | } |
||
1269 | |||
1270 | 3 | return self::innerRound($result_a, $out_scale); |
|
1271 | } |
||
1272 | |||
1273 | /** |
||
1274 | * Auxiliar method. It helps us to decompose the exponent into many summands. |
||
1275 | * |
||
1276 | * @param string $exponent_remaining |
||
1277 | * @param int $actual_index |
||
1278 | * @param int $exp_scale Number of $exponent's significative digits |
||
1279 | * @param int $inner_scale ceil($exp_scale*log(10)/log(2))+1; |
||
1280 | * @return array |
||
1281 | */ |
||
1282 | 3 | private static function computeSquareIndex( |
|
1283 | string $exponent_remaining, |
||
1284 | int $actual_index, |
||
1285 | int $exp_scale, |
||
1286 | int $inner_scale |
||
1287 | ): array |
||
1288 | { |
||
1289 | 3 | $actual_rt = \bcpow('0.5', (string)$actual_index, $exp_scale); |
|
1290 | 3 | $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale); |
|
1291 | |||
1292 | 3 | while (\bccomp($r, '0', $exp_scale) === -1) { |
|
1293 | 3 | ++$actual_index; |
|
1294 | 3 | $actual_rt = \bcmul('0.5', $actual_rt, $inner_scale); |
|
1295 | 3 | $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale); |
|
1296 | } |
||
1297 | |||
1298 | 3 | return [$actual_index, $r]; |
|
1299 | } |
||
1300 | |||
1301 | /** |
||
1302 | * Auxiliar method. Computes $base^((1/2)^$index) |
||
1303 | * |
||
1304 | * @param string $base |
||
1305 | * @param integer $index |
||
1306 | * @param integer $out_scale |
||
1307 | * @return string |
||
1308 | */ |
||
1309 | 3 | private static function compute2NRoot(string $base, int $index, int $out_scale): string |
|
1310 | { |
||
1311 | 3 | $result = $base; |
|
1312 | |||
1313 | 3 | for ($i = 0; $i < $index; $i++) { |
|
1314 | 3 | $result = \bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1); |
|
1315 | } |
||
1316 | |||
1317 | 3 | return self::innerRound($result, $out_scale); |
|
1318 | } |
||
1319 | |||
1320 | /** |
||
1321 | * Validates basic constructor's arguments |
||
1322 | * @param mixed $value |
||
1323 | * @param null|int $scale |
||
1324 | */ |
||
1325 | 174 | protected static function paramsValidation($value, int $scale = null) |
|
1326 | { |
||
1327 | 174 | if (null === $value) { |
|
1328 | throw new \InvalidArgumentException('$value must be a non null number'); |
||
1329 | } |
||
1330 | |||
1331 | 174 | if (null !== $scale && $scale < 0) { |
|
1332 | 2 | throw new \InvalidArgumentException('$scale must be a positive integer'); |
|
1333 | } |
||
1334 | 173 | } |
|
1335 | |||
1336 | /** |
||
1337 | * @return string |
||
1338 | */ |
||
1339 | 131 | private static function normalizeSign(string $sign): string |
|
1340 | { |
||
1341 | 131 | if ('+' === $sign) { |
|
1342 | 4 | return ''; |
|
1343 | } |
||
1344 | |||
1345 | 131 | return $sign; |
|
1346 | } |
||
1347 | |||
1348 | /** |
||
1349 | * Counts the number of significant digits of $val. |
||
1350 | * Assumes a consistent internal state (without zeros at the end or the start). |
||
1351 | * |
||
1352 | * @param Decimal $val |
||
1353 | * @param Decimal $abs $val->abs() |
||
1354 | * @return int |
||
1355 | */ |
||
1356 | 22 | private static function countSignificativeDigits(Decimal $val, Decimal $abs): int |
|
1357 | { |
||
1358 | 22 | return \strlen($val->value) - ( |
|
1359 | 22 | ($abs->comp(DecimalConstants::One()) === -1) ? 2 : \max($val->scale, 1) |
|
1360 | 22 | ) - ($val->isNegative() ? 1 : 0); |
|
1361 | } |
||
1362 | } |
||
1363 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.