#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::cargo)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_qualifications)]
#![warn(variant_size_differences)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![allow(clippy::multiple_crate_versions)]
#![doc = include_str!("../README.md")]
#[doc(no_inline)] pub use ascii::AsciiChar;
use itertools::Itertools;
use std::{cmp::Ordering, error::Error, fmt::Display, ops::Range};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SortedString<'a> {
string: &'a str,
sep: AsciiChar,
}
impl Display for SortedString<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SortedString({:?}, {:?})", self.string, self.sep)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SearchError(
)
pub Range<usize>,
);
impl Error for SearchError {}
impl Display for SearchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let SearchError(range) = self;
write!(f, "needle not found, last looked at {range:?}")
}
}
pub type SearchResult = Result<Range<usize>, SearchError>;
impl<'a> SortedString<'a> {
pub fn new_checked(
haystack: &'a str,
sep: AsciiChar,
) -> Result<Self, SortedStringCreationError> {
if haystack.is_empty() {
return Err(SortedStringCreationError::EmptyHaystack);
}
let sorted_string = Self::new(haystack, sep);
if sorted_string.is_sorted() {
Ok(sorted_string)
} else {
Err(SortedStringCreationError::NotSorted)
}
}
pub fn binary_search<U>(&self, needle: U) -> SearchResult
where
U: AsRef<str>,
{
let haystack = self.string;
let needle = needle.as_ref();
let leftmost = 0;
let rightmost = haystack.len();
let mut low = leftmost;
let mut high = rightmost;
let mut start = leftmost;
let mut end = rightmost;
let haystack = haystack.as_bytes();
let pred = |c: &&u8| **c == self.sep.as_byte();
while low < high {
let mid = low + (high - low) / 2;
start = match haystack[..mid].iter().rev().find_position(pred) {
Some((delta, _)) => mid - delta,
None => leftmost,
};
end = match haystack[mid..].iter().find_position(pred) {
Some((delta, _)) => mid + delta,
None => rightmost,
};
let haystack_word = std::str::from_utf8(&haystack[start..end])
.expect("Indices aren't valid for slicing into haystack. They are at ASCII chars and therefore always assumed valid.");
match needle.cmp(haystack_word) {
Ordering::Less => high = mid.saturating_sub(1),
Ordering::Equal => return Ok(Range { start, end }),
Ordering::Greater => low = mid + 1,
}
}
Err(SearchError(Range { start, end }))
}
#[must_use]
pub const fn new_unchecked(string: &'a str, sep: AsciiChar) -> Self {
Self::new(string, sep)
}
#[must_use]
pub fn sort(string: &str, sep: AsciiChar) -> String {
string
.split(sep.as_char())
.sorted()
.collect::<Vec<&str>>()
.join(&sep.to_string())
}
const fn new(string: &'a str, sep: AsciiChar) -> Self {
Self { string, sep }
}
fn is_sorted(&self) -> bool {
self.string
.split(self.sep.as_char())
.tuple_windows()
.all(|(a, b)| a <= b)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SortedStringCreationError {
NotSorted,
EmptyHaystack,
}
impl Error for SortedStringCreationError {}
impl Display for SortedStringCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotSorted => write!(f, "The provided string is not sorted."),
Self::EmptyHaystack => write!(f, "The provided string is empty."),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::error::Error;
#[rstest]
#[case(Box::new(SortedStringCreationError::NotSorted))]
#[case(Box::new(SortedStringCreationError::EmptyHaystack))]
#[case(Box::new(SearchError(Range { start: 0, end: 1 })))]
fn test_error_trait_implementations_are_present(#[case] err: Box<dyn Error>) {
assert!(!err.to_string().is_empty());
}
}