Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace Brick\Math\Internal\Calculator;
6
 
7
use Brick\Math\Internal\Calculator;
8
 
9
/**
10
 * Calculator implementation using only native PHP code.
11
 *
12
 * @internal
13
 *
14
 * @psalm-immutable
15
 */
16
class NativeCalculator extends Calculator
17
{
18
    /**
19
     * The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
20
     * For multiplication, this represents the max sum of the lengths of both operands.
21
     *
399 lars 22
     * In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
148 lars 23
     * Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
24
     *          64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
25
     */
26
    private int $maxDigits;
27
 
28
    /**
29
     * @codeCoverageIgnore
30
     */
31
    public function __construct()
32
    {
33
        switch (PHP_INT_SIZE) {
34
            case 4:
35
                $this->maxDigits = 9;
36
                break;
37
 
38
            case 8:
39
                $this->maxDigits = 18;
40
                break;
41
 
42
            default:
43
                throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
44
        }
45
    }
46
 
47
    public function add(string $a, string $b) : string
48
    {
49
        /**
50
         * @psalm-var numeric-string $a
51
         * @psalm-var numeric-string $b
52
         */
53
        $result = $a + $b;
54
 
55
        if (is_int($result)) {
56
            return (string) $result;
57
        }
58
 
59
        if ($a === '0') {
60
            return $b;
61
        }
62
 
63
        if ($b === '0') {
64
            return $a;
65
        }
66
 
67
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
68
 
69
        $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
70
 
71
        if ($aNeg) {
72
            $result = $this->neg($result);
73
        }
74
 
75
        return $result;
76
    }
77
 
78
    public function sub(string $a, string $b) : string
79
    {
80
        return $this->add($a, $this->neg($b));
81
    }
82
 
83
    public function mul(string $a, string $b) : string
84
    {
85
        /**
86
         * @psalm-var numeric-string $a
87
         * @psalm-var numeric-string $b
88
         */
89
        $result = $a * $b;
90
 
91
        if (is_int($result)) {
92
            return (string) $result;
93
        }
94
 
95
        if ($a === '0' || $b === '0') {
96
            return '0';
97
        }
98
 
99
        if ($a === '1') {
100
            return $b;
101
        }
102
 
103
        if ($b === '1') {
104
            return $a;
105
        }
106
 
107
        if ($a === '-1') {
108
            return $this->neg($b);
109
        }
110
 
111
        if ($b === '-1') {
112
            return $this->neg($a);
113
        }
114
 
115
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
116
 
117
        $result = $this->doMul($aDig, $bDig);
118
 
119
        if ($aNeg !== $bNeg) {
120
            $result = $this->neg($result);
121
        }
122
 
123
        return $result;
124
    }
125
 
126
    public function divQ(string $a, string $b) : string
127
    {
128
        return $this->divQR($a, $b)[0];
129
    }
130
 
131
    public function divR(string $a, string $b): string
132
    {
133
        return $this->divQR($a, $b)[1];
134
    }
135
 
136
    public function divQR(string $a, string $b) : array
137
    {
138
        if ($a === '0') {
139
            return ['0', '0'];
140
        }
141
 
142
        if ($a === $b) {
143
            return ['1', '0'];
144
        }
145
 
146
        if ($b === '1') {
147
            return [$a, '0'];
148
        }
149
 
150
        if ($b === '-1') {
151
            return [$this->neg($a), '0'];
152
        }
153
 
154
        /** @psalm-var numeric-string $a */
155
        $na = $a * 1; // cast to number
156
 
157
        if (is_int($na)) {
158
            /** @psalm-var numeric-string $b */
159
            $nb = $b * 1;
160
 
161
            if (is_int($nb)) {
162
                // the only division that may overflow is PHP_INT_MIN / -1,
163
                // which cannot happen here as we've already handled a divisor of -1 above.
164
                $r = $na % $nb;
165
                $q = ($na - $r) / $nb;
166
 
167
                assert(is_int($q));
168
 
169
                return [
170
                    (string) $q,
171
                    (string) $r
172
                ];
173
            }
174
        }
175
 
176
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
177
 
178
        [$q, $r] = $this->doDiv($aDig, $bDig);
179
 
180
        if ($aNeg !== $bNeg) {
181
            $q = $this->neg($q);
182
        }
183
 
184
        if ($aNeg) {
185
            $r = $this->neg($r);
186
        }
187
 
188
        return [$q, $r];
189
    }
190
 
191
    public function pow(string $a, int $e) : string
192
    {
193
        if ($e === 0) {
194
            return '1';
195
        }
196
 
197
        if ($e === 1) {
198
            return $a;
199
        }
200
 
201
        $odd = $e % 2;
202
        $e -= $odd;
203
 
204
        $aa = $this->mul($a, $a);
205
 
206
        /** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
207
        $result = $this->pow($aa, $e / 2);
208
 
209
        if ($odd === 1) {
210
            $result = $this->mul($result, $a);
211
        }
212
 
213
        return $result;
214
    }
215
 
216
    /**
217
     * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
218
     */
219
    public function modPow(string $base, string $exp, string $mod) : string
220
    {
221
        // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
222
        if ($base === '0' && $exp === '0' && $mod === '1') {
223
            return '0';
224
        }
225
 
226
        // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
227
        if ($exp === '0' && $mod === '1') {
228
            return '0';
229
        }
230
 
231
        $x = $base;
232
 
233
        $res = '1';
234
 
235
        // numbers are positive, so we can use remainder instead of modulo
236
        $x = $this->divR($x, $mod);
237
 
238
        while ($exp !== '0') {
239
            if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
240
                $res = $this->divR($this->mul($res, $x), $mod);
241
            }
242
 
243
            $exp = $this->divQ($exp, '2');
244
            $x = $this->divR($this->mul($x, $x), $mod);
245
        }
246
 
247
        return $res;
248
    }
249
 
250
    /**
251
     * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
252
     */
253
    public function sqrt(string $n) : string
254
    {
255
        if ($n === '0') {
256
            return '0';
257
        }
258
 
259
        // initial approximation
260
        $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
261
 
262
        $decreased = false;
263
 
264
        for (;;) {
265
            $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
266
 
267
            if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
268
                break;
269
            }
270
 
271
            $decreased = $this->cmp($nx, $x) < 0;
272
            $x = $nx;
273
        }
274
 
275
        return $x;
276
    }
277
 
278
    /**
279
     * Performs the addition of two non-signed large integers.
280
     */
281
    private function doAdd(string $a, string $b) : string
282
    {
283
        [$a, $b, $length] = $this->pad($a, $b);
284
 
285
        $carry = 0;
286
        $result = '';
287
 
288
        for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
289
            $blockLength = $this->maxDigits;
290
 
291
            if ($i < 0) {
292
                $blockLength += $i;
293
                /** @psalm-suppress LoopInvalidation */
294
                $i = 0;
295
            }
296
 
297
            /** @psalm-var numeric-string $blockA */
298
            $blockA = \substr($a, $i, $blockLength);
299
 
300
            /** @psalm-var numeric-string $blockB */
301
            $blockB = \substr($b, $i, $blockLength);
302
 
303
            $sum = (string) ($blockA + $blockB + $carry);
304
            $sumLength = \strlen($sum);
305
 
306
            if ($sumLength > $blockLength) {
307
                $sum = \substr($sum, 1);
308
                $carry = 1;
309
            } else {
310
                if ($sumLength < $blockLength) {
311
                    $sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
312
                }
313
                $carry = 0;
314
            }
315
 
316
            $result = $sum . $result;
317
 
318
            if ($i === 0) {
319
                break;
320
            }
321
        }
322
 
323
        if ($carry === 1) {
324
            $result = '1' . $result;
325
        }
326
 
327
        return $result;
328
    }
329
 
330
    /**
331
     * Performs the subtraction of two non-signed large integers.
332
     */
333
    private function doSub(string $a, string $b) : string
334
    {
335
        if ($a === $b) {
336
            return '0';
337
        }
338
 
339
        // Ensure that we always subtract to a positive result: biggest minus smallest.
340
        $cmp = $this->doCmp($a, $b);
341
 
342
        $invert = ($cmp === -1);
343
 
344
        if ($invert) {
345
            $c = $a;
346
            $a = $b;
347
            $b = $c;
348
        }
349
 
350
        [$a, $b, $length] = $this->pad($a, $b);
351
 
352
        $carry = 0;
353
        $result = '';
354
 
355
        $complement = 10 ** $this->maxDigits;
356
 
357
        for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
358
            $blockLength = $this->maxDigits;
359
 
360
            if ($i < 0) {
361
                $blockLength += $i;
362
                /** @psalm-suppress LoopInvalidation */
363
                $i = 0;
364
            }
365
 
366
            /** @psalm-var numeric-string $blockA */
367
            $blockA = \substr($a, $i, $blockLength);
368
 
369
            /** @psalm-var numeric-string $blockB */
370
            $blockB = \substr($b, $i, $blockLength);
371
 
372
            $sum = $blockA - $blockB - $carry;
373
 
374
            if ($sum < 0) {
375
                $sum += $complement;
376
                $carry = 1;
377
            } else {
378
                $carry = 0;
379
            }
380
 
381
            $sum = (string) $sum;
382
            $sumLength = \strlen($sum);
383
 
384
            if ($sumLength < $blockLength) {
385
                $sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
386
            }
387
 
388
            $result = $sum . $result;
389
 
390
            if ($i === 0) {
391
                break;
392
            }
393
        }
394
 
395
        // Carry cannot be 1 when the loop ends, as a > b
396
        assert($carry === 0);
397
 
398
        $result = \ltrim($result, '0');
399
 
400
        if ($invert) {
401
            $result = $this->neg($result);
402
        }
403
 
404
        return $result;
405
    }
406
 
407
    /**
408
     * Performs the multiplication of two non-signed large integers.
409
     */
410
    private function doMul(string $a, string $b) : string
411
    {
412
        $x = \strlen($a);
413
        $y = \strlen($b);
414
 
415
        $maxDigits = \intdiv($this->maxDigits, 2);
416
        $complement = 10 ** $maxDigits;
417
 
418
        $result = '0';
419
 
420
        for ($i = $x - $maxDigits;; $i -= $maxDigits) {
421
            $blockALength = $maxDigits;
422
 
423
            if ($i < 0) {
424
                $blockALength += $i;
425
                /** @psalm-suppress LoopInvalidation */
426
                $i = 0;
427
            }
428
 
429
            $blockA = (int) \substr($a, $i, $blockALength);
430
 
431
            $line = '';
432
            $carry = 0;
433
 
434
            for ($j = $y - $maxDigits;; $j -= $maxDigits) {
435
                $blockBLength = $maxDigits;
436
 
437
                if ($j < 0) {
438
                    $blockBLength += $j;
439
                    /** @psalm-suppress LoopInvalidation */
440
                    $j = 0;
441
                }
442
 
443
                $blockB = (int) \substr($b, $j, $blockBLength);
444
 
445
                $mul = $blockA * $blockB + $carry;
446
                $value = $mul % $complement;
447
                $carry = ($mul - $value) / $complement;
448
 
449
                $value = (string) $value;
450
                $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
451
 
452
                $line = $value . $line;
453
 
454
                if ($j === 0) {
455
                    break;
456
                }
457
            }
458
 
459
            if ($carry !== 0) {
460
                $line = $carry . $line;
461
            }
462
 
463
            $line = \ltrim($line, '0');
464
 
465
            if ($line !== '') {
466
                $line .= \str_repeat('0', $x - $blockALength - $i);
467
                $result = $this->add($result, $line);
468
            }
469
 
470
            if ($i === 0) {
471
                break;
472
            }
473
        }
474
 
475
        return $result;
476
    }
477
 
478
    /**
479
     * Performs the division of two non-signed large integers.
480
     *
481
     * @return string[] The quotient and remainder.
482
     */
483
    private function doDiv(string $a, string $b) : array
484
    {
485
        $cmp = $this->doCmp($a, $b);
486
 
487
        if ($cmp === -1) {
488
            return ['0', $a];
489
        }
490
 
491
        $x = \strlen($a);
492
        $y = \strlen($b);
493
 
494
        // we now know that a >= b && x >= y
495
 
496
        $q = '0'; // quotient
497
        $r = $a; // remainder
498
        $z = $y; // focus length, always $y or $y+1
499
 
500
        for (;;) {
501
            $focus = \substr($a, 0, $z);
502
 
503
            $cmp = $this->doCmp($focus, $b);
504
 
505
            if ($cmp === -1) {
506
                if ($z === $x) { // remainder < dividend
507
                    break;
508
                }
509
 
510
                $z++;
511
            }
512
 
513
            $zeros = \str_repeat('0', $x - $z);
514
 
515
            $q = $this->add($q, '1' . $zeros);
516
            $a = $this->sub($a, $b . $zeros);
517
 
518
            $r = $a;
519
 
520
            if ($r === '0') { // remainder == 0
521
                break;
522
            }
523
 
524
            $x = \strlen($a);
525
 
526
            if ($x < $y) { // remainder < dividend
527
                break;
528
            }
529
 
530
            $z = $y;
531
        }
532
 
533
        return [$q, $r];
534
    }
535
 
536
    /**
537
     * Compares two non-signed large numbers.
538
     *
539
     * @return int [-1, 0, 1]
540
     */
541
    private function doCmp(string $a, string $b) : int
542
    {
543
        $x = \strlen($a);
544
        $y = \strlen($b);
545
 
546
        $cmp = $x <=> $y;
547
 
548
        if ($cmp !== 0) {
549
            return $cmp;
550
        }
551
 
552
        return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
553
    }
554
 
555
    /**
556
     * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
557
     *
558
     * The numbers must only consist of digits, without leading minus sign.
559
     *
560
     * @return array{string, string, int}
561
     */
562
    private function pad(string $a, string $b) : array
563
    {
564
        $x = \strlen($a);
565
        $y = \strlen($b);
566
 
567
        if ($x > $y) {
568
            $b = \str_repeat('0', $x - $y) . $b;
569
 
570
            return [$a, $b, $x];
571
        }
572
 
573
        if ($x < $y) {
574
            $a = \str_repeat('0', $y - $x) . $a;
575
 
576
            return [$a, $b, $y];
577
        }
578
 
579
        return [$a, $b, $x];
580
    }
581
}