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}