Remove indices from string builder

This commit is contained in:
Andras Schmelczer 2025-06-21 13:18:02 +01:00
parent 1e974c4c9a
commit 5afb2c21f8
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
2 changed files with 171 additions and 73 deletions

View file

@ -15,7 +15,7 @@ use crate::{
/// Represents a change that can be applied on a `StringBuilder`.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, PartialEq)]
#[derive(Clone)]
pub enum Operation<T>
where
T: PartialEq + Clone + std::fmt::Debug,
@ -42,6 +42,40 @@ where
},
}
impl<T> PartialEq for Operation<T>
where
T: PartialEq + Clone + std::fmt::Debug,
{
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Operation::Equal { length, .. },
Operation::Equal {
length: other_length,
..
},
) => length == other_length,
(
Operation::Insert { text, .. },
Operation::Insert {
text: other_text, ..
},
) => text == other_text,
(
Operation::Delete {
deleted_character_count,
..
},
Operation::Delete {
deleted_character_count: other_deleted_character_count,
..
},
) => deleted_character_count == other_deleted_character_count,
_ => false,
}
}
}
impl<T> Operation<T>
where
T: PartialEq + Clone + std::fmt::Debug,
@ -129,24 +163,28 @@ where
Operation::Equal {
#[cfg(debug_assertions)]
text,
length,
..
} => {
#[cfg(debug_assertions)]
debug_assert!(
text.as_ref()
.is_none_or(|text| builder.get_slice(self.range()) == *text),
"Text which is supposed to be equal does not match the text in the range"
"Text (`{}`) which is supposed to be equal does not match the text in the \
range: `{}`",
text.as_ref().unwrap_or(&"".to_owned()),
builder.get_slice(self.range())
);
return builder;
builder.retain(*length)
}
Operation::Insert { text, .. } => {
builder.insert(&text.iter().map(Token::original).collect::<String>())
}
Operation::Insert { text, .. } => builder.insert(
self.start_index(),
&text.iter().map(Token::original).collect::<String>(),
),
Operation::Delete {
#[cfg(debug_assertions)]
deleted_text,
deleted_character_count,
..
} => {
#[cfg(debug_assertions)]
@ -154,10 +192,12 @@ where
deleted_text
.as_ref()
.is_none_or(|text| builder.get_slice(self.range()) == *text),
"Text to delete does not match the text in the range"
"Text to delete (`{}`) does not match the text in the range: {}",
deleted_text.as_ref().unwrap_or(&"".to_owned()),
builder.get_slice(self.range())
);
builder.delete(self.range());
builder.delete(*deleted_character_count)
}
}
@ -424,7 +464,7 @@ where
f,
"<equal {} from index {}>",
text.as_ref()
.map(|text| format!("'{text}'"))
.map(|text| format!("'{}'", text.replace('\n', "\\n")))
.unwrap_or(format!("{length} characters")),
index
)?;
@ -438,7 +478,10 @@ where
write!(
f,
"<insert '{}' from index {}>",
text.iter().map(Token::original).collect::<String>(),
text.iter()
.map(Token::original)
.collect::<String>()
.replace('\n', "\\n"),
index
)
}
@ -455,7 +498,7 @@ where
"<delete {} from index {}>",
deleted_text
.as_ref()
.map(|text| format!("'{text}'"))
.map(|text| format!("'{}'", text.replace('\n', "\\n")))
.unwrap_or(format!("{deleted_character_count} characters")),
index
)?;
@ -498,16 +541,26 @@ mod tests {
#[test]
fn test_apply_delete_with_create() {
let builder = StringBuilder::new("hello world");
let operation = Operation::<()>::create_delete_with_text(5, " world".to_owned()).unwrap();
let delete_operation =
Operation::<()>::create_delete_with_text(0, "hello ".to_owned()).unwrap();
let retain_operation = Operation::<()>::create_equal(5, 5).unwrap();
assert_eq!(operation.apply(builder).build(), "hello");
let mut builder = delete_operation.apply(builder);
builder = retain_operation.apply(builder);
assert_eq!(builder.build(), "world");
}
#[test]
fn test_apply_insert() {
let builder = StringBuilder::new("hello");
let operation = Operation::create_insert(5, vec![" my friend".into()]).unwrap();
assert_eq!(operation.apply(builder).build(), "hello my friend");
let retain_operation = Operation::<()>::create_equal(5, 5).unwrap();
let insert_operation = Operation::create_insert(5, vec![" my friend".into()]).unwrap();
let mut builder = retain_operation.apply(builder);
builder = insert_operation.apply(builder);
assert_eq!(builder.build(), "hello my friend");
}
}

View file

@ -1,78 +1,67 @@
use core::ops::Range;
use std::iter::Iterator;
/// A helper for building a string in order based on an original string and a
/// series of insertions and deletions applied to it. It is safe to use with
/// UTF-8 strings as all operations are based on character indices.
#[derive(Debug, Clone)]
/// A helper for building a string in-order based on an original string and a
/// series of insertions, deletions, and copies applied to it. It is safe to use
/// with UTF-8 strings as all operations are based on character indices. The
/// methods must be called in-order.
pub struct StringBuilder<'a> {
original: &'a str,
last_old_char_index: usize,
original: Box<dyn Iterator<Item = char> + 'a>,
buffer: String,
#[cfg(debug_assertions)]
remaining: String,
}
impl StringBuilder<'_> {
pub fn new(original: &str) -> StringBuilder<'_> {
StringBuilder {
original,
last_old_char_index: 0,
original: Box::new(original.chars()),
buffer: String::with_capacity(original.len()),
#[cfg(debug_assertions)]
remaining: original.to_string(),
}
}
/// Insert a string at the given index after copying the original string up
/// to that index from the last insertion or deletion.
pub fn insert(&mut self, from: usize, text: &str) {
self.copy_until(from);
self.buffer.push_str(text);
/// Insert a string at the end of the built buffer.
pub fn insert(&mut self, text: &str) { self.buffer.push_str(text); }
/// Skip copying `length` characters from the original string to the built
/// buffer.
pub fn delete(&mut self, length: usize) {
if length == 0 {
return;
}
self.original.nth(length - 1);
if cfg!(debug_assertions) {
self.remaining = self.remaining.chars().skip(length).collect();
}
}
/// Delete a string at the given index after copying the original string up
/// to that index from the last insertion or deletion.
pub fn delete(&mut self, range: core::ops::Range<usize>) {
self.copy_until(range.start);
self.last_old_char_index += range.len();
}
/// Copy `length` characters from the original string to the built buffer.
pub fn retain(&mut self, length: usize) {
self.buffer.extend(self.original.by_ref().take(length));
fn copy_until(&mut self, index: usize) {
let current_char_count = self.buffer.chars().count();
debug_assert!(
index >= current_char_count,
"String builder only support building in order"
);
let jump = index - current_char_count;
self.buffer.push_str(
&self
.original
.chars()
.skip(self.last_old_char_index)
.take(jump)
.collect::<String>(),
);
self.last_old_char_index += jump;
if cfg!(debug_assertions) {
self.remaining = self.remaining.chars().skip(length).collect();
}
}
/// Finish building the string after copying the remaining original string
/// since the last insertion or deletion.
pub fn build(mut self) -> String {
self.buffer.push_str(
&self
.original
.chars()
.skip(self.last_old_char_index)
.collect::<String>(),
);
pub fn build(self) -> String { self.buffer }
self.buffer
}
#[allow(dead_code)]
#[cfg(debug_assertions)]
/// Get a slice of the built string and the remaining original string.
/// The implementation is quite suboptimal but it's only used for debugging.
pub fn get_slice(&self, range: Range<usize>) -> String {
let result = self
.buffer
.chars()
.chain(self.original.chars().skip(self.last_old_char_index))
.chain(self.remaining.chars())
.skip(range.start)
.take(range.end - range.start)
.collect::<String>();
@ -85,6 +74,8 @@ impl StringBuilder<'_> {
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
@ -92,20 +83,74 @@ mod tests {
let original = "aaa bbb ccc";
let mut builder = StringBuilder::new(original);
builder.insert(0, "ddd ");
builder.delete(4..8);
builder.insert(11, " eee");
builder.insert("ddd");
builder.delete(3);
builder.retain(8);
builder.insert(" eee");
assert_eq!(builder.build(), "ddd bbb ccc eee");
}
#[test]
fn test_string_builder2() {
let original = "abcde";
let mut builder = StringBuilder::new(original);
builder.delete(1..4);
builder.retain(1);
builder.delete(3);
builder.retain(1);
assert_eq!(builder.build(), "ae");
}
#[test]
fn test_empty_original() {
let original = "";
let mut builder = StringBuilder::new(original);
builder.insert("test");
assert_eq!(builder.build(), "test");
}
#[test]
fn test_unicode_characters() {
let original = "こんにちは";
let mut builder = StringBuilder::new(original);
builder.retain(3);
builder.insert("世界, "); // Insert "World, "
builder.retain(2);
assert_eq!(builder.build(), "こんに世界, ちは");
}
#[test]
fn test_get_slice() {
let original = "abcdef";
let builder = StringBuilder::new(original);
// Test getting a slice of the original string
assert_eq!(builder.get_slice(1..4), "bcd");
// Test getting a slice that includes both buffer and remaining original
let mut builder = StringBuilder::new(original);
builder.retain(2); // "ab" in buffer
assert_eq!(builder.get_slice(1..5), "bcde");
}
#[test]
fn test_retain_all() {
let original = "Hello, world!";
let mut builder = StringBuilder::new(original);
builder.retain(original.len());
assert_eq!(builder.build(), original);
}
#[test]
fn test_delete_all() {
let original = "Hello";
let mut builder = StringBuilder::new(original);
builder.delete(original.len());
builder.insert("Hi");
assert_eq!(builder.build(), "Hi");
}
}