# intervalkit — canonical half-open interval-set algebra Stdlib only, no dependencies. Two files: the library and its harness. intervalkit.py the library + a small CLI test_intervalkit.py the verification harness (run it: exit 0 = correct) ## The model A *set* is a finite sequence of half-open intervals `[lo, hi)`. `lo` and `hi` are numbers or `None`; `None` on the left is `-inf`, on the right is `+inf`. The empty set is the empty sequence. A sequence is **canonical** iff it is sorted by `lo`, every interval is non-degenerate (`lo < hi`), and consecutive intervals are strictly separated (`next.lo > prev.hi`). Every set has exactly one canonical form. Touching intervals merge (`[1,2) U [2,3) == [1,3)`); a one-unit gap does not (`[1,2) U [3,4)` stays two intervals). ## API normalize(a) unique canonical form; idempotent, order-blind union(*sets) canonical union intersection(*sets) canonical intersection difference(a, b) a minus b symmetric_difference(a, b) points in exactly one complement(a) over the whole line (-inf, +inf); an involution contains(a, x) half-open membership measure(a) total length; inf when unbounded from_points(points) integer runs -> canonical set sample_points(a, lo, hi) integers in the window that are in the set is_canonical(a) structural predicate assert_canonical(a) raises AssertionError with detail parse(text) / format_set(a) "[1,3) U [5,+inf)" <-> canonical set main(argv) CLI entry point CLI: $ python3 intervalkit.py union "[1,3)" "[2,5)" [1,5) $ python3 intervalkit.py complement "[1,3)" [-inf,1) U [3,+inf) $ python3 intervalkit.py contains "[1,3)" 3 false $ python3 intervalkit.py measure "[-inf,1)" inf ## Guarantees (and what is deliberately excluded) * `normalize` is idempotent and order-independent; it is the unique normal form. * `union`, `intersection`, `difference`, `symmetric_difference` and `complement` are total, closed, and always return canonical sets. * No operation mutates its input, and none invents an endpoint: every result is built from max/min combinations of input endpoints. Integer endpoints stay integer, and `Fraction` endpoints stay exact (`measure` of `[1/3, 2/3)` is exactly `1/3`). * **Degenerate intervals (single points) are not representable.** `[x, x)` is rejected. That is the price of a half-open model, and it is what makes every operation above total and closed. `{0, 2}` is therefore not expressible; `{0, 1, 2}` is, as `[0, 3)`. Malformed input raises `IntervalError` — it is never silently coerced. ## Verification python3 test_intervalkit.py # exit code IS the verdict: 0 = correct Last run: **12/12 checks passed, 641,123 comparisons executed, ~13 s.** ### Why the differential test is exact, not sampled Membership over the integers is decided by `in_set` — an independent five-line predicate written in the harness, not imported from the library. Operations are compared against set operations on bitmaps over the universe `[-24, 25)`. This is exact because the algebra is endpoint-based: results are max/min combinations of input endpoints, so integer endpoints never become non-integers, and a half-open interval with integer endpoints covers exactly its integer points. The integer bitmap therefore *is* the set, with no sampling error and no tolerance. The real-line claim (dense, unbounded domain) rests on the same endpoint arithmetic plus the density argument, not on a separate test. The library is declared over the reals; the oracle pins the integer-endpoint case. ### What the harness checks | # | check | volume | |---|-------|--------| | 1 | fuzz: real implementation, 0 semantic / 0 structural failures | 3000 trials, 75,750 comparisons | | 2 | teeth: 7/7 mutants detected, each in the expected leg | 7 x 3000 trials | | 3 | laws: De Morgan (both ways), involution, idempotence, commutativity, associativity, `a == (a n b) U (a \ b)`, `a n ~a == {}`, `a U ~a == line`, measure additivity | 600 random triples | | 4 | laws: `normalize` idempotent + order-independent; malformed input rejected; regression cases | 600 random sets | | 5 | boundary: touching, gaps, shared endpoints, unbounded bounds, full line, measure, points | 30 hand-checked cases | | 6 | boundary: text parse/format round-trips, 11 rejection cases | 29 cases | | 7 | points: `from_points`/`sample_points` vs the bitmap oracle | 300 point sets | | 8 | endpoints: no invented endpoints; float and `Fraction` exactness | 400 random pairs | | 9 | scale: 1500 x 1500 intervals vs brute-force enumeration | 600-point universe | | 10 | CLI: subprocess smoke over real argv, stdout, exit codes, rejection | 16 cases | | 11 | CLI: exit code 0 and the value on stdout | 3 cases | | 12 | oracle self-check: `in_set` vs `ik.contains`; bitmap size vs real measure | 500 sets x 49 points | Fuzz coverage floors (anti-vacuity): 2049 trials with unbounded bounds, 2288 with a merge, 830 with multi-interval results, 1113 with an empty set, 1052 distinct canonical sets. ### The teeth A harness that cannot fail proves nothing. Seven deliberately broken implementations are pushed through the *same* oracle; all seven must be caught, and in the leg that is supposed to see them (semantic = bitmap mismatch or crash, structural = result not canonical): | mutant | semantic | structural | expected | |--------|---------:|-----------:|----------| | `normalize`: strict-overlap merge (misses touching) | 145 | 118 | structural | | `intersection`: always advance the left cursor | 270 | 0 | semantic | | `complement`: bounds written to wrong slots | 3000 | 0 | semantic | | `measure`: counts integer slots (`hi - lo - 1`) | 1032 | 0 | semantic | | `contains`: ignores the unbounded lower bound | 197 | 0 | semantic | | `union`: unnormalized concatenation | 0 | 1835 | structural | | `normalize`: pre-fix body, duplicate `-inf` lower bound crashes | 145 | 0 | semantic | The last mutant is the pre-fix body of `ik.normalize`; the fuzz found that crash in the real library. Two bugs were found this way while the suite was being written, and both are now pinned by named regression cases in check 4: 1. `normalize` compared a second `-inf` lower bound with `<=` and raised `TypeError`. Reachable from `union`/`symmetric_difference`, which concatenate normal forms and re-normalize. Fix: treat `lo is None` as "always joins". 2. `is_canonical([(None, 5), (1, 2)])` raised `TypeError` instead of returning `False`. Fix: only compare `plo <= lo` when `plo` is not `None`. ### Anti-vacuity guard The runner counts executed comparisons and refuses to exit 0 if fewer than 12 checks are registered or fewer than 100,000 comparisons were actually executed. A test run that silently stops asserting cannot report success. ## Limits, stated plainly * The exact differential oracle is over integer endpoints on a bounded universe. Unbounded and float/`Fraction` behaviour is covered by hand-checked cases, algebraic laws and endpoint-preservation properties, not by the bitmap oracle. * `contains` and `measure` are linear scans; `measure` normalizes first so it cannot double-count overlapping parts. There is no interval tree here — this is an algebra, not an index. * Degenerate (single-point) intervals are out of scope by design, as above.