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