pub fn to_camel_case(s: &str) -> String {
let words = split_words(s);
words
.iter()
.enumerate()
.map(|(i, w)| {
if i == 0 {
w.to_lowercase()
} else {
capitalize(w)
}
})
.collect()
}
pub fn to_pascal_case(s: &str) -> String {
split_words(s).iter().map(|w| capitalize(w)).collect()
}
pub fn to_snake_case(s: &str) -> String {
split_words(s)
.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join("_")
}
pub fn to_kebab_case(s: &str) -> String {
split_words(s)
.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join("-")
}
pub fn to_screaming_snake(s: &str) -> String {
split_words(s)
.iter()
.map(|w| w.to_uppercase())
.collect::<Vec<_>>()
.join("_")
}
pub fn to_dot_case(s: &str) -> String {
split_words(s)
.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join(".")
}
pub fn to_title_case(s: &str) -> String {
split_words(s)
.iter()
.map(|w| capitalize(w))
.collect::<Vec<_>>()
.join(" ")
}
pub fn to_headline(s: &str) -> String {
to_title_case(s)
}
pub fn to_sentence_case(s: &str) -> String {
let words = split_words(s);
if words.is_empty() {
return String::new();
}
let mut result = words[0].to_lowercase();
for word in &words[1..] {
result.push(' ');
result.push_str(&word.to_lowercase());
}
result
}
pub fn to_no_case(s: &str) -> String {
split_words(s)
.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join(" ")
}
pub fn to_upper(s: &str) -> String {
s.to_uppercase()
}
pub fn to_lower(s: &str) -> String {
s.to_lowercase()
}
pub fn ucfirst(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn lcfirst(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
pub fn invert_case(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_uppercase() {
c.to_lowercase().collect()
} else if c.is_lowercase() {
c.to_uppercase().collect()
} else {
c.to_string()
}
})
.collect()
}
pub fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
let cut = max.saturating_sub(3);
let mut end = cut;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &s[..end])
}
pub fn pluralize(word: &str) -> String {
if word.is_empty() {
return word.to_string();
}
let lower = word.to_lowercase();
if lower.ends_with("sis") {
return format!("{}es", &word[..word.len() - 3]);
}
if lower.ends_with("fe") {
return format!("{}ves", &word[..word.len() - 2]);
}
if lower.ends_with('f') && !lower.ends_with("ff") {
return format!("{}ves", &word[..word.len() - 1]);
}
if lower.ends_with("us") {
return format!("{}i", &word[..word.len() - 2]);
}
if lower.ends_with("ch") || lower.ends_with("sh") {
return format!("{}es", word);
}
if lower.ends_with('s') || lower.ends_with('x') || lower.ends_with('z') {
return format!("{}es", word);
}
if lower.ends_with('y') {
let prev = lower.chars().rev().nth(1);
if !matches!(prev, Some('a' | 'e' | 'i' | 'o' | 'u')) {
return format!("{}ies", &word[..word.len() - 1]);
}
}
format!("{}s", word)
}
pub fn slug(s: &str, separator: char) -> String {
let words = split_words(s);
words
.iter()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join(&separator.to_string())
}
pub fn squish(s: &str) -> String {
let mut result = String::new();
let mut last_was_space = false;
let mut started = false;
for c in s.chars() {
if c.is_whitespace() {
if !started {
continue;
}
if !last_was_space {
result.push(' ');
last_was_space = true;
}
} else {
result.push(c);
last_was_space = false;
started = true;
}
}
if result.ends_with(' ') {
result.pop();
}
result
}
pub fn mask(s: &str, mask_char: char, index: usize) -> String {
if index >= s.len() {
return s.chars().map(|_| mask_char).collect();
}
let mut result = s[..index].to_string();
result.extend(s.chars().skip(index).map(|_| mask_char));
result
}
pub fn wrap(s: &str, before: &str, after: &str) -> String {
format!("{}{}{}", before, s, after)
}
pub fn unwrap(s: &str, before: &str, after: &str) -> String {
if s.starts_with(before) && s.ends_with(after) {
let start = before.len();
let end = s.len() - after.len();
s[start..end].to_string()
} else {
s.to_string()
}
}
pub fn pad_left(s: &str, length: usize, pad: char) -> String {
if s.len() >= length {
return s.to_string();
}
let count = length - s.len();
let padding: String = pad.to_string().repeat(count);
format!("{}{}", padding, s)
}
pub fn pad_right(s: &str, length: usize, pad: char) -> String {
if s.len() >= length {
return s.to_string();
}
let count = length - s.len();
let padding: String = pad.to_string().repeat(count);
format!("{}{}", s, padding)
}
pub fn pad_both(s: &str, length: usize, pad: char) -> String {
if s.len() >= length {
return s.to_string();
}
let count = length - s.len();
let left = count / 2;
let right = count - left;
let left_pad: String = pad.to_string().repeat(left);
let right_pad: String = pad.to_string().repeat(right);
format!("{}{}{}", left_pad, s, right_pad)
}
pub fn repeat(s: &str, times: usize) -> String {
s.repeat(times)
}
pub fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
pub fn replace_first(s: &str, from: &str, to: &str) -> String {
if let Some(pos) = s.find(from) {
let mut result = s[..pos].to_string();
result.push_str(to);
result.push_str(&s[pos + from.len()..]);
result
} else {
s.to_string()
}
}
pub fn replace_last(s: &str, from: &str, to: &str) -> String {
if let Some(pos) = s.rfind(from) {
let mut result = s[..pos].to_string();
result.push_str(to);
result.push_str(&s[pos + from.len()..]);
result
} else {
s.to_string()
}
}
pub fn finish(s: &str, cap: &str) -> String {
if s.ends_with(cap) {
s.to_string()
} else {
format!("{}{}", s, cap)
}
}
pub fn ensure_start(s: &str, prefix: &str) -> String {
if s.starts_with(prefix) {
s.to_string()
} else {
format!("{}{}", prefix, s)
}
}
fn split_words(s: &str) -> Vec<String> {
let mut words: Vec<String> = Vec::new();
let mut current = String::new();
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '_' || c == '-' || c == ' ' {
if !current.is_empty() {
words.push(current.clone());
current.clear();
}
} else if c.is_uppercase() {
let prev_lower = i > 0 && chars[i - 1].is_lowercase();
let next_lower = chars
.get(i + 1)
.map(|ch| ch.is_lowercase())
.unwrap_or(false);
let acronym_end = i > 0 && chars[i - 1].is_uppercase() && next_lower;
if !current.is_empty() && (prev_lower || acronym_end) {
words.push(current.clone());
current.clear();
}
current.push(c);
} else {
current.push(c);
}
}
if !current.is_empty() {
words.push(current);
}
words
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
}
pub fn is_empty(s: &str) -> bool {
s.trim().is_empty()
}
pub fn is_ascii(s: &str) -> bool {
s.is_ascii()
}
#[cfg(feature = "json")]
pub fn is_json(s: &str) -> bool {
serde_json::from_str::<serde_json::Value>(s).is_ok()
}
pub fn is_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://") || s.starts_with("ftp://")
}
#[cfg(feature = "ids")]
pub fn is_uuid(s: &str) -> bool {
uuid::Uuid::parse_str(s).is_ok()
}
pub fn is_ulid(s: &str) -> bool {
s.len() == 26 && s.chars().all(|c| c.is_ascii_alphanumeric())
}
pub fn is_alphanumeric(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_alphanumeric())
}
pub fn length(s: &str) -> usize {
s.chars().count()
}
pub fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
pub fn char_at(s: &str, index: usize) -> Option<char> {
s.chars().nth(index)
}
pub fn position(s: &str, needle: &str) -> Option<usize> {
s.find(needle)
}
pub fn substr_count(s: &str, needle: &str) -> usize {
s.matches(needle).count()
}
pub fn starts_with(s: &str, needle: &str) -> bool {
s.starts_with(needle)
}
pub fn ends_with(s: &str, needle: &str) -> bool {
s.ends_with(needle)
}
pub fn contains(s: &str, needle: &str) -> bool {
s.contains(needle)
}
pub fn contains_all(s: &str, needles: &[&str]) -> bool {
needles.iter().all(|n| s.contains(n))
}
pub fn doesnt_contain(s: &str, needle: &str) -> bool {
!s.contains(needle)
}
pub fn pretty_duration(nanos: u64) -> String {
if nanos < 1_000 {
return format!("{}ns", nanos);
}
let micros = nanos / 1_000;
if micros < 1_000 {
return format!("{}μs", micros);
}
let millis = micros / 1_000;
if millis < 1_000 {
return format!("{}ms", millis);
}
let seconds = millis / 1_000;
if seconds < 60 {
return format!("{}s", seconds);
}
let minutes = seconds / 60;
if minutes < 60 {
return format!("{}m", minutes);
}
let hours = minutes / 60;
format!("{}h", hours)
}
#[cfg(feature = "random")]
pub fn random(length: usize) -> String {
use rand::RngCore;
let mut bytes = vec![0u8; length];
rand::thread_rng().fill_bytes(&mut bytes);
use base64::Engine;
base64::engine::general_purpose::URL_SAFE.encode(&bytes)[..length].to_string()
}
#[cfg(feature = "random")]
pub fn password(length: usize, symbols: bool) -> String {
use rand::Rng;
const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &[u8] = b"0123456789";
const SYMBOLS: &[u8] = b"!@#$%^&*()_+-=[]{}|;:,.<>?";
let mut rng = rand::thread_rng();
let mut chars = Vec::new();
chars.extend_from_slice(LETTERS);
chars.extend_from_slice(DIGITS);
if symbols {
chars.extend_from_slice(SYMBOLS);
}
(0..length)
.map(|_| chars[rng.gen_range(0..chars.len())] as char)
.collect()
}
pub fn to_base64(s: &str) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
}
#[cfg(feature = "json")]
pub fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_to_camel() {
assert_eq!(to_camel_case("hello_world"), "helloWorld");
assert_eq!(to_camel_case("foo_bar_baz"), "fooBarBaz");
}
#[test]
fn snake_to_pascal() {
assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
}
#[test]
fn camel_to_snake() {
assert_eq!(to_snake_case("helloWorld"), "hello_world");
assert_eq!(to_snake_case("FooBarBaz"), "foo_bar_baz");
}
#[test]
fn camel_to_kebab() {
assert_eq!(to_kebab_case("helloWorld"), "hello-world");
}
#[test]
fn screaming() {
assert_eq!(to_screaming_snake("hello_world"), "HELLO_WORLD");
}
#[test]
fn passthrough_unchanged() {
assert_eq!(to_snake_case("already_snake"), "already_snake");
assert_eq!(to_pascal_case("AlreadyPascal"), "AlreadyPascal");
}
#[test]
fn truncate_short_string_unchanged() {
assert_eq!(truncate("hi", 10), "hi");
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn truncate_long_string() {
assert_eq!(truncate("hello world", 5), "he...");
assert_eq!(truncate("abcdefgh", 6), "abc...");
}
#[test]
fn pluralize_regular() {
assert_eq!(pluralize("user"), "users");
assert_eq!(pluralize("item"), "items");
}
#[test]
fn pluralize_sibilant() {
assert_eq!(pluralize("box"), "boxes");
assert_eq!(pluralize("church"), "churches");
assert_eq!(pluralize("dish"), "dishes");
assert_eq!(pluralize("buzz"), "buzzes");
}
#[test]
fn pluralize_y_ending() {
assert_eq!(pluralize("category"), "categories");
assert_eq!(pluralize("city"), "cities");
assert_eq!(pluralize("day"), "days"); }
#[test]
fn pluralize_f_ending() {
assert_eq!(pluralize("leaf"), "leaves");
assert_eq!(pluralize("knife"), "knives");
}
#[test]
fn pluralize_us_ending() {
assert_eq!(pluralize("cactus"), "cacti");
assert_eq!(pluralize("focus"), "foci");
}
}