//! A set of generic utilities used across the codebase. //! //! It currently includes [`ToTitleCase`], [`get_random_u64`], and [`Bye`]. use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; /// Converts a string to title case. /// /// It removes all duplicate whitespaces and capitalizes the first letter of every word. pub trait ToTitleCase { fn to_title_case(&self) -> String; } impl ToTitleCase for T where T: ToString, { /// Removes duplicated whitespaces and capitalizes the first letter of each word. /// /// Returns: /// [`String`] - The title-cased version of the string. fn to_title_case(&self) -> String { self.to_string() .split_whitespace() .map(|word| { let mut chars = word.chars(); match chars.next() { Some(first) => first.to_uppercase().collect::() + chars.as_str(), None => String::new(), } }) .collect::>() .join(" ") } } /// Returns a random `u64` generated using a cryptographically secure random number generator. /// /// Returns: /// [`u64`] - A randomly generated 64-bit unsigned integer. pub fn get_random_u64() -> u64 { let mut rng = ChaCha20Rng::from_os_rng(); rng.random() } /// Consumes any value. Used to normalize return values when the output of a function doesn't /// matter yet all functions being called return different types. pub trait Bye: Sized { /// Whatever the value is, it consumes it and returns a unit value. Used to remove warnings /// about unused results. fn bye(self) {} } impl Bye for T {} /// Trims a string to a maximum length, adding an ellipsis if it exceeds the limit. /// /// Arguments: /// * `string` - The string to trim with ellipse. /// * `max_length` - The max length the string may have. /// /// Returns: /// [`String`] - The trimmed string with an ellipsis if it exceeded the limit. pub fn ellipse(string: String, max_length: usize) -> String { if string.len() <= max_length { string } else { let len = max_length.saturating_sub(3); format!("{}...", &string[..len]) } } /// Copies the bytes from [`i64`] to [`u64`]. /// /// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact, /// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes. /// /// Arguments: /// * `i` - The [`i64`] to convert. /// /// Returns: /// [`u64`] - The resulting [`u64`]. pub fn i64_to_u64(i: i64) -> u64 { u64::from_ne_bytes(i.to_ne_bytes()) } /// Copies the bytes from [`u64`] to [`i64`]. /// /// WARNING: This does NOT keep the same "represented" value. This only keeps the bytes intact, /// e.g. 0b0101 will still be 0b0101, but the value represented will change since the type changes. /// /// Arguments: /// * `u` - The [`u64`] to convert. /// /// Returns: /// [`i64`] - The resulting [`i64`]. pub fn u64_to_i64(u: u64) -> i64 { i64::from_ne_bytes(u.to_ne_bytes()) } /// Splits at the `n`th occurrence of `pattern` counting from the end of the string. /// /// Arguments: /// * `s` - The string to split. /// * `pattern` - The pattern to split at. /// * `n` - The occurrence to split at (counting from the end). /// /// Returns: /// [`Option<(&str, &str)>`] - The splitted parts of the string, or [`None`] if no occurrences of /// `pattern` were found in `s`. pub fn split_at_nth_from_end<'a, 'b>( s: &'a str, pattern: &'b str, n: usize, ) -> Option<(&'a str, &'a str)> { let mut end = s.len(); let mut count = 0; while let Some(pos) = s[..end].rfind(pattern) { count += 1; if count == n { let before = &s[..pos]; let after = &s[pos + pattern.len()..]; return Some((before, after)); } end = pos; // move left and keep searching } None }