use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ByteRange {
pub start: usize,
pub end: usize,
}
impl ByteRange {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn is_empty(&self) -> bool {
self.end <= self.start
}
pub fn contains(&self, other: &ByteRange) -> bool {
self.start <= other.start && other.end <= self.end
}
pub fn validate(&self, text: &str) -> Result<(), RangeError> {
if self.start > self.end {
return Err(RangeError::Inverted {
start: self.start,
end: self.end,
});
}
if self.end > text.len() {
return Err(RangeError::OutOfBounds {
end: self.end,
len: text.len(),
});
}
if !text.is_char_boundary(self.start) {
return Err(RangeError::NotCharBoundary { offset: self.start });
}
if !text.is_char_boundary(self.end) {
return Err(RangeError::NotCharBoundary { offset: self.end });
}
Ok(())
}
pub fn slice<'a>(&self, text: &'a str) -> Result<&'a str, RangeError> {
self.validate(text)?;
Ok(&text[self.start..self.end])
}
}
pub fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> Option<usize> {
let mut byte_pos = 0usize;
let mut utf16_pos = 0usize;
for ch in text.chars() {
if utf16_pos == utf16_offset {
return Some(byte_pos);
}
if utf16_pos > utf16_offset {
return None;
}
utf16_pos += ch.len_utf16();
byte_pos += ch.len_utf8();
}
(utf16_pos == utf16_offset).then_some(byte_pos)
}
impl From<std::ops::Range<usize>> for ByteRange {
fn from(r: std::ops::Range<usize>) -> Self {
Self::new(r.start, r.end)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
pub enum RangeError {
#[error("range start {start} is greater than end {end}")]
Inverted { start: usize, end: usize },
#[error("range end {end} exceeds text length {len}")]
OutOfBounds { end: usize, len: usize },
#[error("offset {offset} is not a UTF-8 character boundary")]
NotCharBoundary { offset: usize },
}