Completed
Push — master ( e80728...a678e6 )
by Marcos
14:33
created

IntMath::assertInteger()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
 * This file is part of the phpcommon/intmath package.
5
 *
6
 * (c) Marcos Passos <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE file
9
 * that was distributed with this source code.
10
 */
11
12
namespace PhpCommon\IntMath;
13
14
use InvalidArgumentException;
15
16
// Only available on PHP >= 7
17 2
if (!defined('PHP_INT_MIN')) {
18 2
    define('PHP_INT_MIN', -PHP_INT_MAX - 1);
19 2
}
20
21
/**
22
 * Utility class for arithmetic operations on integers that wraps around upon
23
 * overflow.
24
 *
25
 * **Important**
26
 *
27
 * This class is not intended for use as a way to properly perform arithmetic
28
 * operations on integers and should not be used in place of the native
29
 * arithmetic operators or any other library designed for such purpose.
30
 *
31
 * Unlike other languages that overflow large positive integers into large
32
 * negative integers, PHP actually overflows integers to floating-point
33
 * numbers. In most cases, arithmetic overflows must be treated as an unusual
34
 * circumstance which requires special handling. However, there are some cases
35
 * where such _wrap-around_ behavior is actually useful - for example with TCP
36
 * sequence numbers or certain algorithms, such as hash calculation. This
37
 * utility class provides basic arithmetic functions that operate in accordance
38
 * to that behaviour.
39
 *
40
 * To illustrate, consider the following example:
41
 *
42
 * ```php
43
 * // Output on 64-bit system: float(9.2233720368548E+18)
44
 * var_dump(PHP_MAX_INT + 1);
45
 *
46
 * // Output on 64-bit system: int(-9223372036854775808)
47
 * var_dump(IntMath::add(PHP_MAX_INT, 1));
48
 * ```
49
 *
50
 * As previously shown, adding one to the largest supported integer using
51
 * native arithmetic operators will result in a floating-point number. By
52
 * contrast, using {@link IntMath::add()} will cause an overflow, resulting
53
 * in the smallest integer supported in this build of PHP.
54
 *
55
 * @author Marcos Passos <[email protected]>
56
 */
57
class IntMath
58
{
59
    /**
60
     * The largest supported integer
61
     */
62
    const MAX_INT = PHP_INT_MAX;
63
64
    /**
65
     * The smallest supported integer
66
     */
67
    const MIN_INT = PHP_INT_MIN;
68
69
    /**
70
     * Returns the negation of the argument.
71
     *
72
     * For integer values, negation is the same as subtraction from zero.
73
     * Because PHP uses two's-complement representation for integers and the
74
     * range of two's-complement values is not symmetric, the negation of the
75
     * maximum negative integer results in that same maximum negative number.
76
     * Despite the fact that overflow has occurred, no exception is thrown.
77
     *
78
     * For all integer values `$a`, `-$a` equals `(~$a) + 1`.
79
     *
80
     * @param integer $a The value.
81
     *
82
     * @return integer The result.
83
     *
84
     * @throws InvalidArgumentException If the argument is not an integer.
85
     */
86 26
    public static function negate($a)
87
    {
88 26
        self::assertInteger($a);
89
90 4
        if ($a === self::MIN_INT) {
91 2
            return $a;
92
        }
93
94 2
        return -$a;
95
    }
96
97
    /**
98
     * Returns the sum of the arguments.
99
     *
100
     * The result is the low-order bits of the true mathematical result as
101
     * represented in a sufficiently wide two's-complement format. If overflow
102
     * occurs, then the sign of the result may not be the same as the sign of
103
     * the mathematical sum of the two values. Despite the overflow, no
104
     * exception is thrown in this case.
105
     *
106
     * @param integer $a The addend.
107
     * @param integer $b The addend.
108
     *
109
     * @return integer The sum.
110
     *
111
     * @throws InvalidArgumentException If one of the arguments is not an
112
     *                                  integer.
113
     */
114 26
    public static function add($a, $b)
115
    {
116 26
        self::assertInteger($a);
117 4
        self::assertInteger($b);
118
119 4
        if (($b > 0) && ($a <= (PHP_INT_MAX - $b))) {
120 2
            return $a + $b;
121
        }
122
123 4
        if (($b < 0) && ($a >= (PHP_INT_MIN - $b))) {
124 2
            return $a + $b;
125
        }
126
127 4
        while ($b !== 0) {
128
            // Carry now contains common set bits of the addends
129 2
            $carry = $a & $b;
130
            // Sum of bits of $x and $y,
131
            // where at least one of the bits is not set
132 2
            $a ^= $b;
133
            // Left-shift by one
134 2
            $b = $carry << 1;
135 2
        }
136
137 4
        return $a;
138
    }
139
140
    /**
141
     * Returns the difference of the arguments.
142
     *
143
     * The subtraction of a positive number yields the same result as the
144
     * addition of a negative number of equal magnitude. Furthermore, the
145
     * subtraction from zero is the same as negation.
146
     *
147
     * The result is the low-order bits of the true mathematical result as
148
     * represented in a sufficiently wide two's-complement format. If overflow
149
     * occurs, then the sign of the result may not be the same as the sign of
150
     * the mathematical difference of the two values. Despite the overflow, no
151
     * exception is thrown in this case.
152
     *
153
     * @param integer $a The minuend.
154
     * @param integer $b The subtrahend.
155
     *
156
     * @return integer The difference.
157
     *
158
     * @throws InvalidArgumentException If one of the arguments is not an
159
     *                                  integer.
160
     */
161 26
    public static function subtract($a, $b)
162
    {
163 26
        self::assertInteger($a);
164 4
        self::assertInteger($b);
165
166 4
        return self::add((int) $a, self::negate($b));
167
    }
168
169
    /**
170
     * Returns the product of the arguments.
171
     *
172
     * The result is the low-order bits of the true mathematical result as
173
     * represented in a sufficiently wide two's-complement format. If overflow
174
     * occurs, then the sign of the result may not be the same as the sign of
175
     * the mathematical product of the two values. Despite the overflow, no
176
     * exception is thrown in this case.
177
     *
178
     * @param integer $a The multiplicand.
179
     * @param integer $b The multiplier.
180
     *
181
     * @return integer The product.
182
     *
183
     * @throws InvalidArgumentException If one of the arguments is not an
184
     *                                  integer.
185
     */
186 26
    public static function multiply($a, $b)
187
    {
188 26
        self::assertInteger($a);
189 4
        self::assertInteger($b);
190
191 4
        if ($a === 0 || $b === 0) {
192 2
            return 0;
193
        }
194
195
        // If the multiplicand or the multiplier are the smallest integer
196
        // supported, then the product is `0` or the smallest integer supported,
197
        // according as the other operand is odd or even respectively.
198 4
        if ($a === self::MIN_INT) {
199 2
            return $b & 0x01 ? $a : 0;
200
        }
201
202 4
        if ($b === self::MIN_INT) {
203 4
            return $a & 0x01 ? $b : 0;
204
        }
205
206 4
        $max = self::MIN_INT;
207
208
        // Same sign
209 4
        if ($a >= 0 && $b >= 0 || $a < 0 && $b < 0) {
210 4
            $max = self::MAX_INT;
211 4
        }
212
213
        // Use native operators unless the operation causes an overflow
214 4
        if (($b > 0 && $b <= ($max / $a)) || ($b < 0 && $b >= ($max / $a))) {
215 2
            return $a * $b;
216
        }
217
218
        // Signed multiplication can be accomplished by doing an unsigned
219
        // multiplication and taking manually care of the negative-signs.
220 2
        $sign = false;
221
222 2
        if ($a < 0) {
223
            // Toggle the signed flag
224 2
            $sign = !$sign;
225
            // Negate $a
226 2
            $a = self::negate($a);
227 2
        }
228
229 2
        if ($b < 0) {
230
            // Toggle the signed flag
231 2
            $sign = !$sign;
232
            // Negate $b
233 2
            $b = self::negate($b);
234 2
        }
235
236 2
        $product = 0;
237
        // Both operands are now positive, perform unsigned multiplication
238 2
        while ($a !== 0) {
239
            // Test the least significant bit (LSB) of multiplier
240 2
            if (($a & 0x01) !== 0) {
241
                // If 1, add the multiplicand to the product
242 2
                $product = self::add($product, $b);
243 2
            }
244
245
            // Left-shift by one, or divide by 2
246 2
            $a >>= 1;
247
248
            // Right-shift by one, or multiply by 2
249 2
            $b <<= 1;
250 2
        }
251
252 2
        if ($sign) {
253
            // Negate the product
254 2
            $product = self::negate($product);
255 2
        }
256
257 2
        return $product;
258
    }
259
260
    /**
261
     * Returns the quotient of the arguments.
262
     *
263
     * The division rounds the result towards zero. Thus the absolute value of
264
     * the result is the largest possible integer that is less than or equal to
265
     * the absolute value of the quotient of the two operands. The result is
266
     * zero or positive when the two operands have the same sign and zero or
267
     * negative when the two operands have opposite signs.
268
     *
269
     * There is one special case that does not satisfy this rule: if the
270
     * dividend is the negative integer of largest possible magnitude for its
271
     * type, and the divisor is `-1`, then integer overflow occurs and the
272
     * result is equal to the dividend. Despite the overflow, no exception is
273
     * thrown in this case. On the other hand if the value of the divisor in an
274
     * integer division is `0`, then a `DivisionByZeroException` is thrown.
275
     *
276
     * @param integer $a The dividend.
277
     * @param integer $b The divisor.
278
     *
279
     * @return integer The quotient.
280
     *
281
     * @throws InvalidArgumentException If one of the arguments is not an
282
     *                                  integer.
283
     * @throws DivisionByZeroException  If the divisor is zero.
284
     */
285 30
    public static function divide($a, $b)
286
    {
287 30
        self::assertInteger($a);
288 8
        self::assertInteger($b);
289
290 8
        if (0 === $b) {
291 2
            throw new DivisionByZeroException('Division by zero.');
292
        }
293
294 6
        if ($a === self::MIN_INT && $b === -1) {
295 2
            return $a;
296
        }
297
298 4
        return ($a - ($a % $b)) / $b;
299
    }
300
301
    /**
302
     * Asserts the specified value is an integer.
303
     *
304
     * @param mixed $value The value to assert.
305
     *
306
     * @throws InvalidArgumentException If the value is not an integer.
307
     */
308 112
    private static function assertInteger($value)
309
    {
310 112
        if (is_int($value)) {
311 2
            return;
312
        }
313
314 110
        throw new InvalidArgumentException(sprintf(
315 110
            'Expected an integer, but got "%s"',
316 20
            self::identify($value)
317 20
        ));
318
    }
319 90
320 10
    /**
321 10
     * Returns a string that identifies the specified value.
322
     *
323 80
     * @param mixed $value The value to identify.
324 80
     *
325 80
     * @return string A string that identifies the specified value.
326
     */
327 110
    private static function identify($value)
328 110
    {
329
        if (!is_numeric($value) || !is_float($value)) {
330 110
            return gettype($value);
331
        }
332
333
        if (is_infinite($value)) {
334
            return $value < 0 ? '-INF' : 'INF';
335
        }
336
337
        if (is_nan($value)) {
338
            return 'NAN';
339
        }
340
341
        return 'float';
342
    }
343
}
344