use crate::error::NFLZError;
use crate::math::count_digits_without_leading_zeroes;
use regex::Regex;
use std::cmp::Ordering;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct FileInfo {
path: PathBuf,
original_filename: String,
number_group_indices: (u16, u16),
#[allow(unused)]
number_group_str: String,
number_group_value: u64,
}
impl FileInfo {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, NFLZError> {
let filename = path_to_filename(path.as_ref()).to_owned();
let number_group_indices = get_number_group_indices_from_actual_filename(&filename)?;
let (from, to) = number_group_indices;
let number_group_value_str = &filename[from as usize..to as usize];
let number_group_value = u64::from_str(number_group_value_str).map_err(|_| {
NFLZError::ValueInNumberedGroupNotANumber(number_group_value_str.to_string())
})?;
Ok(Self {
path: PathBuf::from(path.as_ref()),
number_group_str: number_group_value_str.to_string(),
original_filename: filename,
number_group_indices,
number_group_value,
})
}
pub fn filename_prefix(&self) -> &str {
let (prefix, _) =
get_filename_prefix_and_suffix(self.original_filename(), self.number_group_indices());
prefix
}
pub fn filename_suffix(&self) -> &str {
let (_, suffix) =
get_filename_prefix_and_suffix(self.original_filename(), self.number_group_indices());
suffix
}
const fn number_group_indices(&self) -> (u16, u16) {
self.number_group_indices
}
pub const fn number_group_value(&self) -> u64 {
self.number_group_value
}
pub fn original_filename(&self) -> &str {
self.original_filename.as_ref()
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl PartialOrd for FileInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.number_group_value
.partial_cmp(&other.number_group_value)
}
}
impl PartialEq for FileInfo {
fn eq(&self, other: &Self) -> bool {
self.original_filename() == other.original_filename()
}
}
impl Eq for FileInfo {}
impl Ord for FileInfo {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
#[derive(Debug, Clone)]
pub struct FileInfoWithRenameAdvice {
file_info: FileInfo,
new_filename: Option<String>,
}
impl FileInfoWithRenameAdvice {
pub fn new(file_info: FileInfo, max_digits: u64) -> Self {
assert_ne!(max_digits, 0, "max digits must be bigger than zero");
let digits = count_digits_without_leading_zeroes(file_info.number_group_value());
let digits_to_add_count = max_digits - digits;
if digits_to_add_count == 0 {
log::debug!(
"No rename required. File '{}' already has the correct name.",
file_info.original_filename()
);
Self {
file_info,
new_filename: None,
}
} else {
let value_str_with_leading_zeros = format!(
"{}{}",
String::from("0").repeat(digits_to_add_count as usize),
file_info.number_group_value()
);
let new_filename = format!(
"{}{}{}",
file_info.filename_prefix(),
value_str_with_leading_zeros,
file_info.filename_suffix(),
);
assert_ne!(
file_info.original_filename, new_filename,
"original_filename and new_filename are equal!"
);
Self {
file_info,
new_filename: Some(new_filename),
}
}
}
pub const fn needs_rename(&self) -> bool {
self.new_filename.is_some()
}
pub const fn is_already_properly_named(&self) -> bool {
self.new_filename.is_none()
}
pub const fn file_info(&self) -> &FileInfo {
&self.file_info
}
pub fn path_with_new_filename(&self) -> Option<PathBuf> {
self.new_filename.as_ref().map(|new_filename| {
let mut parent_dir = PathBuf::from(self.file_info.path.parent().unwrap());
parent_dir.push(new_filename);
parent_dir
})
}
pub fn new_filename(&self) -> Option<&str> {
self.new_filename.as_deref()
}
pub fn renamed_file_already_exists(&self) -> bool {
self.path_with_new_filename()
.map(|x| x.exists())
.unwrap_or(false)
}
}
impl PartialOrd for FileInfoWithRenameAdvice {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.file_info.partial_cmp(&other.file_info)
}
}
impl PartialEq for FileInfoWithRenameAdvice {
fn eq(&self, other: &Self) -> bool {
self.file_info.eq(&other.file_info)
}
}
impl Eq for FileInfoWithRenameAdvice {}
impl Ord for FileInfoWithRenameAdvice {
fn cmp(&self, other: &Self) -> Ordering {
self.file_info.partial_cmp(other.file_info()).unwrap()
}
}
pub(crate) fn path_to_filename(path: &Path) -> &str {
match path.components().last().unwrap() {
Component::Normal(name) => name.to_str().expect("path must be valid utf-8"),
_ => panic!("Unexpected file path component."),
}
}
fn get_number_group_indices_from_actual_filename(
actual_filename: &str,
) -> Result<(u16, u16), NFLZError> {
let regex = Regex::new(r"(\([0-9]+\))").unwrap();
let match_indices = regex
.find_iter(actual_filename)
.map(|m| (m.start() as u16, m.end() as u16))
.collect::<Vec<(u16, u16)>>();
if match_indices.is_empty() || match_indices.len() > 1 {
Err(NFLZError::FilenameMustIncludeExactlyOneNumberedGroup(
actual_filename.to_string(),
))
} else {
let from = match_indices[0].0 + 1;
let to = match_indices[0].1 - 1;
Ok((from, to))
}
}
fn get_filename_prefix_and_suffix(actual_filename: &str, (begin, end): (u16, u16)) -> (&str, &str) {
(
&actual_filename[0..begin as usize],
&actual_filename[end as usize..actual_filename.len()],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_number_group_indices_from_actual_filename() {
let input1 = "img (100)";
let input2 = "img (1) (100)";
let input3 = "img (1) 100)";
let actual1 = get_number_group_indices_from_actual_filename(input1).unwrap();
assert_eq!(
5, actual1.0,
"Number parentheses group starts at index 4 (inclusive)"
);
assert_eq!(
8, actual1.1,
"Number parentheses group ends at index 9 (exclusive)"
);
let actual2 = get_number_group_indices_from_actual_filename(input2);
assert!(actual2.is_err());
let actual3 = get_number_group_indices_from_actual_filename(input3).unwrap();
assert_eq!(
5, actual3.0,
"Number parentheses group starts at index 4 (inclusive)"
);
assert_eq!(
6, actual3.1,
"Number parentheses group ends at index 9 (exclusive)"
);
}
#[test]
fn test_get_filename_prefix_and_suffix() {
let input1 = "img (100).jpg";
let indices1 = get_number_group_indices_from_actual_filename(input1).unwrap();
let (prefix1, suffix1) = get_filename_prefix_and_suffix(input1, indices1);
assert_eq!("img (", prefix1);
assert_eq!(").jpg", suffix1);
let input2 = "(100) foobar.png";
let indices2 = get_number_group_indices_from_actual_filename(input2).unwrap();
let (prefix2, suffix2) = get_filename_prefix_and_suffix(input2, indices2);
assert_eq!("(", prefix2);
assert_eq!(") foobar.png", suffix2);
}
}