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}