Passed
Push — master ( 913c0a...fd8403 )
by Ondřej
05:45
created

Range::compareBounds()   D

Complexity

Conditions 10
Paths 8

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 18
nc 8
nop 5

How to fix   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\ImmutableException;
6
use Ivory\Exception\IncomparableException;
7
use Ivory\Exception\UnsupportedException;
8
use Ivory\Ivory;
9
use Ivory\Type\Std\BigIntSafeType;
10
use Ivory\Value\Alg\BigIntSafeStepper;
11
use Ivory\Value\Alg\IComparable;
12
use Ivory\Value\Alg\IDiscreteStepper;
13
use Ivory\Value\Alg\IntegerStepper;
14
use Ivory\Value\Alg\IValueComparator;
15
16
/**
17
 * A range of values.
18
 *
19
 * The class resembles the range values manipulated by PostgreSQL. Just a brief summary:
20
 * - The range is defined above a type of values, called "subtype".
21
 * - The subtype must have a total order. For this implementation, it means the lower and the upper bounds must be
22
 *   comparable using the {@link Ivory::getDefaultValueComparator() default value comparator}, or using a custom
23
 *   {@link IValueComparator} supplied besides the bounds.
24
 * - A range may be empty, meaning it contains nothing. See {@link Range::isEmpty()} for distinguishing it.
25
 * - A non-empty range has the lower and the upper bounds, either of which may either be inclusive or exclusive. See
26
 *   {@link Range::getLower()} and {@link Range::getUpper()} for getting the boundaries. Also see
27
 *   {@link Range::isLowerInc()} and {@link Range::isUpperInc()} for finding out whichever boundary is inclusive.
28
 * - A range may be unbounded in either direction, covering all subtype values from the range towards minus infinity or
29
 *   towards infinity, respectively.
30
 * - A range might even cover just a single point. {@link Range::isSinglePoint()} may be used for checking out.
31
 *
32
 * In PostgreSQL, ranges on discrete subtypes may have a canonical function defined on them, the purpose of which is to
33
 * convert semantically equivalent ranges to syntactically equivalent ones (e.g., one such function might convert the
34
 * range `[1,3]` to `[1,4)`). Such a feature is *not* implemented on the PHP side. The ranges constructed in PHP are not
35
 * normalized in any way, they may only be converted manually using the {@link Range::toBounds()} method.
36
 *
37
 * A range is `IComparable` with another range provided both have the same subtype. Besides, an empty range may safely
38
 * be compared to any other range.
39
 *
40
 * Alternatively to the {@link Range::getLower()} and {@link Range::getUpper()} methods, `ArrayAccess` is implemented.
41
 * Indexes `0` and `1` may be used to get the lower and upper bound, respectively. Either of them returns `null` if the
42
 * range is empty or unbounded in the given direction.
43
 *
44
 * Note the range value is immutable, i.e., once constructed, its values cannot be changed. Thus, `ArrayAccess` write
45
 * operations ({@link \ArrayAccess::offsetSet()} and {@link \ArrayAccess::offsetUnset()}) throw an
46
 * {@link \Ivory\Exception\ImmutableException}.
47
 *
48
 * @see http://www.postgresql.org/docs/9.4/static/rangetypes.html
49
 */
50
class Range implements IComparable, \ArrayAccess
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
51
{
52
    /** @var bool */
53
    private $empty;
54
    /** @var mixed */
55
    private $lower;
56
    /** @var mixed */
57
    private $upper;
58
    /** @var bool */
59
    private $lowerInc;
60
    /** @var bool */
61
    private $upperInc;
62
    /** @var IValueComparator */
63
    private $comparator;
64
    /** @var IDiscreteStepper */
65
    private $discreteStepper;
66
67
68
    //region construction
69
70
    /**
71
     * Creates a new range with given lower and upper bounds.
72
     *
73
     * There are two ways of calling this factory method: whether the bounds are inclusive or exclusive may be
74
     * specified:
75
     * - either by passing a specification string as the `$boundsOrLowerInc` argument (e.g., `'[)'`),
76
     * - or by passing two boolean values as the `$boundsOrLowerInc` and `$upperInc` telling whichever bound is
77
     *   inclusive.
78
     *
79
     * When the constructed range is effectively empty, an empty range gets created, forgetting the given lower and
80
     * upper bound (just as PostgreSQL does). That happens in one of the following cases:
81
     * * the lower bound is greater than the upper bound, or
82
     * * both bounds are equal but any of them is exclusive, or
83
     * * the subtype is discrete, the upper bound is just one step after the lower bound and both the bounds are
84
     *   exclusive.
85
     *
86
     * For creating an empty range explicitly, see {@link Range::empty()}.
87
     *
88
     * @param mixed $lower the range lower bound, or <tt>null</tt> if unbounded
89
     * @param mixed $upper the range upper bound, or <tt>null</tt> if unbounded
90
     * @param bool|string $boundsOrLowerInc
91
     *                          either the string of complete bounds specification, or a boolean telling whether the
92
     *                            lower bound is inclusive;
93
     *                          the complete specification is similar to PostgreSQL - it is a two-character string of
94
     *                            <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt> (brackets denoting an
95
     *                            inclusive bound, parentheses denoting an exclusive bound;
96
     *                          when the boolean is used, also the <tt>$upperInc</tt> argument is used;
97
     *                          either bound specification is irrelevant if the corresponding range edge is
98
     *                            <tt>null</tt> - the range is open by definition on that side, and thus will be created
99
     *                            as such
100
     * @param bool|null $upperInc whether the upper bound is inclusive;
101
     *                            only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
102
     * @param IValueComparator|null $customComparator
103
     *                          a custom comparator to use for the values;
104
     *                          skip with <tt>null</tt> to use
105
     *                            {@link Ivory::getDefaultValueComparator() the default value comparator}
106
     * @param IDiscreteStepper|null $customDiscreteStepper
107
     *                          a custom discrete stepper, used for converting from/to inclusive/exclusive bounds;
108
     *                          if not given, it is inferred from the lower or upper bound value, whichever is non-null:
109
     *                            <ul>
110
     *                            <li>if the value object implements {@link IDiscreteStepper}, it is used directly;
111
     *                            <li>if the value is an integer, an {@link IntegerStepper} is used;
112
     *                            <li>if an integer string (which is a case of a {@link BigIntSafeType}),
113
     *                                a {@link BigIntSafeStepper} is used;
114
     *                            <li>otherwise, no discrete stepper is used (especially, if both bounds are
115
     *                                <tt>null</tt>, no discrete stepper is actually useful)
116
     *                            </ul>
117
     * @return Range
118
     */
119
    public static function fromBounds(
120
        $lower,
121
        $upper,
122
        $boundsOrLowerInc = '[)',
123
        ?bool $upperInc = null,
124
        ?IValueComparator $customComparator = null,
125
        ?IDiscreteStepper $customDiscreteStepper = null
126
    ): Range {
127
        list($loInc, $upInc) = self::processBoundSpec($boundsOrLowerInc, $upperInc);
0 ignored issues
show
Bug introduced by
It seems like $boundsOrLowerInc can also be of type boolean; however, Ivory\Value\Range::processBoundSpec() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
128
129
        $comparator = ($customComparator ?? Ivory::getDefaultValueComparator());
130
        $discreteStepper = ($customDiscreteStepper ?? self::inferDiscreteStepper($lower ?? $upper));
131
132
        if ($lower !== null && $upper !== null) {
133
            $comp = $comparator->compareValues($lower, $upper);
134
            if ($comp > 0) {
135
                return self::empty();
136
            } elseif ($comp == 0) {
137
                if (!$loInc || !$upInc) {
138
                    return self::empty();
139
                }
140
            } elseif (!$loInc && !$upInc && $discreteStepper !== null) {
141
                $upperPred = $discreteStepper->step(-1, $upper);
142
                $isEmpty = ($comparator->compareValues($lower, $upperPred) == 0);
143
                if ($isEmpty) {
144
                    return self::empty();
145
                }
146
            }
147
        } else {
148
            if ($lower === null) {
149
                $loInc = false;
150
            }
151
            if ($upper === null) {
152
                $upInc = false;
153
            }
154
        }
155
156
        return new Range(false, $lower, $upper, $loInc, $upInc, $comparator, $discreteStepper);
157
    }
158
159
    private static function inferDiscreteStepper($value): ?IDiscreteStepper
160
    {
161
        if (is_int($value)) {
162
            static $intStepper = null;
163
            if ($intStepper === null) {
164
                $intStepper = new IntegerStepper();
165
            }
166
            return $intStepper;
167
        } elseif ($value instanceof IDiscreteStepper) {
168
            return $value;
169
        } elseif (is_string($value) && BigIntSafeType::isIntegerString($value)) {
170
            static $bigIntSafeStepper = null;
171
            if ($bigIntSafeStepper === null) {
172
                $bigIntSafeStepper = new BigIntSafeStepper();
173
            }
174
            return $bigIntSafeStepper;
175
        } else {
176
            return null;
177
        }
178
    }
179
180
    /**
181
     * Creates a new empty range.
182
     *
183
     * @return Range
184
     */
185
    public static function empty(): Range
186
    {
187
        $comparator = Ivory::getDefaultValueComparator(); // we must provide one, although for empty range it is useless
188
        return new Range(true, null, null, null, null, $comparator, null);
189
    }
190
191
    private static function processBoundSpec($boundsOrLowerInc = '[)', ?bool $upperInc = null)
192
    {
193
        if (is_string($boundsOrLowerInc)) {
194
            if ($upperInc !== null) {
195
                trigger_error('$upperInc is irrelevant - string specification for $boundsOrLowerInc given', E_USER_NOTICE);
196
            }
197
            // OPT: measure whether preg_match('~^[[(][\])]$~', $boundsOrLowerInc) would not be faster
198
            if (strlen($boundsOrLowerInc) != 2 || // OPT: isset($boundsOrLowerInc[2] might be faster
199
                strpos('([', $boundsOrLowerInc[0]) === false ||
200
                strpos(')]', $boundsOrLowerInc[1]) === false
201
            ) {
202
                $msg = "Invalid bounds inclusive/exclusive specification string: $boundsOrLowerInc";
203
                throw new \InvalidArgumentException($msg);
204
            }
205
206
            $loInc = ($boundsOrLowerInc[0] == '[');
207
            $upInc = ($boundsOrLowerInc[1] == ']');
208
        } else {
209
            $loInc = (bool)$boundsOrLowerInc;
210
            $upInc = (bool)$upperInc;
211
        }
212
213
        return [$loInc, $upInc];
214
    }
215
216
217
    private function __construct(
218
        bool $empty,
219
        $lower,
220
        $upper,
221
        ?bool $lowerInc,
222
        ?bool $upperInc,
223
        IValueComparator $comparator,
224
        ?IDiscreteStepper $discreteStepper
225
    ) {
226
        $this->empty = $empty;
227
        $this->lower = $lower;
228
        $this->upper = $upper;
229
        $this->lowerInc = $lowerInc;
230
        $this->upperInc = $upperInc;
231
        $this->comparator = $comparator;
232
        $this->discreteStepper = $discreteStepper;
233
    }
234
235
    //endregion
236
237
    //region getters
238
239
    final public function isEmpty(): bool
240
    {
241
        return $this->empty;
242
    }
243
244
    /**
245
     * @return mixed lower bound, or <tt>null</tt> if the range is lower-unbounded or empty
246
     */
247
    final public function getLower()
248
    {
249
        return $this->lower;
250
    }
251
252
    /**
253
     * @return mixed upper bound, or <tt>null</tt> if the range is upper-unbounded or empty
254
     */
255
    final public function getUpper()
256
    {
257
        return $this->upper;
258
    }
259
260
    /**
261
     * @return bool|null whether the range includes its lower bound, or <tt>null</tt> if the range is empty;
262
     *                   for lower-unbounded ranges, <tt>false</tt> is returned by definition
263
     */
264
    final public function isLowerInc(): ?bool
265
    {
266
        return $this->lowerInc;
267
    }
268
269
    /**
270
     * @return bool|null whether the range includes its upper bound, or <tt>null</tt> if the range is empty;
271
     *                   for upper-unbounded ranges, <tt>false</tt> is returned by definition
272
     */
273
    final public function isUpperInc(): ?bool
274
    {
275
        return $this->upperInc;
276
    }
277
278
    /**
279
     * @return string|null the bounds inclusive/exclusive specification, as accepted by {@link fromBounds()}, or
280
     *                     <tt>null</tt> if the range is empty
281
     */
282
    final public function getBoundsSpec(): ?string
283
    {
284
        if ($this->empty) {
285
            return null;
286
        } else {
287
            return ($this->lowerInc ? '[' : '(') . ($this->upperInc ? ']' : ')');
288
        }
289
    }
290
291
    /**
292
     * @return bool whether the range is just a single point
293
     */
294
    final public function isSinglePoint(): bool
295
    {
296
        if ($this->empty || $this->lower === null || $this->upper === null) {
297
            return false;
298
        }
299
300
        $cmp = $this->comparator->compareValues($this->lower, $this->upper);
301
        if ($cmp == 0) {
302
            return ($this->lowerInc && $this->upperInc);
303
        }
304
        if ($this->lowerInc && $this->upperInc) { // optimization
305
            return false;
306
        }
307
        if ($this->discreteStepper !== null) {
308
            $lo = ($this->lowerInc ? $this->lower : $this->discreteStepper->step(1, $this->lower));
309
            $up = ($this->upperInc ? $this->upper : $this->discreteStepper->step(-1, $this->upper));
310
            return ($this->comparator->compareValues($lo, $up) == 0);
311
        } else {
312
            return false;
313
        }
314
    }
315
316
    /**
317
     * Returns the range bounds according to the requested bound specification.
318
     *
319
     * **Only defined on ranges of {@link IDiscreteStepper discrete} subtypes.**
320
     *
321
     * E.g., on an integer range `(null,3)`, if bounds `[]` are requested, a pair of `null` and `2` is returned.
322
     *
323
     * @param bool|string $boundsOrLowerInc either the string of complete bounds specification, or a boolean telling
324
     *                                        whether the lower bound is inclusive;
325
     *                                      the complete specification is similar to PostgreSQL - it is a two-character
326
     *                                        string of <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt>
327
     *                                        (brackets denoting an inclusive bound, parentheses denoting an exclusive
328
     *                                        bound;
329
     *                                      when the boolean is used, also the <tt>$upperInc</tt> argument is used
330
     * @param bool $upperInc whether the upper bound is inclusive;
0 ignored issues
show
Documentation introduced by
Should the type for parameter $upperInc not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
331
     *                       only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
332
     * @return array|null pair of the lower and upper bound, or <tt>null</tt> if the range is empty
333
     * @throws UnsupportedException if the range subtype is not discrete and no custom discrete stepper has been
334
     *                                provided
335
     */
336
    public function toBounds($boundsOrLowerInc, ?bool $upperInc = null): ?array
337
    {
338
        if ($this->empty) {
339
            return null;
340
        }
341
342
        if ($this->lower === null && $this->upper === null) {
343
            return [null, null]; // no matter of the actually requested bounds, the result is (-inf,inf)
344
        }
345
346
        if ($this->discreteStepper === null) {
347
            throw new UnsupportedException(
348
                'Cannot convert the range bounds - the subtype was not recognized as a ' . IDiscreteStepper::class .
349
                ', and no custom discrete stepper has been provided.'
350
            );
351
        }
352
353
        list($loInc, $upInc) = self::processBoundSpec($boundsOrLowerInc, $upperInc);
0 ignored issues
show
Bug introduced by
It seems like $boundsOrLowerInc can also be of type boolean; however, Ivory\Value\Range::processBoundSpec() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
354
355
        if ($this->lower === null) {
356
            $lo = null;
357
        } else {
358
            $lo = $this->lower;
359
            $step = (int)$loInc - (int)$this->lowerInc;
360
            if ($step) {
361
                $lo = $this->discreteStepper->step($step, $lo);
362
            }
363
        }
364
365
        if ($this->upper === null) {
366
            $up = null;
367
        } else {
368
            $up = $this->upper;
369
            $step = (int)$this->upperInc - (int)$upInc;
370
            if ($step) {
371
                $up = $this->discreteStepper->step($step, $up);
372
            }
373
        }
374
375
        return [$lo, $up];
376
    }
377
378
    final public function __toString()
379
    {
380
        if ($this->empty) {
381
            return 'empty';
382
        }
383
384
        $bounds = $this->getBoundsSpec();
385
        return sprintf('%s%s,%s%s',
386
            $bounds[0],
387
            ($this->lower === null ? '-infinity' : $this->lower),
388
            ($this->upper === null ? 'infinity' : $this->upper),
389
            $bounds[1]
390
        );
391
    }
392
393
    //endregion
394
395
    //region range operations
396
397
    /**
398
     * @param mixed $element a value of the range subtype
399
     * @return bool|null whether this range contains the given element;
400
     *                   <tt>null</tt> on <tt>null</tt> input
401
     */
402
    public function containsElement($element): ?bool
403
    {
404
        if ($element === null) {
405
            return null;
406
        }
407
        if ($this->empty) {
408
            return false;
409
        }
410
411
        if ($this->lower !== null) {
412
            $cmp = $this->comparator->compareValues($element, $this->lower);
413
            if ($cmp < 0 || ($cmp == 0 && !$this->lowerInc)) {
414
                return false;
415
            }
416
        }
417
418
        if ($this->upper !== null) {
419
            $cmp = $this->comparator->compareValues($element, $this->upper);
420
            if ($cmp > 0 || ($cmp == 0 && !$this->upperInc)) {
421
                return false;
422
            }
423
        }
424
425
        return true;
426
    }
427
428
    /**
429
     * @param mixed $element value of the range subtype
430
     * @return bool|null <tt>true</tt> iff this range is left of the given element - value of the range subtype;
431
     *                   <tt>false</tt> otherwise, especially if this range is empty;
432
     *                   <tt>null</tt> on <tt>null</tt> input
433
     */
434
    public function leftOfElement($element): ?bool
435
    {
436
        if ($element === null) {
437
            return null;
438
        }
439
        if ($this->empty) {
440
            return false;
441
        }
442
443
        if ($this->upper === null) {
444
            return false;
445
        }
446
        $cmp = $this->comparator->compareValues($element, $this->upper);
447
        return ($cmp > 0 || ($cmp == 0 && !$this->upperInc));
448
    }
449
450
    /**
451
     * @param mixed $element value of the range subtype
452
     * @return bool|null <tt>true</tt> iff this range is right of the given element - value of the range subtype;
453
     *                   <tt>false</tt> otherwise, especially if this range is empty;
454
     *                   <tt>null</tt> on <tt>null</tt> input
455
     */
456
    public function rightOfElement($element): ?bool
457
    {
458
        if ($element === null) {
459
            return null;
460
        }
461
        if ($this->empty) {
462
            return false;
463
        }
464
465
        if ($this->lower === null) {
466
            return false;
467
        }
468
        $cmp = $this->comparator->compareValues($element, $this->lower);
469
        return ($cmp < 0 || ($cmp == 0 && !$this->lowerInc));
470
    }
471
472
    /**
473
     * @param Range $other a range of the same subtype as this range
474
     * @return bool|null whether this range entirely contains the other range;
475
     *                   an empty range is considered to be contained in any range, even an empty one;
476
     *                   <tt>null</tt> on <tt>null</tt> input
477
     */
478
    public function containsRange(?Range $other): ?bool
479
    {
480
        if ($other === null) {
481
            return null;
482
        }
483
        if ($other->empty) {
484
            return true;
485
        }
486
        if ($this->empty) {
487
            return false;
488
        }
489
490
        if ($this->lower !== null) {
491
            if ($other->lower === null) {
492
                return false;
493
            } else {
494
                $cmp = $this->comparator->compareValues($this->lower, $other->lower);
495
                if ($cmp > 0 || ($cmp == 0 && !$this->lowerInc && $other->lowerInc)) {
496
                    return false;
497
                }
498
            }
499
        }
500
501
        if ($this->upper !== null) {
502
            if ($other->upper === null) {
503
                return false;
504
            } else {
505
                $cmp = $this->comparator->compareValues($this->upper, $other->upper);
506
                if ($cmp < 0 || ($cmp == 0 && !$this->upperInc && $other->upperInc)) {
507
                    return false;
508
                }
509
            }
510
        }
511
512
        return true;
513
    }
514
515
    /**
516
     * @param Range $other a range of the same subtype as this range
517
     * @return bool|null whether this range is entirely contained in the other range;
518
     *                   an empty range is considered to be contained in any range, even an empty one;
519
     *                   <tt>null</tt> on <tt>null</tt> input
520
     */
521
    public function containedInRange(?Range $other): ?bool
522
    {
523
        if ($other === null) {
524
            return null;
525
        }
526
        return $other->containsRange($this);
527
    }
528
529
    /**
530
     * @param Range $other a range of the same subtype as this range
531
     * @return bool|null whether this and the other range overlap, i.e., have a non-empty intersection;
532
     *                   <tt>null</tt> on <tt>null</tt> input
533
     */
534
    public function overlaps(?Range $other): ?bool
535
    {
536
        if ($other === null) {
537
            return null;
538
        }
539
        if ($this->empty || $other->empty) {
540
            return false;
541
        }
542
543
        if ($this->lower !== null && $other->upper !== null) {
544
            $cmp = $this->comparator->compareValues($this->lower, $other->upper);
545
            if ($cmp > 0 || ($cmp == 0 && (!$this->lowerInc || !$other->upperInc))) {
546
                return false;
547
            }
548
        }
549
        if ($other->lower !== null && $this->upper !== null) {
550
            $cmp = $this->comparator->compareValues($other->lower, $this->upper);
551
            if ($cmp > 0 || ($cmp == 0 && (!$other->lowerInc || !$this->upperInc))) {
552
                return false;
553
            }
554
        }
555
        return true;
556
    }
557
558
    /**
559
     * Computes the intersection of this range with another range.
560
     *
561
     * @param Range $other a range of the same subtype as this range
562
     * @return Range|null intersection of this and the other range
563
     *                    <tt>null</tt> on <tt>null</tt> input
564
     */
565
    public function intersect(?Range $other): ?Range
566
    {
567
        if ($other === null) {
568
            return null;
569
        }
570
        if ($this->empty) {
571
            return $this;
572
        }
573
        if ($other->empty) {
574
            return $other;
575
        }
576
577
        if ($this->lower === null) {
578
            $lo = $other->lower;
579
            $loInc = $other->lowerInc;
580
        } elseif ($other->lower === null) {
581
            $lo = $this->lower;
582
            $loInc = $this->lowerInc;
583
        } else {
584
            $cmp = $this->comparator->compareValues($this->lower, $other->lower);
585
            if ($cmp < 0) {
586
                $lo = $other->lower;
587
                $loInc = $other->lowerInc;
588
            } elseif ($cmp > 0) {
589
                $lo = $this->lower;
590
                $loInc = $this->lowerInc;
591
            } else {
592
                $lo = $this->lower;
593
                $loInc = ($this->lowerInc && $other->lowerInc);
594
            }
595
        }
596
597
        if ($this->upper === null) {
598
            $up = $other->upper;
599
            $upInc = $other->upperInc;
600
        } elseif ($other->upper === null) {
601
            $up = $this->upper;
602
            $upInc = $this->upperInc;
603
        } else {
604
            $cmp = $this->comparator->compareValues($this->upper, $other->upper);
605
            if ($cmp < 0) {
606
                $up = $this->upper;
607
                $upInc = $this->upperInc;
608
            } elseif ($cmp > 0) {
609
                $up = $other->upper;
610
                $upInc = $other->upperInc;
611
            } else {
612
                $up = $this->upper;
613
                $upInc = ($this->upperInc && $other->upperInc);
614
            }
615
        }
616
617
        return self::fromBounds($lo, $up, $loInc, $upInc, $this->comparator, $this->discreteStepper);
618
    }
619
620
    /**
621
     * @return bool whether the range is finite, i.e., neither starts nor ends in the infinity;
622
     *              note that an empty range is considered as finite
623
     */
624
    final public function isFinite(): bool
625
    {
626
        return ($this->empty || ($this->lower !== null && $this->upper !== null));
627
    }
628
629
    /**
630
     * @param Range|null $other a range of the same subtype as this range
631
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
632
     *                     other starts;
633
     *                   <tt>false</tt> otherwise, especially if either range is empty;
634
     *                   <tt>null</tt> on <tt>null</tt> input
635
     */
636
    public function strictlyLeftOf(?Range $other): ?bool
637
    {
638
        if ($other === null) {
639
            return null;
640
        }
641
        if ($this->empty || $other->empty) {
642
            return false;
643
        }
644
645
        if ($this->upper === null || $other->lower === null) {
646
            return false;
647
        }
648
        $cmp = $this->comparator->compareValues($this->upper, $other->lower);
649
        return ($cmp < 0 || ($cmp == 0 && (!$this->upperInc || !$other->lowerInc)));
650
    }
651
652
    /**
653
     * @param Range|null $other a range of the same subtype as this range
654
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
655
     *                     other starts;
656
     *                   <tt>false</tt> otherwise, especially if either range is empty;;
657
     *                   <tt>null</tt> on <tt>null</tt> input
658
     */
659
    public function strictlyRightOf(?Range $other): ?bool
660
    {
661
        if ($other === null) {
662
            return null;
663
        }
664
        if ($this->empty || $other->empty) {
665
            return false;
666
        }
667
668
        if ($other->upper === null || $this->lower === null) {
669
            return false;
670
        }
671
        $cmp = $this->comparator->compareValues($other->upper, $this->lower);
672
        return ($cmp < 0 || ($cmp == 0 && (!$other->upperInc || !$this->lowerInc)));
673
    }
674
675
    //endregion
676
677
    //region IComparable
678
679
    public function equals($other): bool
680
    {
681
        if (!$other instanceof Range) {
682
            return false;
683
        }
684
685
        if ($this->empty) {
686
            return $other->empty;
687
        }
688
        if ($other->empty) {
689
            return false;
690
        }
691
692
        $objLower = $other->lower;
693
        $objUpper = $other->upper;
694
695
        if ($this->lowerInc != $other->lowerInc) {
696
            if ($this->discreteStepper === null) {
697
                return false; // no chance of converting to equivalent lower bounds
698
            }
699
700
            if ($objLower !== null) {
701
                $objLower = $this->discreteStepper->step(($other->lowerInc ? -1 : 1), $objLower);
702
            }
703
        }
704
        if ($this->upperInc != $other->upperInc) {
705
            if ($this->discreteStepper === null) {
706
                return false; // no chance of converting to equivalent upper bounds
707
            }
708
            if ($objUpper !== null) {
709
                $objUpper = $this->discreteStepper->step(($other->upperInc ? 1 : -1), $objUpper);
710
            }
711
        }
712
713
        if ($this->lower === null) {
714
            if ($objLower !== null) {
715
                return false;
716
            }
717
        } else {
718
            if ($this->comparator->compareValues($this->lower, $objLower) != 0) {
719
                return false;
720
            }
721
        }
722
723
        if ($this->upper === null) {
724
            if ($objUpper !== null) {
725
                return false;
726
            }
727
        } else {
728
            if ($this->comparator->compareValues($this->upper, $objUpper) != 0) {
729
                return false;
730
            }
731
        }
732
733
        return true;
734
    }
735
736
    public function compareTo($other): int
737
    {
738
        if ($other === null) {
739
            throw new \InvalidArgumentException('comparing with null');
740
        }
741
        if (!$other instanceof Range) {
742
            throw new IncomparableException('$other is not a ' . Range::class);
743
        }
744
745
        if ($this->isEmpty() && $other->isEmpty()) {
746
            return 0;
747
        } elseif ($this->isEmpty()) {
748
            return -1;
749
        } elseif ($other->isEmpty()) {
750
            return 1;
751
        }
752
753
        $cmp = $this->compareBounds(-1, $this->getLower(), $this->isLowerInc(), $other->getLower(), $other->isLowerInc());
0 ignored issues
show
Bug introduced by
It seems like $this->isLowerInc() targeting Ivory\Value\Range::isLowerInc() can also be of type null; however, Ivory\Value\Range::compareBounds() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $other->isLowerInc() targeting Ivory\Value\Range::isLowerInc() can also be of type null; however, Ivory\Value\Range::compareBounds() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
754
        if ($cmp != 0) {
755
            return $cmp;
756
        } else {
757
            return $this->compareBounds(1, $this->getUpper(), $this->isUpperInc(), $other->getUpper(), $other->isUpperInc());
0 ignored issues
show
Bug introduced by
It seems like $this->isUpperInc() targeting Ivory\Value\Range::isUpperInc() can also be of type null; however, Ivory\Value\Range::compareBounds() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $other->isUpperInc() targeting Ivory\Value\Range::isUpperInc() can also be of type null; however, Ivory\Value\Range::compareBounds() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
758
        }
759
    }
760
761
    private function compareBounds(int $sgn, $aVal, bool $aIsInc, $bVal, bool $bIsInc): int
762
    {
763
        if ($aVal === null && $bVal === null) {
764
            return 0;
765
        } elseif ($aVal === null) {
766
            return 1 * $sgn;
767
        } elseif ($bVal === null) {
768
            return -1 * $sgn;
769
        }
770
771
        $cmp = $this->comparator->compareValues($aVal, $bVal);
772
        if ($cmp != 0) {
773
            return $cmp;
774
        }
775
776
        if ($aIsInc && $bIsInc) {
777
            return 0;
778
        } elseif ($aIsInc) {
779
            return 1 * $sgn;
780
        } elseif ($bIsInc) {
781
            return -1 * $sgn;
782
        } else {
783
            return 0;
784
        }
785
    }
786
787
    //endregion
788
789
    //region ArrayAccess
790
791
    public function offsetExists($offset)
792
    {
793
        return (!$this->empty && ($offset == 0 || $offset == 1));
794
    }
795
796
    public function offsetGet($offset)
797
    {
798
        if ($offset == 0) {
799
            return ($this->empty ? null : $this->lower);
800
        } elseif ($offset == 1) {
801
            return ($this->empty ? null : $this->upper);
802
        } else {
803
            trigger_error("Undefined range offset: $offset", E_USER_WARNING);
804
            return null;
805
        }
806
    }
807
808
    public function offsetSet($offset, $value)
809
    {
810
        throw new ImmutableException();
811
    }
812
813
    public function offsetUnset($offset)
814
    {
815
        throw new ImmutableException();
816
    }
817
818
    //endregion
819
}
820