Skip to main content

int_interval/
i8.rs

1use crate::res::{OneTwo, ZeroOneTwo};
2
3#[cfg(test)]
4mod basic_tests;
5#[cfg(test)]
6mod between_tests;
7#[cfg(test)]
8mod checked_minkowski_tests;
9#[cfg(test)]
10mod convex_hull_tests;
11#[cfg(test)]
12mod difference_tests;
13#[cfg(test)]
14mod intersection_tests;
15#[cfg(test)]
16mod symmetric_difference_tests;
17#[cfg(test)]
18mod union_tests;
19
20#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
21pub struct I8CO {
22    start: i8,
23    end_excl: i8,
24}
25
26// ------------------------------------------------------------
27// low-level api: construction / accessors / predicates
28// ------------------------------------------------------------
29mod basic {
30
31    use super::*;
32
33    impl I8CO {
34        #[inline]
35        pub const fn try_new(start: i8, end_excl: i8) -> Option<Self> {
36            if start < end_excl {
37                Some(Self { start, end_excl })
38            } else {
39                None
40            }
41        }
42
43        #[inline]
44        pub const unsafe fn new_unchecked(start: i8, end_excl: i8) -> Self {
45            debug_assert!(start < end_excl);
46            Self { start, end_excl }
47        }
48
49        #[inline]
50        pub const fn start(self) -> i8 {
51            self.start
52        }
53
54        #[inline]
55        pub const fn end_excl(self) -> i8 {
56            self.end_excl
57        }
58
59        #[inline]
60        pub const fn end_incl(self) -> i8 {
61            // i8_low_bound =< start < end_excl
62            self.end_excl - 1
63        }
64
65        #[inline]
66        pub const fn len(self) -> u8 {
67            const SIGN_MASK: u8 = 1 << (i8::BITS - 1);
68            ((self.end_excl as u8) ^ SIGN_MASK) - ((self.start as u8) ^ SIGN_MASK)
69        }
70
71        /// Returns the midpoint of the interval `[start, end_excl)`,
72        /// using floor division if the length is even.
73        ///
74        /// # Guarantees
75        /// - `midpoint()` ∈ `[self.start, self.end_excl - 1]`
76        /// - Works for intervals with maximum length (entire `i8` range)
77        #[inline]
78        pub const fn midpoint(self) -> i8 {
79            self.start + (self.len() / 2) as i8
80        }
81
82        /// Constructs an `I8CO` interval from a midpoint and length (`u8`).
83        ///
84        /// # Parameters
85        /// - `mid`: the desired midpoint of the interval
86        /// - `len`: the desired length of the interval in units, must be `1..=u8::MAX`
87        ///
88        /// # Returns
89        /// - `Some(I8CO)` if the interval `[start, end_excl)` can be represented in `i8`
90        /// - `None` if `len = 0` or the computed `start` / `end_excl` would overflow `i8`
91        ///
92        /// # Guarantees
93        /// - Returned interval satisfies `start < end_excl`
94        /// - Maximum accepted input length is `u8::MAX`
95        #[inline]
96        pub const fn checked_from_midpoint_len(mid: i8, len: u8) -> Option<Self> {
97            if len == 0 {
98                return None;
99            }
100
101            let half = (len / 2) as i8;
102
103            let Some(start) = mid.checked_sub(half) else {
104                return None;
105            };
106            let Some(end_incl) = mid.checked_add(half) else {
107                return None;
108            };
109            let Some(end_excl) = end_incl.checked_add((len % 2) as i8) else {
110                return None;
111            };
112
113            // # Safety
114            // This function uses `unsafe { Self::new_unchecked(start, end_excl) }` internally.
115            // The safety is guaranteed by the following checks:
116            // 1. `mid.checked_sub(half)` ensures `start` does not underflow `i8`.
117            // 2. `mid.checked_add(the_other_half)` ensures `end_excl` does not overflow `i8`.
118            // 3. Because `half >= 0` and `the_other_half > 0`, we have `start < end_excl`.
119            // 4. Therefore, the half-open interval invariant `[start, end_excl)` is preserved.
120            Some(unsafe { Self::new_unchecked(start, end_excl) })
121        }
122
123        /// Constructs an `I8CO` interval from a midpoint and length (`u8`) with saturating semantics.
124        ///
125        /// # Parameters
126        /// - `mid`: the desired midpoint of the interval
127        /// - `len`: the desired length of the interval in units, must be `1..=u8::MAX`
128        ///
129        /// # Behavior
130        /// - Values are saturated at `i8::MIN` / `i8::MAX` to prevent overflow.
131        /// - If `len = 0`, returns `None`.
132        ///
133        /// # Guarantees
134        /// - Returned interval satisfies `start < end_excl`
135        /// - Maximum accepted input length is `u8::MAX`
136        /// - Fully compatible with codegen for other signed integer interval types
137        #[inline]
138        pub const fn saturating_from_midpoint_len(mid: i8, len: u8) -> Option<Self> {
139            if len == 0 {
140                return None;
141            }
142
143            let half = (len / 2) as i8;
144
145            let start = mid.saturating_sub(half);
146            let end_incl = mid.saturating_add(half);
147            let end_excl = end_incl.saturating_add((len % 2) as i8);
148
149            Self::try_new(start, end_excl)
150        }
151
152        #[inline]
153        pub const fn contains(self, x: i8) -> bool {
154            self.start <= x && x < self.end_excl
155        }
156
157        #[inline]
158        pub const fn contains_interval(self, other: Self) -> bool {
159            self.start <= other.start && other.end_excl <= self.end_excl
160        }
161
162        #[inline]
163        pub const fn iter(self) -> core::ops::Range<i8> {
164            self.start..self.end_excl
165        }
166
167        #[inline]
168        pub const fn to_range(self) -> core::ops::Range<i8> {
169            self.start..self.end_excl
170        }
171
172        #[inline]
173        pub const fn intersects(self, other: Self) -> bool {
174            !(self.end_excl <= other.start || other.end_excl <= self.start)
175        }
176
177        #[inline]
178        pub const fn is_adjacent(self, other: Self) -> bool {
179            self.end_excl == other.start || other.end_excl == self.start
180        }
181
182        #[inline]
183        pub const fn is_contiguous_with(self, other: Self) -> bool {
184            self.intersects(other) || self.is_adjacent(other)
185        }
186    }
187}
188
189// ------------------------------------------------------------
190// interval algebra api: intersection / convex_hull / between / union / difference / symmetric_difference
191// ------------------------------------------------------------
192
193mod interval_algebra {
194
195    use super::*;
196
197    impl I8CO {
198        /// Returns the intersection of two intervals.
199        ///
200        /// If the intervals do not overlap, returns `None`.
201        #[inline]
202        pub const fn intersection(self, other: Self) -> Option<Self> {
203            let start = if self.start >= other.start {
204                self.start
205            } else {
206                other.start
207            };
208
209            let end_excl = if self.end_excl <= other.end_excl {
210                self.end_excl
211            } else {
212                other.end_excl
213            };
214
215            Self::try_new(start, end_excl)
216        }
217
218        /// Returns the convex hull (smallest interval containing both) of two intervals.
219        ///
220        /// Always returns a valid `I8CO`.
221        #[inline]
222        pub const fn convex_hull(self, other: Self) -> Self {
223            let start = if self.start <= other.start {
224                self.start
225            } else {
226                other.start
227            };
228
229            let end_excl = if self.end_excl >= other.end_excl {
230                self.end_excl
231            } else {
232                other.end_excl
233            };
234
235            Self { start, end_excl }
236        }
237
238        /// Returns the interval strictly between two intervals.
239        ///
240        /// If the intervals are contiguous or overlap, returns `None`.
241        #[inline]
242        pub const fn between(self, other: Self) -> Option<Self> {
243            let (left, right) = if self.start <= other.start {
244                (self, other)
245            } else {
246                (other, self)
247            };
248
249            Self::try_new(left.end_excl, right.start)
250        }
251
252        /// Returns the union of two intervals.
253        ///
254        /// - If intervals are contiguous or overlapping, returns `One` containing the merged interval.
255        /// - Otherwise, returns `Two` containing both intervals in ascending order.
256        #[inline]
257        pub const fn union(self, other: Self) -> OneTwo<Self> {
258            if self.is_contiguous_with(other) {
259                OneTwo::One(self.convex_hull(other))
260            } else if self.start <= other.start {
261                OneTwo::Two(self, other)
262            } else {
263                OneTwo::Two(other, self)
264            }
265        }
266
267        /// Returns the difference of two intervals (self - other).
268        ///
269        /// - If no overlap, returns `One(self)`.
270        /// - If partial overlap, returns `One` or `Two` depending on remaining segments.
271        /// - If fully contained, returns `Zero`.
272        #[inline]
273        pub const fn difference(self, other: Self) -> ZeroOneTwo<Self> {
274            match self.intersection(other) {
275                None => ZeroOneTwo::One(self),
276                Some(inter) => {
277                    let left = Self::try_new(self.start, inter.start);
278                    let right = Self::try_new(inter.end_excl, self.end_excl);
279
280                    match (left, right) {
281                        (None, None) => ZeroOneTwo::Zero,
282                        (Some(x), None) | (None, Some(x)) => ZeroOneTwo::One(x),
283                        (Some(x), Some(y)) => ZeroOneTwo::Two(x, y),
284                    }
285                }
286            }
287        }
288
289        /// Returns the symmetric difference of two intervals.
290        ///
291        /// Equivalent to `(self - other) ∪ (other - self)`.
292        /// - If intervals do not overlap, returns `Two(self, other)` in order.
293        /// - If intervals partially overlap, returns remaining non-overlapping segments as `One` or `Two`.
294        #[inline]
295        pub const fn symmetric_difference(self, other: Self) -> ZeroOneTwo<Self> {
296            match self.intersection(other) {
297                None => {
298                    if self.start <= other.start {
299                        ZeroOneTwo::Two(self, other)
300                    } else {
301                        ZeroOneTwo::Two(other, self)
302                    }
303                }
304                Some(inter) => {
305                    let hull = self.convex_hull(other);
306                    let left = Self::try_new(hull.start, inter.start);
307                    let right = Self::try_new(inter.end_excl, hull.end_excl);
308
309                    match (left, right) {
310                        (None, None) => ZeroOneTwo::Zero,
311                        (Some(x), None) | (None, Some(x)) => ZeroOneTwo::One(x),
312                        (Some(x), Some(y)) => ZeroOneTwo::Two(x, y),
313                    }
314                }
315            }
316        }
317    }
318}
319
320// ------------------------------------------------------------
321// Module: Minkowski arithmetic for I8CO
322// Provides checked and saturating Minkowski operations for intervals
323// ------------------------------------------------------------
324
325pub mod minkowski {
326    use super::*;
327
328    type Min = i8;
329    type Max = i8;
330
331    #[inline]
332    const fn min_max4(a: i8, b: i8, c: i8, d: i8) -> (Min, Max) {
333        let (min1, max1) = if a < b { (a, b) } else { (b, a) };
334        let (min2, max2) = if c < d { (c, d) } else { (d, c) };
335        let min = if min1 < min2 { min1 } else { min2 };
336        let max = if max1 > max2 { max1 } else { max2 };
337        (min, max)
338    }
339
340    #[inline]
341    const fn min_max2(a: i8, b: i8) -> (Min, Max) {
342        if a < b { (a, b) } else { (b, a) }
343    }
344
345    pub mod checked {
346        use super::*;
347
348        // --------------------------------------------------------
349        // Interval-to-interval
350        // --------------------------------------------------------
351        impl I8CO {
352            #[inline]
353            pub const fn checked_minkowski_add(self, other: Self) -> Option<Self> {
354                let Some(start) = self.start.checked_add(other.start) else {
355                    return None;
356                };
357                let Some(end_excl) = self.end_excl.checked_add(other.end_incl()) else {
358                    return None;
359                };
360
361                // SAFETY:
362                // `checked_add` guarantees both endpoint computations succeed without overflow.
363                // For half-open intervals, let `a_last = self.end_incl()` and `b_last = other.end_incl()`.
364                // Since `self.start <= a_last` and `other.start <= b_last`, we have
365                // `self.start + other.start <= a_last + b_last < self.end_excl + other.end_incl()`,
366                // hence the computed bounds satisfy `start < end_excl`.
367                Some(unsafe { Self::new_unchecked(start, end_excl) })
368            }
369
370            #[inline]
371            pub const fn checked_minkowski_sub(self, other: Self) -> Option<Self> {
372                let Some(start) = self.start.checked_sub(other.end_incl()) else {
373                    return None;
374                };
375                let Some(end_excl) = self.end_excl.checked_sub(other.start) else {
376                    return None;
377                };
378
379                // SAFETY:
380                // `checked_sub` guarantees both endpoint computations succeed without overflow.
381                // For interval subtraction, the minimum is attained at `self.start - other.end_incl()`
382                // and the exclusive upper bound is `self.end_excl - other.start`.
383                // Because `other.start <= other.end_incl()`, we get
384                // `self.start - other.end_incl() < self.end_excl - other.start`,
385                // so the resulting half-open interval is valid.
386                Some(unsafe { Self::new_unchecked(start, end_excl) })
387            }
388
389            #[inline]
390            pub const fn checked_minkowski_mul(self, other: Self) -> Option<Self> {
391                let a = self.start;
392                let b = self.end_incl();
393                let c = other.start;
394                let d = other.end_incl();
395
396                let Some(p1) = a.checked_mul(c) else {
397                    return None;
398                };
399                let Some(p2) = a.checked_mul(d) else {
400                    return None;
401                };
402                let Some(p3) = b.checked_mul(c) else {
403                    return None;
404                };
405                let Some(p4) = b.checked_mul(d) else {
406                    return None;
407                };
408
409                let (start, end_incl) = min_max4(p1, p2, p3, p4);
410
411                let Some(end_excl) = end_incl.checked_add(1) else {
412                    return None;
413                };
414
415                // SAFETY:
416                // All four corner products are computed with `checked_mul`, so no intermediate
417                // multiplication overflows. For multiplication over a closed integer rectangle
418                // `[a, b] × [c, d]`, every attainable extremum occurs at a corner, so
419                // `min_max4(p1, p2, p3, p4)` yields the true inclusive lower/upper bounds.
420                // Therefore `start <= end_incl` holds by construction.
421                // `checked_add(1)` then safely converts the inclusive upper bound to the exclusive
422                // upper bound, and implies `end_excl = end_incl + 1`, hence `start < end_excl`.
423                Some(unsafe { Self::new_unchecked(start, end_excl) })
424            }
425
426            #[inline]
427            pub const fn checked_minkowski_div(self, other: Self) -> Option<Self> {
428                if other.start <= 0 && other.end_incl() >= 0 {
429                    return None;
430                }
431
432                let a = self.start;
433                let b = self.end_incl();
434                let c = other.start;
435                let d = other.end_incl();
436
437                let Some(p1) = a.checked_div(c) else {
438                    return None;
439                };
440                let Some(p2) = a.checked_div(d) else {
441                    return None;
442                };
443                let Some(p3) = b.checked_div(c) else {
444                    return None;
445                };
446                let Some(p4) = b.checked_div(d) else {
447                    return None;
448                };
449
450                let (start, end_incl) = min_max4(p1, p2, p3, p4);
451
452                let Some(end_excl) = end_incl.checked_add(1) else {
453                    return None;
454                };
455
456                // SAFETY:
457                // The guard `other.start <= 0 && other.end_incl() >= 0` rejects any divisor interval
458                // that contains zero, so division by zero cannot occur anywhere in the divisor set.
459                // Each corner quotient is computed with `checked_div`, so exceptional signed cases
460                // such as `MIN / -1` are also rejected.
461                // On each connected component of the divisor domain that excludes zero, integer division
462                // is monotone with respect to the rectangle corners relevant to the extremum search;
463                // thus the global inclusive bounds over the interval pair are captured by the four
464                // corner quotients and recovered by `min_max4`, giving `start <= end_incl`.
465                // `checked_add(1)` safely converts the inclusive upper bound to half-open form, so
466                // the final bounds satisfy `start < end_excl`.
467                Some(unsafe { Self::new_unchecked(start, end_excl) })
468            }
469        }
470
471        // --------------------------------------------------------
472        // Scalar
473        // --------------------------------------------------------
474        impl I8CO {
475            #[inline]
476            pub const fn checked_minkowski_add_n(self, n: i8) -> Option<Self> {
477                let Some(start) = self.start.checked_add(n) else {
478                    return None;
479                };
480                let Some(end_excl) = self.end_excl.checked_add(n) else {
481                    return None;
482                };
483
484                // SAFETY:
485                // `checked_add` guarantees both translated bounds are computed without overflow.
486                // Adding the same scalar to both endpoints preserves the interval width exactly,
487                // so a valid half-open interval remains valid and still satisfies `start < end_excl`.
488                Some(unsafe { Self::new_unchecked(start, end_excl) })
489            }
490
491            #[inline]
492            pub const fn checked_minkowski_sub_n(self, n: i8) -> Option<Self> {
493                let Some(start) = self.start.checked_sub(n) else {
494                    return None;
495                };
496                let Some(end_excl) = self.end_excl.checked_sub(n) else {
497                    return None;
498                };
499
500                // SAFETY:
501                // `checked_sub` guarantees both translated bounds are computed without overflow.
502                // Subtracting the same scalar from both endpoints preserves the interval width exactly,
503                // so the strict half-open ordering is unchanged and `start < end_excl` still holds.
504                Some(unsafe { Self::new_unchecked(start, end_excl) })
505            }
506
507            #[inline]
508            pub const fn checked_minkowski_mul_n(self, n: i8) -> Option<Self> {
509                let Some(a) = self.start.checked_mul(n) else {
510                    return None;
511                };
512                let Some(b) = self.end_incl().checked_mul(n) else {
513                    return None;
514                };
515
516                let (start, end_incl) = min_max2(a, b);
517
518                let Some(end_excl) = end_incl.checked_add(1) else {
519                    return None;
520                };
521
522                // SAFETY:
523                // Both endpoint products are computed with `checked_mul`, so no signed overflow occurs.
524                // Multiplication by a scalar maps the closed source interval endpoints to the two extreme
525                // candidates; `min_max2` therefore recovers the true inclusive lower/upper bounds whether
526                // `n` is positive, zero, or negative, giving `start <= end_incl`.
527                // `checked_add(1)` safely converts the inclusive upper bound into the exclusive upper bound,
528                // which guarantees the final half-open interval satisfies `start < end_excl`.
529                Some(unsafe { Self::new_unchecked(start, end_excl) })
530            }
531
532            #[inline]
533            pub const fn checked_minkowski_div_n(self, n: i8) -> Option<Self> {
534                if n == 0 {
535                    return None;
536                }
537
538                let Some(a) = self.start.checked_div(n) else {
539                    return None;
540                };
541                let Some(b) = self.end_incl().checked_div(n) else {
542                    return None;
543                };
544
545                let (start, end_incl) = min_max2(a, b);
546
547                let Some(end_excl) = end_incl.checked_add(1) else {
548                    return None;
549                };
550
551                // SAFETY:
552                // The guard `n != 0` excludes division by zero, and `checked_div` additionally rejects
553                // the only overflowing signed case (`MIN / -1`).
554                // Division by a fixed nonzero scalar sends the source closed interval endpoints to the
555                // two extreme candidates for the image interval, so `min_max2` yields the true inclusive
556                // lower/upper bounds and ensures `start <= end_incl`.
557                // `checked_add(1)` safely restores half-open representation, therefore the constructed
558                // interval satisfies `start < end_excl`.
559                Some(unsafe { Self::new_unchecked(start, end_excl) })
560            }
561        }
562    }
563
564    // ========================================================
565    // SATURATING
566    // ========================================================
567    pub mod saturating {
568        use super::*;
569
570        impl I8CO {
571            #[inline]
572            pub const fn saturating_minkowski_add(self, other: Self) -> Option<Self> {
573                let start = self.start.saturating_add(other.start);
574                let end_excl = self.end_excl.saturating_add(other.end_incl());
575                Self::try_new(start, end_excl)
576            }
577
578            #[inline]
579            pub const fn saturating_minkowski_sub(self, other: Self) -> Option<Self> {
580                let start = self.start.saturating_sub(other.end_incl());
581                let end_excl = self.end_excl.saturating_sub(other.start);
582                Self::try_new(start, end_excl)
583            }
584
585            #[inline]
586            pub const fn saturating_minkowski_mul(self, other: Self) -> Option<Self> {
587                let a = self.start.saturating_mul(other.start);
588                let b = self.start.saturating_mul(other.end_incl());
589                let c = self.end_incl().saturating_mul(other.start);
590                let d = self.end_incl().saturating_mul(other.end_incl());
591
592                let (start, end_incl) = min_max4(a, b, c, d);
593
594                let end_excl = end_incl.saturating_add(1);
595                Self::try_new(start, end_excl)
596            }
597
598            #[inline]
599            pub const fn saturating_minkowski_div(self, other: Self) -> Option<Self> {
600                if other.start <= 0 && other.end_incl() >= 0 {
601                    return None;
602                }
603
604                let a = self.start / other.start;
605                let b = self.start / other.end_incl();
606                let c = self.end_incl() / other.start;
607                let d = self.end_incl() / other.end_incl();
608
609                let (start, end_incl) = min_max4(a, b, c, d);
610
611                let end_excl = end_incl.saturating_add(1);
612                Self::try_new(start, end_excl)
613            }
614        }
615
616        impl I8CO {
617            #[inline]
618            pub const fn saturating_minkowski_add_n(self, n: i8) -> Option<Self> {
619                let start = self.start.saturating_add(n);
620                let end_excl = self.end_excl.saturating_add(n);
621                Self::try_new(start, end_excl)
622            }
623
624            #[inline]
625            pub const fn saturating_minkowski_sub_n(self, n: i8) -> Option<Self> {
626                let start = self.start.saturating_sub(n);
627                let end_excl = self.end_excl.saturating_sub(n);
628                Self::try_new(start, end_excl)
629            }
630
631            #[inline]
632            pub const fn saturating_minkowski_mul_n(self, n: i8) -> Option<Self> {
633                let a = self.start.saturating_mul(n);
634                let b = self.end_incl().saturating_mul(n);
635
636                let (start, end_incl) = min_max2(a, b);
637
638                let end_excl = end_incl.saturating_add(1);
639                Self::try_new(start, end_excl)
640            }
641
642            #[inline]
643            pub const fn saturating_minkowski_div_n(self, n: i8) -> Option<Self> {
644                if n == 0 {
645                    return None;
646                }
647
648                let a = self.start / n;
649                let b = self.end_incl() / n;
650
651                let (start, end_incl) = min_max2(a, b);
652
653                let end_excl = end_incl.saturating_add(1);
654                Self::try_new(start, end_excl)
655            }
656        }
657    }
658}