|
| 1 | +# Future: IntervalMap<K, V> for java-util + Guava TreeRangeMap Replacement |
| 2 | + |
| 3 | +*Drafted: 2026-02-06. Deferred for future implementation.* |
| 4 | + |
| 5 | +## Summary |
| 6 | + |
| 7 | +Add `IntervalMap<K, V>` to java-util as a thread-safe map from non-overlapping intervals to values. This would allow replacing Guava's `TreeRangeMap` in n-cube's `Axis.java`, further reducing the Guava dependency surface. |
| 8 | + |
| 9 | +## Motivation |
| 10 | + |
| 11 | +After replacing Guava's Joiner/Splitter/Iterables in n-cube with JDK equivalents (commit `83c10d8e`), the remaining Guava surface area is: |
| 12 | +1. **TreeRangeMap** in `Axis.java` - maps intervals to Column objects for RANGE/SET axes |
| 13 | +2. **Cache** in `GCacheManager.java` / `GuavaCache.java` - keeping for now |
| 14 | + |
| 15 | +IntervalMap would also be useful for general-purpose interval-to-value mappings (e.g., partition tracking with metadata, time-range scheduling with associated data). |
| 16 | + |
| 17 | +## Why IntervalSet Cannot Wrap IntervalMap |
| 18 | + |
| 19 | +We explored whether `IntervalSet` could be simplified to wrap `IntervalMap` (like `CompactSet` wraps `CompactMap`). The answer is **no**: |
| 20 | + |
| 21 | +- **IntervalSet auto-merges**: Adding [1,5) then [3,8) merges to [1,8). This is core to its semantics. |
| 22 | +- **IntervalMap cannot auto-merge**: [1,5)->A and [3,8)->B cannot merge because A != B. |
| 23 | +- IntervalSet's merge logic is tightly coupled to `ConcurrentSkipListMap` operations. Wrapping would either break encapsulation or duplicate all merge logic with no code reuse. |
| 24 | +- **Decision**: IntervalMap should be a standalone class sharing design patterns with IntervalSet. |
| 25 | + |
| 26 | +## Standard Interface Fit Analysis |
| 27 | + |
| 28 | +Neither class maps cleanly to standard Java collection interfaces: |
| 29 | + |
| 30 | +- **IntervalMap vs NavigableMap/SortedMap**: Map interfaces have one key type for both storage and lookup. IntervalMap stores by interval (two bounds) but looks up by point (one value). `put(K, V)` can't express an interval. |
| 31 | +- **IntervalSet vs NavigableSet/SortedSet**: Auto-merge violates Set's contract. After `add(a)` then `add(b)`, the set may contain neither original element. |
| 32 | +- **Good fits**: `Iterable` (both), `Serializable` (both), `Function<K, V>` (IntervalMap point lookup), `Predicate<T>` (IntervalSet point containment). |
| 33 | + |
| 34 | +## IntervalMap<K, V> Design |
| 35 | + |
| 36 | +### Key Requirement: Point Intervals |
| 37 | + |
| 38 | +n-cube's SET axis stores **both** half-open ranges [low, high) and discrete point values. Currently handled via Guava's `Range.closedOpen()` and `Range.closed()`. IntervalMap must support both: |
| 39 | +- `put(start, end, value)` for half-open [start, end) ranges |
| 40 | +- `putPoint(point, value)` for discrete point values |
| 41 | + |
| 42 | +### Internal Storage |
| 43 | + |
| 44 | +```java |
| 45 | +private final ConcurrentSkipListMap<K, Node<K, V>> intervals = new ConcurrentSkipListMap<>(); |
| 46 | +private final transient ReentrantLock lock = new ReentrantLock(); |
| 47 | + |
| 48 | +private static class Node<K, V> { |
| 49 | + final K end; // null for point intervals |
| 50 | + final V value; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +### Proposed API |
| 55 | + |
| 56 | +```java |
| 57 | +public class IntervalMap<K extends Comparable<? super K>, V> implements Iterable<IntervalMap.Entry<K, V>> { |
| 58 | + |
| 59 | + public static final class Entry<K extends Comparable<? super K>, V> { |
| 60 | + // start, end (null for points), value |
| 61 | + // isPoint(), getStart(), getEnd(), getValue() |
| 62 | + } |
| 63 | + |
| 64 | + // Mutations (write-locked) |
| 65 | + void put(K start, K end, V value); // [start, end) range |
| 66 | + void putPoint(K point, V value); // exact point |
| 67 | + boolean remove(K start, K end); // remove exact range |
| 68 | + boolean removePoint(K point); // remove exact point |
| 69 | + void clear(); |
| 70 | + |
| 71 | + // Point lookup (lock-free) |
| 72 | + V get(K point); // searches both ranges and points |
| 73 | + |
| 74 | + // Overlap queries (lock-free) -- replaces Guava's subRangeMap() |
| 75 | + boolean overlaps(K start, K end); // any entry overlaps [start, end)? |
| 76 | + boolean containsPoint(K point); // any entry contains this point? |
| 77 | + List<Entry<K, V>> getOverlapping(K start, K end); |
| 78 | + |
| 79 | + // Iteration (lock-free, weakly consistent) |
| 80 | + Iterator<Entry<K, V>> iterator(); // all entries ordered by start |
| 81 | + List<Entry<K, V>> snapshot(); // atomic copy |
| 82 | + int size(); |
| 83 | + boolean isEmpty(); |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +### Core Algorithm: get(K point) |
| 88 | +1. `floorEntry(point)` -- find largest key <= point |
| 89 | +2. If node.end == null -- point interval, return value only if exact match |
| 90 | +3. If node.end != null -- range [start, end), return value if end > point |
| 91 | + |
| 92 | +### Core Algorithm: overlaps(K start, K end) |
| 93 | +1. Check `floorEntry(start)` -- a range starting before `start` might extend into [start, end) |
| 94 | +2. Check all entries in `subMap(start, false, end, false)` -- any entry starting within (start, end) overlaps |
| 95 | +3. For point entries, check if point falls within [start, end) |
| 96 | + |
| 97 | +## n-cube Axis.java Migration |
| 98 | + |
| 99 | +**File**: `src/main/java/com/cedarsoftware/ncube/Axis.java` |
| 100 | + |
| 101 | +Axis.java is the **only file** in n-cube using TreeRangeMap. The migration involves 13 localized changes: |
| 102 | + |
| 103 | +### Current Guava Usage |
| 104 | + |
| 105 | +| Operation | Line(s) | Purpose | IntervalMap Replacement | |
| 106 | +|-----------|---------|---------|------------------------| |
| 107 | +| `TreeRangeMap.create()` | 110 | Field init | `new IntervalMap<>()` | |
| 108 | +| `put(Range, Column)` | 618, 620-625 | Index column | `put(low, high, col)` / `putPoint(val, col)` | |
| 109 | +| `get(point)` | 1739 | Point lookup | `get(point)` (same signature) | |
| 110 | +| `remove(Range)` | 1093, 1095-1100 | Deindex column | `remove(low, high)` / `removePoint(val)` | |
| 111 | +| `clear()` | 829-830 | Clear indexes | `clear()` (same) | |
| 112 | +| `subRangeMap()` | 1867, 1882 | Overlap detection | `overlaps(low, high)` / `containsPoint(val)` | |
| 113 | +| `asMapOfRanges().values()` | 1922 | Get unique columns | `for (Entry e : map)` + LinkedHashSet | |
| 114 | +| `asMapOfRanges().size()` | 1997 | Size (testing) | `size()` | |
| 115 | +| `asMapOfRanges().forEach()` | 2055-2059 | Iterate entries | `for (Entry e : map)` | |
| 116 | + |
| 117 | +### Key Migration Details |
| 118 | + |
| 119 | +**indexColumn() -- SET axis** needs to distinguish Range from discrete: |
| 120 | +```java |
| 121 | +// Before: |
| 122 | +rangeToCol.put(valueToRange(elem), column); |
| 123 | + |
| 124 | +// After: |
| 125 | +if (elem instanceof com.cedarsoftware.ncube.Range range) { |
| 126 | + rangeToCol.put(range.getLow(), range.getHigh(), column); |
| 127 | +} else { |
| 128 | + rangeToCol.putPoint(elem, column); |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +**doesOverlap()** simplifies significantly: |
| 133 | +```java |
| 134 | +// Before (3 lines): |
| 135 | +RangeMap<Comparable, Column> ranges = rangeToCol.subRangeMap(valueToRange(range)); |
| 136 | +return !ranges.asMapOfRanges().isEmpty(); |
| 137 | + |
| 138 | +// After (1 line): |
| 139 | +return rangeToCol.overlaps(range.getLow(), range.getHigh()); |
| 140 | +``` |
| 141 | + |
| 142 | +**valueToRange() helper** (lines 1844-1856) can be deleted entirely -- conversion is inlined at each call site. |
| 143 | + |
| 144 | +### Post-Migration Guava Status |
| 145 | + |
| 146 | +After this migration, only `GCacheManager.java` and `GuavaCache.java` would reference Guava (for Cache). The `com.google.common.collect.Range`, `RangeMap`, and `TreeRangeMap` imports would be fully eliminated. |
| 147 | + |
| 148 | +## Implementation Sequence |
| 149 | + |
| 150 | +1. Create `IntervalMap.java` in java-util (~600-800 lines) |
| 151 | +2. Create `IntervalMapTest.java` with comprehensive tests |
| 152 | +3. Update java-util changelog, release |
| 153 | +4. Update n-cube's java-util dependency version |
| 154 | +5. Apply 13 changes to Axis.java, delete `valueToRange()` |
| 155 | +6. Run full n-cube test suite (`./gradlew clean test`) |
| 156 | +7. Verify no Guava Range/RangeMap imports remain |
| 157 | + |
| 158 | +## Verification Criteria |
| 159 | + |
| 160 | +1. All java-util tests pass, IntervalMapTest >90% coverage |
| 161 | +2. All n-cube tests pass after migration |
| 162 | +3. `grep -r "com.google.common.collect.Range" src/main/` returns no hits |
0 commit comments