use smartstring::alias::String as SmartString;
#[cfg(not(feature = "std"))]
use alloc::borrow::Cow;
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(feature = "std")]
use std::borrow::Cow;
#[must_use]
pub fn add_prefix_optimized(key: &str, prefix: &str) -> SmartString {
let total = prefix.len() + key.len();
if total <= 23 {
let mut result = SmartString::new();
result.push_str(prefix);
result.push_str(key);
result
} else {
let mut s = String::with_capacity(total);
s.push_str(prefix);
s.push_str(key);
SmartString::from(s)
}
}
#[must_use]
pub fn add_suffix_optimized(key: &str, suffix: &str) -> SmartString {
let total = key.len() + suffix.len();
if total <= 23 {
let mut result = SmartString::new();
result.push_str(key);
result.push_str(suffix);
result
} else {
let mut s = String::with_capacity(total);
s.push_str(key);
s.push_str(suffix);
SmartString::from(s)
}
}
#[inline]
#[must_use]
pub fn new_split_cache(s: &str, delimiter: char) -> core::str::Split<'_, char> {
s.split(delimiter)
}
#[must_use]
pub fn join_optimized(parts: &[&str], delimiter: &str) -> String {
if parts.is_empty() {
return String::new();
}
if parts.len() == 1 {
return parts[0].to_string();
}
let total_content_len: usize = parts.iter().map(|s| s.len()).sum();
let delimiter_len = delimiter.len() * (parts.len().saturating_sub(1));
let total_capacity = total_content_len + delimiter_len;
let mut result = String::with_capacity(total_capacity);
for (i, part) in parts.iter().enumerate() {
if i > 0 {
result.push_str(delimiter);
}
result.push_str(part);
}
result
}
#[cfg(test)]
#[must_use]
pub fn count_char(s: &str, target: char) -> usize {
if target.is_ascii() {
let byte = target as u8;
#[expect(
clippy::naive_bytecount,
reason = "intentional simple byte scan — fast enough for ASCII char counting"
)]
s.as_bytes().iter().filter(|&&b| b == byte).count()
} else {
s.chars().filter(|&c| c == target).count()
}
}
#[cfg(test)]
#[must_use]
pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
let mut count = 0;
for (pos, c) in s.char_indices() {
if c == target {
if count == n {
return Some(pos);
}
count += 1;
}
}
None
}
#[must_use]
pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
let trimmed = s.trim();
let needs_trim = trimmed.len() != s.len();
let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
match (needs_trim, needs_lowercase) {
(false, false) => Cow::Borrowed(s),
(true, false) => Cow::Borrowed(trimmed),
(_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
}
}
pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
where
F: Fn(char) -> Option<char>,
{
let mut chars = s.char_indices();
while let Some((i, c)) = chars.next() {
if let Some(replacement) = replacer(c) {
let mut result = String::with_capacity(s.len());
result.push_str(&s[..i]);
result.push(replacement);
for (_, c) in chars {
if let Some(r) = replacer(c) {
result.push(r);
} else {
result.push(c);
}
}
return Cow::Owned(result);
}
}
Cow::Borrowed(s)
}
#[expect(
clippy::cast_possible_truncation,
reason = "index is always < 128 so truncation from usize to u8 is safe"
)]
pub mod char_validation {
const ASCII_ALPHANUMERIC: [bool; 128] = {
let mut table = [false; 128];
let mut i = 0;
while i < 128 {
table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
i += 1;
}
table
};
const KEY_CHARS: [bool; 128] = {
let mut table = [false; 128];
let mut i = 0;
while i < 128 {
table[i] =
matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
i += 1;
}
table
};
#[inline]
#[must_use]
pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
if c.is_ascii() {
ASCII_ALPHANUMERIC[c as u8 as usize]
} else {
false
}
}
#[inline]
#[must_use]
pub fn is_key_char_fast(c: char) -> bool {
if c.is_ascii() {
KEY_CHARS[c as u8 as usize]
} else {
false
}
}
#[inline]
#[must_use]
pub fn is_separator(c: char) -> bool {
matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
}
#[inline]
#[must_use]
pub fn is_whitespace_fast(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
}
}
#[must_use]
pub const fn hash_algorithm() -> &'static str {
#[cfg(feature = "fast")]
{
#[cfg(any(
all(target_arch = "x86_64", target_feature = "aes"),
all(target_arch = "aarch64", target_feature = "aes")
))]
return "GxHash";
#[cfg(not(any(
all(target_arch = "x86_64", target_feature = "aes"),
all(target_arch = "aarch64", target_feature = "aes")
)))]
return "AHash (GxHash fallback)";
}
#[cfg(all(feature = "secure", not(feature = "fast")))]
return "AHash";
#[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
return "Blake3";
#[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
{
#[cfg(feature = "std")]
return "DefaultHasher";
#[cfg(not(feature = "std"))]
return "FNV-1a";
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ValidationResult;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[test]
fn test_add_prefix_suffix() {
let result = add_prefix_optimized("test", "prefix_");
assert_eq!(result, "prefix_test");
let result = add_suffix_optimized("test", "_suffix");
assert_eq!(result, "test_suffix");
}
#[test]
fn test_join_optimized() {
let parts = vec!["a", "b", "c"];
let result = join_optimized(&parts, "_");
assert_eq!(result, "a_b_c");
let empty: Vec<&str> = vec![];
let result = join_optimized(&empty, "_");
assert_eq!(result, "");
let single = vec!["alone"];
let result = join_optimized(&single, "_");
assert_eq!(result, "alone");
}
#[test]
fn test_char_validation() {
use char_validation::*;
assert!(is_ascii_alphanumeric_fast('a'));
assert!(is_ascii_alphanumeric_fast('Z'));
assert!(is_ascii_alphanumeric_fast('5'));
assert!(!is_ascii_alphanumeric_fast('_'));
assert!(!is_ascii_alphanumeric_fast('ñ'));
assert!(is_key_char_fast('a'));
assert!(is_key_char_fast('_'));
assert!(is_key_char_fast('-'));
assert!(is_key_char_fast('.'));
assert!(!is_key_char_fast(' '));
assert!(is_separator('_'));
assert!(is_separator('/'));
assert!(!is_separator('a'));
assert!(is_whitespace_fast(' '));
assert!(is_whitespace_fast('\t'));
assert!(!is_whitespace_fast('a'));
}
#[test]
fn test_string_utilities() {
assert_eq!(count_char("hello_world_test", '_'), 2);
assert_eq!(count_char("no_underscores", '_'), 1);
assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
}
#[test]
fn test_normalize_string() {
let result = normalize_string(" Hello ", true);
assert_eq!(result, "hello");
let result = normalize_string("hello", true);
assert_eq!(result, "hello");
let result = normalize_string(" hello ", false);
assert_eq!(result, "hello");
let result = normalize_string("hello", false);
assert!(matches!(result, Cow::Borrowed("hello")));
}
#[test]
fn test_float_comparison() {
const EPSILON: f64 = 1e-10;
let result = ValidationResult {
total_processed: 2,
valid: vec!["key1".to_string(), "key2".to_string()],
errors: vec![],
};
assert!((result.success_rate() - 100.0).abs() < EPSILON);
}
#[test]
fn test_replace_chars() {
let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
assert_eq!(result, "hello_world");
let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
assert!(matches!(result, Cow::Borrowed("hello_world")));
}
#[test]
fn test_replace_chars_fixed() {
let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
assert_eq!(result, "hello_world");
let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
assert!(matches!(result, Cow::Borrowed("hello_world")));
let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
assert_eq!(result, "a_b_c");
let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
assert!(matches!(result, Cow::Borrowed(_)));
let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
assert_eq!(result, "");
}
}