Passed
Push — master ( c327c2...0adcf0 )
by Ondřej
02:45
created

Range   F

Complexity

Total Complexity 199

Size/Duplication

Total Lines 766
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 766
rs 1.263
c 0
b 0
f 0
wmc 199

31 Methods

Rating   Name   Duplication   Size   Complexity  
A offsetUnset() 0 3 1
A getLower() 0 3 1
A isLowerInc() 0 3 1
A isUpperInc() 0 3 1
B isSinglePoint() 0 19 11
C containsRange() 0 35 16
A getBoundsSpec() 0 6 4
B processBoundSpec() 0 23 6
A offsetSet() 0 3 1
B strictlyLeftOf() 0 14 9
B rightOfElement() 0 14 6
C intersect() 0 53 14
A getUpper() 0 3 1
A __construct() 0 16 1
A __toString() 0 12 4
C compareBounds() 0 23 10
C equals() 0 55 18
C containsElement() 0 24 11
B offsetGet() 0 9 5
B leftOfElement() 0 14 6
C fromBounds() 0 38 13
B strictlyRightOf() 0 14 9
D toBounds() 0 40 9
A empty() 0 4 1
C overlaps() 0 22 16
A isFinite() 0 3 3
A offsetExists() 0 3 3
B inferDiscreteStepper() 0 18 7
C compareTo() 0 22 8
A containedInRange() 0 6 2
A isEmpty() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Range often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Range, and based on these observations, apply Extract Interface, too.

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
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);
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;
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);
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());
754
        if ($cmp != 0) {
755
            return $cmp;
756
        } else {
757
            return $this->compareBounds(1, $this->getUpper(), $this->isUpperInc(), $other->getUpper(), $other->isUpperInc());
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