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)
normalizeis idempotent and order-independent; it is the unique normal form.union,intersection,difference,symmetric_differenceandcomplementare 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
Fractionendpoints stay exact (measureof[1/3, 2/3)is exactly1/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 raisesIntervalError— 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:
normalizecompared a second-inflower bound with<=and raisedTypeError. Reachable fromunion/symmetric_difference, which concatenate normal forms and re-normalize. Fix: treatlo is Noneas "always joins".is_canonical([(None, 5), (1, 2)])raisedTypeErrorinstead of returningFalse. Fix: only compareplo <= lowhenplois notNone.
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/
Fractionbehaviour is covered by hand-checked cases, algebraic laws and endpoint-preservation properties, not by the bitmap oracle. containsandmeasureare linear scans;measurenormalizes 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.