Skip to main content

int_interval/
i8.rs

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