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