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#[must_use]
92pub fn new_split_cache(s: &str, delimiter: char) -> core::str::Split<'_, char> {
93 s.split(delimiter)
94}
95
96#[must_use]
110pub fn join_optimized(parts: &[&str], delimiter: &str) -> String {
111 if parts.is_empty() {
112 return String::new();
113 }
114
115 if parts.len() == 1 {
116 return parts[0].to_string();
117 }
118
119 let total_content_len: usize = parts.iter().map(|s| s.len()).sum();
121 let delimiter_len = delimiter.len() * (parts.len().saturating_sub(1));
122 let total_capacity = total_content_len + delimiter_len;
123
124 let mut result = String::with_capacity(total_capacity);
125
126 for (i, part) in parts.iter().enumerate() {
127 if i > 0 {
128 result.push_str(delimiter);
129 }
130 result.push_str(part);
131 }
132
133 result
134}
135
136#[inline]
148#[must_use]
149pub fn is_ascii_only(s: &str) -> bool {
150 s.is_ascii()
151}
152
153#[must_use]
168pub fn count_char(s: &str, target: char) -> usize {
169 if target.is_ascii() {
170 let byte = target as u8;
171 #[expect(
172 clippy::naive_bytecount,
173 reason = "not worth adding bytecount dep for one use"
174 )]
175 s.as_bytes().iter().filter(|&&b| b == byte).count()
176 } else {
177 s.chars().filter(|&c| c == target).count()
178 }
179}
180
181#[must_use]
196pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
197 let mut count = 0;
198 for (pos, c) in s.char_indices() {
199 if c == target {
200 if count == n {
201 return Some(pos);
202 }
203 count += 1;
204 }
205 }
206 None
207}
208
209#[must_use]
227pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
228 let trimmed = s.trim();
229 let needs_trim = trimmed.len() != s.len();
230 let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
231
232 match (needs_trim, needs_lowercase) {
233 (false, false) => Cow::Borrowed(s),
234 (true, false) => Cow::Owned(trimmed.to_string()),
235 (_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
236 }
237}
238
239pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
254where
255 F: Fn(char) -> Option<char>,
256{
257 let mut chars = s.char_indices();
259 while let Some((i, c)) = chars.next() {
260 if let Some(replacement) = replacer(c) {
261 let mut result = String::with_capacity(s.len());
263 result.push_str(&s[..i]);
264 result.push(replacement);
265 for (_, c) in chars {
266 if let Some(r) = replacer(c) {
267 result.push(r);
268 } else {
269 result.push(c);
270 }
271 }
272 return Cow::Owned(result);
273 }
274 }
275 Cow::Borrowed(s)
276}
277
278#[expect(
287 clippy::cast_possible_truncation,
288 reason = "indices are within 0..128 ASCII range"
289)]
290pub mod char_validation {
291 const ASCII_ALPHANUMERIC: [bool; 128] = {
293 let mut table = [false; 128];
294 let mut i = 0;
295 while i < 128 {
296 table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
297 i += 1;
298 }
299 table
300 };
301
302 const KEY_CHARS: [bool; 128] = {
303 let mut table = [false; 128];
304 let mut i = 0;
305 while i < 128 {
306 table[i] =
307 matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
308 i += 1;
309 }
310 table
311 };
312
313 #[inline]
315 #[must_use]
316 pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
317 if c.is_ascii() {
318 ASCII_ALPHANUMERIC[c as u8 as usize]
319 } else {
320 false
321 }
322 }
323
324 #[inline]
326 #[must_use]
327 pub fn is_key_char_fast(c: char) -> bool {
328 if c.is_ascii() {
329 KEY_CHARS[c as u8 as usize]
330 } else {
331 false
332 }
333 }
334
335 #[inline]
337 #[must_use]
338 pub fn is_separator(c: char) -> bool {
339 matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
340 }
341
342 #[inline]
344 #[must_use]
345 pub fn is_whitespace_fast(c: char) -> bool {
346 matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
347 }
348}
349
350#[must_use]
367pub fn string_memory_usage(s: &str) -> usize {
368 core::mem::size_of::<String>() + s.len()
370}
371
372#[must_use]
385pub fn smart_string_memory_usage(s: &str) -> usize {
386 if s.len() <= 23 {
388 core::mem::size_of::<SmartString>()
389 } else {
390 core::mem::size_of::<SmartString>() + s.len()
391 }
392}
393
394#[must_use]
413pub const fn hash_algorithm() -> &'static str {
414 #[cfg(feature = "fast")]
415 {
416 #[cfg(any(
417 all(target_arch = "x86_64", target_feature = "aes"),
418 all(target_arch = "aarch64", target_feature = "aes")
419 ))]
420 return "GxHash";
421
422 #[cfg(not(any(
423 all(target_arch = "x86_64", target_feature = "aes"),
424 all(target_arch = "aarch64", target_feature = "aes")
425 )))]
426 return "AHash (GxHash fallback)";
427 }
428
429 #[cfg(all(feature = "secure", not(feature = "fast")))]
430 return "AHash";
431
432 #[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
433 return "Blake3";
434
435 #[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
436 {
437 #[cfg(feature = "std")]
438 return "DefaultHasher";
439
440 #[cfg(not(feature = "std"))]
441 return "FNV-1a";
442 }
443}
444
445#[cfg(test)]
450mod tests {
451 use super::*;
452
453 use crate::ValidationResult;
454 #[cfg(not(feature = "std"))]
455 use alloc::vec;
456 #[cfg(not(feature = "std"))]
457 use alloc::vec::Vec;
458
459 #[test]
460 fn test_add_prefix_suffix() {
461 let result = add_prefix_optimized("test", "prefix_");
462 assert_eq!(result, "prefix_test");
463
464 let result = add_suffix_optimized("test", "_suffix");
465 assert_eq!(result, "test_suffix");
466 }
467
468 #[test]
469 fn test_join_optimized() {
470 let parts = vec!["a", "b", "c"];
471 let result = join_optimized(&parts, "_");
472 assert_eq!(result, "a_b_c");
473
474 let empty: Vec<&str> = vec![];
475 let result = join_optimized(&empty, "_");
476 assert_eq!(result, "");
477
478 let single = vec!["alone"];
479 let result = join_optimized(&single, "_");
480 assert_eq!(result, "alone");
481 }
482
483 #[test]
484 fn test_char_validation() {
485 use char_validation::*;
486
487 assert!(is_ascii_alphanumeric_fast('a'));
488 assert!(is_ascii_alphanumeric_fast('Z'));
489 assert!(is_ascii_alphanumeric_fast('5'));
490 assert!(!is_ascii_alphanumeric_fast('_'));
491 assert!(!is_ascii_alphanumeric_fast('ñ'));
492
493 assert!(is_key_char_fast('a'));
494 assert!(is_key_char_fast('_'));
495 assert!(is_key_char_fast('-'));
496 assert!(is_key_char_fast('.'));
497 assert!(!is_key_char_fast(' '));
498
499 assert!(is_separator('_'));
500 assert!(is_separator('/'));
501 assert!(!is_separator('a'));
502
503 assert!(is_whitespace_fast(' '));
504 assert!(is_whitespace_fast('\t'));
505 assert!(!is_whitespace_fast('a'));
506 }
507
508 #[test]
509 fn test_string_utilities() {
510 assert!(is_ascii_only("hello"));
511 assert!(!is_ascii_only("héllo"));
512
513 assert_eq!(count_char("hello_world_test", '_'), 2);
514 assert_eq!(count_char("no_underscores", '_'), 1);
515
516 assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
517 assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
518 assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
519 assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
520 }
521
522 #[test]
523 fn test_normalize_string() {
524 let result = normalize_string(" Hello ", true);
525 assert_eq!(result, "hello");
526
527 let result = normalize_string("hello", true);
528 assert_eq!(result, "hello");
529
530 let result = normalize_string(" hello ", false);
531 assert_eq!(result, "hello");
532
533 let result = normalize_string("hello", false);
534 assert!(matches!(result, Cow::Borrowed("hello")));
535 }
536
537 #[test]
538 fn test_memory_utilities() {
539 let s = "hello";
540 let usage = string_memory_usage(s);
541 assert!(usage >= s.len());
542 }
543
544 #[test]
545 fn test_float_comparison() {
546 const EPSILON: f64 = 1e-10;
547 let result = ValidationResult {
548 total_processed: 2,
549 valid: vec!["key1".to_string(), "key2".to_string()],
550 errors: vec![],
551 };
552
553 assert!((result.success_rate() - 100.0).abs() < EPSILON);
556 }
557
558 #[test]
559 fn test_replace_chars() {
560 let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
561 assert_eq!(result, "hello_world");
562
563 let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
564 assert!(matches!(result, Cow::Borrowed("hello_world")));
565 }
566
567 #[test]
568 fn test_replace_chars_fixed() {
569 let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
570 assert_eq!(result, "hello_world");
571
572 let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
573 assert!(matches!(result, Cow::Borrowed("hello_world")));
574
575 let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
577 assert_eq!(result, "a_b_c");
578
579 let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
581 assert!(matches!(result, Cow::Borrowed(_)));
582
583 let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
585 assert_eq!(result, "");
586 }
587}