pub mod text;
pub mod files;
pub mod async_utils;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn generate_id() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn generate_short_id() -> String {
uuid::Uuid::new_v4().to_string()[..8].to_string()
}
pub fn current_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis() as u64
}
pub fn current_timestamp_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
}
pub fn format_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
const THRESHOLD: u64 = 1024;
if bytes < THRESHOLD {
return format!("{} B", bytes);
}
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 {
size /= THRESHOLD as f64;
unit_index += 1;
}
format!("{:.1} {}", size, UNITS[unit_index])
}
pub fn format_duration(duration: Duration) -> String {
let total_secs = duration.as_secs();
if total_secs < 60 {
format!("{}s", total_secs)
} else if total_secs < 3600 {
let mins = total_secs / 60;
let secs = total_secs % 60;
if secs == 0 {
format!("{}m", mins)
} else {
format!("{}m {}s", mins, secs)
}
} else {
let hours = total_secs / 3600;
let mins = (total_secs % 3600) / 60;
if mins == 0 {
format!("{}h", hours)
} else {
format!("{}h {}m", hours, mins)
}
}
}
pub fn truncate_text(text: &str, max_length: usize) -> String {
if text.len() <= max_length {
text.to_string()
} else if max_length <= 3 {
"...".to_string()
} else {
format!("{}...", &text[..max_length - 3])
}
}
pub fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
c if c.is_control() => '_',
c => c,
})
.collect::<String>()
.trim()
.to_string()
}
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let a_len = a_chars.len();
let b_len = b_chars.len();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
for i in 0..=a_len {
matrix[i][0] = i;
}
for j in 0..=b_len {
matrix[0][j] = j;
}
for i in 1..=a_len {
for j in 1..=b_len {
let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
matrix[i][j] = std::cmp::min(
std::cmp::min(
matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, ),
matrix[i - 1][j - 1] + cost, );
}
}
matrix[a_len][b_len]
}
pub fn string_similarity(a: &str, b: &str) -> f64 {
if a == b {
return 1.0;
}
let max_len = std::cmp::max(a.len(), b.len());
if max_len == 0 {
return 1.0;
}
let distance = levenshtein_distance(a, b);
1.0 - (distance as f64 / max_len as f64)
}
pub fn find_most_similar<'a>(target: &str, candidates: &[&'a str]) -> Option<&'a str> {
candidates
.iter()
.map(|&candidate| (candidate, string_similarity(target, candidate)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(candidate, _)| candidate)
}
pub async fn retry_with_backoff<F, Fut, T, E>(
mut operation: F,
max_retries: usize,
initial_delay: Duration,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
let mut delay = initial_delay;
for attempt in 0..=max_retries {
match operation().await {
Ok(result) => return Ok(result),
Err(e) => {
if attempt == max_retries {
return Err(e);
}
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, Duration::from_secs(60));
}
}
}
unreachable!()
}
pub struct RateLimiter {
max_requests: u32,
window_duration: Duration,
requests: std::collections::VecDeque<std::time::Instant>,
}
impl RateLimiter {
pub fn new(max_requests: u32, window_duration: Duration) -> Self {
Self {
max_requests,
window_duration,
requests: std::collections::VecDeque::new(),
}
}
pub fn can_proceed(&mut self) -> bool {
let now = std::time::Instant::now();
while let Some(&front) = self.requests.front() {
if now.duration_since(front) > self.window_duration {
self.requests.pop_front();
} else {
break;
}
}
if self.requests.len() < self.max_requests as usize {
self.requests.push_back(now);
true
} else {
false
}
}
pub fn time_until_next(&self) -> Option<Duration> {
if self.requests.len() < self.max_requests as usize {
None
} else if let Some(&front) = self.requests.front() {
let elapsed = std::time::Instant::now().duration_since(front);
if elapsed < self.window_duration {
Some(self.window_duration - elapsed)
} else {
None
}
} else {
None
}
}
}
pub struct Cache<K, V> {
data: std::collections::HashMap<K, (V, std::time::Instant)>,
ttl: Duration,
}
impl<K: std::hash::Hash + Eq + Clone, V: Clone> Cache<K, V> {
pub fn new(ttl: Duration) -> Self {
Self {
data: std::collections::HashMap::new(),
ttl,
}
}
pub fn insert(&mut self, key: K, value: V) {
self.data.insert(key, (value, std::time::Instant::now()));
}
pub fn get(&mut self, key: &K) -> Option<V> {
if let Some((value, timestamp)) = self.data.get(key) {
if timestamp.elapsed() < self.ttl {
Some(value.clone())
} else {
self.data.remove(key);
None
}
} else {
None
}
}
pub fn cleanup(&mut self) {
let now = std::time::Instant::now();
self.data.retain(|_, (_, timestamp)| now.duration_since(*timestamp) < self.ttl);
}
pub fn clear(&mut self) {
self.data.clear();
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_id() {
let id1 = generate_id();
let id2 = generate_id();
assert_ne!(id1, id2);
assert_eq!(id1.len(), 36); }
#[test]
fn test_generate_short_id() {
let id = generate_short_id();
assert_eq!(id.len(), 8);
}
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1536), "1.5 KB");
assert_eq!(format_bytes(1048576), "1.0 MB");
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(Duration::from_secs(30)), "30s");
assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
assert_eq!(format_duration(Duration::from_secs(3690)), "1h 1m");
}
#[test]
fn test_truncate_text() {
assert_eq!(truncate_text("hello", 10), "hello");
assert_eq!(truncate_text("hello world", 8), "hello...");
assert_eq!(truncate_text("hi", 2), "hi");
assert_eq!(truncate_text("hello", 3), "...");
}
#[test]
fn test_sanitize_filename() {
assert_eq!(sanitize_filename("hello/world"), "hello_world");
assert_eq!(sanitize_filename("file:name"), "file_name");
assert_eq!(sanitize_filename("normal_file.txt"), "normal_file.txt");
}
#[test]
fn test_levenshtein_distance() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("hello", "hello"), 0);
assert_eq!(levenshtein_distance("hello", "hallo"), 1);
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
}
#[test]
fn test_string_similarity() {
assert_eq!(string_similarity("hello", "hello"), 1.0);
assert!(string_similarity("hello", "hallo") > 0.7);
assert!(string_similarity("hello", "world") < 0.5);
}
#[test]
fn test_find_most_similar() {
let candidates = &["apple", "banana", "orange", "grape"];
assert_eq!(find_most_similar("aple", candidates), Some("apple"));
assert_eq!(find_most_similar("ornge", candidates), Some("orange"));
}
#[test]
fn test_rate_limiter() {
let mut limiter = RateLimiter::new(2, Duration::from_secs(1));
assert!(limiter.can_proceed());
assert!(limiter.can_proceed());
assert!(!limiter.can_proceed()); }
#[test]
fn test_cache() {
let mut cache = Cache::new(Duration::from_millis(100));
cache.insert("key1", "value1");
assert_eq!(cache.get(&"key1"), Some("value1"));
assert_eq!(cache.get(&"key2"), None);
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
}