rstring 0.1.0

A comprehensive set of string manipulation utilities inspired by Apache Commons Lang3 StringUtils
Documentation
//! String reversal and rotation utilities.
//!
//! This module provides functions for reversing and rotating strings.
//!
//! # Usage
//!
//! Import the [`StringReverse`] trait to use methods directly on strings:
//!
//! ```
//! use rstring::StringReverse;
//!
//! assert_eq!("bat".reverse_str(), "tab");
//! assert_eq!("a.b.c".reverse_delimited('.'), "c.b.a");
//! assert_eq!("abcdefg".rotate(2), "fgabcde");
//! ```

/// Extension trait for string reversal and rotation methods.
///
/// This trait is implemented for `str`, allowing you to call reversal
/// methods directly on `&str`, `String`, and other string types.
///
/// # Examples
///
/// ```
/// use rstring::StringReverse;
///
/// assert_eq!("backwards".reverse_str(), "sdrawkcab");
/// assert_eq!("a.b.c".reverse_delimited('.'), "c.b.a");
/// assert_eq!("abcdefg".rotate(2), "fgabcde");
/// assert_eq!("abcdefg".rotate(-2), "cdefgab");
/// ```
pub trait StringReverse {
    /// Reverses a string.
    ///
    /// This method reverses the string character by character (Unicode-aware).
    ///
    /// Note: Named `reverse_str` to avoid conflict with `Iterator::rev` and
    /// `slice::reverse`.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringReverse;
    ///
    /// assert_eq!("".reverse_str(), "");
    /// assert_eq!("bat".reverse_str(), "tab");
    /// assert_eq!("sdrawkcab".reverse_str(), "backwards");
    /// ```
    #[must_use]
    fn reverse_str(&self) -> String;

    /// Reverses a string that is delimited by a specific character.
    ///
    /// The segments between the delimiters are not reversed, only their order is.
    /// For example, `"a.b.c"` becomes `"c.b.a"` with delimiter `'.'`.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringReverse;
    ///
    /// assert_eq!("".reverse_delimited('.'), "");
    /// assert_eq!("a.b.c".reverse_delimited('.'), "c.b.a");
    /// assert_eq!("a b c".reverse_delimited('.'), "a b c");
    /// assert_eq!("www.domain.com".reverse_delimited('.'), "com.domain.www");
    /// ```
    #[must_use]
    fn reverse_delimited(&self, separator: char) -> String;

    /// Rotates (circular shift) a string by `shift` characters.
    ///
    /// - If `shift > 0`, performs a right circular shift (e.g., `"ABCDEF"` with shift 2 becomes `"EFABCD"`).
    /// - If `shift < 0`, performs a left circular shift (e.g., `"ABCDEF"` with shift -2 becomes `"CDEFAB"`).
    /// - If `shift == 0` or is a multiple of the string length, returns the original string.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringReverse;
    ///
    /// assert_eq!("abcdefg".rotate(0), "abcdefg");
    /// assert_eq!("abcdefg".rotate(2), "fgabcde");
    /// assert_eq!("abcdefg".rotate(-2), "cdefgab");
    /// assert_eq!("abcdefg".rotate(7), "abcdefg");
    /// assert_eq!("abcdefg".rotate(-7), "abcdefg");
    /// assert_eq!("abcdefg".rotate(9), "fgabcde");
    /// assert_eq!("abcdefg".rotate(-9), "cdefgab");
    /// ```
    #[must_use]
    fn rotate(&self, shift: i32) -> String;
}

impl StringReverse for str {
    fn reverse_str(&self) -> String {
        self.chars().rev().collect()
    }

    fn reverse_delimited(&self, separator: char) -> String {
        if self.is_empty() {
            return String::new();
        }
        self.split(separator)
            .rev()
            .collect::<Vec<_>>()
            .join(&separator.to_string())
    }

    fn rotate(&self, shift: i32) -> String {
        let char_count = self.chars().count();
        if char_count == 0 || shift == 0 {
            return self.to_string();
        }
        let effective = shift.rem_euclid(char_count as i32) as usize;
        if effective == 0 {
            return self.to_string();
        }
        let split_point = char_count - effective;
        let byte_index = self
            .char_indices()
            .nth(split_point)
            .map(|(i, _)| i)
            .unwrap_or(self.len());
        let mut result = String::with_capacity(self.len());
        result.push_str(&self[byte_index..]);
        result.push_str(&self[..byte_index]);
        result
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod reverse_str {
        use super::*;

        #[test]
        fn empty_string() {
            assert_eq!("".reverse_str(), "");
        }

        #[test]
        fn single_char() {
            assert_eq!("a".reverse_str(), "a");
        }

        #[test]
        fn simple_word() {
            assert_eq!("bat".reverse_str(), "tab");
        }

        #[test]
        fn longer_word() {
            assert_eq!("sdrawkcab".reverse_str(), "backwards");
        }

        #[test]
        fn palindrome() {
            assert_eq!("racecar".reverse_str(), "racecar");
        }

        #[test]
        fn unicode() {
            assert_eq!("café".reverse_str(), "éfac");
        }

        #[test]
        fn with_spaces() {
            assert_eq!("hello world".reverse_str(), "dlrow olleh");
        }
    }

    mod reverse_delimited {
        use super::*;

        #[test]
        fn empty_string() {
            assert_eq!("".reverse_delimited('.'), "");
        }

        #[test]
        fn no_delimiter_found() {
            assert_eq!("a b c".reverse_delimited('.'), "a b c");
        }

        #[test]
        fn dot_delimiter() {
            assert_eq!("a.b.c".reverse_delimited('.'), "c.b.a");
        }

        #[test]
        fn web_address() {
            assert_eq!("www.domain.com".reverse_delimited('.'), "com.domain.www");
        }

        #[test]
        fn single_segment() {
            assert_eq!("abc".reverse_delimited('.'), "abc");
        }

        #[test]
        fn slash_delimiter() {
            assert_eq!("a/b/c".reverse_delimited('/'), "c/b/a");
        }

        #[test]
        fn trailing_delimiter() {
            assert_eq!("a.b.".reverse_delimited('.'), ".b.a");
        }

        #[test]
        fn leading_delimiter() {
            assert_eq!(".a.b".reverse_delimited('.'), "b.a.");
        }
    }

    mod rotate {
        use super::*;

        #[test]
        fn empty_string() {
            assert_eq!("".rotate(1), "");
        }

        #[test]
        fn zero_shift() {
            assert_eq!("abcdefg".rotate(0), "abcdefg");
        }

        #[test]
        fn positive_shift() {
            assert_eq!("abcdefg".rotate(2), "fgabcde");
        }

        #[test]
        fn negative_shift() {
            assert_eq!("abcdefg".rotate(-2), "cdefgab");
        }

        #[test]
        fn full_rotation() {
            assert_eq!("abcdefg".rotate(7), "abcdefg");
        }

        #[test]
        fn negative_full_rotation() {
            assert_eq!("abcdefg".rotate(-7), "abcdefg");
        }

        #[test]
        fn shift_greater_than_length() {
            assert_eq!("abcdefg".rotate(9), "fgabcde");
        }

        #[test]
        fn negative_shift_greater_than_length() {
            assert_eq!("abcdefg".rotate(-9), "cdefgab");
        }

        #[test]
        fn large_positive_shift() {
            assert_eq!("abcdefg".rotate(17), "efgabcd");
        }

        #[test]
        fn large_negative_shift() {
            assert_eq!("abcdefg".rotate(-17), "defgabc");
        }

        #[test]
        fn single_char() {
            assert_eq!("a".rotate(5), "a");
        }

        #[test]
        fn unicode() {
            assert_eq!("café".rotate(1), "écaf");
        }
    }

    mod string_types {
        use super::*;

        #[test]
        fn string_type_reverse() {
            assert_eq!(String::from("bat").reverse_str(), "tab");
        }

        #[test]
        fn string_type_rotate() {
            assert_eq!(String::from("abcdefg").rotate(2), "fgabcde");
        }

        #[test]
        fn boxed_str() {
            let s: Box<str> = "a.b.c".into();
            assert_eq!(s.reverse_delimited('.'), "c.b.a");
        }
    }
}