diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6ae7e1f..3111a16 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod ff; mod media; mod models; +pub mod strings; use ff::extract_streams; use media::{detect_media_type, is_media_file}; diff --git a/src-tauri/src/strings.rs b/src-tauri/src/strings.rs new file mode 100644 index 0000000..be2a437 --- /dev/null +++ b/src-tauri/src/strings.rs @@ -0,0 +1,122 @@ +/// Transforms a filename to fit within a character limit while preserving the most useful context +/// +/// This function prioritizes preserving: +/// 1. File extension (if reasonable length ≤ 5 chars including dot) +/// 2. Beginning of filename (for identification) +/// 3. End of filename before extension (often contains important info like numbers) +/// +/// # Arguments +/// * `filename` - The filename to transform +/// * `limit` - Maximum number of characters +/// +/// # Returns +/// * Transformed filename that fits within the limit, using ellipsis (...) to indicate truncation +/// +/// # Examples +/// ``` +/// use byte_me_lib::strings::transform_filename; +/// +/// // Short filenames remain unchanged +/// assert_eq!(transform_filename("test.mp4", 20), "test.mp4"); +/// +/// // Long filename with extension - preserve extension and context +/// assert_eq!(transform_filename("very_long_video_file_name.mp4", 18), "ver...ile_name.mp4"); +/// +/// // Numeric sequences - preserve start and end numbers +/// assert_eq!(transform_filename("43509374693.TS.mp4", 15), "435...93.TS.mp4"); +/// +/// // No extension - preserve start and end of name +/// assert_eq!(transform_filename("very_long_document_name", 15), "ver...ment_name"); +/// +/// // Long extension treated as part of name +/// assert_eq!(transform_filename("file.verylongextension", 15), "fil...extension"); +/// ``` +pub fn transform_filename(filename: &str, limit: usize) -> String { + // Handle edge cases + if limit == 0 || filename.is_empty() { + return String::new(); + } + + if filename.len() <= limit { + return filename.to_string(); + } + + // Find potential extension (last dot, not at start or end) + let extension_start = filename + .rfind('.') + .filter(|&pos| pos > 0 && pos < filename.len() - 1); + + let (name_part, extension_part) = if let Some(ext_pos) = extension_start { + let ext = &filename[ext_pos..]; + // Only treat as extension if it's reasonable length (≤ 5 chars including dot) + // and doesn't contain additional dots (compound extensions like .TS.mp4) + if ext.len() <= 5 && !ext[1..].contains('.') { + (&filename[..ext_pos], ext) + } else { + (filename, "") + } + } else { + (filename, "") + }; + + // If even just the extension is too long, truncate the whole thing + if extension_part.len() >= limit { + return truncate_string(filename, limit); + } + + // Calculate space available for the name part + let name_limit = limit - extension_part.len(); + + // If name fits in available space, no truncation needed + if name_part.len() <= name_limit { + return filename.to_string(); + } + + // Need to truncate the name part + let truncated_name = truncate_string(name_part, name_limit); + format!("{}{}", truncated_name, extension_part) +} + +/// Helper function to truncate a string with ellipsis, preserving start and end context +pub fn truncate_string(s: &str, limit: usize) -> String { + if s.len() <= limit { + return s.to_string(); + } + + // For very small limits, just truncate without ellipsis + if limit < 5 { + return s.chars().take(limit).collect(); + } + + // For limits 5 and above, use start + "..." + end pattern + // Strategy: Use 3 chars for ellipsis, split remaining between start and end + // But ensure we get meaningful chunks from both ends + + let available_for_content = limit - 3; // Reserve 3 for "..." + + // Determine start and end characters based on available space + let (start_chars, end_chars) = if available_for_content <= 4 { + // Very limited space: minimal start, rest for end + (1, available_for_content - 1) + } else if available_for_content <= 6 { + // Medium space: balanced approach + let start = available_for_content / 2; + (start, available_for_content - start) + } else { + // Plenty of space: cap start at 3, use more for end to preserve context + let start = 3; + (start, available_for_content - start) + }; + + let start: String = s.chars().take(start_chars).collect(); + let end: String = s + .chars() + .rev() + .take(end_chars) + .collect::() + .chars() + .rev() + .collect(); + + format!("{}...{}", start, end) +} diff --git a/src-tauri/tests/strings.rs b/src-tauri/tests/strings.rs new file mode 100644 index 0000000..9f61c5c --- /dev/null +++ b/src-tauri/tests/strings.rs @@ -0,0 +1,105 @@ +use byte_me_lib::strings::{transform_filename, truncate_string}; + +#[test] +fn test_transform_filename() { + // Test cases focusing on practical, readable outputs + + // 1. Short filenames should remain unchanged + assert_eq!(transform_filename("test.mp4", 20), "test.mp4"); + assert_eq!(transform_filename("short.txt", 15), "short.txt"); + assert_eq!(transform_filename("a.b", 10), "a.b"); + + // 2. No extension cases - preserve meaningful start and end + assert_eq!(transform_filename("short_name", 15), "short_name"); + assert_eq!( + transform_filename("very_long_document_name", 15), + "ver...ment_name" + ); + assert_eq!( + transform_filename("medium_length_name", 13), + "med...th_name" + ); + + // 3. Normal extension cases (preserving extension) + assert_eq!( + transform_filename("very_long_video_file_name.mp4", 18), + "ver...ile_name.mp4" + ); + assert_eq!( + transform_filename("document_with_long_name.pdf", 15), + "doc..._name.pdf" + ); + assert_eq!( + transform_filename("image_file_name.jpeg", 15), + "ima...name.jpeg" + ); + + // 4. Numeric sequences (like user's example) - preserve start and end numbers + assert_eq!( + transform_filename("43509374693.TS.mp4", 15), + "435...93.TS.mp4" + ); + assert_eq!( + transform_filename("20231201_video.mp4", 15), + "202...video.mp4" + ); + assert_eq!(transform_filename("file_v2.1.3.tar", 12), "fi...1.3.tar"); + + // 5. Long extensions (treated as part of filename) + assert_eq!( + transform_filename("file.verylongextension", 15), + "fil...extension" + ); + assert_eq!( + transform_filename("document.backup_old", 15), + "doc...ackup_old" + ); + + // 6. Edge cases + assert_eq!(transform_filename("", 10), ""); + assert_eq!(transform_filename("a", 0), ""); + assert_eq!(transform_filename("test", 4), "test"); + assert_eq!(transform_filename("test", 3), "tes"); + assert_eq!(transform_filename("ab", 2), "ab"); + + // 7. Very short limits - graceful degradation + assert_eq!(transform_filename("test.mp4", 8), "test.mp4"); + assert_eq!(transform_filename("verylongname", 8), "ve...ame"); + assert_eq!(transform_filename("test.mp4", 7), "tes.mp4"); + assert_eq!(transform_filename("hello.txt", 9), "hello.txt"); + + // 8. Extension edge cases + assert_eq!(transform_filename("file.", 10), "file."); + assert_eq!(transform_filename(".hidden", 10), ".hidden"); + assert_eq!(transform_filename("test.a", 10), "test.a"); + + // 9. Real-world examples + assert_eq!( + transform_filename("IMG_20231201_143022.jpg", 15), + "IMG...43022.jpg" + ); + assert_eq!( + transform_filename("meeting_recording_final_v2.mp4", 20), + "mee...g_final_v2.mp4" + ); + assert_eq!( + transform_filename("my document (copy).docx", 15), + "my ...opy).docx" + ); +} + +#[test] +fn test_truncate_string() { + // Test the helper function directly + assert_eq!(truncate_string("hello", 10), "hello"); + assert_eq!(truncate_string("hello", 5), "hello"); + assert_eq!(truncate_string("hello_world", 8), "he...rld"); + assert_eq!(truncate_string("test", 4), "test"); + assert_eq!(truncate_string("test", 3), "tes"); + assert_eq!(truncate_string("ab", 2), "ab"); + assert_eq!(truncate_string("a", 1), "a"); + assert_eq!(truncate_string("hello", 1), "h"); + assert_eq!(truncate_string("hello", 0), ""); + assert_eq!(truncate_string("very_long_name", 10), "ver...name"); + assert_eq!(truncate_string("document_name", 9), "doc...ame"); +}