use std::ops::Range;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodePointString {
inner: String,
indices: Vec<usize>,
}
impl CodePointString {
pub fn new() -> Self {
Self::from_string(String::new())
}
pub fn from_string(s: String) -> Self {
let mut indices = Vec::with_capacity(s.chars().count() + 1);
for (byte_idx, _ch) in s.char_indices() {
indices.push(byte_idx);
}
indices.push(s.len());
CodePointString { inner: s, indices }
}
pub fn clear(&mut self) {
self.inner.clear();
self.indices.clear();
self.indices.push(0);
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn len(&self) -> usize {
self.indices.len().saturating_sub(1)
}
#[allow(unused)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[allow(unused)]
pub fn char_at(&self, idx: usize) -> Option<char> {
if idx >= self.len() {
return None;
}
let start = self.indices[idx];
let end = self.indices[idx + 1];
self.inner[start..end].chars().next()
}
pub fn push_char(&mut self, ch: char) {
self.inner.push(ch);
let new_end = self.inner.len();
self.indices.push(new_end);
}
pub fn insert_char(&mut self, idx: usize, ch: char) {
let pos = idx.min(self.len());
let byte_idx = self.indices[pos];
self.inner.insert(byte_idx, ch);
let ch_len = ch.len_utf8();
for off in &mut self.indices[pos + 1..] {
*off += ch_len;
}
self.indices.insert(pos + 1, byte_idx + ch_len);
}
pub fn remove_char(&mut self, idx: usize) -> Option<char> {
if idx >= self.len() {
return None;
}
let start = self.indices[idx];
let end = self.indices[idx + 1];
let ch = self.inner[start..end].chars().next().unwrap();
self.inner.replace_range(start..end, "");
let ch_len = end - start;
for off in &mut self.indices[idx + 1..] {
*off -= ch_len;
}
self.indices.remove(idx + 1);
Some(ch)
}
pub fn slice_chars(&self, range: Range<usize>) -> Option<&str> {
if range.start > range.end || range.end > self.len() {
return None;
}
let b_start = self.indices[range.start];
let b_end = self.indices[range.end];
Some(&self.inner[b_start..b_end])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let mut s = CodePointString::from_string("héllo🌍".into());
assert_eq!(s.len(), 6); assert_eq!(s.char_at(1), Some('é'));
s.insert_char(5, '🥳');
assert_eq!(s.as_str(), "héllo🥳🌍");
let s = s.slice_chars(1..4).unwrap(); assert_eq!("éll", s)
}
#[test]
fn empty_string() {
let s = CodePointString::from_string(String::new());
assert_eq!(s.len(), 0);
assert_eq!(s.as_str(), "");
assert_eq!(s.char_at(0), None);
assert_eq!(s.slice_chars(0..0), Some(""));
}
#[test]
fn basic_construction_and_indexing() {
let raw = "héllo🌍";
let s = CodePointString::from_string(raw.into());
assert_eq!(s.len(), raw.chars().count());
assert_eq!(s.as_str(), raw);
for (i, ch) in raw.chars().enumerate() {
assert_eq!(s.char_at(i), Some(ch));
}
assert_eq!(s.char_at(s.len()), None);
}
#[test]
fn slicing_various_ranges() {
let s = CodePointString::from_string("abcdef".into());
assert_eq!(s.slice_chars(0..3), Some("abc"));
assert_eq!(s.slice_chars(3..6), Some("def"));
assert_eq!(s.slice_chars(2..4), Some("cd"));
assert_eq!(s.slice_chars(6..6), Some("")); assert_eq!(s.slice_chars(0..0), Some("")); assert_eq!(s.slice_chars(4..2), None); assert_eq!(s.slice_chars(0..7), None); }
#[test]
fn insert_at_beginning_middle_end() {
let mut s = CodePointString::from_string("ace".into());
s.insert_char(1, 'b');
assert_eq!(s.as_str(), "abce");
s.insert_char(0, '0');
assert_eq!(s.as_str(), "0abce");
let end = s.len();
s.insert_char(end, 'Z');
assert_eq!(s.as_str(), "0abceZ");
let expected: String = "0abceZ".chars().collect();
for (i, ch) in expected.chars().enumerate() {
assert_eq!(s.char_at(i), Some(ch));
}
}
#[test]
fn remove_at_beginning_middle_end() {
let mut s = CodePointString::from_string("0abceZ".into());
assert_eq!(s.remove_char(0), Some('0'));
assert_eq!(s.as_str(), "abceZ");
assert_eq!(s.remove_char(1), Some('b'));
assert_eq!(s.as_str(), "aceZ");
let last_idx = s.len() - 1;
assert_eq!(s.remove_char(last_idx), Some('Z'));
assert_eq!(s.as_str(), "ace");
assert_eq!(s.remove_char(10), None);
}
#[test]
fn multi_byte_characters() {
let mut s = CodePointString::from_string("🐱🌮".into());
assert_eq!(s.len(), 2);
assert_eq!(s.char_at(0), Some('🐱'));
assert_eq!(s.char_at(1), Some('🌮'));
s.insert_char(1, '🚀');
assert_eq!(s.as_str(), "🐱🚀🌮");
assert_eq!(s.len(), 3);
assert_eq!(s.remove_char(1), Some('🚀'));
assert_eq!(s.as_str(), "🐱🌮");
}
#[test]
fn complex_sequence_of_operations() {
let mut s = CodePointString::from_string("start".into());
s.insert_char(5, '!'); s.insert_char(0, '*'); s.insert_char(3, '-'); assert_eq!(s.as_str(), "*st-art!");
let sliced = s.slice_chars(1..4).unwrap(); assert_eq!(sliced, "st-");
assert_eq!(s.remove_char(0), Some('*')); assert_eq!(s.remove_char(3), Some('a')); assert_eq!(s.remove_char(s.len() - 1), Some('!')); assert_eq!(s.as_str(), "st-rt");
}
#[test]
fn push_char() {
let mut s = CodePointString::from_string(String::new());
assert_eq!(s.len(), 0);
s.push_char('A');
assert_eq!(s.len(), 1);
assert_eq!(s.as_str(), "A");
s.push_char('Ω');
assert_eq!(s.len(), 2);
assert_eq!(s.as_str(), "AΩ");
assert_eq!(s.char_at(1), Some('Ω'));
s.push_char('8');
assert_eq!(s.len(), 3);
assert_eq!(s.as_str(), "AΩ8");
assert_eq!(s.char_at(2), Some('8'));
}
}