mod sealed {
pub trait Sealed {}
}
pub trait PatternRef: sealed::Sealed {
fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str;
fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str;
}
macro_rules! impl_pattern_ref {
([$($gen:tt)*], $ty:ty, $to_pattern:expr) => {
impl<$($gen)*> sealed::Sealed for $ty {}
impl<$($gen)*> PatternRef for $ty {
fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str {
s.trim_start_matches(($to_pattern)(self))
}
fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str {
s.trim_end_matches(($to_pattern)(self))
}
}
};
}
impl_pattern_ref!([], char, Clone::clone);
impl_pattern_ref!([const N: usize], [char; N], Clone::clone);
impl_pattern_ref!([const N: usize], &[char; N], Clone::clone);
impl_pattern_ref!([], &[char], Clone::clone);
impl_pattern_ref!([], &str, Clone::clone);
impl_pattern_ref!([], &&str, Clone::clone);
impl_pattern_ref!([F: FnMut(char) -> bool], F, std::convert::identity);
pub trait TrimExt {
fn trim_matches_in_place<P: PatternRef>(&mut self, pattern: P);
fn trim_start_matches_in_place<P: PatternRef>(&mut self, pattern: P);
fn trim_end_matches_in_place<P: PatternRef>(&mut self, pattern: P);
fn trim_in_place(&mut self) {
self.trim_matches_in_place(char::is_whitespace);
}
fn trim_start_in_place(&mut self) {
self.trim_start_matches_in_place(char::is_whitespace);
}
fn trim_end_in_place(&mut self) {
self.trim_end_matches_in_place(char::is_whitespace);
}
}
fn trim_start_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
s.drain(..s.len() - pattern.trim_start_matches(s).len());
}
fn trim_end_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
s.truncate(pattern.trim_end_matches(s).len());
}
impl TrimExt for String {
fn trim_start_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
trim_start_matches_in_place(self, &mut pattern);
}
fn trim_end_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
trim_end_matches_in_place(self, &mut pattern);
}
fn trim_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
trim_start_matches_in_place(self, &mut pattern);
trim_end_matches_in_place(self, &mut pattern);
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rstest::rstest;
use super::TrimExt;
fn trimmed(input: &str, pattern: impl super::PatternRef) -> String {
let mut string = input.to_string();
string.trim_matches_in_place(pattern);
string
}
#[rstest]
#[case::both_ends("xxhixx", "hi")]
#[case::leading_only("xxhi", "hi")]
#[case::trailing_only("hixx", "hi")]
#[case::interior_kept("xhixhix", "hixhi")]
#[case::no_match("hi", "hi")]
#[case::all_pattern("xxxx", "")]
#[case::empty("", "")]
#[case::single_char("x", "")]
fn trims_a_char_pattern(#[case] input: &str, #[case] expected: &str) {
assert_eq!(trimmed(input, 'x'), expected);
}
#[rstest]
#[case::blank_lines_and_spaces("\n\n hi there \n\n", "hi there")]
#[case::mixed_run(" \n \n hi", "hi")]
#[case::interior_newline_kept("\none\ntwo\n", "one\ntwo")]
fn trims_a_char_array_pattern(#[case] input: &str, #[case] expected: &str) {
assert_eq!(trimmed(input, ['\n', ' ']), expected);
assert_eq!(trimmed(input, &['\n', ' ']), expected);
assert_eq!(trimmed(input, &['\n', ' '][..]), expected);
}
#[rstest]
#[case::str_pattern("abcXabc", "abc", "X")]
#[case::repeated_str_pattern("abcabcXabcabc", "abc", "X")]
#[case::partial_match_kept("abXab", "abc", "abXab")]
fn trims_a_str_pattern(#[case] input: &str, #[case] pattern: &str, #[case] expected: &str) {
assert_eq!(trimmed(input, pattern), expected);
assert_eq!(trimmed(input, &pattern), expected);
}
#[rstest]
#[case::digits("123hi456", "hi")]
#[case::only_digits("123", "")]
#[case::interior_digits_kept("1h2i3", "h2i")]
#[case::nothing_to_trim("hi", "hi")]
fn trims_a_closure_pattern(#[case] input: &str, #[case] expected: &str) {
assert_eq!(trimmed(input, |c: char| c.is_ascii_digit()), expected);
}
#[rstest]
#[case::multibyte_pattern("——hi——", '—', "hi")]
#[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界")]
#[case::multibyte_content_all_trimmed("🦀🦀", '🦀', "")]
fn handles_multibyte_characters(
#[case] input: &str,
#[case] pattern: char,
#[case] expected: &str,
) {
assert_eq!(trimmed(input, pattern), expected);
}
#[rstest]
#[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi")]
#[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi")]
#[case::interior_kept(" a b ", "a b")]
#[case::nothing_to_trim("hi", "hi")]
fn trim_in_place_matches_str_trim(#[case] input: &str, #[case] expected: &str) {
let mut string = input.to_string();
string.trim_in_place();
assert_eq!(string, expected);
assert_eq!(string, input.trim());
}
fn trimmed_start(input: &str, pattern: impl super::PatternRef) -> String {
let mut string = input.to_string();
string.trim_start_matches_in_place(pattern);
string
}
fn trimmed_end(input: &str, pattern: impl super::PatternRef) -> String {
let mut string = input.to_string();
string.trim_end_matches_in_place(pattern);
string
}
#[rstest]
#[case::both_ends("\n\nhi\n\n", "hi\n\n", "\n\nhi")]
#[case::leading_only("\n\nhi", "hi", "\n\nhi")]
#[case::trailing_only("hi\n\n", "hi\n\n", "hi")]
#[case::interior_kept("\none\ntwo\n", "one\ntwo\n", "\none\ntwo")]
#[case::all_pattern("\n\n", "", "")]
#[case::empty("", "", "")]
#[case::nothing_to_trim("hi", "hi", "hi")]
fn trims_only_the_requested_end(
#[case] input: &str,
#[case] start_trimmed: &str,
#[case] end_trimmed: &str,
) {
assert_eq!(trimmed_start(input, '\n'), start_trimmed);
assert_eq!(trimmed_end(input, '\n'), end_trimmed);
}
#[rstest]
#[case::multibyte_pattern("——hi——", '—', "hi——", "——hi")]
#[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界xx", "xx🦀 世界")]
fn one_sided_trims_handle_multibyte_characters(
#[case] input: &str,
#[case] pattern: char,
#[case] start_trimmed: &str,
#[case] end_trimmed: &str,
) {
assert_eq!(trimmed_start(input, pattern), start_trimmed);
assert_eq!(trimmed_end(input, pattern), end_trimmed);
}
#[rstest]
#[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi \t\r\n", " \t\r\nhi")]
#[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi\u{3000}", "\u{3000}hi")]
#[case::nothing_to_trim("hi", "hi", "hi")]
fn one_sided_whitespace_trims_match_str(
#[case] input: &str,
#[case] start_trimmed: &str,
#[case] end_trimmed: &str,
) {
let mut start = input.to_string();
start.trim_start_in_place();
assert_eq!(start, start_trimmed);
assert_eq!(start, input.trim_start());
let mut end = input.to_string();
end.trim_end_in_place();
assert_eq!(end, end_trimmed);
assert_eq!(end, input.trim_end());
}
#[rstest]
fn a_stateful_pattern_is_reused_rather_than_consumed() {
let mut string = "abhixy".to_string();
let mut chars: std::collections::HashSet<char> = string.chars().collect();
string.trim_matches_in_place(|c| {
assert!(chars.remove(&c), "matcher called on nonexistent char, or same char twice");
!c.is_ascii_alphabetic() || "abxy".contains(c)
});
assert_eq!(string, "hi");
assert!(chars.is_empty(), "matcher not called on every char");
}
proptest! {
#[rstest]
fn agrees_with_str_trim_matches(input in ".{0,64}", pattern in prop::char::range('a', 'e')) {
let mut string = input.clone();
string.trim_matches_in_place(pattern);
prop_assert_eq!(string, input.trim_matches(pattern));
}
#[rstest]
fn agrees_with_str_trim(input in ".{0,64}") {
let mut string = input.clone();
string.trim_in_place();
prop_assert_eq!(string, input.trim());
}
#[rstest]
fn agrees_with_str_trim_start_matches(
input in ".{0,64}",
pattern in prop::char::range('a', 'e'),
) {
let mut string = input.clone();
string.trim_start_matches_in_place(pattern);
prop_assert_eq!(string, input.trim_start_matches(pattern));
}
#[rstest]
fn agrees_with_str_trim_end_matches(
input in ".{0,64}",
pattern in prop::char::range('a', 'e'),
) {
let mut string = input.clone();
string.trim_end_matches_in_place(pattern);
prop_assert_eq!(string, input.trim_end_matches(pattern));
}
#[rstest]
fn agrees_with_str_trim_start(input in ".{0,64}") {
let mut string = input.clone();
string.trim_start_in_place();
prop_assert_eq!(string, input.trim_start());
}
#[rstest]
fn agrees_with_str_trim_end(input in ".{0,64}") {
let mut string = input.clone();
string.trim_end_in_place();
prop_assert_eq!(string, input.trim_end());
}
#[rstest]
fn both_ends_is_the_two_one_sided_trims(
input in ".{0,64}",
pattern in prop::char::range('a', 'e'),
) {
let mut both = input.clone();
both.trim_matches_in_place(pattern);
let mut start_then_end = input.clone();
start_then_end.trim_start_matches_in_place(pattern);
start_then_end.trim_end_matches_in_place(pattern);
let mut end_then_start = input;
end_then_start.trim_end_matches_in_place(pattern);
end_then_start.trim_start_matches_in_place(pattern);
prop_assert_eq!(&both, &start_then_end);
prop_assert_eq!(&both, &end_then_start);
}
}
}