2023-06-15 08:57:19 -04:00
|
|
|
use chrono::naive::NaiveDate;
|
|
|
|
|
use regex::Regex;
|
2024-03-18 20:17:38 -04:00
|
|
|
use std::cmp::min;
|
2023-06-15 08:57:19 -04:00
|
|
|
use std::convert::TryFrom;
|
|
|
|
|
use std::fs::DirEntry;
|
2024-03-18 20:17:38 -04:00
|
|
|
use std::path::PathBuf;
|
2023-06-15 08:57:19 -04:00
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
2024-03-18 20:17:38 -04:00
|
|
|
use crate::file::FileNameParseError;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
2023-06-25 06:58:08 -04:00
|
|
|
pub struct File {
|
2024-03-18 20:17:38 -04:00
|
|
|
pub file: PathBuf,
|
2023-06-15 08:57:19 -04:00
|
|
|
pub date: NaiveDate,
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 20:17:38 -04:00
|
|
|
pub enum FileError {
|
2023-06-25 12:59:28 -04:00
|
|
|
//IOError(&'static str),
|
2024-03-18 20:17:38 -04:00
|
|
|
ParseError(&'static str),
|
2023-06-25 12:59:28 -04:00
|
|
|
}
|
|
|
|
|
|
2023-06-25 06:58:08 -04:00
|
|
|
impl File {
|
2023-06-15 08:57:19 -04:00
|
|
|
fn capture_as_number<T: FromStr>(capture: ®ex::Captures, name: &str) -> Result<T, String> {
|
|
|
|
|
Ok(capture
|
|
|
|
|
.name(name)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.as_str()
|
|
|
|
|
.parse::<T>()
|
|
|
|
|
.ok()
|
|
|
|
|
.ok_or("Something went wrong".to_owned())?)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-25 06:58:08 -04:00
|
|
|
pub fn latest_file(a: File, b: File) -> File {
|
2023-06-15 08:57:19 -04:00
|
|
|
if a.date > b.date {
|
|
|
|
|
a
|
|
|
|
|
} else {
|
|
|
|
|
b
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_file_regex() -> Regex {
|
|
|
|
|
//TODO This would ideally be configurable
|
|
|
|
|
Regex::new(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}).md")
|
|
|
|
|
.expect("could not create regex")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-25 06:58:08 -04:00
|
|
|
impl TryFrom<DirEntry> for File {
|
2023-06-25 12:59:28 -04:00
|
|
|
type Error = FileError;
|
2023-06-15 08:57:19 -04:00
|
|
|
|
|
|
|
|
fn try_from(direntry: DirEntry) -> Result<Self, Self::Error> {
|
2023-06-25 06:58:08 -04:00
|
|
|
let re = File::get_file_regex();
|
2024-03-18 20:17:38 -04:00
|
|
|
// println!("{:?}", re);
|
2023-06-15 08:57:19 -04:00
|
|
|
let file_name = direntry.file_name();
|
|
|
|
|
let file_name_str = match file_name.to_str() {
|
|
|
|
|
Some(name) => name,
|
|
|
|
|
_ => "",
|
|
|
|
|
};
|
2024-03-18 20:17:38 -04:00
|
|
|
// println!("{:?}", file_name_str);
|
2023-06-15 08:57:19 -04:00
|
|
|
|
|
|
|
|
if let Some(caps) = re.captures(file_name_str) {
|
|
|
|
|
let year: i32 = Self::capture_as_number(&caps, "year").unwrap();
|
|
|
|
|
let month: u32 = Self::capture_as_number(&caps, "month").unwrap();
|
|
|
|
|
let day: u32 = Self::capture_as_number(&caps, "day").unwrap();
|
|
|
|
|
|
|
|
|
|
return Ok(Self {
|
2024-03-18 20:17:38 -04:00
|
|
|
file: direntry.path(),
|
2023-06-15 08:57:19 -04:00
|
|
|
date: NaiveDate::from_ymd_opt(year, month, day).unwrap(),
|
|
|
|
|
});
|
|
|
|
|
};
|
2023-06-25 12:59:28 -04:00
|
|
|
Err(FileError::ParseError("Could not parse file name"))
|
2023-06-15 08:57:19 -04:00
|
|
|
}
|
|
|
|
|
}
|
2024-03-18 20:17:38 -04:00
|
|
|
|
|
|
|
|
fn try_get_date(file: &PathBuf) -> Result<NaiveDate, FileNameParseError> {
|
|
|
|
|
let file_name = file
|
|
|
|
|
.file_name()
|
|
|
|
|
.ok_or(FileNameParseError::TypeConversionError(
|
|
|
|
|
"Could not get filename from path: {:?}",
|
|
|
|
|
))?
|
|
|
|
|
.to_str()
|
|
|
|
|
.ok_or(FileNameParseError::TypeConversionError(
|
|
|
|
|
"Could not get filename from path: {:?}",
|
|
|
|
|
))?;
|
|
|
|
|
|
|
|
|
|
NaiveDate::parse_from_str(file_name, "%Y-%m-%d.md")
|
|
|
|
|
.or_else(|e| Err(FileNameParseError::ParseError(e)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TryFrom<PathBuf> for File {
|
|
|
|
|
type Error = FileNameParseError;
|
|
|
|
|
|
|
|
|
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
|
|
|
|
Ok(Self {
|
|
|
|
|
date: try_get_date(&path)?,
|
|
|
|
|
file: path.into(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl File {
|
|
|
|
|
pub fn get_closest_files(files: Vec<PathBuf>, target: NaiveDate, n: usize) -> Vec<File> {
|
|
|
|
|
let mut dated_files = files
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|file| File::try_from(file).ok())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
dated_files.sort_by_cached_key(|dated_file| (dated_file.date - target).num_days().abs());
|
|
|
|
|
|
|
|
|
|
let count = min(n, dated_files.len());
|
|
|
|
|
dated_files[..count].to_vec()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use super::*;
|
|
|
|
|
use chrono::NaiveDate;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_closest_date() {
|
|
|
|
|
let files = vec![
|
|
|
|
|
PathBuf::from("./2024-01-01.md"),
|
|
|
|
|
PathBuf::from("./2024-01-02.md"),
|
|
|
|
|
PathBuf::from("./2024-01-03.md"),
|
|
|
|
|
PathBuf::from("./2024-02-01.md"),
|
|
|
|
|
PathBuf::from("./2024-03-01.md"),
|
|
|
|
|
PathBuf::from("./2024-04-01.md"),
|
|
|
|
|
PathBuf::from("./2024-04-02.md"),
|
|
|
|
|
PathBuf::from("./2024-04-03.md"),
|
|
|
|
|
PathBuf::from("./2024-04-04.md"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let res = File::get_closest_files(
|
|
|
|
|
files.clone(),
|
|
|
|
|
NaiveDate::from_ymd_opt(2023, 12, 30).unwrap(),
|
|
|
|
|
3,
|
|
|
|
|
);
|
|
|
|
|
let expected_res = vec![
|
|
|
|
|
File::try_from(PathBuf::from("./2024-01-01.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-01-02.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-01-03.md")).unwrap(),
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(res, expected_res);
|
|
|
|
|
|
|
|
|
|
let res = File::get_closest_files(
|
|
|
|
|
files.clone(),
|
|
|
|
|
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
|
|
|
|
|
3,
|
|
|
|
|
);
|
|
|
|
|
let expected_res = vec![
|
|
|
|
|
File::try_from(PathBuf::from("./2024-02-01.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-01-03.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-03-01.md")).unwrap(),
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(res, expected_res);
|
|
|
|
|
|
|
|
|
|
let res = File::get_closest_files(
|
|
|
|
|
files.clone(),
|
|
|
|
|
NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(),
|
|
|
|
|
3,
|
|
|
|
|
);
|
|
|
|
|
let expected_res = vec![
|
|
|
|
|
File::try_from(PathBuf::from("./2024-04-04.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-04-03.md")).unwrap(),
|
|
|
|
|
File::try_from(PathBuf::from("./2024-04-02.md")).unwrap(),
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(res, expected_res);
|
|
|
|
|
|
|
|
|
|
let res = File::get_closest_files(
|
|
|
|
|
files[..1].to_vec(),
|
|
|
|
|
NaiveDate::from_ymd_opt(2023, 12, 30).unwrap(),
|
|
|
|
|
3,
|
|
|
|
|
);
|
|
|
|
|
let expected_res = vec![
|
|
|
|
|
File::try_from(PathBuf::from("./2024-01-01.md")).unwrap(),
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(res, expected_res);
|
|
|
|
|
}
|
|
|
|
|
}
|