Completed
Push — master ( 8a9a36...5d30b0 )
by Ondřej
03:00
created

Range::overlaps()   C

Complexity

Conditions 16
Paths 9

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 5.2678
c 0
b 0
f 0
cc 16
eloc 14
nc 9
nop 1

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
4
namespace Ivory\Value;
5
6
use Ivory\Exception\ImmutableException;
7
use Ivory\Exception\UnsupportedException;
8
use Ivory\Type\IDiscreteType;
9
use Ivory\Type\IRangeCanonicalFunc;
10
use Ivory\Type\ITotallyOrderedType;
11
use Ivory\Utils\IEqualable;
12
13
/**
14
 * A range of values.
15
 *
16
 * The class resembles the range values manipulated by PostgreSQL. Just a brief summary:
17
 * - The range is defined above a type of values, called "subtype" (see {@link Range::getSubtype()}).
18
 * - The subtype must have a total order.
19
 * - A range may be empty, meaning it contains nothing. See {@link Range::isEmpty()} for distinguishing it.
20
 * - A non-empty range has the lower and the upper bounds, either of which may either be inclusive or exclusive. See
21
 *   {@link Range::getLower()} and {@link Range::getUpper()} for getting the boundaries. Also see
22
 *   {@link Range::isLowerInc()} and {@link Range::isUpperInc()} for finding out whichever boundary is inclusive.
23
 * - A range may be unbounded in either direction, covering all subtype values from the range towards minus infinity or
24
 *   towards infinity, respectively.
25
 * - A range might even cover just a single point. {@link Range::isSinglePoint()} may be used for checking out.
26
 *
27
 * Ranges of some types may have a canonical function, the purpose of which is to convert semantically equivalent ranges
28
 * on {@link \Ivory\Type\IDiscreteType discrete types} to syntactically equivalent ranges. E.g., one such function might
29
 * convert the range `[1,3]` to `[1,4)`. Like in PostgreSQL, such canonicalization is performed automatically upon
30
 * constructing a range. If other than canonical representation of a range is desired, e.g., for presenting the range
31
 * using both bounds inclusive, method {@link Range::toBounds()} may be useful.
32
 *
33
 * A range is `IEqualable` to another range. Two ranges are equal only if they are above the same subtype and if the
34
 * effective range equals.
35
 *
36
 * Alternatively to the {@link Range::getLower()} and {@link Range::getUpper()} methods, `ArrayAccess` is implemented.
37
 * Indexes `0` and `1` may be used to get the lower and upper bound, respectively. Either of them returns `null` if the
38
 * range is empty or unbounded in the given direction.
39
 *
40
 * Note the range value is immutable, i.e., once constructed, its values cannot be changed. Thus, `ArrayAccess` write
41
 * operations ({@link \ArrayAccess::offsetSet()} and {@link \ArrayAccess::offsetUnset()}) throw an
42
 * {@link \Ivory\Exception\ImmutableException}.
43
 *
44
 * @see http://www.postgresql.org/docs/9.4/static/rangetypes.html
45
 */
46
class Range implements IEqualable, \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...
47
{
48
    /** @var ITotallyOrderedType */
49
    private $subtype;
50
    /** @var IRangeCanonicalFunc */
51
    private $canonicalFunc;
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
63
64
    //region construction
65
66
    /**
67
     * Creates a new range with given lower and upper bounds.
68
     *
69
     * There are two ways of calling this factory method: whether the bounds are inclusive or exclusive may be
70
     * specified:
71
     * - either by passing a specification string as the `$boundsOrLowerInc` argument (e.g., `'[)'`),
72
     * - or by passing two boolean values as the `$boundsOrLowerInc` and `$upperInc` telling whichever bound is
73
     *   inclusive.
74
     *
75
     * If the lower bound is greater than the upper bound, or if both bounds are equal but any of them is exclusive, an
76
     * empty range gets created, forgetting the given upper and lower bound (just as PostgreSQL does). For creating an
77
     * empty range explicitly, see {@link createEmpty()}.
78
     *
79
     * @param ITotallyOrderedType $subtype the range subtype
80
     * @param IRangeCanonicalFunc $canonicalFunc if given, the range will be created in the canonical form according to
0 ignored issues
show
Documentation introduced by
Should the type for parameter $canonicalFunc not be IRangeCanonicalFunc|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...
81
     *                                             this function;
82
     *                                           skip with <tt>null</tt> to create the range as is;
83
     *                                           NOTE: while one could expect the canonical function usage at the range
84
     *                                             *type* level, passing it to this factory method makes it clear that
85
     *                                             if no canonical function is given, the range will not get
86
     *                                             canonicalized
87
     * @param mixed $lower the range lower bound, or <tt>null</tt> if unbounded
88
     * @param mixed $upper the range upper bound, or <tt>null</tt> if unbounded
89
     * @param bool|string $boundsOrLowerInc either the string of complete bounds specification, or a boolean telling
90
     *                                        whether the lower bound is inclusive;
91
     *                                      the complete specification is similar to PostgreSQL - it is a two-character
92
     *                                        string of <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt>
93
     *                                        (brackets denoting an inclusive bound, parentheses denoting an exclusive
94
     *                                        bound;
95
     *                                      when the boolean is used, also the <tt>$upperInc</tt> argument is used;
96
     *                                      either bound specification is irrelevant if the corresponding range edge is
97
     *                                        <tt>null</tt> - the range is open by definition on that side, and thus
98
     *                                        will be created as such
99
     * @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...
100
     *                       only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
101
     * @return Range
102
     */
103
    public static function createFromBounds(
104
        ITotallyOrderedType $subtype,
105
        ?IRangeCanonicalFunc $canonicalFunc = null,
106
        $lower,
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
107
        $upper,
108
        $boundsOrLowerInc = '[)',
109
        ?bool $upperInc = null
110
    ): Range {
111
        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...
112
113
        if ($lower !== null && $upper !== null) {
114
            $comp = $subtype->compareValues($lower, $upper);
115
            if ($comp > 0) {
116
                return self::createEmpty($subtype);
117
            } elseif ($comp == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $comp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
118
                if (!$loInc || !$upInc) {
119
                    return self::createEmpty($subtype);
120
                }
121
            }
122
        } else {
123
            if ($lower === null) {
124
                $loInc = false;
125
            }
126
            if ($upper === null) {
127
                $upInc = false;
128
            }
129
        }
130
131
        if ($canonicalFunc !== null) {
132
            list($lower, $loInc, $upper, $upInc) = $canonicalFunc->canonicalize($lower, $loInc, $upper, $upInc);
133
134
            /* Check everything once again:
135
             * - to find out whether the range became empty after canonicalization, and
136
             * - as a defensive measure against poorly written canonical functions.
137
             */
138
            if ($lower !== null && $upper !== null) {
139
                $comp = $subtype->compareValues($lower, $upper);
140
                if ($comp > 0) {
141
                    return self::createEmpty($subtype);
142
                } elseif ($comp == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $comp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
143
                    if (!$loInc || !$upInc) {
144
                        return self::createEmpty($subtype);
145
                    }
146
                }
147
            } else {
148
                if ($lower === null) {
149
                    $loInc = false;
150
                }
151
                if ($upper === null) {
152
                    $upInc = false;
153
                }
154
            }
155
        }
156
157
        return new Range($subtype, $canonicalFunc, false, $lower, $upper, $loInc, $upInc);
158
    }
159
160
    /**
161
     * Creates a new empty range.
162
     *
163
     * @param ITotallyOrderedType $subtype the range subtype
164
     * @return Range
165
     */
166
    public static function createEmpty(ITotallyOrderedType $subtype): Range
167
    {
168
        return new Range($subtype, null, true, null, null, null, null);
169
    }
170
171
    private static function processBoundSpec($boundsOrLowerInc = '[)', ?bool $upperInc = null)
172
    {
173
        if (is_string($boundsOrLowerInc)) {
174
            if ($upperInc !== null) {
175
                trigger_error('$upperInc is irrelevant - string specification for $boundsOrLowerInc given', E_USER_NOTICE);
176
            }
177
            // OPT: measure whether preg_match('~^[[(][\])]$~', $boundsOrLowerInc) would not be faster
178
            if (strlen($boundsOrLowerInc) != 2 || // OPT: isset($boundsOrLowerInc[2] might be faster
179
                strpos('([', $boundsOrLowerInc[0]) === false ||
180
                strpos(')]', $boundsOrLowerInc[1]) === false
181
            ) {
182
                $msg = "Invalid bounds inclusive/exclusive specification string: $boundsOrLowerInc";
183
                throw new \InvalidArgumentException($msg);
184
            }
185
186
            $loInc = ($boundsOrLowerInc[0] == '[');
187
            $upInc = ($boundsOrLowerInc[1] == ']');
188
        } else {
189
            $loInc = (bool)$boundsOrLowerInc;
190
            $upInc = (bool)$upperInc;
191
        }
192
193
        return [$loInc, $upInc];
194
    }
195
196
197
    private function __construct(
198
        ITotallyOrderedType $subtype,
199
        ?IRangeCanonicalFunc $canonicalFunc,
200
        bool $empty,
201
        $lower,
202
        $upper,
203
        ?bool $lowerInc,
204
        ?bool $upperInc
205
    ) {
206
        $this->subtype = $subtype;
207
        $this->canonicalFunc = $canonicalFunc;
208
        $this->empty = $empty;
209
        $this->lower = $lower;
210
        $this->upper = $upper;
211
        $this->lowerInc = $lowerInc;
212
        $this->upperInc = $upperInc;
213
    }
214
215
    //endregion
216
217
    //region getters
218
219
    final public function getSubtype(): ITotallyOrderedType
220
    {
221
        return $this->subtype;
222
    }
223
224
    /**
225
     * @return bool whether the range is empty
226
     */
227
    final public function isEmpty(): bool
228
    {
229
        return $this->empty;
230
    }
231
232
    /**
233
     * @return mixed lower bound, or <tt>null</tt> if the range is lower-unbounded or empty
234
     */
235
    final public function getLower()
236
    {
237
        return $this->lower;
238
    }
239
240
    /**
241
     * @return mixed upper bound, or <tt>null</tt> if the range is upper-unbounded or empty
242
     */
243
    final public function getUpper()
244
    {
245
        return $this->upper;
246
    }
247
248
    /**
249
     * @return bool|null whether the range includes its lower bound, or <tt>null</tt> if the range is empty;
250
     *                   for lower-unbounded ranges, <tt>false</tt> is returned by definition
251
     */
252
    final public function isLowerInc(): ?bool
253
    {
254
        return $this->lowerInc;
255
    }
256
257
    /**
258
     * @return bool|null whether the range includes its upper bound, or <tt>null</tt> if the range is empty;
259
     *                   for upper-unbounded ranges, <tt>false</tt> is returned by definition
260
     */
261
    final public function isUpperInc(): ?bool
262
    {
263
        return $this->upperInc;
264
    }
265
266
    /**
267
     * @return string|null the bounds inclusive/exclusive specification, as accepted by {@link createFromBounds()}, or
268
     *                     <tt>null</tt> if the range is empty
269
     */
270
    final public function getBoundsSpec(): ?string
271
    {
272
        if ($this->empty) {
273
            return null;
274
        } else {
275
            return ($this->lowerInc ? '[' : '(') . ($this->upperInc ? ']' : ')');
276
        }
277
    }
278
279
    /**
280
     * @return bool whether the range is just a single point
281
     */
282
    final public function isSinglePoint(): bool
283
    {
284
        if ($this->empty || $this->lower === null || $this->upper === null) {
285
            return false;
286
        }
287
288
        $cmp = $this->subtype->compareValues($this->lower, $this->upper);
289
        if ($cmp == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
290
            return ($this->lowerInc && $this->upperInc);
291
        }
292
        if ($this->lowerInc && $this->upperInc) { // optimization
293
            return false;
294
        }
295
        if ($this->subtype instanceof IDiscreteType) {
296
            $lo = ($this->lowerInc ? $this->lower : $this->subtype->step(1, $this->lower));
297
            $up = ($this->upperInc ? $this->upper : $this->subtype->step(-1, $this->upper));
298
            return ($this->subtype->compareValues($lo, $up) == 0);
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->subtype->compareValues($lo, $up) of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
299
        } else {
300
            return false;
301
        }
302
    }
303
304
    /**
305
     * Returns the range bounds according to the requested bound specification.
306
     *
307
     * **Only defined on ranges of {@link IDiscreteType discrete} subtypes.**
308
     *
309
     * E.g., on an integer range `(null,3)`, if bounds `[]` are requested, a pair of `null` and `2` is returned.
310
     *
311
     * @param bool|string $boundsOrLowerInc either the string of complete bounds specification, or a boolean telling
312
     *                                        whether the lower bound is inclusive;
313
     *                                      the complete specification is similar to PostgreSQL - it is a two-character
314
     *                                        string of <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt>
315
     *                                        (brackets denoting an inclusive bound, parentheses denoting an exclusive
316
     *                                        bound;
317
     *                                      when the boolean is used, also the <tt>$upperInc</tt> argument is used
318
     * @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...
319
     *                       only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
320
     * @return array|null pair of the lower and upper bound, or <tt>null</tt> if the range is empty
321
     * @throws UnsupportedException if the range subtype is not an {@link IDiscreteType}
322
     */
323
    public function toBounds($boundsOrLowerInc, ?bool $upperInc = null): ?array
324
    {
325
        if (!$this->subtype instanceof IDiscreteType) {
326
            throw new UnsupportedException('Range subtype is not ' . IDiscreteType::class . ', cannot convert bounds');
327
        }
328
329
        if ($this->empty) {
330
            return null;
331
        }
332
333
        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...
334
335
        if ($this->lower === null) {
336
            $lo = null;
337
        } else {
338
            $lo = $this->lower;
339
            $step = $loInc - $this->lowerInc;
340
            if ($step) {
341
                $lo = $this->subtype->step($step, $lo);
342
            }
343
        }
344
345
        if ($this->upper === null) {
346
            $up = null;
347
        } else {
348
            $up = $this->upper;
349
            $step = $this->upperInc - $upInc;
350
            if ($step) {
351
                $up = $this->subtype->step($step, $up);
352
            }
353
        }
354
355
        return [$lo, $up];
356
    }
357
358
    final public function __toString()
359
    {
360
        if ($this->empty) {
361
            return 'empty';
362
        }
363
364
        $bounds = $this->getBoundsSpec();
365
        return sprintf('%s%s,%s%s',
366
            $bounds[0],
367
            ($this->lower === null ? '-infinity' : $this->lower),
368
            ($this->upper === null ? 'infinity' : $this->upper),
369
            $bounds[1]
370
        );
371
    }
372
373
    //endregion
374
375
    //region range operations
376
377
    /**
378
     * @param mixed $element a value of the range subtype
379
     * @return bool|null whether this range contains the given element;
380
     *                   <tt>null</tt> on <tt>null</tt> input
381
     */
382
    public function containsElement($element): ?bool
383
    {
384
        if ($element === null) {
385
            return null;
386
        }
387
        if ($this->empty) {
388
            return false;
389
        }
390
391
        if ($this->lower !== null) {
392
            $cmp = $this->subtype->compareValues($element, $this->lower);
393
            if ($cmp < 0 || ($cmp == 0 && !$this->lowerInc)) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
394
                return false;
395
            }
396
        }
397
398
        if ($this->upper !== null) {
399
            $cmp = $this->subtype->compareValues($element, $this->upper);
400
            if ($cmp > 0 || ($cmp == 0 && !$this->upperInc)) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
401
                return false;
402
            }
403
        }
404
405
        return true;
406
    }
407
408
    /**
409
     * @param mixed $element value of the range subtype
410
     * @return bool|null <tt>true</tt> iff this range is left of the given element - value of the range subtype;
411
     *                   <tt>false</tt> otherwise, especially if this range is empty;
412
     *                   <tt>null</tt> on <tt>null</tt> input
413
     */
414
    public function leftOfElement($element): ?bool
415
    {
416
        if ($element === null) {
417
            return null;
418
        }
419
        if ($this->empty) {
420
            return false;
421
        }
422
423
        if ($this->upper === null) {
424
            return false;
425
        }
426
        $cmp = $this->subtype->compareValues($element, $this->upper);
427
        return ($cmp > 0 || ($cmp == 0 && !$this->upperInc));
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
428
    }
429
430
    /**
431
     * @param mixed $element value of the range subtype
432
     * @return bool|null <tt>true</tt> iff this range is right of the given element - value of the range subtype;
433
     *                   <tt>false</tt> otherwise, especially if this range is empty;
434
     *                   <tt>null</tt> on <tt>null</tt> input
435
     */
436
    public function rightOfElement($element): ?bool
437
    {
438
        if ($element === null) {
439
            return null;
440
        }
441
        if ($this->empty) {
442
            return false;
443
        }
444
445
        if ($this->lower === null) {
446
            return false;
447
        }
448
        $cmp = $this->subtype->compareValues($element, $this->lower);
449
        return ($cmp < 0 || ($cmp == 0 && !$this->lowerInc));
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
450
    }
451
452
    /**
453
     * @param Range $other a range of the same subtype as this range
454
     * @return bool|null whether this range entirely contains the other range;
455
     *                   an empty range is considered to be contained in any range, even an empty one;
456
     *                   <tt>null</tt> on <tt>null</tt> input
457
     */
458
    public function containsRange(?Range $other): ?bool
459
    {
460
        if ($other === null) {
461
            return null;
462
        }
463
        if ($other->empty) {
464
            return true;
465
        }
466
        if ($this->empty) {
467
            return false;
468
        }
469
470
        if ($this->lower !== null) {
471
            if ($other->lower === null) {
472
                return false;
473
            } else {
474
                $cmp = $this->subtype->compareValues($this->lower, $other->lower);
475
                if ($cmp > 0 || ($cmp == 0 && !$this->lowerInc && $other->lowerInc)) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
476
                    return false;
477
                }
478
            }
479
        }
480
481
        if ($this->upper !== null) {
482
            if ($other->upper === null) {
483
                return false;
484
            } else {
485
                $cmp = $this->subtype->compareValues($this->upper, $other->upper);
486
                if ($cmp < 0 || ($cmp == 0 && !$this->upperInc && $other->upperInc)) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
487
                    return false;
488
                }
489
            }
490
        }
491
492
        return true;
493
    }
494
495
    /**
496
     * @param Range $other a range of the same subtype as this range
497
     * @return bool|null whether this range is entirely contained in the other range;
498
     *                   an empty range is considered to be contained in any range, even an empty one;
499
     *                   <tt>null</tt> on <tt>null</tt> input
500
     */
501
    public function containedInRange(?Range $other): ?bool
502
    {
503
        if ($other === null) {
504
            return null;
505
        }
506
        return $other->containsRange($this);
507
    }
508
509
    /**
510
     * @param Range $other a range of the same subtype as this range
511
     * @return bool|null whether this and the other range overlap, i.e., have a non-empty intersection;
512
     *                   <tt>null</tt> on <tt>null</tt> input
513
     */
514
    public function overlaps(?Range $other): ?bool
515
    {
516
        if ($other === null) {
517
            return null;
518
        }
519
        if ($this->empty || $other->empty) {
520
            return false;
521
        }
522
523
        if ($this->lower !== null && $other->upper !== null) {
524
            $cmp = $this->subtype->compareValues($this->lower, $other->upper);
525
            if ($cmp > 0 || ($cmp == 0 && (!$this->lowerInc || !$other->upperInc))) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
526
                return false;
527
            }
528
        }
529
        if ($other->lower !== null && $this->upper !== null) {
530
            $cmp = $this->subtype->compareValues($other->lower, $this->upper);
531
            if ($cmp > 0 || ($cmp == 0 && (!$other->lowerInc || !$this->upperInc))) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
532
                return false;
533
            }
534
        }
535
        return true;
536
    }
537
538
    /**
539
     * Computes the intersection of this range with another range.
540
     *
541
     * @param Range $other a range of the same subtype as this range
542
     * @return Range|null intersection of this and the other range
543
     *                    <tt>null</tt> on <tt>null</tt> input
544
     */
545
    public function intersect(?Range $other): ?Range
546
    {
547
        if ($other === null) {
548
            return null;
549
        }
550
        if ($this->empty) {
551
            return $this;
552
        }
553
        if ($other->empty) {
554
            return $other;
555
        }
556
557
        if ($this->lower === null) {
558
            $lo = $other->lower;
559
            $loInc = $other->lowerInc;
560
        } elseif ($other->lower === null) {
561
            $lo = $this->lower;
562
            $loInc = $this->lowerInc;
563
        } else {
564
            $cmp = $this->subtype->compareValues($this->lower, $other->lower);
565
            if ($cmp < 0) {
566
                $lo = $other->lower;
567
                $loInc = $other->lowerInc;
568
            } elseif ($cmp > 0) {
569
                $lo = $this->lower;
570
                $loInc = $this->lowerInc;
571
            } else {
572
                $lo = $this->lower;
573
                $loInc = ($this->lowerInc && $other->lowerInc);
574
            }
575
        }
576
577
        if ($this->upper === null) {
578
            $up = $other->upper;
579
            $upInc = $other->upperInc;
580
        } elseif ($other->upper === null) {
581
            $up = $this->upper;
582
            $upInc = $this->upperInc;
583
        } else {
584
            $cmp = $this->subtype->compareValues($this->upper, $other->upper);
585
            if ($cmp < 0) {
586
                $up = $this->upper;
587
                $upInc = $this->upperInc;
588
            } elseif ($cmp > 0) {
589
                $up = $other->upper;
590
                $upInc = $other->upperInc;
591
            } else {
592
                $up = $this->upper;
593
                $upInc = ($this->upperInc && $other->upperInc);
594
            }
595
        }
596
597
        return self::createFromBounds($this->subtype, $this->canonicalFunc, $lo, $up, $loInc, $upInc);
598
    }
599
600
    /**
601
     * @return bool whether the range is finite, i.e., neither starts nor ends in the infinity;
602
     *              note that an empty range is considered as finite
603
     */
604
    final public function isFinite(): bool
605
    {
606
        return ($this->empty || ($this->lower !== null && $this->upper !== null));
607
    }
608
609
    /**
610
     * @param Range|null $other a range of the same subtype as this range
611
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
612
     *                     other starts;
613
     *                   <tt>false</tt> otherwise, especially if either range is empty;
614
     *                   <tt>null</tt> on <tt>null</tt> input
615
     */
616
    public function strictlyLeftOf(?Range $other): ?bool
617
    {
618
        if ($other === null) {
619
            return null;
620
        }
621
        if ($this->empty || $other->empty) {
622
            return false;
623
        }
624
625
        if ($this->upper === null || $other->lower === null) {
626
            return false;
627
        }
628
        $cmp = $this->subtype->compareValues($this->upper, $other->lower);
629
        return ($cmp < 0 || ($cmp == 0 && (!$this->upperInc || !$other->lowerInc)));
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
630
    }
631
632
    /**
633
     * @param Range|null $other a range of the same subtype as this range
634
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
635
     *                     other starts;
636
     *                   <tt>false</tt> otherwise, especially if either range is empty;;
637
     *                   <tt>null</tt> on <tt>null</tt> input
638
     */
639
    public function strictlyRightOf(?Range $other): ?bool
640
    {
641
        if ($other === null) {
642
            return null;
643
        }
644
        if ($this->empty || $other->empty) {
645
            return false;
646
        }
647
648
        if ($other->upper === null || $this->lower === null) {
649
            return false;
650
        }
651
        $cmp = $this->subtype->compareValues($other->upper, $this->lower);
652
        return ($cmp < 0 || ($cmp == 0 && (!$other->upperInc || !$this->lowerInc)));
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $cmp of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
653
    }
654
655
    //endregion
656
657
    //region IEqualable
658
659
    public function equals($object): ?bool
660
    {
661
        if ($object === null) {
662
            return null;
663
        }
664
        if (!$object instanceof Range) {
665
            return false;
666
        }
667
        if ($this->subtype !== $object->subtype) {
668
            return false;
669
        }
670
671
        if ($this->empty) {
672
            return $object->empty;
673
        }
674
        if ($object->empty) {
675
            return false;
676
        }
677
678
        if ($this->lowerInc != $object->lowerInc) {
679
            return false;
680
        }
681
        if ($this->upperInc != $object->upperInc) {
682
            return false;
683
        }
684
685
        if ($this->lower === null) {
686
            if ($object->lower !== null) {
687
                return false;
688
            }
689
        } elseif ($this->lower instanceof IEqualable) {
690
            if (!$this->lower->equals($object->lower)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->lower->equals($object->lower) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
691
                return false;
692
            }
693
        } else {
694
            if ($this->lower != $object->lower) {
695
                return false;
696
            }
697
        }
698
699
        if ($this->upper === null) {
700
            if ($object->upper !== null) {
701
                return false;
702
            }
703
        } elseif ($this->upper instanceof IEqualable) {
704
            if (!$this->upper->equals($object->upper)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->upper->equals($object->upper) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
705
                return false;
706
            }
707
        } else {
708
            if ($this->upper != $object->upper) {
709
                return false;
710
            }
711
        }
712
713
        return true;
714
    }
715
716
    //endregion
717
718
    //region ArrayAccess
719
720
    public function offsetExists($offset)
721
    {
722
        return (!$this->empty && ($offset == 0 || $offset == 1));
723
    }
724
725
    public function offsetGet($offset)
726
    {
727
        if ($offset == 0) {
728
            return ($this->empty ? null : $this->lower);
729
        } elseif ($offset == 1) {
730
            return ($this->empty ? null : $this->upper);
731
        } else {
732
            trigger_error("Undefined range offset: $offset", E_USER_WARNING);
733
            return null;
734
        }
735
    }
736
737
    public function offsetSet($offset, $value)
738
    {
739
        throw new ImmutableException();
740
    }
741
742
    public function offsetUnset($offset)
743
    {
744
        throw new ImmutableException();
745
    }
746
747
    //endregion
748
}
749