Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | 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;
6
 
7
use Brick\Math\Exception\DivisionByZeroException;
8
use Brick\Math\Exception\MathException;
9
use Brick\Math\Exception\NumberFormatException;
10
use Brick\Math\Exception\RoundingNecessaryException;
11
 
12
/**
13
 * Common interface for arbitrary-precision rational numbers.
14
 *
15
 * @psalm-immutable
16
 */
17
abstract class BigNumber implements \Serializable, \JsonSerializable
18
{
19
    /**
20
     * The regular expression used to parse integer, decimal and rational numbers.
21
     */
22
    private const PARSE_REGEXP =
23
        '/^' .
24
            '(?<sign>[\-\+])?' .
25
            '(?:' .
26
                '(?:' .
27
                    '(?<integral>[0-9]+)?' .
28
                    '(?<point>\.)?' .
29
                    '(?<fractional>[0-9]+)?' .
30
                    '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
31
                ')|(?:' .
32
                    '(?<numerator>[0-9]+)' .
33
                    '\/?' .
34
                    '(?<denominator>[0-9]+)' .
35
                ')' .
36
            ')' .
37
        '$/';
38
 
39
    /**
40
     * Creates a BigNumber of the given value.
41
     *
42
     * The concrete return type is dependent on the given value, with the following rules:
43
     *
44
     * - BigNumber instances are returned as is
45
     * - integer numbers are returned as BigInteger
46
     * - floating point numbers are converted to a string then parsed as such
47
     * - strings containing a `/` character are returned as BigRational
48
     * - strings containing a `.` character or using an exponential notation are returned as BigDecimal
49
     * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
50
     *
51
     * @param BigNumber|int|float|string $value
52
     *
53
     * @return BigNumber
54
     *
55
     * @throws NumberFormatException   If the format of the number is not valid.
56
     * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
57
     *
58
     * @psalm-pure
59
     */
60
    public static function of($value) : BigNumber
61
    {
62
        if ($value instanceof BigNumber) {
63
            return $value;
64
        }
65
 
66
        if (\is_int($value)) {
67
            return new BigInteger((string) $value);
68
        }
69
 
70
        /** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */
71
        $value = \is_float($value) ? self::floatToString($value) : (string) $value;
72
 
73
        $throw = static function() use ($value) : void {
74
            throw new NumberFormatException(\sprintf(
75
                'The given value "%s" does not represent a valid number.',
76
                $value
77
            ));
78
        };
79
 
80
        if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
81
            $throw();
82
        }
83
 
84
        $getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
85
 
86
        $sign        = $getMatch('sign');
87
        $numerator   = $getMatch('numerator');
88
        $denominator = $getMatch('denominator');
89
 
90
        if ($numerator !== null) {
91
            assert($denominator !== null);
92
 
93
            if ($sign !== null) {
94
                $numerator = $sign . $numerator;
95
            }
96
 
97
            $numerator   = self::cleanUp($numerator);
98
            $denominator = self::cleanUp($denominator);
99
 
100
            if ($denominator === '0') {
101
                throw DivisionByZeroException::denominatorMustNotBeZero();
102
            }
103
 
104
            return new BigRational(
105
                new BigInteger($numerator),
106
                new BigInteger($denominator),
107
                false
108
            );
109
        }
110
 
111
        $point      = $getMatch('point');
112
        $integral   = $getMatch('integral');
113
        $fractional = $getMatch('fractional');
114
        $exponent   = $getMatch('exponent');
115
 
116
        if ($integral === null && $fractional === null) {
117
            $throw();
118
        }
119
 
120
        if ($integral === null) {
121
            $integral = '0';
122
        }
123
 
124
        if ($point !== null || $exponent !== null) {
125
            $fractional = ($fractional ?? '');
126
            $exponent = ($exponent !== null) ? (int) $exponent : 0;
127
 
128
            if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
129
                throw new NumberFormatException('Exponent too large.');
130
            }
131
 
132
            $unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
133
 
134
            $scale = \strlen($fractional) - $exponent;
135
 
136
            if ($scale < 0) {
137
                if ($unscaledValue !== '0') {
138
                    $unscaledValue .= \str_repeat('0', - $scale);
139
                }
140
                $scale = 0;
141
            }
142
 
143
            return new BigDecimal($unscaledValue, $scale);
144
        }
145
 
146
        $integral = self::cleanUp(($sign ?? '') . $integral);
147
 
148
        return new BigInteger($integral);
149
    }
150
 
151
    /**
152
     * Safely converts float to string, avoiding locale-dependent issues.
153
     *
154
     * @see https://github.com/brick/math/pull/20
155
     *
156
     * @param float $float
157
     *
158
     * @return string
159
     *
160
     * @psalm-pure
161
     * @psalm-suppress ImpureFunctionCall
162
     */
163
    private static function floatToString(float $float) : string
164
    {
165
        $currentLocale = \setlocale(LC_NUMERIC, '0');
166
        \setlocale(LC_NUMERIC, 'C');
167
 
168
        $result = (string) $float;
169
 
170
        \setlocale(LC_NUMERIC, $currentLocale);
171
 
172
        return $result;
173
    }
174
 
175
    /**
176
     * Proxy method to access protected constructors from sibling classes.
177
     *
178
     * @internal
179
     *
180
     * @param mixed ...$args The arguments to the constructor.
181
     *
182
     * @return static
183
     *
184
     * @psalm-pure
185
     * @psalm-suppress TooManyArguments
186
     * @psalm-suppress UnsafeInstantiation
187
     */
188
    protected static function create(... $args) : BigNumber
189
    {
190
        return new static(... $args);
191
    }
192
 
193
    /**
194
     * Returns the minimum of the given values.
195
     *
196
     * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
197
     *                                              to an instance of the class this method is called on.
198
     *
199
     * @return static The minimum value.
200
     *
201
     * @throws \InvalidArgumentException If no values are given.
202
     * @throws MathException             If an argument is not valid.
203
     *
204
     * @psalm-suppress LessSpecificReturnStatement
205
     * @psalm-suppress MoreSpecificReturnType
206
     * @psalm-pure
207
     */
208
    public static function min(...$values) : BigNumber
209
    {
210
        $min = null;
211
 
212
        foreach ($values as $value) {
213
            $value = static::of($value);
214
 
215
            if ($min === null || $value->isLessThan($min)) {
216
                $min = $value;
217
            }
218
        }
219
 
220
        if ($min === null) {
221
            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
222
        }
223
 
224
        return $min;
225
    }
226
 
227
    /**
228
     * Returns the maximum of the given values.
229
     *
230
     * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
231
     *                                              to an instance of the class this method is called on.
232
     *
233
     * @return static The maximum value.
234
     *
235
     * @throws \InvalidArgumentException If no values are given.
236
     * @throws MathException             If an argument is not valid.
237
     *
238
     * @psalm-suppress LessSpecificReturnStatement
239
     * @psalm-suppress MoreSpecificReturnType
240
     * @psalm-pure
241
     */
242
    public static function max(...$values) : BigNumber
243
    {
244
        $max = null;
245
 
246
        foreach ($values as $value) {
247
            $value = static::of($value);
248
 
249
            if ($max === null || $value->isGreaterThan($max)) {
250
                $max = $value;
251
            }
252
        }
253
 
254
        if ($max === null) {
255
            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
256
        }
257
 
258
        return $max;
259
    }
260
 
261
    /**
262
     * Returns the sum of the given values.
263
     *
264
     * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
265
     *                                              to an instance of the class this method is called on.
266
     *
267
     * @return static The sum.
268
     *
269
     * @throws \InvalidArgumentException If no values are given.
270
     * @throws MathException             If an argument is not valid.
271
     *
272
     * @psalm-suppress LessSpecificReturnStatement
273
     * @psalm-suppress MoreSpecificReturnType
274
     * @psalm-pure
275
     */
276
    public static function sum(...$values) : BigNumber
277
    {
278
        /** @var BigNumber|null $sum */
279
        $sum = null;
280
 
281
        foreach ($values as $value) {
282
            $value = static::of($value);
283
 
284
            $sum = $sum === null ? $value : self::add($sum, $value);
285
        }
286
 
287
        if ($sum === null) {
288
            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
289
        }
290
 
291
        return $sum;
292
    }
293
 
294
    /**
295
     * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
296
     *
297
     * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
298
     *       concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
299
     *       depending on their ability to perform the operation. This will also require a version bump because we're
300
     *       potentially breaking custom BigNumber implementations (if any...)
301
     *
302
     * @param BigNumber $a
303
     * @param BigNumber $b
304
     *
305
     * @return BigNumber
306
     *
307
     * @psalm-pure
308
     */
309
    private static function add(BigNumber $a, BigNumber $b) : BigNumber
310
    {
311
        if ($a instanceof BigRational) {
312
            return $a->plus($b);
313
        }
314
 
315
        if ($b instanceof BigRational) {
316
            return $b->plus($a);
317
        }
318
 
319
        if ($a instanceof BigDecimal) {
320
            return $a->plus($b);
321
        }
322
 
323
        if ($b instanceof BigDecimal) {
324
            return $b->plus($a);
325
        }
326
 
327
        /** @var BigInteger $a */
328
 
329
        return $a->plus($b);
330
    }
331
 
332
    /**
333
     * Removes optional leading zeros and + sign from the given number.
334
     *
335
     * @param string $number The number, validated as a non-empty string of digits with optional leading sign.
336
     *
337
     * @return string
338
     *
339
     * @psalm-pure
340
     */
341
    private static function cleanUp(string $number) : string
342
    {
343
        $firstChar = $number[0];
344
 
345
        if ($firstChar === '+' || $firstChar === '-') {
346
            $number = \substr($number, 1);
347
        }
348
 
349
        $number = \ltrim($number, '0');
350
 
351
        if ($number === '') {
352
            return '0';
353
        }
354
 
355
        if ($firstChar === '-') {
356
            return '-' . $number;
357
        }
358
 
359
        return $number;
360
    }
361
 
362
    /**
363
     * Checks if this number is equal to the given one.
364
     *
365
     * @param BigNumber|int|float|string $that
366
     *
367
     * @return bool
368
     */
369
    public function isEqualTo($that) : bool
370
    {
371
        return $this->compareTo($that) === 0;
372
    }
373
 
374
    /**
375
     * Checks if this number is strictly lower than the given one.
376
     *
377
     * @param BigNumber|int|float|string $that
378
     *
379
     * @return bool
380
     */
381
    public function isLessThan($that) : bool
382
    {
383
        return $this->compareTo($that) < 0;
384
    }
385
 
386
    /**
387
     * Checks if this number is lower than or equal to the given one.
388
     *
389
     * @param BigNumber|int|float|string $that
390
     *
391
     * @return bool
392
     */
393
    public function isLessThanOrEqualTo($that) : bool
394
    {
395
        return $this->compareTo($that) <= 0;
396
    }
397
 
398
    /**
399
     * Checks if this number is strictly greater than the given one.
400
     *
401
     * @param BigNumber|int|float|string $that
402
     *
403
     * @return bool
404
     */
405
    public function isGreaterThan($that) : bool
406
    {
407
        return $this->compareTo($that) > 0;
408
    }
409
 
410
    /**
411
     * Checks if this number is greater than or equal to the given one.
412
     *
413
     * @param BigNumber|int|float|string $that
414
     *
415
     * @return bool
416
     */
417
    public function isGreaterThanOrEqualTo($that) : bool
418
    {
419
        return $this->compareTo($that) >= 0;
420
    }
421
 
422
    /**
423
     * Checks if this number equals zero.
424
     *
425
     * @return bool
426
     */
427
    public function isZero() : bool
428
    {
429
        return $this->getSign() === 0;
430
    }
431
 
432
    /**
433
     * Checks if this number is strictly negative.
434
     *
435
     * @return bool
436
     */
437
    public function isNegative() : bool
438
    {
439
        return $this->getSign() < 0;
440
    }
441
 
442
    /**
443
     * Checks if this number is negative or zero.
444
     *
445
     * @return bool
446
     */
447
    public function isNegativeOrZero() : bool
448
    {
449
        return $this->getSign() <= 0;
450
    }
451
 
452
    /**
453
     * Checks if this number is strictly positive.
454
     *
455
     * @return bool
456
     */
457
    public function isPositive() : bool
458
    {
459
        return $this->getSign() > 0;
460
    }
461
 
462
    /**
463
     * Checks if this number is positive or zero.
464
     *
465
     * @return bool
466
     */
467
    public function isPositiveOrZero() : bool
468
    {
469
        return $this->getSign() >= 0;
470
    }
471
 
472
    /**
473
     * Returns the sign of this number.
474
     *
475
     * @return int -1 if the number is negative, 0 if zero, 1 if positive.
476
     */
477
    abstract public function getSign() : int;
478
 
479
    /**
480
     * Compares this number to the given one.
481
     *
482
     * @param BigNumber|int|float|string $that
483
     *
484
     * @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
485
     *
486
     * @throws MathException If the number is not valid.
487
     */
488
    abstract public function compareTo($that) : int;
489
 
490
    /**
491
     * Converts this number to a BigInteger.
492
     *
493
     * @return BigInteger The converted number.
494
     *
495
     * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
496
     */
497
    abstract public function toBigInteger() : BigInteger;
498
 
499
    /**
500
     * Converts this number to a BigDecimal.
501
     *
502
     * @return BigDecimal The converted number.
503
     *
504
     * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
505
     */
506
    abstract public function toBigDecimal() : BigDecimal;
507
 
508
    /**
509
     * Converts this number to a BigRational.
510
     *
511
     * @return BigRational The converted number.
512
     */
513
    abstract public function toBigRational() : BigRational;
514
 
515
    /**
516
     * Converts this number to a BigDecimal with the given scale, using rounding if necessary.
517
     *
518
     * @param int $scale        The scale of the resulting `BigDecimal`.
519
     * @param int $roundingMode A `RoundingMode` constant.
520
     *
521
     * @return BigDecimal
522
     *
523
     * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
524
     *                                    This only applies when RoundingMode::UNNECESSARY is used.
525
     */
526
    abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
527
 
528
    /**
529
     * Returns the exact value of this number as a native integer.
530
     *
531
     * If this number cannot be converted to a native integer without losing precision, an exception is thrown.
532
     * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
533
     *
534
     * @return int The converted value.
535
     *
536
     * @throws MathException If this number cannot be exactly converted to a native integer.
537
     */
538
    abstract public function toInt() : int;
539
 
540
    /**
541
     * Returns an approximation of this number as a floating-point value.
542
     *
543
     * Note that this method can discard information as the precision of a floating-point value
544
     * is inherently limited.
545
     *
546
     * If the number is greater than the largest representable floating point number, positive infinity is returned.
547
     * If the number is less than the smallest representable floating point number, negative infinity is returned.
548
     *
549
     * @return float The converted value.
550
     */
551
    abstract public function toFloat() : float;
552
 
553
    /**
554
     * Returns a string representation of this number.
555
     *
556
     * The output of this method can be parsed by the `of()` factory method;
557
     * this will yield an object equal to this one, without any information loss.
558
     *
559
     * @return string
560
     */
561
    abstract public function __toString() : string;
562
 
563
    /**
564
     * {@inheritdoc}
565
     */
566
    public function jsonSerialize() : string
567
    {
568
        return $this->__toString();
569
    }
570
}