Decimal::__construct()   F
last analyzed

Complexity

Conditions 26
Paths 137

Size

Total Lines 78
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 59
nc 137
nop 2
dl 0
loc 78
rs 3.8583
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
use Ivory\Exception\UndefinedOperationException;
6
use Ivory\Value\Alg\IComparable;
7
use Ivory\Utils\System;
8
9
/**
10
 * A decimal number with fixed precision.
11
 *
12
 * The objects are immutable, i.e., operations always produce a new object.
13
 *
14
 * The representation and operations resemble the specification for numeric types in PostgreSQL, especially regarding
15
 * the precision of operation results. Generally, the scale of the result is computed such that the result is exactly
16
 * precise. However, operations which are inherently inexact have special treatment by PostgreSQL, which this class
17
 * tries to resemble: the result is of scale no less than any of the operands, and at least of a constant chosen such
18
 * that the result is at least as precise as the float8 operation would. This class uses similar result scale
19
 * computation and it also rounds the result at the computed scale, like PostgreSQL does (that is, the result is not
20
 * just truncated).
21
 *
22
 * The special static instance retrieved by {@link self::NaN()} represents the not-a-number value. In conformance with
23
 * PostgreSQL, a not-a-number only equals to a not-a-number, it is greater than any numeric value, and any operation
24
 * results in not-a-number if any of the operands is not-a-number.
25
 *
26
 * Regarding the implementation, using the GMP extension might be applicable for many operations, even for exact
27
 * operations on fractional numbers. The GMP extension is not built in PHP by default, however, so this is left for an
28
 * extension. Currently, only few integer operations use GMP if it is available. It would be better if an alternative
29
 * class was used if the presence of the GMP extension was discovered at runtime.
30
 *
31
 * @see https://www.postgresql.org/docs/11/datatype-numeric.html PostgreSQL Numeric Types
32
 * @see https://www.postgresql.org/docs/11/functions-math.html PostgreSQL Mathematical Functions and Operators
33
 */
34
class Decimal implements IComparable
35
{
36
    /** Maximal number of decimal digits considered by PostgreSQL. */
37
    const SCALE_LIMIT = 16383;
38
    /**
39
     * Minimal number of significant digits for inexact calculations, like division or square root. It reflects the
40
     * behaviour of PostgreSQL.
41
     */
42
    const MIN_SIG_DIGITS = 16;
43
44
    /** @var string|null string of decimal digits, including the sign and decimal point; null for the not-a-number */
45
    private $val;
46
    /** @var int number of decimal digits in the fractional part, to the right of the decimal point (if any) */
47
    private $scale;
48
49
    /**
50
     * @param string|int|float|Decimal|object $decimalNumber the value of the decimal number;
51
     *                                                       leading zeros are ignored (even after the minus sign), as
52
     *                                                         well as leading and trailing whitespace;
53
     *                                                       if an object is passed, it gets cast to string (which works
54
     *                                                         for {@link \GMP} objects, besides others)
55
     * @param int|null $scale the requested scale (i.e., the number of decimal digits in the fractional part) of the
56
     *                          number;
57
     *                        must be non-negative, or null;
58
     *                        if smaller than the scale of the given number, the number gets mathematically rounded to
59
     *                          the requested scale;
60
     *                        if greater than the scale of the given number, the number gets padded to have such many
61
     *                          decimal places;
62
     *                        if not given, it is computed from <tt>$decimalNumber</tt> automatically
63
     * @return Decimal
64
     */
65
    public static function fromNumber($decimalNumber, ?int $scale = null): Decimal
66
    {
67
        if ($decimalNumber === null) {
68
            throw new \InvalidArgumentException('decimalNumber');
69
        }
70
        if ($scale < 0) {
71
            throw new \InvalidArgumentException('scale');
72
        }
73
        if ($decimalNumber instanceof Decimal) {
74
            if ($scale === null || $scale == $decimalNumber->scale) {
75
                return $decimalNumber;
76
            } else {
77
                return $decimalNumber->round($scale);
78
            }
79
        }
80
81
        if (is_int($decimalNumber)) {
82
            $val = (string)$decimalNumber;
83
        } elseif (is_float($decimalNumber)) {
84
            $val = (string)$decimalNumber;
85
            $ePos = stripos($val, 'e');
86
            if ($ePos > 0) {
87
                $exp = (int)substr($val, $ePos + 1);
88
                $decs = substr($val, 0, $ePos);
89
                [$wp, $dp] = explode('.', $decs, 2);
90
91
                if ($exp >= 0) {
92
                    $dpLen = strlen($dp);
93
                    if ($exp >= $dpLen) {
94
                        $val = $wp . $dp . str_repeat('0', $exp - $dpLen);
95
                    } else {
96
                        $val = $wp . substr($dp, 0, $exp) . '.' . substr($dp, $exp);
97
                    }
98
                } else {
99
                    $mn = ($wp[0] == '-' ? 1 : 0);
100
                    $ord = strlen($wp) - $mn;
101
                    if (-$exp >= $ord) {
102
                        $prefix = ($mn ? '-' : '') . '0.' . str_repeat('0', -$exp - $ord);
103
                        if (($mn ? ($wp != '-0') : ($wp != '0'))) {
104
                            $prefix .= substr($wp, $mn);
105
                        }
106
                        $val = $prefix . $dp;
107
                    } else {
108
                        $val = substr($wp, 0, $mn + $ord + $exp) . '.' . substr($wp, $mn + $ord + $exp) . $dp;
109
                    }
110
                }
111
            }
112
        } else {
113
            if (!preg_match('~^\s*(-?)(?|(\.[0-9]+)|0*([0-9]+(?:\.[0-9]*)?))\s*$~', (string)$decimalNumber, $m)) {
114
                throw new \InvalidArgumentException('decimalNumber');
115
            }
116
            $val = $m[1] . $m[2]; // cuts off unnecessary leading zeros
117
        }
118
119
        return new Decimal($val, $scale);
120
    }
121
122
    /**
123
     * Gets the not-a-number special value. Returns the same object every time.
124
     *
125
     * It only equals to the not-a-number, and is greater than any number value. The result of any operation is, again,
126
     * the not-a-number.
127
     */
128
    public static function createNaN(): Decimal
129
    {
130
        static $dec = null;
131
        if ($dec === null) {
132
            $dec = new Decimal(null);
133
        }
134
        return $dec;
135
    }
136
137
    /**
138
     * Gets the decimal object representing the number zero. Returns the same object every time.
139
     */
140
    public static function zero(): Decimal
141
    {
142
        static $dec = null;
143
        if ($dec === null) {
144
            $dec = new Decimal('0');
145
        }
146
        return $dec;
147
    }
148
149
    /**
150
     * NOTE: The constructor is kept private so that extra sanity checks are performed on user input, while trusting the
151
     *       results of the operations without these extra checks.
152
     *
153
     * @param string|null $val
154
     * @param int|null $scale
155
     */
156
    private function __construct(?string $val, ?int $scale = null)
157
    {
158
        if ($val === null) {
159
            $this->val = null;
160
            $this->scale = null;
161
            return;
162
        }
163
164
        $this->val = $val;
165
166
        $decPoint = strpos($this->val, '.');
167
        if ($scale < 0) {
168
            $this->scale = 0;
169
            $minus = ($this->val[0] == '-' ? 1 : 0);
170
            if ($decPoint !== false) {
171
                $digits = substr($this->val, $minus, $decPoint - $minus);
172
            } else {
173
                $digits = ($minus ? substr($this->val, $minus) : $this->val);
174
            }
175
            $len = strlen($digits);
176
            $sigPos = $len + $scale;
177
            if ($sigPos < 0) {
178
                $this->val = '0';
179
                return;
180
            }
181
            $inc = ($digits[$sigPos] >= 5);
182
            if (!$inc && $sigPos == 0) {
183
                $this->val = '0';
184
                return;
185
            }
186
            for ($i = $sigPos; $i < $len; $i++) {
187
                $digits[$i] = '0';
188
            }
189
            if ($inc) {
190
                $augend = '1' . str_repeat('0', -$scale);
191
                $digits = bcadd($digits, $augend, 0);
192
            }
193
            $this->val = ($minus ? '-' : '') . $digits;
194
        } elseif ($decPoint === false) {
195
            if ($scale > 0) {
196
                $this->val .= '.' . str_repeat('0', $scale);
0 ignored issues
show
Bug introduced by
It seems like $scale can also be of type null; however, parameter $times of str_repeat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
                $this->val .= '.' . str_repeat('0', /** @scrutinizer ignore-type */ $scale);
Loading history...
197
                $this->scale = $scale;
198
            } else {
199
                $this->scale = 0;
200
            }
201
        } else {
202
            $len = strlen($this->val);
203
            $this->scale = $len - $decPoint - 1;
204
            $scale = min(self::SCALE_LIMIT, ($scale === null ? $this->scale : max(0, $scale)));
205
            if ($this->scale < $scale) {
206
                $this->val .= str_repeat('0', $scale - $this->scale);
207
                $this->scale = $scale;
208
            } elseif ($this->scale > $scale) {
209
                $newLen = $len - ($this->scale - $scale);
210
                $inc = ($this->val[$newLen] >= 5);
211
                if ($scale == 0) {
212
                    $newLen--;
213
                }
214
                $this->val = substr($this->val, 0, $newLen);
215
                if ($inc) {
216
                    $mn = ($this->val[0] == '-' ? '-' : '');
217
                    $augend = $mn . ($scale == 0 ? '1' : '.' . str_repeat('0', $scale - 1) . '1');
218
                    $this->val = bcadd($this->val, $augend, $scale);
219
                }
220
                $this->scale = $scale;
221
            }
222
223
            if ($this->val[0] == '.') {
224
                $this->val = '0' . $this->val;
225
            } elseif ($this->val[0] == '-' && $this->val[1] == '.') {
226
                $this->val[0] = '0';
227
                $this->val = '-' . $this->val;
228
            }
229
        }
230
231
        // eliminate negative zero - PostgreSQL does not differentiate between zeros
232
        if ($this->val[0] == '-' && strspn($this->val, '-0.') == strlen($this->val)) {
233
            $this->val = substr($this->val, 1);
234
        }
235
    }
236
237
238
    /**
239
     * @return int|null number of decimal digits in the fractional part, to the right of the decimal point (if any), or
240
     *                  <tt>null</tt> for the not-a-number
241
     */
242
    public function getScale(): ?int
243
    {
244
        return $this->scale;
245
    }
246
247
    /**
248
     * Compare this number numerically with another number.
249
     *
250
     * Note that using the `==` operator checks that the two {@link Decimal} objects are of the same value and scale,
251
     * which might or might not be desired. Such a difference only arises in the trailing fractional zero digits,
252
     * though.
253
     *
254
     * @param string|int|float|Decimal|object $other number to compare this number with
255
     * @return bool whether this number numerically equals to <tt>$number</tt>
256
     */
257
    public function equals($other): bool
258
    {
259
        if ($other === null) {
260
            return false;
261
        }
262
        return ($this->compareTo($other) == 0);
263
    }
264
265
    /**
266
     * @param string|int|float|Decimal|object $number
267
     * @return bool <tt>true</tt> iff this number is numerically less than <tt>$number</tt>
268
     */
269
    public function lessThan($number): bool
270
    {
271
        return ($this->compareTo($number) < 0);
272
    }
273
274
    /**
275
     * @param string|int|float|Decimal|object $number
276
     * @return bool <tt>true</tt> iff this number is numerically greater than <tt>$number</tt>
277
     */
278
    public function greaterThan($number): bool
279
    {
280
        return ($this->compareTo($number) > 0);
281
    }
282
283
    /**
284
     * @param string|int|float|Decimal|object $other number to compare this number with
285
     * @return int -1, 0, or 1 if this number is numerically less than, equal to, or greater than <tt>$number</tt>
286
     */
287
    public function compareTo($other): int
288
    {
289
        $arg = self::fromNumber($other);
290
        if ($this->isNaN()) {
291
            if ($arg->isNaN()) {
292
                return 0;
293
            } else {
294
                return 1;
295
            }
296
        } elseif ($arg->isNaN()) {
297
            return -1;
298
        } else {
299
            return bccomp($this->val, $arg->val, max($this->scale, $arg->scale));
300
        }
301
    }
302
303
    /**
304
     * @return bool whether this is a not-a-number
305
     */
306
    public function isNaN(): bool
307
    {
308
        return ($this->val === null);
309
    }
310
311
    /**
312
     * @return bool whether this is zero
313
     */
314
    public function isZero(): bool
315
    {
316
        return $this->equals(self::zero());
317
    }
318
319
    /**
320
     * @return bool whether this is a positive number
321
     */
322
    public function isPositive(): bool
323
    {
324
        return (!$this->isNaN() && $this->compareTo(self::zero()) > 0);
325
    }
326
327
    /**
328
     * @return bool whether this is a negative number
329
     */
330
    public function isNegative(): bool
331
    {
332
        return (!$this->isNaN() && $this->val[0] == '-');
333
    }
334
335
    /**
336
     * @return bool whether this is an integer, i.e., a number without decimal part (or the decimal part equal to zero)
337
     */
338
    public function isInteger(): bool
339
    {
340
        if ($this->isNaN()) {
341
            return false;
342
        }
343
        if (strpos($this->val, '.') === false) {
344
            return true;
345
        }
346
        for ($i = strlen($this->val) - 1; ; $i--) {
347
            switch ($this->val[$i]) {
348
                case '.':
349
                    return true;
350
                case '0':
351
                    break;
352
                default:
353
                    return false;
354
            }
355
        }
356
        return true;
357
    }
358
359
    /**
360
     * @param string|int|float|Decimal|object $augend
361
     * @return Decimal a new decimal number, representing the result of sum of this and the given number
362
     */
363
    public function add($augend): Decimal
364
    {
365
        if ($this->isNaN()) {
366
            return $this;
367
        }
368
        $arg = self::fromNumber($augend);
369
        if ($arg->isNaN()) {
370
            return $arg;
371
        }
372
        $scale = max($this->scale, $arg->scale);
373
        return new Decimal(bcadd($this->val, $arg->val, $scale + 1), $scale);
374
    }
375
376
    /**
377
     * @param string|int|float|Decimal|object $subtrahend
378
     * @return Decimal a new decimal number, representing the result of subtraction of this and the given number
379
     */
380
    public function subtract($subtrahend): Decimal
381
    {
382
        if ($this->isNaN()) {
383
            return $this;
384
        }
385
        $arg = self::fromNumber($subtrahend);
386
        if ($arg->isNaN()) {
387
            return $arg;
388
        }
389
        $scale = max($this->scale, $arg->scale);
390
        return new Decimal(bcsub($this->val, $arg->val, $scale + 1), $scale);
391
    }
392
393
    /**
394
     * @param string|int|float|Decimal|object $multiplicand
395
     * @return Decimal a new decimal number, representing the result of multiplication of this and the given number
396
     */
397
    public function multiply($multiplicand): Decimal
398
    {
399
        if ($this->isNaN()) {
400
            return $this;
401
        }
402
        $arg = self::fromNumber($multiplicand);
403
        if ($arg->isNaN()) {
404
            return $arg;
405
        }
406
        $scale = min($this->scale + $arg->scale, self::SCALE_LIMIT);
407
        return new Decimal(bcmul($this->val, $arg->val, $scale + 1), $scale);
408
    }
409
410
    /**
411
     * @param string|int|float|Decimal|object $divisor
412
     * @return Decimal a new decimal number, representing the result of division of this number with the given number
413
     * @throws \RuntimeException if <tt>$divisor</tt> is zero
414
     */
415
    public function divide($divisor): Decimal
416
    {
417
        if ($this->isNaN()) {
418
            return $this;
419
        }
420
        $arg = self::fromNumber($divisor);
421
        if ($arg->isNaN()) {
422
            return $arg;
423
        }
424
        $scale = max($this->scale, $arg->scale, self::MIN_SIG_DIGITS);
425
        return new Decimal(bcdiv($this->val, $arg->val, $scale + 1), $scale);
426
    }
427
428
    /**
429
     * @param string|int|float|Decimal|object $modulus
430
     * @return Decimal remainder of this number divided by <tt>$modulus</tt>
431
     * @throws \RuntimeException if <tt>$modulus</tt> is zero
432
     */
433
    public function mod($modulus): Decimal
434
    {
435
        if ($this->isNaN()) {
436
            return $this;
437
        }
438
        $arg = self::fromNumber($modulus);
439
        if ($arg->isNaN()) {
440
            return $arg;
441
        }
442
        if ($arg->isZero()) {
443
            throw new \RuntimeException('Division by zero');
444
        }
445
446
        // NOTE: bcmod() only calculates integer modulus
447
        $a = $this->abs();
448
        $b = $arg->abs();
449
        $m = $a->subtract($b->multiply($a->divide($b)->floor()));
450
        if ($this->isNegative()) {
451
            return $m->negate();
452
        } else {
453
            return $m;
454
        }
455
    }
456
457
    /**
458
     * @todo more precise calculation of fractional powers - now only double precision is used
459
     * @param string|int|float|Decimal|object $power
460
     * @return Decimal this number powered to <tt>$number</tt>
461
     * @throws \RuntimeException if this number is negative while <tt>$power</tt> is non-integer (that would lead to a
462
     *                             complex result)
463
     */
464
    public function pow($power): Decimal
465
    {
466
        if ($this->isNaN()) {
467
            return $this;
468
        }
469
        $arg = self::fromNumber($power);
470
        if ($arg->isNaN()) {
471
            return $arg;
472
        }
473
474
        $scale = max($this->scale, $arg->scale, self::MIN_SIG_DIGITS);
475
476
        // NOTE: bcpow() only takes integer powers
477
        if ($arg->isInteger()) {
478
            return new Decimal(bcpow($this->val, $arg->val, $scale + 1), $scale);
479
        } else {
480
            if ($this->isNegative()) {
481
                throw new \RuntimeException('Negative number raised to a non-integer power yields a complex result');
482
            }
483
            return self::fromNumber(pow($this->toFloat(), $arg->toFloat()));
484
        }
485
    }
486
487
    /**
488
     * @return Decimal the absolute value of this number
489
     */
490
    public function abs(): Decimal
491
    {
492
        if ($this->isNegative()) {
493
            return $this->negate();
494
        } else {
495
            return $this;
496
        }
497
    }
498
499
    /**
500
     * @return Decimal a new decimal number, representing the negative value of this number
501
     */
502
    public function negate(): Decimal
503
    {
504
        if ($this->isNaN() || $this->isZero()) {
505
            return $this;
506
        } elseif ($this->val[0] == '-') {
507
            return new Decimal(substr($this->val, 1));
508
        } else {
509
            return new Decimal('-' . $this->val);
510
        }
511
    }
512
513
    /**
514
     * Computes the exact factorial of this number.
515
     * Only defined on numbers of scale zero, i.e., those without decimal part, and on the not-a-number.
516
     *
517
     * In conformance with PostgreSQL, the factorial of non-positive numbers is defined to be 1.
518
     *
519
     * @return Decimal the factorial of this number
520
     * @throws UndefinedOperationException if this number is neither a zero-scale number nor the not-a-number
521
     */
522
    public function factorial(): Decimal
523
    {
524
        if ($this->isNaN()) {
525
            return $this;
526
        }
527
        if ($this->scale > 0) {
528
            throw new UndefinedOperationException('Number with a decimal part');
529
        }
530
        if ($this->lessThan(2)) {
531
            return new Decimal('1');
532
        }
533
534
        if (System::hasGMP()) {
535
            /** @noinspection PhpComposerExtensionStubsInspection */
536
            return new Decimal(gmp_strval(gmp_fact($this->toGMP())));
537
        }
538
539
        // OPT: there are more efficient algorithms calculating the factorial; see http://www.luschny.de/math/factorial/
540
        $result = new Decimal('2');
541
        for ($i = new Decimal('3'); $i->compareTo($this) <= 0; $i = $i->add(1)) {
542
            $result = $result->multiply($i);
543
        }
544
        return $result;
545
    }
546
547
    /**
548
     * @param int $scale number of decimal places to round this number to; may be negative to round to higher orders
549
     * @return Decimal a new decimal number, representing this number rounded to <tt>$scale</tt> decimal places
550
     */
551
    public function round(int $scale = 0): Decimal
552
    {
553
        return new Decimal($this->val, $scale);
554
    }
555
556
    /**
557
     * @return Decimal largest integer not greater than this number
558
     */
559
    public function floor(): Decimal
560
    {
561
        if ($this->isNaN()) {
562
            return $this;
563
        }
564
565
        $decPoint = strpos($this->val, '.');
566
        if ($decPoint === false) {
567
            return $this;
568
        }
569
570
        if ($this->val[0] != '-') {
571
            return new Decimal(substr($this->val, 0, $decPoint));
572
        } elseif ($this->isInteger()) {
573
            return new Decimal($this->val, 0);
574
        } else {
575
            // negative number with non-zero fractional part
576
            return new Decimal(bcsub(substr($this->val, 0, $decPoint), '1', 0));
577
        }
578
    }
579
580
    /**
581
     * @return Decimal smallest integer not less than this number
582
     */
583
    public function ceil(): Decimal
584
    {
585
        if ($this->isNaN()) {
586
            return $this;
587
        }
588
589
        $decPoint = strpos($this->val, '.');
590
        if ($decPoint === false) {
591
            return $this;
592
        }
593
594
        if ($this->val[0] == '-') {
595
            return new Decimal(substr($this->val, 0, $decPoint));
596
        } elseif ($this->isInteger()) {
597
            return new Decimal($this->val, 0);
598
        } else {
599
            // non-negative number with non-zero fractional part
600
            return new Decimal(bcadd(substr($this->val, 0, $decPoint), '1', 0));
601
        }
602
    }
603
604
    /**
605
     * @param string|int|float|Decimal|object $number
606
     * @return Decimal the greater of this number and <tt>$number</tt>, preferably this number if numerically equal
607
     */
608
    public function max($number): Decimal
609
    {
610
        $arg = self::fromNumber($number);
611
        if ($this->lessThan($arg)) {
612
            return $arg;
613
        } else {
614
            return $this;
615
        }
616
    }
617
618
    /**
619
     * @param string|int|float|Decimal|object $number
620
     * @return Decimal the smaller of this number and <tt>$number</tt>, preferably this number if numerically equal
621
     */
622
    public function min($number): Decimal
623
    {
624
        $arg = self::fromNumber($number);
625
        if ($this->greaterThan($arg)) {
626
            return $arg;
627
        } else {
628
            return $this;
629
        }
630
    }
631
632
    /**
633
     * @return Decimal a new decimal number, representing the square root of this number
634
     * @throws UndefinedOperationException if this is a negative number
635
     */
636
    public function sqrt(): Decimal
637
    {
638
        if ($this->isNaN()) {
639
            return $this;
640
        }
641
        if ($this->isNegative()) {
642
            throw new UndefinedOperationException('square root of negative number');
643
        }
644
        $scale = max($this->scale, self::MIN_SIG_DIGITS);
645
        return new Decimal(bcsqrt($this->val, $scale + 1), $scale);
646
    }
647
648
649
    /**
650
     * @return int|null the value of this number cast explicitly to <tt>int</tt>;
651
     *                  for big numbers, this yields the maximal available integer value (i.e., <tt>PHP_INT_MAX</tt> or
652
     *                    <tt>PHP_INT_MIN</tt>);
653
     *                  <tt>null</tt> is returned if this is the not-a-number
654
     */
655
    public function toInt(): ?int
656
    {
657
        return ($this->val === null ? null : (int)$this->val);
658
    }
659
660
    /**
661
     * @return float the value of this number cast explicitly to <tt>float</tt>, or the float <tt>NAN</tt> value if this
662
     *                 is the not-a-number
663
     */
664
    public function toFloat(): float
665
    {
666
        return ($this->val === null ? NAN : (float)$this->val);
667
    }
668
669
    /**
670
     * @return string the value of this number, or the special string <tt>'NaN'</tt> for the not-a-number value
671
     */
672
    public function toString(): string
673
    {
674
        return ($this->val === null ? 'NaN' : $this->val);
675
    }
676
677
    /** @noinspection PhpComposerExtensionStubsInspection */
678
    /**
679
     * Converts the value to a {@link \GMP} object.
680
     *
681
     * Requires the `gmp` PHP extension.
682
     *
683
     * @return \GMP a {@link \GMP} object representing the same integer as this number
684
     * @throws UndefinedOperationException if this number is not an integer, i.e., has some non-zero fractional digits
685
     */
686
    public function toGMP(): \GMP
687
    {
688
        if (!System::hasGMP()) {
689
            throw new \RuntimeException('The PHP gmp extension is not available');
690
        }
691
        if (!$this->isInteger()) {
692
            throw new UndefinedOperationException('toGMP() is only defined for integers');
693
        }
694
        $decPoint = strpos($this->val, '.');
695
        $str = ($decPoint === false ? $this->val : substr($this->val, 0, $decPoint - 1));
696
        /** @noinspection PhpComposerExtensionStubsInspection */
697
        return gmp_init($str);
0 ignored issues
show
Bug Best Practice introduced by
The expression return gmp_init($str) could return the type resource which is incompatible with the type-hinted return GMP. Consider adding an additional type-check to rule them out.
Loading history...
698
    }
699
700
    public function __toString()
701
    {
702
        return $this->toString();
703
    }
704
}
705