Skip to main content

int_interval/
i8.rs

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