Skip to content

Commit b6572be

Browse files
committed
Add RangeList util
1 parent 1c49d19 commit b6572be

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed

packages/hub/src/utils/RangeList.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Code generated with this prompt by Cursor:
3+
*
4+
* I want to build a class to manage ranges
5+
*
6+
* I can add ranges to it with a start& an end (both integer, end > start). It should store those ranges efficently.
7+
*
8+
* When several ranges overlap, eg [1, 100] and [30, 50], I want the class to split the range into non-overlapping ranges, and add a "ref counter" to the ranges. For example, [1, 30], [30, 50] * 2, [50, 100]
9+
*
10+
* I also want to be able to remove ranges, it will decrease the ref counter or remove the range altogether. I can only remove ranges at existing boundaries. For example, with the [1, 30], [30, 50] * 2, [50, 100] configuration
11+
*
12+
* - removing [1, 100] => the only range remaning is [30, 50]
13+
* - removing [2, 50] => error, because "2' is not a boundary
14+
* - removing [30, 50] => [1, 30], [30, 50], [50, 100] (do not "merge" the ranges back together)
15+
*
16+
* I want to be able to associate data to each range. And I want to be able to get the ranges inside boundaries. For example , with [1, 30], [30, 50] * 2, [50, 100] configuration
17+
*
18+
* - getting [30, 100] => I receive [30, 50] * 2, [50, 100], and I can get / modify the data assocaited to each range by accessing their data prop. Note the "*2" is just the ref counter, there is onlly one range object for the interval returned
19+
* - getting [2, 50] => error because "2" is not a boundary
20+
*
21+
* ----
22+
*
23+
* Could optimize with binary search, but the ranges we want to handle are not that many.
24+
*/
25+
interface Range<T> {
26+
start: number;
27+
end: number;
28+
refCount: number;
29+
data: T | null;
30+
}
31+
32+
export class RangeList<T> {
33+
private ranges: Range<T>[] = [];
34+
35+
/**
36+
* Add a range to the list. If it overlaps with existing ranges,
37+
* it will split them and increment reference counts accordingly.
38+
*/
39+
add(start: number, end: number): void {
40+
if (end <= start) {
41+
throw new TypeError("End must be greater than start");
42+
}
43+
44+
// Find all ranges that overlap with the new range
45+
const overlappingRanges: { index: number; range: Range<T> }[] = [];
46+
for (let i = 0; i < this.ranges.length; i++) {
47+
const range = this.ranges[i];
48+
if (start <= range.end && end >= range.start) {
49+
overlappingRanges.push({ index: i, range });
50+
}
51+
if (range.data !== null) {
52+
throw new Error("Overlapping range already has data");
53+
}
54+
}
55+
56+
if (overlappingRanges.length === 0) {
57+
// No overlaps, just add the new range
58+
this.ranges.push({ start, end, refCount: 1, data: null });
59+
this.ranges.sort((a, b) => a.start - b.start);
60+
return;
61+
}
62+
63+
// Handle overlaps by splitting ranges
64+
const newRanges: Range<T>[] = [];
65+
let currentPos = start;
66+
67+
for (let i = 0; i < overlappingRanges.length; i++) {
68+
const { range } = overlappingRanges[i];
69+
70+
// Add range before overlap if exists
71+
if (currentPos < range.start) {
72+
newRanges.push({
73+
start: currentPos,
74+
end: range.start,
75+
refCount: 1,
76+
data: null,
77+
});
78+
} else if (range.start < currentPos) {
79+
newRanges.push({
80+
start: range.start,
81+
end: currentPos,
82+
refCount: range.refCount,
83+
data: null,
84+
});
85+
}
86+
87+
// Add overlapping part with increased ref count
88+
newRanges.push({
89+
start: Math.max(currentPos, range.start),
90+
end: Math.min(end, range.end),
91+
refCount: range.refCount + 1,
92+
data: null,
93+
});
94+
95+
// Add remaining part of existing range if exists
96+
if (range.end > end) {
97+
newRanges.push({
98+
start: end,
99+
end: range.end,
100+
refCount: range.refCount,
101+
data: null,
102+
});
103+
}
104+
105+
currentPos = Math.max(currentPos, range.end);
106+
}
107+
108+
// Add remaining part after last overlap if exists
109+
if (currentPos < end) {
110+
newRanges.push({
111+
start: currentPos,
112+
end,
113+
refCount: 1,
114+
data: null,
115+
});
116+
}
117+
118+
// Remove old overlapping ranges and insert new ones
119+
const firstIndex = overlappingRanges[0].index;
120+
const lastIndex = overlappingRanges[overlappingRanges.length - 1].index;
121+
this.ranges.splice(firstIndex, lastIndex - firstIndex + 1, ...newRanges);
122+
this.ranges.sort((a, b) => a.start - b.start);
123+
}
124+
125+
/**
126+
* Remove a range from the list. The range must start and end at existing boundaries.
127+
*/
128+
remove(start: number, end: number): void {
129+
if (end <= start) {
130+
throw new TypeError("End must be greater than start");
131+
}
132+
133+
// Find ranges that need to be modified
134+
const affectedRanges: { index: number; range: Range<T> }[] = [];
135+
for (let i = 0; i < this.ranges.length; i++) {
136+
const range = this.ranges[i];
137+
if (start <= range.end && end >= range.start) {
138+
affectedRanges.push({ index: i, range });
139+
}
140+
}
141+
142+
if (affectedRanges.length === 0) {
143+
throw new Error("No ranges found to remove");
144+
}
145+
146+
// Verify boundaries match
147+
if (start !== affectedRanges[0].range.start || end !== affectedRanges[affectedRanges.length - 1].range.end) {
148+
throw new Error("Range boundaries must match existing boundaries");
149+
}
150+
151+
// Todo: also check if there's a gap in the middle but it should not happen with our usage
152+
153+
for (let i = 0; i < affectedRanges.length; i++) {
154+
const { range } = affectedRanges[i];
155+
156+
range.refCount--;
157+
}
158+
159+
this.ranges = this.ranges.filter((range) => range.refCount > 0);
160+
}
161+
162+
/**
163+
* Get all ranges within the specified boundaries. The boundaries must match existing ones.
164+
*/
165+
getRanges(start: number, end: number): Range<T>[] {
166+
if (end <= start) {
167+
throw new TypeError("End must be greater than start");
168+
}
169+
170+
// Find ranges that overlap with the requested boundaries
171+
const result: Range<T>[] = [];
172+
for (const range of this.ranges) {
173+
if (start <= range.end && end >= range.start) {
174+
result.push(range);
175+
}
176+
}
177+
178+
// Verify boundaries match
179+
if (result.length === 0 || result[0].start !== start || result[result.length - 1].end !== end) {
180+
throw new Error("Range boundaries must match existing boundaries");
181+
}
182+
183+
// Todo: also check if there's a gap in the middle but it should not happen with our usage
184+
185+
return result;
186+
}
187+
188+
/**
189+
* Get all ranges in the list
190+
*/
191+
getAllRanges(): Range<T>[] {
192+
return [...this.ranges];
193+
}
194+
}

0 commit comments

Comments
 (0)