Passed
Push — master ( 94f321...811ae6 )
by Ondřej
02:56 queued 12s
created

Range::strictlyRightOf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 6
rs 10
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 https://www.postgresql.org/docs/11/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
        [$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(
189
                    '$upperInc is irrelevant - string specification for $boundsOrLowerInc given',
190
                    E_USER_NOTICE
191
                );
192
            }
193
            // OPT: measure whether preg_match('~^[[(][\])]$~', $boundsOrLowerInc) would not be faster
194
            if (strlen($boundsOrLowerInc) != 2 || // OPT: isset($boundsOrLowerInc[2] might be faster
195
                strpos('([', $boundsOrLowerInc[0]) === false ||
196
                strpos(')]', $boundsOrLowerInc[1]) === false
197
            ) {
198
                $msg = "Invalid bounds inclusive/exclusive specification string: $boundsOrLowerInc";
199
                throw new \InvalidArgumentException($msg);
200
            }
201
202
            $loInc = ($boundsOrLowerInc[0] == '[');
203
            $upInc = ($boundsOrLowerInc[1] == ']');
204
        } else {
205
            $loInc = (bool)$boundsOrLowerInc;
206
            $upInc = (bool)$upperInc;
207
        }
208
209
        return [$loInc, $upInc];
210
    }
211
212
213
    private function __construct(
214
        bool $empty,
215
        $lower,
216
        $upper,
217
        ?bool $lowerInc,
218
        ?bool $upperInc,
219
        IValueComparator $comparator,
220
        ?IDiscreteStepper $discreteStepper
221
    ) {
222
        $this->empty = $empty;
223
        $this->lower = $lower;
224
        $this->upper = $upper;
225
        $this->lowerInc = $lowerInc;
226
        $this->upperInc = $upperInc;
227
        $this->comparator = $comparator;
228
        $this->discreteStepper = $discreteStepper;
229
    }
230
231
    //endregion
232
233
    //region getters
234
235
    final public function isEmpty(): bool
236
    {
237
        return $this->empty;
238
    }
239
240
    /**
241
     * @return mixed lower bound, or <tt>null</tt> if the range is lower-unbounded or empty
242
     */
243
    final public function getLower()
244
    {
245
        return $this->lower;
246
    }
247
248
    /**
249
     * @return mixed upper bound, or <tt>null</tt> if the range is upper-unbounded or empty
250
     */
251
    final public function getUpper()
252
    {
253
        return $this->upper;
254
    }
255
256
    /**
257
     * @return bool|null whether the range includes its lower bound, or <tt>null</tt> if the range is empty;
258
     *                   for lower-unbounded ranges, <tt>false</tt> is returned by definition
259
     */
260
    final public function isLowerInc(): ?bool
261
    {
262
        return $this->lowerInc;
263
    }
264
265
    /**
266
     * @return bool|null whether the range includes its upper bound, or <tt>null</tt> if the range is empty;
267
     *                   for upper-unbounded ranges, <tt>false</tt> is returned by definition
268
     */
269
    final public function isUpperInc(): ?bool
270
    {
271
        return $this->upperInc;
272
    }
273
274
    /**
275
     * @return string|null the bounds inclusive/exclusive specification, as accepted by {@link fromBounds()}, or
276
     *                     <tt>null</tt> if the range is empty
277
     */
278
    final public function getBoundsSpec(): ?string
279
    {
280
        if ($this->empty) {
281
            return null;
282
        } else {
283
            return ($this->lowerInc ? '[' : '(') . ($this->upperInc ? ']' : ')');
284
        }
285
    }
286
287
    /**
288
     * @return bool whether the range is just a single point
289
     */
290
    final public function isSinglePoint(): bool
291
    {
292
        if ($this->empty || $this->lower === null || $this->upper === null) {
293
            return false;
294
        }
295
296
        $cmp = $this->comparator->compareValues($this->lower, $this->upper);
297
        if ($cmp == 0) {
298
            return ($this->lowerInc && $this->upperInc);
299
        }
300
        if ($this->lowerInc && $this->upperInc) { // optimization
301
            return false;
302
        }
303
        if ($this->discreteStepper !== null) {
304
            $lo = ($this->lowerInc ? $this->lower : $this->discreteStepper->step(1, $this->lower));
305
            $up = ($this->upperInc ? $this->upper : $this->discreteStepper->step(-1, $this->upper));
306
            return ($this->comparator->compareValues($lo, $up) == 0);
307
        } else {
308
            return false;
309
        }
310
    }
311
312
    /**
313
     * Returns the range bounds according to the requested bound specification.
314
     *
315
     * **Only defined on ranges of {@link IDiscreteStepper discrete} subtypes.**
316
     *
317
     * E.g., on an integer range `(null,3)`, if bounds `[]` are requested, a pair of `null` and `2` is returned.
318
     *
319
     * @param bool|string $boundsOrLowerInc either the string of complete bounds specification, or a boolean telling
320
     *                                        whether the lower bound is inclusive;
321
     *                                      the complete specification is similar to PostgreSQL - it is a two-character
322
     *                                        string of <tt>'['</tt> or <tt>'('</tt>, and <tt>']'</tt> or <tt>')'</tt>
323
     *                                        (brackets denoting an inclusive bound, parentheses denoting an exclusive
324
     *                                        bound;
325
     *                                      when the boolean is used, also the <tt>$upperInc</tt> argument is used
326
     * @param bool $upperInc whether the upper bound is inclusive;
327
     *                       only relevant if <tt>$boundsOrLowerInc</tt> is also boolean
328
     * @return array|null pair of the lower and upper bound, or <tt>null</tt> if the range is empty
329
     * @throws UnsupportedException if the range subtype is not discrete and no custom discrete stepper has been
330
     *                                provided
331
     */
332
    public function toBounds($boundsOrLowerInc, ?bool $upperInc = null): ?array
333
    {
334
        if ($this->empty) {
335
            return null;
336
        }
337
338
        if ($this->lower === null && $this->upper === null) {
339
            return [null, null]; // no matter of the actually requested bounds, the result is (-inf,inf)
340
        }
341
342
        if ($this->discreteStepper === null) {
343
            throw new UnsupportedException(
344
                'Cannot convert the range bounds - the subtype was not recognized as a ' . IDiscreteStepper::class .
345
                ', and no custom discrete stepper has been provided.'
346
            );
347
        }
348
349
        [$loInc, $upInc] = self::processBoundSpec($boundsOrLowerInc, $upperInc);
350
351
        if ($this->lower === null) {
352
            $lo = null;
353
        } else {
354
            $lo = $this->lower;
355
            $step = (int)$loInc - (int)$this->lowerInc;
356
            if ($step) {
357
                $lo = $this->discreteStepper->step($step, $lo);
358
            }
359
        }
360
361
        if ($this->upper === null) {
362
            $up = null;
363
        } else {
364
            $up = $this->upper;
365
            $step = (int)$this->upperInc - (int)$upInc;
366
            if ($step) {
367
                $up = $this->discreteStepper->step($step, $up);
368
            }
369
        }
370
371
        return [$lo, $up];
372
    }
373
374
    final public function __toString()
375
    {
376
        if ($this->empty) {
377
            return 'empty';
378
        }
379
380
        $bounds = $this->getBoundsSpec();
381
        return sprintf('%s%s,%s%s',
382
            $bounds[0],
383
            ($this->lower === null ? '-infinity' : $this->lower),
384
            ($this->upper === null ? 'infinity' : $this->upper),
385
            $bounds[1]
386
        );
387
    }
388
389
    //endregion
390
391
    //region range operations
392
393
    /**
394
     * @param mixed $element a value of the range subtype
395
     * @return bool|null whether this range contains the given element;
396
     *                   <tt>null</tt> on <tt>null</tt> input
397
     */
398
    public function containsElement($element): ?bool
399
    {
400
        if ($element === null) {
401
            return null;
402
        }
403
        if ($this->empty) {
404
            return false;
405
        }
406
407
        if ($this->lower !== null) {
408
            $cmp = $this->comparator->compareValues($element, $this->lower);
409
            if ($cmp < 0 || ($cmp == 0 && !$this->lowerInc)) {
410
                return false;
411
            }
412
        }
413
414
        if ($this->upper !== null) {
415
            $cmp = $this->comparator->compareValues($element, $this->upper);
416
            if ($cmp > 0 || ($cmp == 0 && !$this->upperInc)) {
417
                return false;
418
            }
419
        }
420
421
        return true;
422
    }
423
424
    /**
425
     * @param mixed $element value of the range subtype
426
     * @return bool|null <tt>true</tt> iff this range is left of the given element - value of the range subtype;
427
     *                   <tt>false</tt> otherwise, especially if this range is empty;
428
     *                   <tt>null</tt> on <tt>null</tt> input
429
     */
430
    public function leftOfElement($element): ?bool
431
    {
432
        if ($element === null) {
433
            return null;
434
        }
435
        if ($this->empty) {
436
            return false;
437
        }
438
439
        if ($this->upper === null) {
440
            return false;
441
        }
442
        $cmp = $this->comparator->compareValues($element, $this->upper);
443
        return ($cmp > 0 || ($cmp == 0 && !$this->upperInc));
444
    }
445
446
    /**
447
     * @param mixed $element value of the range subtype
448
     * @return bool|null <tt>true</tt> iff this range is right of the given element - value of the range subtype;
449
     *                   <tt>false</tt> otherwise, especially if this range is empty;
450
     *                   <tt>null</tt> on <tt>null</tt> input
451
     */
452
    public function rightOfElement($element): ?bool
453
    {
454
        if ($element === null) {
455
            return null;
456
        }
457
        if ($this->empty) {
458
            return false;
459
        }
460
461
        if ($this->lower === null) {
462
            return false;
463
        }
464
        $cmp = $this->comparator->compareValues($element, $this->lower);
465
        return ($cmp < 0 || ($cmp == 0 && !$this->lowerInc));
466
    }
467
468
    /**
469
     * @param Range $other a range of the same subtype as this range
470
     * @return bool|null whether this range entirely contains the other range;
471
     *                   an empty range is considered to be contained in any range, even an empty one;
472
     *                   <tt>null</tt> on <tt>null</tt> input
473
     */
474
    public function containsRange(?Range $other): ?bool
475
    {
476
        if ($other === null) {
477
            return null;
478
        }
479
        if ($other->empty) {
480
            return true;
481
        }
482
        if ($this->empty) {
483
            return false;
484
        }
485
486
        if ($this->lower !== null) {
487
            if ($other->lower === null) {
488
                return false;
489
            } else {
490
                $cmp = $this->comparator->compareValues($this->lower, $other->lower);
491
                if ($cmp > 0 || ($cmp == 0 && !$this->lowerInc && $other->lowerInc)) {
492
                    return false;
493
                }
494
            }
495
        }
496
497
        if ($this->upper !== null) {
498
            if ($other->upper === null) {
499
                return false;
500
            } else {
501
                $cmp = $this->comparator->compareValues($this->upper, $other->upper);
502
                if ($cmp < 0 || ($cmp == 0 && !$this->upperInc && $other->upperInc)) {
503
                    return false;
504
                }
505
            }
506
        }
507
508
        return true;
509
    }
510
511
    /**
512
     * @param Range $other a range of the same subtype as this range
513
     * @return bool|null whether this range is entirely contained in the other range;
514
     *                   an empty range is considered to be contained in any range, even an empty one;
515
     *                   <tt>null</tt> on <tt>null</tt> input
516
     */
517
    public function containedInRange(?Range $other): ?bool
518
    {
519
        if ($other === null) {
520
            return null;
521
        }
522
        return $other->containsRange($this);
523
    }
524
525
    /**
526
     * @param Range $other a range of the same subtype as this range
527
     * @return bool|null whether this and the other range overlap, i.e., have a non-empty intersection;
528
     *                   <tt>null</tt> on <tt>null</tt> input
529
     */
530
    public function overlaps(?Range $other): ?bool
531
    {
532
        if ($other === null) {
533
            return null;
534
        }
535
        if ($this->empty || $other->empty) {
536
            return false;
537
        }
538
539
        if ($this->lower !== null && $other->upper !== null) {
540
            $cmp = $this->comparator->compareValues($this->lower, $other->upper);
541
            if ($cmp > 0 || ($cmp == 0 && (!$this->lowerInc || !$other->upperInc))) {
542
                return false;
543
            }
544
        }
545
        if ($other->lower !== null && $this->upper !== null) {
546
            $cmp = $this->comparator->compareValues($other->lower, $this->upper);
547
            if ($cmp > 0 || ($cmp == 0 && (!$other->lowerInc || !$this->upperInc))) {
548
                return false;
549
            }
550
        }
551
        return true;
552
    }
553
554
    /**
555
     * Computes the intersection of this range with another range.
556
     *
557
     * @param Range $other a range of the same subtype as this range
558
     * @return Range|null intersection of this and the other range
559
     *                    <tt>null</tt> on <tt>null</tt> input
560
     */
561
    public function intersect(?Range $other): ?Range
562
    {
563
        if ($other === null) {
564
            return null;
565
        }
566
        if ($this->empty) {
567
            return $this;
568
        }
569
        if ($other->empty) {
570
            return $other;
571
        }
572
573
        if ($this->lower === null) {
574
            $lo = $other->lower;
575
            $loInc = $other->lowerInc;
576
        } elseif ($other->lower === null) {
577
            $lo = $this->lower;
578
            $loInc = $this->lowerInc;
579
        } else {
580
            $cmp = $this->comparator->compareValues($this->lower, $other->lower);
581
            if ($cmp < 0) {
582
                $lo = $other->lower;
583
                $loInc = $other->lowerInc;
584
            } elseif ($cmp > 0) {
585
                $lo = $this->lower;
586
                $loInc = $this->lowerInc;
587
            } else {
588
                $lo = $this->lower;
589
                $loInc = ($this->lowerInc && $other->lowerInc);
590
            }
591
        }
592
593
        if ($this->upper === null) {
594
            $up = $other->upper;
595
            $upInc = $other->upperInc;
596
        } elseif ($other->upper === null) {
597
            $up = $this->upper;
598
            $upInc = $this->upperInc;
599
        } else {
600
            $cmp = $this->comparator->compareValues($this->upper, $other->upper);
601
            if ($cmp < 0) {
602
                $up = $this->upper;
603
                $upInc = $this->upperInc;
604
            } elseif ($cmp > 0) {
605
                $up = $other->upper;
606
                $upInc = $other->upperInc;
607
            } else {
608
                $up = $this->upper;
609
                $upInc = ($this->upperInc && $other->upperInc);
610
            }
611
        }
612
613
        return self::fromBounds($lo, $up, $loInc, $upInc, $this->comparator, $this->discreteStepper);
614
    }
615
616
    /**
617
     * @return bool whether the range is finite, i.e., neither starts nor ends in the infinity;
618
     *              note that an empty range is considered as finite
619
     */
620
    final public function isFinite(): bool
621
    {
622
        return ($this->empty || ($this->lower !== null && $this->upper !== null));
623
    }
624
625
    /**
626
     * @param Range|null $other a range of the same subtype as this range
627
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
628
     *                     other starts;
629
     *                   <tt>false</tt> otherwise, especially if either range is empty;
630
     *                   <tt>null</tt> on <tt>null</tt> input
631
     */
632
    public function strictlyLeftOf(?Range $other): ?bool
633
    {
634
        if ($other === null) {
635
            return null;
636
        }
637
        if ($this->empty || $other->empty) {
638
            return false;
639
        }
640
641
        if ($this->upper === null || $other->lower === null) {
642
            return false;
643
        }
644
        $cmp = $this->comparator->compareValues($this->upper, $other->lower);
645
        return ($cmp < 0 || ($cmp == 0 && (!$this->upperInc || !$other->lowerInc)));
646
    }
647
648
    /**
649
     * @param Range|null $other a range of the same subtype as this range
650
     * @return bool|null <tt>true</tt> iff this range is strictly left of the other range, i.e., it ends before the
651
     *                     other starts;
652
     *                   <tt>false</tt> otherwise, especially if either range is empty;;
653
     *                   <tt>null</tt> on <tt>null</tt> input
654
     */
655
    public function strictlyRightOf(?Range $other): ?bool
656
    {
657
        if ($other === null) {
658
            return null;
659
        } else {
660
            return $other->strictlyLeftOf($this);
661
        }
662
    }
663
664
    //endregion
665
666
    //region IComparable
667
668
    public function equals($other): bool
669
    {
670
        if (!$other instanceof Range) {
671
            return false;
672
        }
673
674
        if ($this->empty) {
675
            return $other->empty;
676
        }
677
        if ($other->empty) {
678
            return false;
679
        }
680
681
        $objLower = $other->lower;
682
        $objUpper = $other->upper;
683
684
        if ($this->lowerInc != $other->lowerInc) {
685
            if ($this->discreteStepper === null) {
686
                return false; // no chance of converting to equivalent lower bounds
687
            }
688
689
            if ($objLower !== null) {
690
                $objLower = $this->discreteStepper->step(($other->lowerInc ? -1 : 1), $objLower);
691
            }
692
        }
693
        if ($this->upperInc != $other->upperInc) {
694
            if ($this->discreteStepper === null) {
695
                return false; // no chance of converting to equivalent upper bounds
696
            }
697
            if ($objUpper !== null) {
698
                $objUpper = $this->discreteStepper->step(($other->upperInc ? 1 : -1), $objUpper);
699
            }
700
        }
701
702
        if ($this->lower === null) {
703
            if ($objLower !== null) {
704
                return false;
705
            }
706
        } else {
707
            if ($this->comparator->compareValues($this->lower, $objLower) != 0) {
708
                return false;
709
            }
710
        }
711
712
        if ($this->upper === null) {
713
            if ($objUpper !== null) {
714
                return false;
715
            }
716
        } else {
717
            if ($this->comparator->compareValues($this->upper, $objUpper) != 0) {
718
                return false;
719
            }
720
        }
721
722
        return true;
723
    }
724
725
    public function compareTo($other): int
726
    {
727
        if ($other === null) {
728
            throw new \InvalidArgumentException('comparing with null');
729
        }
730
        if (!$other instanceof Range) {
731
            throw new IncomparableException('$other is not a ' . Range::class);
732
        }
733
734
        if ($this->isEmpty() && $other->isEmpty()) {
735
            return 0;
736
        } elseif ($this->isEmpty()) {
737
            return -1;
738
        } elseif ($other->isEmpty()) {
739
            return 1;
740
        }
741
742
        $cmp = $this->compareBounds(
743
            -1, $this->getLower(), $this->isLowerInc(), $other->getLower(), $other->isLowerInc()
744
        );
745
        if ($cmp != 0) {
746
            return $cmp;
747
        }
748
749
        return $this->compareBounds(
750
            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