Generic run-length encoded sequence with O(log n) position lookup
///|
test {
// Create from a string
let rle = @rle.Rle::from_string("hello world")
// Length and lookup
inspect(rle.span(), content="11")
inspect(
rle.find(6),
content=(
#|Some({ run: 0, offset: 6 })
),
)
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
let _ = rle.append("hello")
let _ = rle.append(" world")
inspect(rle.length(), content="1")
inspect(rle.to_string(), content="hello world")
}///|
test {
let rle = @rle.Rle::from_string("hello world")
let (left, right) = rle.split(5).unwrap()
inspect(left.to_string(), content="hello")
inspect(right.to_string(), content=" world")
}///|
test {
let rle = @rle.Rle::from_string("hello world")
let slices = rle.range(start=1, end=4).unwrap().collect()
inspect(slices.length(), content="1")
let s = slices[0]
inspect(
@rle.Sliceable::slice(s.value, start=s.start, end=s.end),
content="Ok(\"ell\")",
)
}///|
test {
let rle = @rle.Rle::from_array(["a", "", "b", "", "c"])
inspect(rle.length(), content="1")
inspect(rle.to_string(), content="abc")
}///|
test {
// Insert at position
let rle = @rle.Rle::from_string("helo")
let elem = @rle.Rle::from_string("l")
let result = rle.insert(2, elem).unwrap()
inspect(result.to_string(), content="hello")
}///|
test {
// Delete a range
let rle = @rle.Rle::from_string("hello world")
let result = rle.delete(start=5, end=6).unwrap()
inspect(result.to_string(), content="helloworld")
}///|
test {
// Splice: replace a range with new content
let rle = @rle.Rle::from_string("hello world")
let replacement = @rle.Rle::from_string("beautiful ")
let result = rle.splice(start=6, end=11, replacement).unwrap()
inspect(result.to_string(), content="hello beautiful ")
}///|
test {
let rle = @rle.Rle::from_string("abcdef")
let cursor = rle.cursor()
inspect(cursor.advance(3), content="true")
inspect(cursor.position(), content="Some(3)")
inspect(cursor.current_item(), content="Some(\"abcdef\")")
// seek() uses binary search โ O(log n)
inspect(cursor.seek(1), content="true")
inspect(cursor.position(), content="Some(1)")
}///|
test {
let rle = @rle.Rle::from_string("abcdef")
let cursor = rle.cursor()
let _ = cursor.advance(3)
let _ = rle.append("ghi")
inspect(cursor.is_stale(), content="true")
inspect(cursor.next(), content="None")
}///|
test {
// Non-mutating concat โ returns a new Rle
let a = @rle.Rle::from_string("hello")
let b = @rle.Rle::from_string(" world")
let c = a.concat(b)
inspect(c.to_string(), content="hello world")
}///|
test {
// In-place extend โ mutates the receiver
let rle = @rle.Rle::from_string("hello")
rle.extend(@rle.Rle::from_string(" world"))
inspect(rle.to_string(), content="hello world")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(
rle.find(0),
content=(
#|Some({ run: 0, offset: 0 })
),
)
inspect(
rle.find(4),
content=(
#|Some({ run: 0, offset: 4 })
),
)
inspect(rle.find(5), content="None")
inspect(rle.find(-1), content="None")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(rle.value_at(2), content="Ok(\"hello\")")
inspect(rle.value_at(5) is Err(_), content="true")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(rle.span(), content="5")
inspect(rle.logical_length(), content="5")
inspect(rle.span() == rle.logical_length(), content="true")
}///|
test {
let rle = @rle.Rle::from_string("hello")
let _ = rle.span() // builds cache
let _ = rle.append(" world") // invalidates cache
// next query rebuilds automatically
inspect(rle.span(), content="11")
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
inspect(rle.get_version(), content="0")
let _ = rle.append("a")
inspect(rle.get_version(), content="1")
let _ = rle.append("b")
inspect(rle.get_version(), content="2")
rle.clear()
inspect(rle.get_version(), content="3")
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.split(100) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(
e.message(),
content="Position 100 is outside the document (length: 5)",
)
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=-1, end=3) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(e.message(), content="Range start (-1) cannot be negative")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=0, end=10) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(e.message(), content="Range end (10) exceeds document length (5)")
}
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
inspect(rle.append(""), content="Err(Internal(EmptyElement))")
}///|
test {
let rle = @rle.Rle::from_string("A๐B")
inspect(rle.span(), content="4") // A(1) + ๐(2) + B(1)
}///|
test {
let rle = @rle.Rle::from_string("๐")
inspect(rle.span(), content="2")
match rle.split(1) {
Ok(_) => fail("should fail on invalid boundary")
Err(e) => inspect(e, content="InvalidSlice(reason=InvalidIndex)")
}
}///|
test {
let rle = @rle.Rle::from_string("A๐B")
// Position 3 is after the emoji, before B โ valid boundary
match rle.range(start=0, end=3) {
Ok(iter) => {
let slices = iter.collect()
inspect(slices.length(), content="1")
}
Err(_) => fail("range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("ใใใซใกใฏ")
inspect(rle.span(), content="5")
inspect(
rle.find(2),
content=(
#|Some({ run: 0, offset: 2 })
),
)
match rle.split(1) {
Ok((left, right)) => {
inspect(left.to_string(), content="ใ")
inspect(right.to_string(), content="ใใซใกใฏ")
}
Err(_) => fail("split should work with unicode")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=2, end=2) {
Ok(iter) => inspect(iter.collect().length(), content="0")
Err(_) => fail("empty range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.delete(start=2, end=2) {
Ok(result) => inspect(result.to_string(), content="hello")
Err(_) => fail("delete empty range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
// Split at start
let (left, right) = rle.split(0).unwrap()
inspect(left.to_string(), content="")
inspect(right.to_string(), content="hello")
}///|
test {
let rle = @rle.Rle::from_string("hello")
// Split at end
let (left, right) = rle.split(5).unwrap()
inspect(left.to_string(), content="hello")
inspect(right.to_string(), content="")
}///|
test {
let rle = @rle.Rle::from_string("hello")
let slices = rle.range_clamped(start=-5, end=100).collect()
inspect(slices.length(), content="1")
let s = slices[0]
inspect(
@rle.Sliceable::slice(s.value, start=s.start, end=s.end),
content="Ok(\"hello\")",
)
}///|
test {
// Create from a string
let rle = @rle.Rle::from_string("hello world")
// Length and lookup
inspect(rle.span(), content="11")
inspect(
rle.find(6),
content=(
#|Some({ run: 0, offset: 6 })
),
)
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
let _ = rle.append("hello")
let _ = rle.append(" world")
inspect(rle.length(), content="1")
inspect(rle.to_string(), content="hello world")
}///|
test {
let rle = @rle.Rle::from_string("hello world")
let (left, right) = rle.split(5).unwrap()
inspect(left.to_string(), content="hello")
inspect(right.to_string(), content=" world")
}///|
test {
let rle = @rle.Rle::from_string("hello world")
let slices = rle.range(start=1, end=4).unwrap().collect()
inspect(slices.length(), content="1")
let s = slices[0]
inspect(
@rle.Sliceable::slice(s.value, start=s.start, end=s.end),
content="Ok(\"ell\")",
)
}///|
test {
let rle = @rle.Rle::from_array(["a", "", "b", "", "c"])
inspect(rle.length(), content="1")
inspect(rle.to_string(), content="abc")
}///|
test {
// Insert at position
let rle = @rle.Rle::from_string("helo")
let elem = @rle.Rle::from_string("l")
let result = rle.insert(2, elem).unwrap()
inspect(result.to_string(), content="hello")
}///|
test {
// Delete a range
let rle = @rle.Rle::from_string("hello world")
let result = rle.delete(start=5, end=6).unwrap()
inspect(result.to_string(), content="helloworld")
}///|
test {
// Splice: replace a range with new content
let rle = @rle.Rle::from_string("hello world")
let replacement = @rle.Rle::from_string("beautiful ")
let result = rle.splice(start=6, end=11, replacement).unwrap()
inspect(result.to_string(), content="hello beautiful ")
}///|
test {
let rle = @rle.Rle::from_string("abcdef")
let cursor = rle.cursor()
inspect(cursor.advance(3), content="true")
inspect(cursor.position(), content="Some(3)")
inspect(cursor.current_item(), content="Some(\"abcdef\")")
// seek() uses binary search โ O(log n)
inspect(cursor.seek(1), content="true")
inspect(cursor.position(), content="Some(1)")
}///|
test {
let rle = @rle.Rle::from_string("abcdef")
let cursor = rle.cursor()
let _ = cursor.advance(3)
let _ = rle.append("ghi")
inspect(cursor.is_stale(), content="true")
inspect(cursor.next(), content="None")
}///|
test {
// Non-mutating concat โ returns a new Rle
let a = @rle.Rle::from_string("hello")
let b = @rle.Rle::from_string(" world")
let c = a.concat(b)
inspect(c.to_string(), content="hello world")
}///|
test {
// In-place extend โ mutates the receiver
let rle = @rle.Rle::from_string("hello")
rle.extend(@rle.Rle::from_string(" world"))
inspect(rle.to_string(), content="hello world")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(
rle.find(0),
content=(
#|Some({ run: 0, offset: 0 })
),
)
inspect(
rle.find(4),
content=(
#|Some({ run: 0, offset: 4 })
),
)
inspect(rle.find(5), content="None")
inspect(rle.find(-1), content="None")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(rle.value_at(2), content="Ok(\"hello\")")
inspect(rle.value_at(5) is Err(_), content="true")
}///|
test {
let rle = @rle.Rle::from_string("hello")
inspect(rle.span(), content="5")
inspect(rle.logical_length(), content="5")
inspect(rle.span() == rle.logical_length(), content="true")
}///|
test {
let rle = @rle.Rle::from_string("hello")
let _ = rle.span() // builds cache
let _ = rle.append(" world") // invalidates cache
// next query rebuilds automatically
inspect(rle.span(), content="11")
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
inspect(rle.get_version(), content="0")
let _ = rle.append("a")
inspect(rle.get_version(), content="1")
let _ = rle.append("b")
inspect(rle.get_version(), content="2")
rle.clear()
inspect(rle.get_version(), content="3")
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.split(100) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(
e.message(),
content="Position 100 is outside the document (length: 5)",
)
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=-1, end=3) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(e.message(), content="Range start (-1) cannot be negative")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=0, end=10) {
Ok(_) => fail("should fail")
Err(e) =>
inspect(e.message(), content="Range end (10) exceeds document length (5)")
}
}///|
test {
let rle : @rle.Rle[String] = @rle.Rle::new()
inspect(rle.append(""), content="Err(Internal(EmptyElement))")
}///|
test {
let rle = @rle.Rle::from_string("A๐B")
inspect(rle.span(), content="4") // A(1) + ๐(2) + B(1)
}///|
test {
let rle = @rle.Rle::from_string("๐")
inspect(rle.span(), content="2")
match rle.split(1) {
Ok(_) => fail("should fail on invalid boundary")
Err(e) => inspect(e, content="InvalidSlice(reason=InvalidIndex)")
}
}///|
test {
let rle = @rle.Rle::from_string("A๐B")
// Position 3 is after the emoji, before B โ valid boundary
match rle.range(start=0, end=3) {
Ok(iter) => {
let slices = iter.collect()
inspect(slices.length(), content="1")
}
Err(_) => fail("range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("ใใใซใกใฏ")
inspect(rle.span(), content="5")
inspect(
rle.find(2),
content=(
#|Some({ run: 0, offset: 2 })
),
)
match rle.split(1) {
Ok((left, right)) => {
inspect(left.to_string(), content="ใ")
inspect(right.to_string(), content="ใใซใกใฏ")
}
Err(_) => fail("split should work with unicode")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.range(start=2, end=2) {
Ok(iter) => inspect(iter.collect().length(), content="0")
Err(_) => fail("empty range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
match rle.delete(start=2, end=2) {
Ok(result) => inspect(result.to_string(), content="hello")
Err(_) => fail("delete empty range should succeed")
}
}///|
test {
let rle = @rle.Rle::from_string("hello")
// Split at start
let (left, right) = rle.split(0).unwrap()
inspect(left.to_string(), content="")
inspect(right.to_string(), content="hello")
}///|
test {
let rle = @rle.Rle::from_string("hello")
// Split at end
let (left, right) = rle.split(5).unwrap()
inspect(left.to_string(), content="hello")
inspect(right.to_string(), content="")
}///|
test {
let rle = @rle.Rle::from_string("hello")
let slices = rle.range_clamped(start=-5, end=100).collect()
inspect(slices.length(), content="1")
let s = slices[0]
inspect(
@rle.Sliceable::slice(s.value, start=s.start, end=s.end),
content="Ok(\"hello\")",
)
}pub(open) trait Addressable {
address(Self, global_start : Int, offset : Int) -> Int
}pub(open) trait FromRange {
from_range(start : Int, count : Int) -> Self
}pub(open) trait HasLength {
length(Self) -> Int
is_empty(Self) -> Bool = _
}pub(open) trait Mergeable {
can_merge(Self, Self) -> Bool
merge(Self, Self) -> Self
}HasLength::length โโโ Spanning::span โโโ Spanning::logical_length
(base) (defaults to length) (defaults to span)impl Show for InternalErrorpub(all) suberror RleError {
PositionOutOfBounds(Int, Int)
InvalidRange(Int, Int, Int, RangeIssue)
InvalidSlice(SliceError)
Internal(InternalError)
} derive(Debug)impl Show for SliceErrorpub struct ErrorMessage {
// private fields
}impl Show for ErrorMessagepub(all) struct PrefixSums {
spans : Array[Int]
content : Array[Int]
} derive(Eq, Debug)
fn PrefixSums::PrefixSums() -> PrefixSumsimpl HasLength for PrefixSumsimpl Spanning for PrefixSumsimpl Show for PrefixSumsimpl Arbitrary for PrefixSumsimpl Shrink for PrefixSumsimpl Show for RangeIssuepub struct Rle[T] {
runs : Runs[T]
prefix : PrefixSums?
version : Int
} derive(Eq, Debug)
fn Rle::Rle() -> Rle[T]Rle: ["abc", "de"] (spans: 3, 2)
each_with_position yields:
f("abc", 0, 3) โ positions [0, 3)
f("de", 3, 5) โ positions [3, 5)Rle::from_sorted_ints([0, 1, 2, 5, 6, 7])
โ groups: [range(0, 3), range(5, 3)]
โ Rle with 2 runs, total span 6let cursor = rle.cursor() // captures version
cursor.advance(5) // move forward
cursor.current_item() // read current run
rle.append(x) // mutation! version bumps
cursor.is_stale() // true โ cursor is now invalid
cursor.next() // None โ refuses to operatefrom_sorted_ints([1, 1, 2, 3, 5, 5])
dedup โ [1, 2, 3, 5]
group โ [range(1, 3), range(5, 1)]Install
Download zipGeneric run-length encoded sequence with O(log n) position lookup