1use smartstring::alias::String as SmartString;
7
8#[cfg(not(feature = "std"))]
9use alloc::borrow::Cow;
10#[cfg(not(feature = "std"))]
11use alloc::string::{String, ToString};
12#[cfg(feature = "std")]
13use std::borrow::Cow;
14
15#[must_use]
33pub fn add_prefix_optimized(key: &str, prefix: &str) -> SmartString {
34 let total = prefix.len() + key.len();
35 if total <= 23 {
36 let mut result = SmartString::new();
38 result.push_str(prefix);
39 result.push_str(key);
40 result
41 } else {
42 let mut s = String::with_capacity(total);
43 s.push_str(prefix);
44 s.push_str(key);
45 SmartString::from(s)
46 }
47}
48
49#[must_use]
63pub fn add_suffix_optimized(key: &str, suffix: &str) -> SmartString {
64 let total = key.len() + suffix.len();
65 if total <= 23 {
66 let mut result = SmartString::new();
67 result.push_str(key);
68 result.push_str(suffix);
69 result
70 } else {
71 let mut s = String::with_capacity(total);
72 s.push_str(key);
73 s.push_str(suffix);
74 SmartString::from(s)
75 }
76}
77
78#[inline]
92#[must_use]
93pub fn new_split_cache(s: &str, delimiter: char) -> core::str::Split<'_, char> {
94 s.split(delimiter)
95}
96
97#[must_use]
111pub fn join_optimized(parts: &[&str], delimiter: &str) -> String {
112 if parts.is_empty() {
113 return String::new();
114 }
115
116 if parts.len() == 1 {
117 return parts[0].to_string();
118 }
119
120 let total_content_len: usize = parts.iter().map(|s| s.len()).sum();
122 let delimiter_len = delimiter.len() * (parts.len().saturating_sub(1));
123 let total_capacity = total_content_len + delimiter_len;
124
125 let mut result = String::with_capacity(total_capacity);
126
127 for (i, part) in parts.iter().enumerate() {
128 if i > 0 {
129 result.push_str(delimiter);
130 }
131 result.push_str(part);
132 }
133
134 result
135}
136
137#[cfg(test)]
152#[must_use]
153pub fn count_char(s: &str, target: char) -> usize {
154 if target.is_ascii() {
155 let byte = target as u8;
156 #[expect(
157 clippy::naive_bytecount,
158 reason = "intentional simple byte scan — fast enough for ASCII char counting"
159 )]
160 s.as_bytes().iter().filter(|&&b| b == byte).count()
161 } else {
162 s.chars().filter(|&c| c == target).count()
163 }
164}
165
166#[cfg(test)]
181#[must_use]
182pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
183 let mut count = 0;
184 for (pos, c) in s.char_indices() {
185 if c == target {
186 if count == n {
187 return Some(pos);
188 }
189 count += 1;
190 }
191 }
192 None
193}
194
195#[must_use]
213pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
214 let trimmed = s.trim();
215 let needs_trim = trimmed.len() != s.len();
216 let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
217
218 match (needs_trim, needs_lowercase) {
219 (false, false) => Cow::Borrowed(s),
220 (true, false) => Cow::Borrowed(trimmed),
221 (_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
222 }
223}
224
225pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
240where
241 F: Fn(char) -> Option<char>,
242{
243 let mut chars = s.char_indices();
245 while let Some((i, c)) = chars.next() {
246 if let Some(replacement) = replacer(c) {
247 let mut result = String::with_capacity(s.len());
249 result.push_str(&s[..i]);
250 result.push(replacement);
251 for (_, c) in chars {
252 if let Some(r) = replacer(c) {
253 result.push(r);
254 } else {
255 result.push(c);
256 }
257 }
258 return Cow::Owned(result);
259 }
260 }
261 Cow::Borrowed(s)
262}
263
264#[expect(
273 clippy::cast_possible_truncation,
274 reason = "index is always < 128 so truncation from usize to u8 is safe"
275)]
276pub mod char_validation {
277 const ASCII_ALPHANUMERIC: [bool; 128] = {
279 let mut table = [false; 128];
280 let mut i = 0;
281 while i < 128 {
282 table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
283 i += 1;
284 }
285 table
286 };
287
288 const KEY_CHARS: [bool; 128] = {
289 let mut table = [false; 128];
290 let mut i = 0;
291 while i < 128 {
292 table[i] =
293 matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
294 i += 1;
295 }
296 table
297 };
298
299 #[inline]
301 #[must_use]
302 pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
303 if c.is_ascii() {
304 ASCII_ALPHANUMERIC[c as u8 as usize]
305 } else {
306 false
307 }
308 }
309
310 #[inline]
312 #[must_use]
313 pub fn is_key_char_fast(c: char) -> bool {
314 if c.is_ascii() {
315 KEY_CHARS[c as u8 as usize]
316 } else {
317 false
318 }
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn is_separator(c: char) -> bool {
325 matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
326 }
327
328 #[inline]
330 #[must_use]
331 pub fn is_whitespace_fast(c: char) -> bool {
332 matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
333 }
334}
335
336#[must_use]
355pub const fn hash_algorithm() -> &'static str {
356 #[cfg(feature = "fast")]
357 {
358 #[cfg(any(
359 all(target_arch = "x86_64", target_feature = "aes"),
360 all(target_arch = "aarch64", target_feature = "aes")
361 ))]
362 return "GxHash";
363
364 #[cfg(not(any(
365 all(target_arch = "x86_64", target_feature = "aes"),
366 all(target_arch = "aarch64", target_feature = "aes")
367 )))]
368 return "AHash (GxHash fallback)";
369 }
370
371 #[cfg(all(feature = "secure", not(feature = "fast")))]
372 return "AHash";
373
374 #[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
375 return "Blake3";
376
377 #[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
378 {
379 #[cfg(feature = "std")]
380 return "DefaultHasher";
381
382 #[cfg(not(feature = "std"))]
383 return "FNV-1a";
384 }
385}
386
387#[cfg(test)]
392mod tests {
393 use super::*;
394
395 use crate::ValidationResult;
396 #[cfg(not(feature = "std"))]
397 use alloc::vec;
398 #[cfg(not(feature = "std"))]
399 use alloc::vec::Vec;
400
401 #[test]
402 fn test_add_prefix_suffix() {
403 let result = add_prefix_optimized("test", "prefix_");
404 assert_eq!(result, "prefix_test");
405
406 let result = add_suffix_optimized("test", "_suffix");
407 assert_eq!(result, "test_suffix");
408 }
409
410 #[test]
411 fn test_join_optimized() {
412 let parts = vec!["a", "b", "c"];
413 let result = join_optimized(&parts, "_");
414 assert_eq!(result, "a_b_c");
415
416 let empty: Vec<&str> = vec![];
417 let result = join_optimized(&empty, "_");
418 assert_eq!(result, "");
419
420 let single = vec!["alone"];
421 let result = join_optimized(&single, "_");
422 assert_eq!(result, "alone");
423 }
424
425 #[test]
426 fn test_char_validation() {
427 use char_validation::*;
428
429 assert!(is_ascii_alphanumeric_fast('a'));
430 assert!(is_ascii_alphanumeric_fast('Z'));
431 assert!(is_ascii_alphanumeric_fast('5'));
432 assert!(!is_ascii_alphanumeric_fast('_'));
433 assert!(!is_ascii_alphanumeric_fast('ñ'));
434
435 assert!(is_key_char_fast('a'));
436 assert!(is_key_char_fast('_'));
437 assert!(is_key_char_fast('-'));
438 assert!(is_key_char_fast('.'));
439 assert!(!is_key_char_fast(' '));
440
441 assert!(is_separator('_'));
442 assert!(is_separator('/'));
443 assert!(!is_separator('a'));
444
445 assert!(is_whitespace_fast(' '));
446 assert!(is_whitespace_fast('\t'));
447 assert!(!is_whitespace_fast('a'));
448 }
449
450 #[test]
451 fn test_string_utilities() {
452 assert_eq!(count_char("hello_world_test", '_'), 2);
453 assert_eq!(count_char("no_underscores", '_'), 1);
454
455 assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
456 assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
457 assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
458 assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
459 }
460
461 #[test]
462 fn test_normalize_string() {
463 let result = normalize_string(" Hello ", true);
464 assert_eq!(result, "hello");
465
466 let result = normalize_string("hello", true);
467 assert_eq!(result, "hello");
468
469 let result = normalize_string(" hello ", false);
470 assert_eq!(result, "hello");
471
472 let result = normalize_string("hello", false);
473 assert!(matches!(result, Cow::Borrowed("hello")));
474 }
475
476 #[test]
477 fn test_float_comparison() {
478 const EPSILON: f64 = 1e-10;
479 let result = ValidationResult {
480 total_processed: 2,
481 valid: vec!["key1".to_string(), "key2".to_string()],
482 errors: vec![],
483 };
484
485 assert!((result.success_rate() - 100.0).abs() < EPSILON);
488 }
489
490 #[test]
491 fn test_replace_chars() {
492 let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
493 assert_eq!(result, "hello_world");
494
495 let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
496 assert!(matches!(result, Cow::Borrowed("hello_world")));
497 }
498
499 #[test]
500 fn test_replace_chars_fixed() {
501 let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
502 assert_eq!(result, "hello_world");
503
504 let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
505 assert!(matches!(result, Cow::Borrowed("hello_world")));
506
507 let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
509 assert_eq!(result, "a_b_c");
510
511 let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
513 assert!(matches!(result, Cow::Borrowed(_)));
514
515 let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
517 assert_eq!(result, "");
518 }
519}