str-utils 0.3.7

This crate provides some traits to extend `[u8]`, `str` and `Cow<str>`.
Documentation
use alloc::{borrow::Cow, string::String};

mod sealed {
    pub trait Sealed {}

    impl Sealed for char {}
    impl Sealed for &str {}
    impl Sealed for &&str {}
    impl Sealed for &[char] {}

    impl<const N: usize> Sealed for [char; N] {}
    impl<const N: usize> Sealed for &[char; N] {}

    impl<F> Sealed for F where F: FnMut(char) -> bool {}
}

#[inline]
fn char_eq_str(c: char, s: &str) -> bool {
    let mut buffer = [0; 4];

    c.encode_utf8(&mut buffer) == s
}

fn remove_matches<'a, I>(s: &'a str, matches: I) -> Cow<'a, str>
where
    I: Iterator<Item = (usize, usize)>, {
    let mut borrowed_start = 0;
    let mut pending: Option<(usize, usize)> = None;
    let mut new_s: Option<String> = None;
    let mut start = 0;

    for (match_start, match_end) in matches {
        if let Some(new_s) = new_s.as_mut() {
            new_s.push_str(&s[start..match_start]);
            start = match_end;

            continue;
        }

        if let Some((pending_start, pending_end)) = pending.as_mut() {
            if match_start == *pending_end {
                *pending_end = match_end;

                continue;
            }

            // A later match proves that the pending removed range is inside the remaining text.
            let mut owned = String::with_capacity(s.len());

            owned.push_str(&s[borrowed_start..*pending_start]);
            owned.push_str(&s[*pending_end..match_start]);

            new_s = Some(owned);
            start = match_end;

            continue;
        }

        if match_start == borrowed_start {
            borrowed_start = match_end;
        } else {
            pending = Some((match_start, match_end));
        }
    }

    if let Some(mut new_s) = new_s {
        new_s.push_str(&s[start..]);

        return Cow::Owned(new_s);
    }

    match pending {
        Some((pending_start, pending_end)) if pending_end == s.len() => {
            Cow::Borrowed(&s[borrowed_start..pending_start])
        },
        Some((pending_start, pending_end)) => {
            let mut new_s = String::with_capacity(s.len());

            new_s.push_str(&s[borrowed_start..pending_start]);
            new_s.push_str(&s[pending_end..]);

            Cow::Owned(new_s)
        },
        None => Cow::Borrowed(&s[borrowed_start..]),
    }
}

fn replace_chars<'a, F>(s: &'a str, mut from: F, to: &str, mut count: usize) -> Cow<'a, str>
where
    F: FnMut(char) -> bool, {
    if count == 0 {
        return Cow::Borrowed(s);
    }

    if to.is_empty() {
        let matches =
            s.char_indices().filter_map(
                |(p, c)| {
                    if from(c) {
                        Some((p, p + c.len_utf8()))
                    } else {
                        None
                    }
                },
            );

        return remove_matches(s, matches.take(count));
    }

    let mut new_s: Option<String> = None;
    let mut start = 0;

    for (p, c) in s.char_indices() {
        if count == 0 {
            break;
        }

        if from(c) {
            let next = p + c.len_utf8();

            if char_eq_str(c, to) {
                count -= 1;

                continue;
            }

            if let Some(new_s) = new_s.as_mut() {
                new_s.push_str(&s[start..p]);
                new_s.push_str(to);
            } else {
                let mut owned = String::with_capacity(s.len());

                owned.push_str(&s[..p]);
                owned.push_str(to);

                new_s = Some(owned);
            }

            start = next;
            count -= 1;
        }
    }

    match new_s {
        Some(mut new_s) => {
            new_s.push_str(&s[start..]);

            Cow::Owned(new_s)
        },
        None => Cow::Borrowed(s),
    }
}

#[inline]
fn replace_str<'a>(s: &'a str, from: &str, to: &str) -> Cow<'a, str> {
    if from == to {
        Cow::Borrowed(s)
    } else if to.is_empty() {
        remove_matches(s, s.match_indices(from).map(|(p, from)| (p, p + from.len())))
    } else if !s.contains(from) {
        Cow::Borrowed(s)
    } else {
        Cow::Owned(s.replace(from, to))
    }
}

#[inline]
fn replacen_str<'a>(s: &'a str, from: &str, to: &str, count: usize) -> Cow<'a, str> {
    if count == 0 || from == to {
        Cow::Borrowed(s)
    } else if to.is_empty() {
        remove_matches(s, s.match_indices(from).map(|(p, from)| (p, p + from.len())).take(count))
    } else if !s.contains(from) {
        Cow::Borrowed(s)
    } else {
        Cow::Owned(s.replacen(from, to, count))
    }
}

/// A stable pattern type that can be used by replacement and trimming methods.
///
/// This trait is local to this crate because the standard library `Pattern` trait is still unstable to name in public APIs.
pub trait Pattern: sealed::Sealed {
    /// Returns a `Cow<str>` after replacing all matches in the given string.
    #[doc(hidden)]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str>;

    /// Returns a `Cow<str>` after replacing at most `count` matches in the given string.
    #[doc(hidden)]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str>;

    /// Returns a string slice after removing all matching prefixes and suffixes.
    #[doc(hidden)]
    fn trim_matches_from(self, s: &str) -> &str;

    /// Returns a string slice after removing all matching prefixes.
    #[doc(hidden)]
    fn trim_start_matches_from(self, s: &str) -> &str;

    /// Returns a string slice after removing all matching suffixes.
    #[doc(hidden)]
    fn trim_end_matches_from(self, s: &str) -> &str;
}

impl Pattern for char {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_chars(s, |c| c == self, to, usize::MAX)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replace_chars(s, |c| c == self, to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_matches(self)
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(self)
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(self)
    }
}

impl Pattern for &str {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_str(s, self, to)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replacen_str(s, self, to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(self).trim_end_matches(self)
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(self)
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(self)
    }
}

impl Pattern for &&str {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        (*self).replace_from(s, to)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        (*self).replacen_from(s, to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        (*self).trim_matches_from(s)
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        (*self).trim_start_matches_from(s)
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        (*self).trim_end_matches_from(s)
    }
}

impl Pattern for &[char] {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }
}

impl<const N: usize> Pattern for [char; N] {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }
}

impl<const N: usize> Pattern for &[char; N] {
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
    }
}

impl<F> Pattern for F
where
    F: FnMut(char) -> bool,
{
    #[inline]
    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
        replace_chars(s, self, to, usize::MAX)
    }

    #[inline]
    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
        replace_chars(s, self, to, count)
    }

    #[inline]
    fn trim_matches_from(self, s: &str) -> &str {
        s.trim_matches(self)
    }

    #[inline]
    fn trim_start_matches_from(self, s: &str) -> &str {
        s.trim_start_matches(self)
    }

    #[inline]
    fn trim_end_matches_from(self, s: &str) -> &str {
        s.trim_end_matches(self)
    }
}