Passed
Push — master ( e99e95...936f9a )
by Ondřej
02:18
created

Range::offsetExists()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 1
nc 3
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
use Ivory\Exception\IncomparableException;
6
use Ivory\Exception\UnsupportedException;
7
use Ivory\Ivory;
8
use Ivory\Type\Std\BigIntSafeType;
9
use Ivory\Value\Alg\BigIntSafeStepper;
10
use Ivory\Value\Alg\IComparable;
11
use Ivory\Value\Alg\IDiscreteStepper;
12
use Ivory\Value\Alg\IntegerStepper;
13
use Ivory\Value\Alg\IValueComparator;
14
15
/**
16
 * A range of values.
17
 *
18
 * The class resembles the range values manipulated by PostgreSQL. Just a brief summary:
19
 * - The range is defined above a type of values, called "subtype".
20
 * - The subtype must have a total order. For this implementation, it means the lower and the upper bounds must be
21
 *   comparable using the {@link Ivory::getDefaultValueComparator() default value comparator}, or using a custom
22
 *   {@link IValueComparator} supplied besides the bounds.
23
 * - A range may be empty, meaning it contains nothing. See {@link Range::isEmpty()} for distinguishing it.
24
 * - A non-empty range has the lower and the upper bounds, either of which may either be inclusive or exclusive. See
25
 *   {@link Range::getLower()} and {@link Range::getUpper()} for getting the boundaries. Also see
26
 *   {@link Range::isLowerInc()} and {@link Range::isUpperInc()} for finding out whichever boundary is inclusive.
27
 * - A range may be unbounded in either direction, covering all subtype values from the range towards minus infinity or
28
 *   towards infinity, respectively.
29
 * - A range might even cover just a single point. {@link Range::isSinglePoint()} may be used for checking out.
30
 *
31
 * In PostgreSQL, ranges on discrete subtypes may have a canonical function defined on them, the purpose of which is to
32
 * convert semantically equivalent ranges to syntactically equivalent ones (e.g., one such function might convert the
33
 * range `[1,3]` to `[1,4)`). Such a feature is *not* implemented on the PHP side. The ranges constructed in PHP are not
34
 * normalized to certain bounds, they may only be converted manually using the {@link Range::toBounds()} method.
35
 *
36
 * A range is `IComparable` with another range provided both have the same subtype. Besides, an empty range may safely
37
 * be compared to any other range.
38
 *
39
 * Note the range value is immutable, i.e., once constructed, it cannot be changed.
40
 *
41
 * @see http://www.postgresql.org/docs/9.4/static/rangetypes.html
42
 */
43
class Range implements IComparable
44
{
45
    /** @var bool */
46
    private $empty;
47
    /** @var mixed */
48
    private $lower;
49
    /** @var mixed */
50
    private $upper;
51
    /** @var bool */
52
    private $lowerInc;
53
    /** @var bool */
54
    private $upperInc;
55
    /** @var IValueComparator */
56
    private $comparator;
57
    /** @var IDiscreteStepper */
58
    private $discreteStepper;
59
60
61
    //region construction
62
63
    /**
64
     * Creates a new range with given lower and upper bounds.
65
     *
66
     * There are two ways of calling this factory method: whether the bounds are inclusive or exclusive may be
67
     * specified:
68
     * - either by passing a specification string as the `$boundsOrLowerInc` argument (e.g., `'[)'`),
69
     * - or by passing two boolean values as the `$boundsOrLowerInc` and `$upperInc` telling whichever bound is
70
     *   inclusive.
71
     *
72
     * When the constructed range is effectively empty, an empty range gets created, forgetting the given lower and
73
     * upper bound (just as PostgreSQL does). That happens in one of the following cases:
74
     * * the lower bound is greater than the upper bound, or
75
     * * both bounds are equal but any of them is exclusive, or
76
     * * the subtype is discrete, the upper bound is just one step after the lower bound and both the bounds are
77
     *   exclusive.
78
     *
79
     * For creating an empty range explicitly, see {@link Range::empty()}.
80
     *
81
     * @param mixed $lower the range lower bound, or <tt>null</tt> if unbounded
82
     * @param mixed $upper the range upper bound, or <tt>null</tt> if unbounded
83
     * @param bool|string $boundsOrLowerInc
84
     *                          either the string of complete bounds specification, or a boolean telling whether the
85
     *                            lower bound is inclusive;
86
     *                          the complete specification is similar to PostgreSQL - it is a two-character string of
87
     *                            <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt> (brackets denoting an
88
     *                            inclusive bound, parentheses denoting an exclusive bound;
89
     *                          when the boolean is used, also the <tt>$upperInc</tt> argument is used;
90
     *                          either bound specification is irrelevant if the corresponding range edge is
91
     *                            <tt>null</tt> - the range is open by definition on that side, and thus will be created
92
     *                            as such
93
     * @param bool|null $upperInc whether the upper bound is inclusive;
94
     *                            only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
95
     * @param IValueComparator|null $customComparator
96
     *                          a custom comparator to use for the values;
97
     *                          skip with <tt>null</tt> to use
98
     *                            {@link Ivory::getDefaultValueComparator() the default value comparator}
99
     * @param IDiscreteStepper|null $customDiscreteStepper
100
     *                          a custom discrete stepper, used for converting from/to inclusive/exclusive bounds;
101
     *                          if not given, it is inferred from the lower or upper bound value, whichever is non-null:
102
     *                            <ul>
103
     *                            <li>if the value object implements {@link IDiscreteStepper}, it is used directly;
104
     *                            <li>if the value is an integer, an {@link IntegerStepper} is used;
105
     *                            <li>if an integer string (which is a case of a {@link BigIntSafeType}),
106
     *                                a {@link BigIntSafeStepper} is used;
107
     *                            <li>otherwise, no discrete stepper is used (especially, if both bounds are
108
     *                                <tt>null</tt>, no discrete stepper is actually useful)
109
     *                            </ul>
110
     * @return Range
111
     */
112
    public static function fromBounds(
113
        $lower,
114
        $upper,
115
        $boundsOrLowerInc = '[)',
116
        ?bool $upperInc = null,
117
        ?IValueComparator $customComparator = null,
118
        ?IDiscreteStepper $customDiscreteStepper = null
119
    ): Range {
120
        list($loInc, $upInc) = self::processBoundSpec($boundsOrLowerInc, $upperInc);
121
122
        $comparator = ($customComparator ?? Ivory::getDefaultValueComparator());
123
        $discreteStepper = ($customDiscreteStepper ?? self::inferDiscreteStepper($lower ?? $upper));
124
125
        if ($lower !== null && $upper !== null) {
126
            $comp = $comparator->compareValues($lower, $upper);
127
            if ($comp > 0) {
128
                return self::empty();
129
            } elseif ($comp == 0) {
130
                if (!$loInc || !$upInc) {
131
                    return self::empty();
132
                }
133
            } elseif (!$loInc && !$upInc && $discreteStepper !== null) {
134
                $upperPred = $discreteStepper->step(-1, $upper);
135
                $isEmpty = ($comparator->compareValues($lower, $upperPred) == 0);
136
                if ($isEmpty) {
137
                    return self::empty();
138
                }
139
            }
140
        } else {
141
            if ($lower === null) {
142
                $loInc = false;
143
            }
144
            if ($upper === null) {
145
                $upInc = false;
146
            }
147
        }
148
149
        return new static(false, $lower, $upper, $loInc, $upInc, $comparator, $discreteStepper);
150
    }
151
152
    private static function inferDiscreteStepper($value): ?IDiscreteStepper
153
    {
154
        if (is_int($value)) {
155
            static $intStepper = null;
156
            if ($intStepper === null) {
157
                $intStepper = new IntegerStepper();
158
            }
159
            return $intStepper;
160
        } elseif ($value instanceof IDiscreteStepper) {
161
            return $value;
162
        } elseif (is_string($value) && BigIntSafeType::isIntegerString($value)) {
163
            static $bigIntSafeStepper = null;
164
            if ($bigIntSafeStepper === null) {
165
                $bigIntSafeStepper = new BigIntSafeStepper();
166
            }
167
            return $bigIntSafeStepper;
168
        } else {
169
            return null;
170
        }
171
    }
172
173
    /**
174
     * Creates a new empty range.
175
     *
176
     * @return Range
177
     */
178
    public static function empty(): Range
179
    {
180
        $comparator = Ivory::getDefaultValueComparator(); // we must provide one, although for empty range it is useless
181
        return new static(true, null, null, null, null, $comparator, null);
182
    }
183
184
    private static function processBoundSpec($boundsOrLowerInc = '[)', ?bool $upperInc = null)
185
    {
186
        if (is_string($boundsOrLowerInc)) {
187
            if ($upperInc !== null) {
188
                trigger_error('$upperInc is irrelevant - string specification for $boundsOrLowerInc given', E_USER_NOTICE);
189
            }
190
            // OPT: measure whether preg_match('~^[[(][\])]$~', $boundsOrLowerInc) would not be faster
191
            if (strlen($boundsOrLowerInc) != 2 || // OPT: isset($boundsOrLowerInc[2] might be faster
192
                strpos('([', $boundsOrLowerInc[0]) === false ||
193
                strpos(')]', $boundsOrLowerInc[1]) === false
194
            ) {
195
                $msg = "Invalid bounds inclusive/exclusive specification string: $boundsOrLowerInc";
196
                throw new \InvalidArgumentException($msg);
197
            }
198
199
            $loInc = ($boundsOrLowerInc[0] == '[');
200
            $upInc = ($boundsOrLowerInc[1] == ']');
201
        } else {
202
            $loInc = (bool)$boundsOrLowerInc;
203
            $upInc = (bool)$upperInc;
204
        }
205
206
        return [$loInc, $upInc];
207
    }
208
209
210
    private function __construct(
211
        bool $empty,
212
        $lower,
213
        $upper,
214
        ?bool $lowerInc,
215
        ?bool $upperInc,
216
        IValueComparator $comparator,
217
        ?IDiscreteStepper $discreteStepper
218
    ) {
219
        $this->empty = $empty;
220
        $this->lower = $lower;
221
        $this->upper = $upper;
222
        $this->lowerInc = $lowerInc;
223
        $this->upperInc = $upperInc;
224
        $this->comparator = $comparator;
225
        $this->discreteStepper = $discreteStepper;
226
    }
227
228
    //endregion
229
230
    //region getters
231
232
    final public function isEmpty(): bool
233
    {
234
        return $this->empty;
235
    }
236
237
    /**
238
     * @return mixed lower bound, or <tt>null</tt> if the range is lower-unbounded or empty
239
     */
240
    final public function getLower()
241
    {
242
        return $this->lower;
243
    }
244
245
    /**
246
     * @return mixed upper bound, or <tt>null</tt> if the range is upper-unbounded or empty
247
     */
248
    final public function getUpper()
249
    {
250
        return $this->upper;
251
    }
252
253
    /**
254
     * @return bool|null whether the range includes its lower bound, or <tt>null</tt> if the range is empty;
255
     *                   for lower-unbounded ranges, <tt>false</tt> is returned by definition
256
     */
257
    final public function isLowerInc(): ?bool
258
    {
259
        return $this->lowerInc;
260
    }
261
262
    /**
263
     * @return bool|null whether the range includes its upper bound, or <tt>null</tt> if the range is empty;
264
     *                   for upper-unbounded ranges, <tt>false</tt> is returned by definition
265
     */
266
    final public function isUpperInc(): ?bool
267
    {
268
        return $this->upperInc;
269
    }
270
271
    /**
272
     * @return string|null the bounds inclusive/exclusive specification, as accepted by {@link fromBounds()}, or
273
     *                     <tt>null</tt> if the range is empty
274
     */
275
    final public function getBoundsSpec(): ?string
276
    {
277
        if ($this->empty) {
278
            return null;
279
        } else {
280
            return ($this->lowerInc ? '[' : '(') . ($this->upperInc ? ']' : ')');
281
        }
282
    }
283
284
    /**
285
     * @return bool whether the range is just a single point
286
     */
287
    final public function isSinglePoint(): bool
288
    {
289
        if ($this->empty || $this->lower === null || $this->upper === null) {
290
            return false;
291
        }
292
293
        $cmp = $this->comparator->compareValues($this->lower, $this->upper);
294
        if ($cmp == 0) {
295
            return ($this->lowerInc && $this->upperInc);
296
        }
297
        if ($this->lowerInc && $this->upperInc) { // optimization
298
            return false;
299
        }
300
        if ($this->discreteStepper !== null) {
301
            $lo = ($this->lowerInc ? $this->lower : $this->discreteStepper->step(1, $this->lower));
302
            $up = ($this->upperInc ? $this->upper : $this->discreteStepper->step(-1, $this->upper));
303
            return ($this->comparator->compareValues($lo, $up) == 0);
304
        } else {
305
            return false;
306
        }
307
    }
308
309
    /**
310
     * Returns the range bounds according to the requested bound specification.
311
     *
312
     * **Only defined on ranges of {@link IDiscreteStepper discrete} subtypes.**
313
     *
314
     * E.g., on an integer range `(null,3)`, if bounds `[]` are requested, a pair of `null` and `2` is returned.
315
     *
316
     * @param bool|string $boundsOrLowerInc either the string of complete bounds specification, or a boolean telling
317
     *                                        whether the lower bound is inclusive;
318
     *                                      the complete specification is similar to PostgreSQL - it is a two-character
319
     *                                        string of <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt>
320
     *                                        (brackets denoting an inclusive bound, parentheses denoting an exclusive
321
     *                                        bound;
322
     *                                      when the boolean is used, also the <tt>$upperInc</tt> argument is used
323
     * @param bool $upperInc whether the upper bound is inclusive;
324
     *                       only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
325
     * @return array|null pair of the lower and upper bound, or <tt>null</tt> if the range is empty
326
     * @throws UnsupportedException if the range subtype is not discrete and no custom discrete stepper has been
327
     *                                provided
328
     */
329
    public function toBounds($boundsOrLowerInc, ?bool $upperInc = null): ?array
330
    {
331
        if ($this->empty) {
332
            return null;
333
        }
334
335
        if ($this->lower === null && $this->upper === null) {
336
            return [null, null]; // no matter of the actually requested bounds, the result is (-inf,inf)
337
        }
338
339
        if ($this->discreteStepper === null) {
340
            throw new UnsupportedException(
341
                'Cannot convert the range bounds - the subtype was not recognized as a ' . IDiscreteStepper::class .
342
                ', and no custom discrete stepper has been provided.'
343
            );
344
        }
345
346
        list($loInc, $upInc) = self::processBoundSpec($boundsOrLowerInc, $upperInc);
347
348
        if ($this->lower === null) {
349
            $lo = null;
350
        } else {
351
            $lo = $this->lower;
352
            $step = (int)$loInc - (int)$this->lowerInc;
353
            if ($step) {
354
                $lo = $this->discreteStepper->step($step, $lo);
355
            }
356
        }
357
358
        if ($this->upper === null) {
359
            $up = null;
360
        } else {
361
            $up = $this->upper;
362
            $step = (int)$this->upperInc - (int)$upInc;
363
            if ($step) {
364
                $up = $this->discreteStepper->step($step, $up);
365
            }
366
        }
367
368
        return [$lo, $up];
369
    }
370
371
    final public function __toString()
372
    {
373
        if ($this->empty) {
374
            return 'empty';
375
        }
376
377
        $bounds = $this->getBoundsSpec();
378
        return sprintf('%s%s,%s%s',
379
            $bounds[0],
380
            ($this->lower === null ? '-infinity' : $this->lower),
381
            ($this->upper === null ? 'infinity' : $this->upper),
382
            $bounds[1]
383
        );
384
    }
385
386
    //endregion
387
388
    //region range operations
389
390
    /**
391
     * @param mixed $element a value of the range subtype
392
     * @return bool|null whether this range contains the given element;
393
     *                   <tt>null</tt> on <tt>null</tt> input
394
     */
395
    public function containsElement($element): ?bool
396
    {
397
        if ($element === null) {
398
            return null;
399
        }
400
        if ($this->empty) {
401
            return false;
402
        }
403
404
        if ($this->lower !== null) {
405
            $cmp = $this->comparator->compareValues($element, $this->lower);
406
            if ($cmp < 0 || ($cmp == 0 && !$this->lowerInc)) {
407
                return false;
408
            }
409
        }
410
411
        if ($this->upper !== null) {
412
            $cmp = $this->comparator->compareValues($element, $this->upper);
413
            if ($cmp > 0 || ($cmp == 0 && !$this->upperInc)) {
414
                return false;
415
            }
416
        }
417
418
        return true;
419
    }
420
421
    /**
422
     * @param mixed $element value of the range subtype
423
     * @return bool|null <tt>true</tt> iff this range is left of the given element - value of the range subtype;
424
     *                   <tt>false</tt> otherwise, especially if this range is empty;
425
     *                   <tt>null</tt> on <tt>null</tt> input
426
     */
427
    public function leftOfElement($element): ?bool
428
    {
429
        if ($element === null) {
430
            return null;
431
        }
432
        if ($this->empty) {
433
            return false;
434
        }
435
436
        if ($this->upper === null) {
437
            return false;
438
        }
439
        $cmp = $this->comparator->compareValues($element, $this->upper);
440
        return ($cmp > 0 || ($cmp == 0 && !$this->upperInc));
441
    }
442
443
    /**
444
     * @param mixed $element value of the range subtype
445
     * @return bool|null <tt>true</tt> iff this range is right of the given element - value of the range subtype;
446
     *                   <tt>false</tt> otherwise, especially if this range is empty;
447
     *                   <tt>null</tt> on <tt>null</tt> input
448
     */
449
    public function rightOfElement($element): ?bool
450
    {
451
        if ($element === null) {
452
            return null;
453
        }
454
        if ($this->empty) {
455
            return false;
456
        }
457
458
        if ($this->lower === null) {
459
            return false;
460
        }
461
        $cmp = $this->comparator->compareValues($element, $this->lower);
462
        return ($cmp < 0 || ($cmp == 0 && !$this->lowerInc));
463
    }
464
465
    /**
466
     * @param Range $other a range of the same subtype as this range
467
     * @return bool|null whether this range entirely contains the other range;
468
     *                   an empty range is considered to be contained in any range, even an empty one;
469
     *                   <tt>null</tt> on <tt>null</tt> input
470
     */
471
    public function containsRange(?Range $other): ?bool
472
    {
473
        if ($other === null) {
474
            return null;
475
        }
476
        if ($other->empty) {
477
            return true;
478
        }
479
        if ($this->empty) {
480
            return false;
481
        }
482
483
        if ($this->lower !== null) {
484
            if ($other->lower === null) {
485
                return false;
486
            } else {
487
                $cmp = $this->comparator->compareValues($this->lower, $other->lower);
488
                if ($cmp > 0 || ($cmp == 0 && !$this->lowerInc && $other->lowerInc)) {
489
                    return false;
490
                }
491
            }
492
        }
493
494
        if ($this->upper !== null) {
495
            if ($other->upper === null) {
496
                return false;
497
            } else {
498
                $cmp = $this->comparator->compareValues($this->upper, $other->upper);
499
                if ($cmp < 0 || ($cmp == 0 && !$this->upperInc && $other->upperInc)) {
500
                    return false;
501
                }
502
            }
503
        }
504
505
        return true;
506
    }
507
508
    /**
509
     * @param Range $other a range of the same subtype as this range
510
     * @return bool|null whether this range is entirely contained in the other range;
511
     *                   an empty range is considered to be contained in any range, even an empty one;
512
     *                   <tt>null</tt> on <tt>null</tt> input
513
     */
514
    public function containedInRange(?Range $other): ?bool
515
    {
516
        if ($other === null) {
517
            return null;
518
        }
519
        return $other->containsRange($this);
520
    }
521
522
    /**
523
     * @param Range $other a range of the same subtype as this range
524
     * @return bool|null whether this and the other range overlap, i.e., have a non-empty intersection;
525
     *                   <tt>null</tt> on <tt>null</tt> input
526
     */
527
    public function overlaps(?Range $other): ?bool
528
    {
529
        if ($other === null) {
530
            return null;
531
        }
532
        if ($this->empty || $other->empty) {
533
            return false;
534
        }
535
536
        if ($this->lower !== null && $other->upper !== null) {
537
            $cmp = $this->comparator->compareValues($this->lower, $other->upper);
538
            if ($cmp > 0 || ($cmp == 0 && (!$this->lowerInc || !$other->upperInc))) {
539
                return false;
540
            }
541
        }
542
        if ($other->lower !== null && $this->upper !== null) {
543
            $cmp = $this->comparator->compareValues($other->lower, $this->upper);
544
            if ($cmp > 0 || ($cmp == 0 && (!$other->lowerInc || !$this->upperInc))) {
545
                return false;
546
            }
547
        }
548
        return true;
549
    }
550
551
    /**
552
     * Computes the intersection of this range with another range.
553
     *
554
     * @param Range $other a range of the same subtype as this range
555
     * @return Range|null intersection of this and the other range
556
     *                    <tt>null</tt> on <tt>null</tt> input
557
     */
558
    public function intersect(?Range $other): ?Range
559
    {
560
        if ($other === null) {
561
            return null;
562
        }
563
        if ($this->empty) {
564
            return $this;
565
        }
566
        if ($other->empty) {
567
            return $other;
568
        }
569
570
        if ($this->lower === null) {
571
            $lo = $other->lower;
572
            $loInc = $other->lowerInc;
573
        } elseif ($other->lower === null) {
574
            $lo = $this->lower;
575
            $loInc = $this->lowerInc;
576
        } else {
577
            $cmp = $this->comparator->compareValues($this->lower, $other->lower);
578
            if ($cmp < 0) {
579
                $lo = $other->lower;
580
                $loInc = $other->lowerInc;
581
            } elseif ($cmp > 0) {
582
                $lo = $this->lower;
583
                $loInc = $this->lowerInc;
584
            } else {
585
                $lo = $this->lower;
586
                $loInc = ($this->lowerInc && $other->lowerInc);
587
            }
588
        }
589
590
        if ($this->upper === null) {
591
            $up = $other->upper;
592
            $upInc = $other->upperInc;
593
        } elseif ($other->upper === null) {
594
            $up = $this->upper;
595
            $upInc = $this->upperInc;
596
        } else {
597
            $cmp = $this->comparator->compareValues($this->upper, $other->upper);
598
            if ($cmp < 0) {
599
                $up = $this->upper;
600
                $upInc = $this->upperInc;
601
            } elseif ($cmp > 0) {
602
                $up = $other->upper;
603
                $upInc = $other->upperInc;
604
            } else {
605
                $up = $this->upper;
606
                $upInc = ($this->upperInc && $other->upperInc);
607
            }
608
        }
609
610
        return self::fromBounds($lo, $up, $loInc, $upInc, $this->comparator, $this->discreteStepper);
611
    }
612
613
    /**
614
     * @return bool whether the range is finite, i.e., neither starts nor ends in the infinity;
615
     *              note that an empty range is considered as finite
616
     */
617
    final public function isFinite(): bool
618
    {
619
        return ($this->empty || ($this->lower !== null && $this->upper !== null));
620
    }
621
622
    /**
623
     * @param Range|null $other a range of the same subtype as this range
624
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
625
     *                     other starts;
626
     *                   <tt>false</tt> otherwise, especially if either range is empty;
627
     *                   <tt>null</tt> on <tt>null</tt> input
628
     */
629
    public function strictlyLeftOf(?Range $other): ?bool
630
    {
631
        if ($other === null) {
632
            return null;
633
        }
634
        if ($this->empty || $other->empty) {
635
            return false;
636
        }
637
638
        if ($this->upper === null || $other->lower === null) {
639
            return false;
640
        }
641
        $cmp = $this->comparator->compareValues($this->upper, $other->lower);
642
        return ($cmp < 0 || ($cmp == 0 && (!$this->upperInc || !$other->lowerInc)));
643
    }
644
645
    /**
646
     * @param Range|null $other a range of the same subtype as this range
647
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
648
     *                     other starts;
649
     *                   <tt>false</tt> otherwise, especially if either range is empty;;
650
     *                   <tt>null</tt> on <tt>null</tt> input
651
     */
652
    public function strictlyRightOf(?Range $other): ?bool
653
    {
654
        if ($other === null) {
655
            return null;
656
        }
657
        if ($this->empty || $other->empty) {
658
            return false;
659
        }
660
661
        if ($other->upper === null || $this->lower === null) {
662
            return false;
663
        }
664
        $cmp = $this->comparator->compareValues($other->upper, $this->lower);
665
        return ($cmp < 0 || ($cmp == 0 && (!$other->upperInc || !$this->lowerInc)));
666
    }
667
668
    //endregion
669
670
    //region IComparable
671
672
    public function equals($other): bool
673
    {
674
        if (!$other instanceof Range) {
675
            return false;
676
        }
677
678
        if ($this->empty) {
679
            return $other->empty;
680
        }
681
        if ($other->empty) {
682
            return false;
683
        }
684
685
        $objLower = $other->lower;
686
        $objUpper = $other->upper;
687
688
        if ($this->lowerInc != $other->lowerInc) {
689
            if ($this->discreteStepper === null) {
690
                return false; // no chance of converting to equivalent lower bounds
691
            }
692
693
            if ($objLower !== null) {
694
                $objLower = $this->discreteStepper->step(($other->lowerInc ? -1 : 1), $objLower);
695
            }
696
        }
697
        if ($this->upperInc != $other->upperInc) {
698
            if ($this->discreteStepper === null) {
699
                return false; // no chance of converting to equivalent upper bounds
700
            }
701
            if ($objUpper !== null) {
702
                $objUpper = $this->discreteStepper->step(($other->upperInc ? 1 : -1), $objUpper);
703
            }
704
        }
705
706
        if ($this->lower === null) {
707
            if ($objLower !== null) {
708
                return false;
709
            }
710
        } else {
711
            if ($this->comparator->compareValues($this->lower, $objLower) != 0) {
712
                return false;
713
            }
714
        }
715
716
        if ($this->upper === null) {
717
            if ($objUpper !== null) {
718
                return false;
719
            }
720
        } else {
721
            if ($this->comparator->compareValues($this->upper, $objUpper) != 0) {
722
                return false;
723
            }
724
        }
725
726
        return true;
727
    }
728
729
    public function compareTo($other): int
730
    {
731
        if ($other === null) {
732
            throw new \InvalidArgumentException('comparing with null');
733
        }
734
        if (!$other instanceof Range) {
735
            throw new IncomparableException('$other is not a ' . Range::class);
736
        }
737
738
        if ($this->isEmpty() && $other->isEmpty()) {
739
            return 0;
740
        } elseif ($this->isEmpty()) {
741
            return -1;
742
        } elseif ($other->isEmpty()) {
743
            return 1;
744
        }
745
746
        $cmp = $this->compareBounds(-1, $this->getLower(), $this->isLowerInc(), $other->getLower(), $other->isLowerInc());
747
        if ($cmp != 0) {
748
            return $cmp;
749
        } else {
750
            return $this->compareBounds(1, $this->getUpper(), $this->isUpperInc(), $other->getUpper(), $other->isUpperInc());
751
        }
752
    }
753
754
    private function compareBounds(int $sgn, $aVal, bool $aIsInc, $bVal, bool $bIsInc): int
755
    {
756
        if ($aVal === null && $bVal === null) {
757
            return 0;
758
        } elseif ($aVal === null) {
759
            return 1 * $sgn;
760
        } elseif ($bVal === null) {
761
            return -1 * $sgn;
762
        }
763
764
        $cmp = $this->comparator->compareValues($aVal, $bVal);
765
        if ($cmp != 0) {
766
            return $cmp;
767
        }
768
769
        if ($aIsInc && $bIsInc) {
770
            return 0;
771
        } elseif ($aIsInc) {
772
            return 1 * $sgn;
773
        } elseif ($bIsInc) {
774
            return -1 * $sgn;
775
        } else {
776
            return 0;
777
        }
778
    }
779
780
    //endregion
781
}
782