150 lines
7.4 KiB
Markdown
150 lines
7.4 KiB
Markdown
# 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.
|