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