pub(crate) const CRATE_NAME: &str = env!("CARGO_PKG_NAME");
pub(crate) const PREFIX_LENGTH: usize = 12;
pub(crate) const PROGRESS_BAR_STYLE_FINISH: &str =
"{prefix:>12.green.bold} {msg} for {human_len} iterations in {elapsed}";
pub(crate) const PROGRESS_BAR_STYLE_FINISH_2: &str =
"{prefix:>12.green.bold} {msg} x{human_len} in {elapsed}";
pub(crate) const PROGRESS_BAR_STYLE_FINISH_3: &str =
"{prefix:>12.green.bold} {msg} ({binary_total_bytes}) in {elapsed}";
pub(crate) const PROGRESS_BAR_STYLE_CYAN_2: &str =
"{prefix:>12.cyan.bold} {human_pos}/{human_len} |{bar}| {msg}";
pub(crate) fn build_resizer_filter(
ty: &str,
) -> anyhow::Result<(fast_image_resize::Resizer, fast_image_resize::ResizeOptions)> {
use fast_image_resize::{FilterType, ResizeAlg, ResizeOptions, Resizer};
let ty = match ty.to_lowercase().as_str() {
"box" => FilterType::Box,
"bilinear" => FilterType::Bilinear,
"hamming" => FilterType::Hamming,
"catmullrom" => FilterType::CatmullRom,
"mitchell" => FilterType::Mitchell,
"gaussian" => FilterType::Gaussian,
"lanczos3" => FilterType::Lanczos3,
_ => anyhow::bail!("Unsupported resizer filter: {}", ty),
};
Ok((
Resizer::new(),
ResizeOptions::new().resize_alg(ResizeAlg::Convolution(ty)),
))
}
pub(crate) fn try_fetch_file_stem<P: AsRef<std::path::Path>>(p: P) -> anyhow::Result<String> {
let p = p.as_ref();
let stem = p
.file_stem()
.ok_or(anyhow::anyhow!(
"Failed to get the `file_stem` of `model_file`: {:?}",
p
))?
.to_str()
.ok_or(anyhow::anyhow!("Failed to convert from `&OsStr` to `&str`"))?;
Ok(stem.to_string())
}
pub(crate) fn build_progress_bar(
n: u64,
prefix: &str,
msg: Option<&str>,
style_temp: &str,
) -> anyhow::Result<indicatif::ProgressBar> {
let pb = indicatif::ProgressBar::new(n);
pb.set_style(indicatif::ProgressStyle::with_template(style_temp)?.progress_chars("██ "));
pb.set_prefix(format!("{:>PREFIX_LENGTH$}", prefix));
pb.set_message(msg.unwrap_or_default().to_string());
Ok(pb)
}
#[allow(dead_code)]
pub(crate) fn human_bytes_decimal(size: f64, decimal_places: usize) -> String {
const DECIMAL_UNITS: [&str; 7] = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
format_bytes_internal(size, 1000.0, &DECIMAL_UNITS, decimal_places)
}
pub(crate) fn human_bytes_binary(size: f64, decimal_places: usize) -> String {
const BINARY_UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
format_bytes_internal(size, 1024.0, &BINARY_UNITS, decimal_places)
}
fn format_bytes_internal(
mut size: f64,
base: f64,
units: &[&str],
decimal_places: usize,
) -> String {
let mut unit_index = 0;
while size >= base && unit_index < units.len() - 1 {
size /= base;
unit_index += 1;
}
format!(
"{:.precision$} {}",
size,
units[unit_index],
precision = decimal_places
)
}
pub(crate) fn generate_random_string(length: usize) -> String {
use rand::{distr::Alphanumeric, rng, Rng};
if length == 0 {
return String::new();
}
let rng = rng();
let mut result = String::with_capacity(length);
result.extend(rng.sample_iter(&Alphanumeric).take(length).map(char::from));
result
}
pub fn timestamp(delimiter: Option<&str>) -> String {
let delimiter = delimiter.unwrap_or("");
let format = format!("%Y{0}%m{0}%d{0}%H{0}%M{0}%S{0}%f", delimiter);
chrono::Local::now().format(&format).to_string()
}
pub(crate) fn natural_compare(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
let mut a_chars = a.chars().peekable();
let mut b_chars = b.chars().peekable();
loop {
match (a_chars.peek(), b_chars.peek()) {
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(&ca), Some(&cb)) => {
if ca.is_ascii_digit() && cb.is_ascii_digit() {
let mut num_a = String::new();
let mut num_b = String::new();
while let Some(&c) = a_chars.peek() {
if c.is_ascii_digit() {
num_a.push(c);
a_chars.next();
} else {
break;
}
}
while let Some(&c) = b_chars.peek() {
if c.is_ascii_digit() {
num_b.push(c);
b_chars.next();
} else {
break;
}
}
match (num_a.parse::<u64>(), num_b.parse::<u64>()) {
(Ok(na), Ok(nb)) => match na.cmp(&nb) {
Ordering::Equal => continue,
other => return other,
},
_ => match num_a.len().cmp(&num_b.len()) {
Ordering::Equal => match num_a.cmp(&num_b) {
Ordering::Equal => continue,
other => return other,
},
other => return other,
},
}
} else {
a_chars.next();
b_chars.next();
match ca.cmp(&cb) {
Ordering::Equal => continue,
other => return other,
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cmp::Ordering;
#[test]
fn test_natural_compare_basic() {
assert_eq!(natural_compare("file1.txt", "file2.txt"), Ordering::Less);
assert_eq!(natural_compare("file2.txt", "file10.txt"), Ordering::Less);
assert_eq!(
natural_compare("file10.txt", "file2.txt"),
Ordering::Greater
);
assert_eq!(natural_compare("file5.txt", "file5.txt"), Ordering::Equal);
}
#[test]
fn test_natural_compare_leading_zeros() {
assert_eq!(natural_compare("img001.jpg", "img002.jpg"), Ordering::Less);
assert_eq!(natural_compare("img001.jpg", "img100.jpg"), Ordering::Less);
assert_eq!(natural_compare("img099.jpg", "img100.jpg"), Ordering::Less);
assert_eq!(natural_compare("img01.jpg", "img1.jpg"), Ordering::Equal); }
#[test]
fn test_natural_compare_version_numbers() {
assert_eq!(natural_compare("v1.0.1", "v1.0.2"), Ordering::Less);
assert_eq!(natural_compare("v1.0.9", "v1.0.10"), Ordering::Less);
assert_eq!(natural_compare("v1.9.0", "v1.10.0"), Ordering::Less);
assert_eq!(natural_compare("v2.0.0", "v1.99.99"), Ordering::Greater);
}
#[test]
fn test_natural_compare_mixed_content() {
assert_eq!(natural_compare("test", "test123"), Ordering::Less);
assert_eq!(natural_compare("test123", "test"), Ordering::Greater);
assert_eq!(natural_compare("abc123def", "abc123def"), Ordering::Equal);
assert_eq!(natural_compare("abc123def", "abc124def"), Ordering::Less);
}
#[test]
fn test_natural_compare_edge_cases() {
assert_eq!(natural_compare("", ""), Ordering::Equal);
assert_eq!(natural_compare("", "a"), Ordering::Less);
assert_eq!(natural_compare("a", ""), Ordering::Greater);
assert_eq!(natural_compare("1", "2"), Ordering::Less);
assert_eq!(natural_compare("9", "10"), Ordering::Less);
assert_eq!(natural_compare("100", "99"), Ordering::Greater);
assert_eq!(natural_compare("apple", "banana"), Ordering::Less);
assert_eq!(natural_compare("zebra", "apple"), Ordering::Greater);
}
#[test]
fn test_natural_compare_complex_filenames() {
assert_eq!(
natural_compare("frame_0001.png", "frame_0010.png"),
Ordering::Less
);
assert_eq!(
natural_compare("shot1_take5.mp4", "shot1_take15.mp4"),
Ordering::Less
);
assert_eq!(
natural_compare("scene2_v3.mov", "scene10_v1.mov"),
Ordering::Less
);
assert_eq!(
natural_compare("data_batch_1", "data_batch_10"),
Ordering::Less
);
assert_eq!(
natural_compare("train_001.jpg", "train_1000.jpg"),
Ordering::Less
);
}
#[test]
fn test_natural_compare_large_numbers() {
let large_a = "file99999999999999999999.txt";
let large_b = "file100000000000000000000.txt";
assert_eq!(natural_compare(large_a, large_b), Ordering::Less);
assert_eq!(
natural_compare(
"file12345678901234567890.txt",
"file12345678901234567891.txt"
),
Ordering::Less
);
}
#[test]
fn test_natural_compare_multiple_numbers() {
assert_eq!(natural_compare("v1.2.3", "v1.2.10"), Ordering::Less);
assert_eq!(
natural_compare("chapter1_page5", "chapter1_page15"),
Ordering::Less
);
assert_eq!(natural_compare("2024_01_05", "2024_01_15"), Ordering::Less);
assert_eq!(natural_compare("2024_12_31", "2025_01_01"), Ordering::Less);
}
#[test]
fn test_natural_compare_case_sensitivity() {
assert_eq!(natural_compare("File1.txt", "file1.txt"), Ordering::Less); assert_eq!(natural_compare("ABC", "abc"), Ordering::Less);
}
#[test]
fn test_natural_sorting() {
let mut files = vec![
"file10.txt",
"file2.txt",
"file1.txt",
"file20.txt",
"file3.txt",
];
files.sort_by(|a, b| natural_compare(a, b));
assert_eq!(
files,
vec![
"file1.txt",
"file2.txt",
"file3.txt",
"file10.txt",
"file20.txt"
]
);
}
}