Remove indices from string builder
This commit is contained in:
parent
1e974c4c9a
commit
5afb2c21f8
2 changed files with 171 additions and 73 deletions
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
|
|
||||||
/// Represents a change that can be applied on a `StringBuilder`.
|
/// Represents a change that can be applied on a `StringBuilder`.
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone)]
|
||||||
pub enum Operation<T>
|
pub enum Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + std::fmt::Debug,
|
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>
|
impl<T> Operation<T>
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + std::fmt::Debug,
|
T: PartialEq + Clone + std::fmt::Debug,
|
||||||
|
|
@ -129,24 +163,28 @@ where
|
||||||
Operation::Equal {
|
Operation::Equal {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
text,
|
text,
|
||||||
|
length,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
text.as_ref()
|
text.as_ref()
|
||||||
.is_none_or(|text| builder.get_slice(self.range()) == *text),
|
.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 {
|
Operation::Delete {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
deleted_text,
|
deleted_text,
|
||||||
|
deleted_character_count,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
@ -154,10 +192,12 @@ where
|
||||||
deleted_text
|
deleted_text
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|text| builder.get_slice(self.range()) == *text),
|
.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,
|
f,
|
||||||
"<equal {} from index {}>",
|
"<equal {} from index {}>",
|
||||||
text.as_ref()
|
text.as_ref()
|
||||||
.map(|text| format!("'{text}'"))
|
.map(|text| format!("'{}'", text.replace('\n', "\\n")))
|
||||||
.unwrap_or(format!("{length} characters")),
|
.unwrap_or(format!("{length} characters")),
|
||||||
index
|
index
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -438,7 +478,10 @@ where
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"<insert '{}' from index {}>",
|
"<insert '{}' from index {}>",
|
||||||
text.iter().map(Token::original).collect::<String>(),
|
text.iter()
|
||||||
|
.map(Token::original)
|
||||||
|
.collect::<String>()
|
||||||
|
.replace('\n', "\\n"),
|
||||||
index
|
index
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -455,7 +498,7 @@ where
|
||||||
"<delete {} from index {}>",
|
"<delete {} from index {}>",
|
||||||
deleted_text
|
deleted_text
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|text| format!("'{text}'"))
|
.map(|text| format!("'{}'", text.replace('\n', "\\n")))
|
||||||
.unwrap_or(format!("{deleted_character_count} characters")),
|
.unwrap_or(format!("{deleted_character_count} characters")),
|
||||||
index
|
index
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -498,16 +541,26 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_delete_with_create() {
|
fn test_apply_delete_with_create() {
|
||||||
let builder = StringBuilder::new("hello world");
|
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]
|
#[test]
|
||||||
fn test_apply_insert() {
|
fn test_apply_insert() {
|
||||||
let builder = StringBuilder::new("hello");
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,67 @@
|
||||||
use core::ops::Range;
|
use core::ops::Range;
|
||||||
|
use std::iter::Iterator;
|
||||||
|
|
||||||
/// A helper for building a string in order based on an original string and a
|
/// 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
|
/// series of insertions, deletions, and copies applied to it. It is safe to use
|
||||||
/// UTF-8 strings as all operations are based on character indices.
|
/// with UTF-8 strings as all operations are based on character indices. The
|
||||||
#[derive(Debug, Clone)]
|
/// methods must be called in-order.
|
||||||
pub struct StringBuilder<'a> {
|
pub struct StringBuilder<'a> {
|
||||||
original: &'a str,
|
original: Box<dyn Iterator<Item = char> + 'a>,
|
||||||
last_old_char_index: usize,
|
|
||||||
buffer: String,
|
buffer: String,
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
remaining: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StringBuilder<'_> {
|
impl StringBuilder<'_> {
|
||||||
pub fn new(original: &str) -> StringBuilder<'_> {
|
pub fn new(original: &str) -> StringBuilder<'_> {
|
||||||
StringBuilder {
|
StringBuilder {
|
||||||
original,
|
original: Box::new(original.chars()),
|
||||||
last_old_char_index: 0,
|
|
||||||
buffer: String::with_capacity(original.len()),
|
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
|
/// Insert a string at the end of the built buffer.
|
||||||
/// to that index from the last insertion or deletion.
|
pub fn insert(&mut self, text: &str) { self.buffer.push_str(text); }
|
||||||
pub fn insert(&mut self, from: usize, text: &str) {
|
|
||||||
self.copy_until(from);
|
/// Skip copying `length` characters from the original string to the built
|
||||||
self.buffer.push_str(text);
|
/// buffer.
|
||||||
|
pub fn delete(&mut self, length: usize) {
|
||||||
|
if length == 0 {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a string at the given index after copying the original string up
|
self.original.nth(length - 1);
|
||||||
/// to that index from the last insertion or deletion.
|
|
||||||
pub fn delete(&mut self, range: core::ops::Range<usize>) {
|
if cfg!(debug_assertions) {
|
||||||
self.copy_until(range.start);
|
self.remaining = self.remaining.chars().skip(length).collect();
|
||||||
self.last_old_char_index += range.len();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_until(&mut self, index: usize) {
|
/// Copy `length` characters from the original string to the built buffer.
|
||||||
let current_char_count = self.buffer.chars().count();
|
pub fn retain(&mut self, length: usize) {
|
||||||
debug_assert!(
|
self.buffer.extend(self.original.by_ref().take(length));
|
||||||
index >= current_char_count,
|
|
||||||
"String builder only support building in order"
|
|
||||||
);
|
|
||||||
|
|
||||||
let jump = index - current_char_count;
|
if cfg!(debug_assertions) {
|
||||||
|
self.remaining = self.remaining.chars().skip(length).collect();
|
||||||
self.buffer.push_str(
|
}
|
||||||
&self
|
|
||||||
.original
|
|
||||||
.chars()
|
|
||||||
.skip(self.last_old_char_index)
|
|
||||||
.take(jump)
|
|
||||||
.collect::<String>(),
|
|
||||||
);
|
|
||||||
self.last_old_char_index += jump;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish building the string after copying the remaining original string
|
/// Finish building the string after copying the remaining original string
|
||||||
/// since the last insertion or deletion.
|
/// since the last insertion or deletion.
|
||||||
pub fn build(mut self) -> String {
|
pub fn build(self) -> String { self.buffer }
|
||||||
self.buffer.push_str(
|
|
||||||
&self
|
|
||||||
.original
|
|
||||||
.chars()
|
|
||||||
.skip(self.last_old_char_index)
|
|
||||||
.collect::<String>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.buffer
|
#[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.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn get_slice(&self, range: Range<usize>) -> String {
|
pub fn get_slice(&self, range: Range<usize>) -> String {
|
||||||
let result = self
|
let result = self
|
||||||
.buffer
|
.buffer
|
||||||
.chars()
|
.chars()
|
||||||
.chain(self.original.chars().skip(self.last_old_char_index))
|
.chain(self.remaining.chars())
|
||||||
.skip(range.start)
|
.skip(range.start)
|
||||||
.take(range.end - range.start)
|
.take(range.end - range.start)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
@ -85,6 +74,8 @@ impl StringBuilder<'_> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -92,20 +83,74 @@ mod tests {
|
||||||
let original = "aaa bbb ccc";
|
let original = "aaa bbb ccc";
|
||||||
let mut builder = StringBuilder::new(original);
|
let mut builder = StringBuilder::new(original);
|
||||||
|
|
||||||
builder.insert(0, "ddd ");
|
builder.insert("ddd");
|
||||||
builder.delete(4..8);
|
builder.delete(3);
|
||||||
builder.insert(11, " eee");
|
builder.retain(8);
|
||||||
|
builder.insert(" eee");
|
||||||
|
|
||||||
assert_eq!(builder.build(), "ddd bbb ccc eee");
|
assert_eq!(builder.build(), "ddd bbb ccc eee");
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_string_builder2() {
|
|
||||||
let original = "abcde";
|
let original = "abcde";
|
||||||
let mut builder = StringBuilder::new(original);
|
let mut builder = StringBuilder::new(original);
|
||||||
|
|
||||||
builder.delete(1..4);
|
builder.retain(1);
|
||||||
|
builder.delete(3);
|
||||||
|
builder.retain(1);
|
||||||
|
|
||||||
assert_eq!(builder.build(), "ae");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue