rstring 0.1.0

A comprehensive set of string manipulation utilities inspired by Apache Commons Lang3 StringUtils
Documentation
//! Case transformation utilities.
//!
//! This module provides functions for transforming the case of strings,
//! such as capitalizing, uncapitalizing, and swapping case.
//!
//! # Usage
//!
//! Import the [`StringCase`] trait to use methods directly on strings:
//!
//! ```
//! use rstring::StringCase;
//!
//! assert_eq!("cat".capitalize(), "Cat");
//! assert_eq!("Cat".uncapitalize(), "cat");
//! assert_eq!("aBc".swap_case(), "AbC");
//! ```

/// Extension trait for case transformation methods.
///
/// This trait is implemented for `str`, allowing you to call case transformation
/// methods directly on `&str`, `String`, and other string types.
///
/// # Examples
///
/// ```
/// use rstring::StringCase;
///
/// // Works with &str
/// assert_eq!("hello".capitalize(), "Hello");
///
/// // Works with String
/// assert_eq!(String::from("HELLO").uncapitalize(), "hELLO");
///
/// // Works with &String
/// let s = String::from("aBc");
/// assert_eq!(s.swap_case(), "AbC");
/// ```
pub trait StringCase {
    /// Capitalizes the first character of the string, converting it to title case.
    ///
    /// The rest of the string is unchanged. If the first character is not a letter,
    /// the string is returned unchanged.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringCase;
    ///
    /// assert_eq!("".capitalize(), "");
    /// assert_eq!("cat".capitalize(), "Cat");
    /// assert_eq!("cAt".capitalize(), "CAt");
    /// assert_eq!("'cat'".capitalize(), "'cat'");
    /// ```
    #[must_use]
    fn capitalize(&self) -> String;

    /// Uncapitalizes the first character of the string, converting it to lowercase.
    ///
    /// The rest of the string is unchanged. If the first character is not a letter,
    /// the string is returned unchanged.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringCase;
    ///
    /// assert_eq!("".uncapitalize(), "");
    /// assert_eq!("Cat".uncapitalize(), "cat");
    /// assert_eq!("CAT".uncapitalize(), "cAT");
    /// ```
    #[must_use]
    fn uncapitalize(&self) -> String;

    /// Swaps the case of each character in the string.
    ///
    /// Uppercase characters become lowercase, and lowercase characters become uppercase.
    /// Non-letter characters are unchanged.
    ///
    /// # Examples
    ///
    /// ```
    /// use rstring::StringCase;
    ///
    /// assert_eq!("".swap_case(), "");
    /// assert_eq!("abc".swap_case(), "ABC");
    /// assert_eq!("ABC".swap_case(), "abc");
    /// assert_eq!("aBc".swap_case(), "AbC");
    /// assert_eq!("The dog has a BONE".swap_case(), "tHE DOG HAS A bone");
    /// ```
    #[must_use]
    fn swap_case(&self) -> String;
}

impl StringCase for str {
    fn capitalize(&self) -> String {
        if self.is_empty() {
            return String::new();
        }
        let mut chars = self.chars();
        let first = chars.next().unwrap();
        let mut result = String::with_capacity(self.len());
        for c in first.to_uppercase() {
            result.push(c);
        }
        result.push_str(chars.as_str());
        result
    }

    fn uncapitalize(&self) -> String {
        if self.is_empty() {
            return String::new();
        }
        let mut chars = self.chars();
        let first = chars.next().unwrap();
        let mut result = String::with_capacity(self.len());
        for c in first.to_lowercase() {
            result.push(c);
        }
        result.push_str(chars.as_str());
        result
    }

    fn swap_case(&self) -> String {
        let mut result = String::with_capacity(self.len());
        for c in self.chars() {
            if c.is_uppercase() {
                for lower in c.to_lowercase() {
                    result.push(lower);
                }
            } else if c.is_lowercase() {
                for upper in c.to_uppercase() {
                    result.push(upper);
                }
            } else {
                result.push(c);
            }
        }
        result
    }
}

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

    mod capitalize {
        use super::*;

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

        #[test]
        fn single_lowercase() {
            assert_eq!("c".capitalize(), "C");
        }

        #[test]
        fn single_uppercase() {
            assert_eq!("C".capitalize(), "C");
        }

        #[test]
        fn lowercase_word() {
            assert_eq!("cat".capitalize(), "Cat");
        }

        #[test]
        fn mixed_case() {
            assert_eq!("cAt".capitalize(), "CAt");
        }

        #[test]
        fn already_capitalized() {
            assert_eq!("Cat".capitalize(), "Cat");
        }

        #[test]
        fn all_uppercase() {
            assert_eq!("CAT".capitalize(), "CAT");
        }

        #[test]
        fn non_letter_first_char() {
            assert_eq!("'cat'".capitalize(), "'cat'");
        }

        #[test]
        fn digit_first_char() {
            assert_eq!("123abc".capitalize(), "123abc");
        }

        #[test]
        fn space_first_char() {
            assert_eq!(" cat".capitalize(), " cat");
        }

        #[test]
        fn unicode_lowercase() {
            assert_eq!("ßeta".capitalize(), "SSeta");
        }

        #[test]
        fn unicode_title_case() {
            // Latin small letter lj (lj) -> Latin capital letter LJ (LJ)
            // \u01C9 is lj (lowercase), \u01C7 is LJ (uppercase)
            let result = "\u{01C9}".capitalize();
            assert_eq!(result, "\u{01C7}"); // LJ (uppercase)
        }
    }

    mod uncapitalize {
        use super::*;

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

        #[test]
        fn single_uppercase() {
            assert_eq!("C".uncapitalize(), "c");
        }

        #[test]
        fn single_lowercase() {
            assert_eq!("c".uncapitalize(), "c");
        }

        #[test]
        fn capitalized_word() {
            assert_eq!("Cat".uncapitalize(), "cat");
        }

        #[test]
        fn all_uppercase() {
            assert_eq!("CAT".uncapitalize(), "cAT");
        }

        #[test]
        fn already_lowercase() {
            assert_eq!("cat".uncapitalize(), "cat");
        }

        #[test]
        fn non_letter_first_char() {
            assert_eq!("'Cat'".uncapitalize(), "'Cat'");
        }

        #[test]
        fn digit_first_char() {
            assert_eq!("123Abc".uncapitalize(), "123Abc");
        }

        #[test]
        fn space_first_char() {
            assert_eq!(" Cat".uncapitalize(), " Cat");
        }

        #[test]
        fn unicode_uppercase() {
            // Turkish capital I with dot (İ) -> i with dot below in some locales
            // Using standard Unicode case folding
            assert_eq!("İstanbul".uncapitalize(), "i\u{0307}stanbul");
        }
    }

    mod swap_case {
        use super::*;

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

        #[test]
        fn all_lowercase() {
            assert_eq!("abc".swap_case(), "ABC");
        }

        #[test]
        fn all_uppercase() {
            assert_eq!("ABC".swap_case(), "abc");
        }

        #[test]
        fn mixed_case() {
            assert_eq!("aBc".swap_case(), "AbC");
        }

        #[test]
        fn sentence() {
            assert_eq!("The dog has a BONE".swap_case(), "tHE DOG HAS A bone");
        }

        #[test]
        fn with_digits() {
            assert_eq!("a1B2c3".swap_case(), "A1b2C3");
        }

        #[test]
        fn with_special_chars() {
            assert_eq!("a-B_c".swap_case(), "A-b_C");
        }

        #[test]
        fn unicode_title_case_unchanged() {
            // Latin capital letter L with small letter j (Lj) - title case
            // \u01C8 is Lj (title case)
            let result = "\u{01C8}".swap_case();
            assert_eq!(result, "\u{01C8}"); // unchanged
        }

        #[test]
        fn german_sharp_s() {
            // German sharp s (ß) is lowercase, so it becomes SSETA when swapped
            assert_eq!("ßeta".swap_case(), "SSETA");
        }

        #[test]
        fn spaces_unchanged() {
            assert_eq!("a b c".swap_case(), "A B C");
        }

        #[test]
        fn single_lowercase() {
            assert_eq!("a".swap_case(), "A");
        }

        #[test]
        fn single_uppercase() {
            assert_eq!("A".swap_case(), "a");
        }
    }

    mod string_types {
        use super::*;

        #[test]
        fn string_type() {
            assert_eq!(String::from("hello").capitalize(), "Hello");
            assert_eq!(String::from("Hello").uncapitalize(), "hello");
            assert_eq!(String::from("HeLLo").swap_case(), "hEllO");
        }

        #[test]
        fn string_ref() {
            let s = String::from("hello");
            assert_eq!(s.capitalize(), "Hello");
        }

        #[test]
        fn boxed_str() {
            let s: Box<str> = "hello".into();
            assert_eq!(s.capitalize(), "Hello");
        }
    }
}